defmodule Corex.ToggleGroup do @moduledoc ~S''' Phoenix implementation of [Zag.js Toggle Group](https://zagjs.com/components/react/toggle-group). ## Anatomy ### Minimal ```heex <.toggle_group class="toggle-group"> <:item value="lorem">Lorem <:item value="duis">Duis <:item value="donec">Donec ``` ### With indicator ```heex <.toggle_group class="toggle-group"> <:item value="bold"> <.heroicon name="hero-bold" /> Bold <.toggle_group class="toggle-group"> <:item value="bold" aria_label="Bold"> <.heroicon name="hero-bold" /> Bold ``` ### Single selection Set `multiple={false}` so only one item can be selected at a time. ```heex <.toggle_group class="toggle-group" multiple={false} value={["duis"]} > <:item value="lorem">Lorem <:item value="duis">Duis <:item value="donec">Donec ``` ## API Requires a stable `id` on `<.toggle_group>`. | Function | Action | Returns | | -------- | ------ | ------- | | [`set_value/2`](#set_value/2) | Set selected values (client) | `%Phoenix.LiveView.JS{}` | | [`set_value/3`](#set_value/3) | Set selected values (server) | `socket` | ## Events Pick an event name and pass it to `on_*` on `<.toggle_group>`. ### Server events | Event | When | Payload | | ----- | ---- | ------- | | `on_value_change="toggle_group_changed"` | Selected values change | `%{"id" => id, "value" => values}` — list of selected item `value` strings | ### on_value_change ```heex <.toggle_group class="toggle-group" on_value_change="toggle_group_changed" multiple > <:item value="lorem">Lorem <:item value="duis">Duis <:item value="donec">Donec ``` ```elixir def handle_event("toggle_group_changed", %{"id" => id, "value" => value}, socket) do {:noreply, assign(socket, :toggle_group_value, value)} end ``` ### Client events | Event | When | `event.detail` | | ----- | ---- | -------------- | | `on_value_change_client="toggle-group-changed"` | Selected values change | `id`, `value` | ### on_value_change_client ```heex <.toggle_group id="toggle-group-events-client" class="toggle-group" on_value_change_client="toggle-group-changed" multiple > <:item value="lorem">Lorem <:item value="duis">Duis <:item value="donec">Donec ``` ```javascript const el = document.getElementById("toggle-group-events-client"); el?.addEventListener("toggle-group-changed", (event) => console.log(event.detail)); ``` ## Patterns ### Controlled For server-owned selection, set `controlled`, bind `value`, and handle `on_value_change` in LiveView. ```heex <.toggle_group class="toggle-group" value={@value} multiple controlled on_value_change="toggle_group_pattern" > <:item value="lorem">Lorem <:item value="duis">Duis <:item value="donec">Donec ``` ```elixir def mount(_params, _session, socket) do {:ok, assign(socket, :value, ["lorem"])} end def handle_event("toggle_group_pattern", %{"value" => v}, socket) do {:noreply, assign(socket, :value, v || [])} end ``` ## Style Target parts with `data-scope` and `data-part`, or use Corex Design: import tokens and `toggle-group.css`, then set `class="toggle-group"` on `<.toggle_group>`. ```css [data-scope="toggle-group"][data-part="root"] {} [data-scope="toggle-group"][data-part="item"] {} ``` ```css @import "../corex/main.css"; @import "../corex/tokens/themes/neo/light.css"; @import "../corex/components/toggle-group.css"; ``` Stack modifiers on the host (`class` on `<.toggle_group>`). ### Color | Modifier | Classes | | -------- | ------- | | Default | `toggle-group` | | Accent | `toggle-group toggle-group--accent` | | Brand | `toggle-group toggle-group--brand` | | Alert | `toggle-group toggle-group--alert` | | Info | `toggle-group toggle-group--info` | | Success | `toggle-group toggle-group--success` | ### Size | Modifier | Classes | | -------- | ------- | | SM | `toggle-group toggle-group--sm` | | MD | `toggle-group toggle-group--md` | | LG | `toggle-group toggle-group--lg` | | XL | `toggle-group toggle-group--xl` | ### Rounded | Modifier | Classes | | -------- | ------- | | None | `toggle-group toggle-group--rounded-none` | | SM | `toggle-group toggle-group--rounded-sm` | | MD | `toggle-group toggle-group--rounded-md` | | LG | `toggle-group toggle-group--rounded-lg` | | XL | `toggle-group toggle-group--rounded-xl` | | Full | `toggle-group toggle-group--rounded-full` | ''' @doc type: :component use Phoenix.Component import Corex.Api.Doc alias Corex.ToggleGroup.Anatomy.{Item, Props, Root} alias Corex.ToggleGroup.Connect alias Phoenix.LiveView alias Phoenix.LiveView.JS @doc """ Renders a toggle group component. """ attr(:id, :string, required: false, doc: "The id of the toggle group, useful for API to identify the toggle group" ) attr(:value, :list, default: [], doc: "The initial value or the controlled value of the toggle group, must be a list of strings" ) attr(:controlled, :boolean, default: false, doc: "Whether the toggle group is controlled") attr(:deselectable, :boolean, default: false, doc: "Whether the toggle group is deselectable") attr(:loopFocus, :boolean, default: true, doc: "Whether the toggle group is loopFocus") attr(:rovingFocus, :boolean, default: true, doc: "Whether the toggle group is rovingFocus") attr(:disabled, :boolean, default: false, doc: "Whether the toggle group is disabled") attr(:multiple, :boolean, default: true, doc: "Whether the toggle group allows multiple items to be selected" ) attr(:orientation, :string, default: "horizontal", values: ["horizontal", "vertical"], doc: "The orientation of the toggle group" ) attr(:dir, :string, default: nil, values: [nil, "ltr", "rtl"], doc: "The direction of the toggle group. When nil, derived from document (html lang + config :rtl_locales)" ) attr(:on_value_change, :string, default: nil, doc: "The server event name when the value change" ) attr(:on_value_change_client, :string, default: nil, doc: "The client event name when the value change" ) attr(:rest, :global) slot :label, required: false, doc: "Optional visible or screen-reader label for the group" do attr(:class, :string, required: false) end slot :item, required: true do attr(:value, :string, doc: "The value of the item, useful in controlled mode and for API to identify the item" ) attr(:disabled, :boolean, doc: "Whether the item is disabled") attr(:class, :string, doc: "The class of the item") attr(:aria_label, :string, doc: "Accessibility label for the item button. If not provided, the value will be used as a fallback." ) end def toggle_group(assigns) do assigns = assigns |> assign_new(:id, fn -> "toggle-group-#{System.unique_integer([:positive])}" end) |> assign_new(:dir, fn -> "ltr" end) label_id = case Map.get(assigns, :label, []) do [] -> nil _ -> "#{assigns.id}-label" end assigns = assign(assigns, :label_id, label_id) ~H"""
{render_slot(@label)}
""" end api_doc(~S""" Set selected values from a control (`phx-click`). Pass a list of item `value` strings (or compatible input validated by the component). ```heex <.action phx-click={Corex.ToggleGroup.set_value("my-toggle-group", ["a"])}>Pick A <.toggle_group id="my-toggle-group" class="toggle-group" multiple={false} value={[]}> <:item value="a">A <:item value="b">B ``` ```javascript document.getElementById("my-toggle-group")?.dispatchEvent( new CustomEvent("corex:toggle-group:set-value", { detail: { value: ["a"] }, }) ); ``` """) def set_value(toggle_group_id, value) when is_binary(toggle_group_id) do JS.dispatch("corex:toggle-group:set-value", to: "##{toggle_group_id}", detail: %{value: Corex.Helpers.validate_value!(value)} ) end api_doc(~S""" Set selected values from `handle_event`. ```heex <.action phx-click="pick_a">Pick A <.toggle_group id="my-toggle-group" class="toggle-group" multiple={false} value={[]}> <:item value="a">A <:item value="b">B ``` ```elixir def handle_event("pick_a", _, socket) do {:noreply, Corex.ToggleGroup.set_value(socket, "my-toggle-group", ["a"])} end ``` """) def set_value(socket, toggle_group_id, value) when is_struct(socket, Phoenix.LiveView.Socket) and is_binary(toggle_group_id) do LiveView.push_event(socket, "toggle-group_set_value", %{ id: toggle_group_id, value: Corex.Helpers.validate_value!(value) }) end end