defmodule Corex.Accordion do @moduledoc ~S''' Phoenix implementation of [Zag.js Accordion](https://zagjs.com/components/react/accordion). ## Examples ### Basic You can use `Corex.Content.new/1` to create a list of content items. The `id` for each item is optional and will be auto-generated if not provided. You can specify `disabled` for each item. ```heex <.accordion class="accordion" items={Corex.Content.new([ [trigger: "Lorem ipsum dolor sit amet", content: "Consectetur adipiscing elit. Sed sodales ullamcorper tristique."], [trigger: "Duis dictum gravida odio ac pharetra?", content: "Nullam eget vestibulum ligula, at interdum tellus."], [trigger: "Donec condimentum ex mi", content: "Congue molestie ipsum gravida a. Sed ac eros luctus."] ])} /> ``` ### With indicator Use the optional `:indicator` slot to add an icon after each trigger. This example assumes the import of `.icon` from `Core Components` ```heex <.accordion class="accordion" items={Corex.Content.new([ [ id: "lorem", trigger: "Lorem ipsum dolor sit amet", content: "Consectetur adipiscing elit. Sed sodales ullamcorper tristique." ], [ trigger: "Duis dictum gravida odio ac pharetra?", content: "Nullam eget vestibulum ligula, at interdum tellus." ], [ id: "donec", trigger: "Donec condimentum ex mi", content: "Congue molestie ipsum gravida a. Sed ac eros luctus." ] ])} > <:indicator> <.icon name="hero-chevron-right" /> ``` You can use switching indicator using css classes `state-open` and `state-closed` on the indicator icon. This classes simply target the data-state attribute of the item indicator. ```heex <.accordion class="accordion" items={ Corex.Content.new([ [ id: "lorem", trigger: "Lorem ipsum dolor sit amet", content: "Consectetur adipiscing elit. Sed sodales ullamcorper tristique." ], [ trigger: "Duis dictum gravida odio ac pharetra?", content: "Nullam eget vestibulum ligula, at interdum tellus." ], [ id: "donec", trigger: "Donec condimentum ex mi", content: "Congue molestie ipsum gravida a. Sed ac eros luctus." ] ]) } > <:indicator> <.icon name="hero-plus" class="state-closed"/> <.icon name="hero-minus" class="state-open"/> ``` ### Custom Use `:trigger` and `:content` together to fully customize how each item is rendered. Add the `:indicator` slot to show an icon after each trigger. Use `:let={item}` on slots to access the item and its `data` (including `meta` for per-item customization). ```heex <.accordion class="accordion" items={ Corex.Content.new([ [ id: "lorem", trigger: "Lorem ipsum dolor sit amet", content: "Consectetur adipiscing elit. Sed sodales ullamcorper tristique.", meta: %{indicator: "hero-arrow-long-right", icon: "hero-chat-bubble-left-right"} ], [ trigger: "Duis dictum gravida ?", content: "Nullam eget vestibulum ligula, at interdum tellus.", meta: %{indicator: "hero-chevron-right", icon: "hero-device-phone-mobile"} ], [ id: "donec", trigger: "Donec condimentum ex mi", content: "Congue molestie ipsum gravida a. Sed ac eros luctus.", disabled: true, meta: %{indicator: "hero-chevron-double-right", icon: "hero-phone"} ] ]) } > <:trigger :let={item}> <.icon name={item.data.meta.icon} />{item.data.trigger} <:content :let={item}>{item.data.content} <:indicator :let={item}> <.icon name={item.data.meta.indicator} /> ``` ### Controlled Render an accordion controlled by the server. You must use the `on_value_change` event to update the value on the server and pass the value as a list of strings. The event will receive the value as a map with the key `value` and the id of the accordion. ```elixir defmodule MyAppWeb.AccordionLive do use MyAppWeb, :live_view def mount(_params, _session, socket) do {:ok, assign(socket, :value, ["lorem"])} end def handle_event("on_value_change", %{"value" => value}, socket) do {:noreply, assign(socket, :value, value)} end def render(assigns) do ~H""" <.accordion controlled value={@value} on_value_change="on_value_change" class="accordion" items={Corex.Content.new([ [id: "lorem", trigger: "Lorem ipsum dolor sit amet", content: "Consectetur adipiscing elit. Sed sodales ullamcorper tristique. Proin quis risus feugiat tellus iaculis fringilla."], [id: "duis", trigger: "Duis dictum gravida odio ac pharetra?", content: "Nullam eget vestibulum ligula, at interdum tellus. Quisque feugiat, dui ut fermentum sodales, lectus metus dignissim ex."] ])} /> """ end end ``` ### Async When the initial props are not available on mount, you can use the `Phoenix.LiveView.assign_async` function to assign the props asynchronously You can use the optional `Corex.Accordion.accordion_skeleton/1` to render a loading or error state ```elixir defmodule MyAppWeb.AccordionAsyncLive do use MyAppWeb, :live_view def mount(_params, _session, socket) do socket = socket |> assign_async(:accordion, fn -> Process.sleep(1000) items = Corex.Content.new([ [ id: "lorem", trigger: "Lorem ipsum dolor sit amet", content: "Consectetur adipiscing elit. Sed sodales ullamcorper tristique.", disabled: true ], [ id: "duis", trigger: "Duis dictum gravida odio ac pharetra?", content: "Nullam eget vestibulum ligula, at interdum tellus." ], [ id: "donec", trigger: "Donec condimentum ex mi", content: "Congue molestie ipsum gravida a. Sed ac eros luctus." ] ]) {:ok, %{ accordion: %{ items: items, value: ["duis", "donec"] } }} end) {:ok, socket} end def render(assigns) do ~H""" <.async_result :let={accordion} assign={@accordion}> <:loading> <.accordion_skeleton count={3} class="accordion" /> <:failed> there was an error loading the accordion <.accordion id="async-accordion" class="accordion" items={accordion.items} value={accordion.value} /> """ end end ``` ## API Control In order to use the API, you must use an id on the component ***Client-side*** ```heex ``` ***Server-side*** ```elixir def handle_event("open_item", _, socket) do {:noreply, Corex.Accordion.set_value(socket, "my-accordion", ["item-1"])} end ``` ## Styling Use data attributes to target elements: ```css [data-scope="accordion"][data-part="root"] {} [data-scope="accordion"][data-part="item"] {} [data-scope="accordion"][data-part="item-trigger"] {} [data-scope="accordion"][data-part="item-content"] {} [data-scope="accordion"][data-part="item-indicator"] {} ``` If you wish to use the default Corex styling, you can use the class `accordion` on the component. This requires to install `Mix.Tasks.Corex.Design` first and import the component css file. ```css @import "../corex/main.css"; @import "../corex/tokens/themes/neo/light.css"; @import "../corex/components/accordion.css"; ``` You can then use modifiers ```heex <.accordion class="accordion accordion--accent accordion--lg"> ``` Learn more about modifiers and [Corex Design](https://corex-ui.com/components/accordion#modifiers) ''' @doc type: :component use Phoenix.Component alias Corex.Accordion.Anatomy.{Props, Root, Item} alias Corex.Accordion.Connect import Corex.Helpers, only: [validate_value!: 1] @doc """ Renders an accordion component. Pass `items` as a list of `%Corex.Content.Item{}` structs. Use the optional `:indicator` slot to add content after each trigger. Use `:trigger` and `:content` together to fully customize item rendering. Each item MUST be a `%Corex.Content.Item{}` struct with: - `:id` (optional, auto-generated) - unique identifier for the item - `:trigger` (required) - content for the trigger button - `:content` (required) - content for the accordion panel - `:disabled` (optional, default: false) - whether the item is disabled - `:meta` (optional) - additional metadata for the item """ attr(:id, :string, required: false, doc: "The id of the accordion, useful for API to identify the accordion" ) attr(:items, :list, required: true, doc: "The items of the accordion, must be a list of %Corex.Content.Item{} structs" ) attr(:value, :list, default: [], doc: "The initial value or the controlled value of the accordion, must be a list of strings" ) attr(:controlled, :boolean, default: false, doc: "Whether the accordion is controlled. Only in LiveView, the on_value_change event is required" ) attr(:collapsible, :boolean, default: true, doc: "Whether the accordion is collapsible") attr(:multiple, :boolean, default: true, doc: "Whether the accordion allows multiple items to be selected" ) attr(:orientation, :string, default: "vertical", values: ["horizontal", "vertical"], doc: "The orientation of the accordion" ) attr(:dir, :string, default: nil, values: [nil, "ltr", "rtl"], doc: "The direction of the accordion. 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(:on_focus_change, :string, default: nil, doc: "The server event name when the focus change" ) attr(:on_focus_change_client, :string, default: nil, doc: "The client event name when the focus change" ) attr(:rest, :global) slot(:indicator, required: false, doc: "Optional slot for content after each trigger. Use :let={item} for per-item customization." ) slot(:trigger, required: false, doc: "Optional slot for custom trigger rendering. When provided with content, replaces default item rendering. Use :let={item} to access the item." ) slot(:content, required: false, doc: "Optional slot for custom content rendering. When provided with trigger, replaces default item rendering. Use :let={item} to access the item." ) def accordion(assigns) do assigns = assigns |> assign_new(:id, fn -> "accordion-#{System.unique_integer([:positive])}" end) |> validate_items() ~H"""