Skip to main content

lib/shadix/components/toggle.ex

defmodule Shadix.Components.Toggle do
  @moduledoc """
  Toggle component translated from shadcn/ui (new-york-v4).

  A two-state toggle button. Unlike a native checkbox, the pressed state lives in
  ARIA (`aria-pressed`) and a `data-state` of `"on"`/`"off"` that the `cva` styles
  key off of (the on-state gets `bg-accent`). It renders a plain
  `<button type="button">` and is *uncontrolled*: `@pressed` only seeds the initial
  markup. The small `ShadixToggle` hook (assets/ts/toggle.ts) flips both
  `aria-pressed` and `data-state` on click by reading the current DOM value, so it
  stays independent of the server-rendered initial state.

  Caller-supplied `class` is appended last; Tailwind cascade layers ensure it wins
  over the defaults.
  """
  use Phoenix.Component

  import Shadix.Cn

  @base "inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-[color,box-shadow] outline-none hover:bg-muted hover:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"

  @variants %{
    "default" => "bg-transparent",
    "outline" =>
      "border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground"
  }

  @sizes %{
    "default" => "h-9 min-w-9 px-2",
    "sm" => "h-8 min-w-8 px-1.5",
    "lg" => "h-10 min-w-10 px-2.5"
  }

  @doc """
  Renders an uncontrolled two-state toggle button.

  `:pressed` seeds the initial `aria-pressed` / `data-state`; thereafter the
  `ShadixToggle` hook owns the state and flips it on click.
  """
  attr(:variant, :string, default: "default", values: ~w(default outline))
  attr(:size, :string, default: "default", values: ~w(default sm lg))
  attr(:pressed, :boolean, default: false)
  attr(:class, :string, default: nil)
  attr(:rest, :global)
  slot(:inner_block, required: true)

  def toggle(assigns) do
    assigns =
      assign(
        assigns,
        :computed_class,
        cn([@base, @variants[assigns.variant], @sizes[assigns.size], assigns.class])
      )

    ~H"""
    <button
      type="button"
      data-slot="toggle"
      aria-pressed={to_string(@pressed)}
      data-state={if @pressed, do: "on", else: "off"}
      phx-hook="ShadixToggle"
      class={@computed_class}
      {@rest}
    >
      {render_slot(@inner_block)}
    </button>
    """
  end
end