Skip to main content

priv/registry/accordion.json

{
  "files": [
    {
      "content": "defmodule Shadix.Components.Accordion do\n  @moduledoc ~S\"\"\"\n  A vertically stacked set of collapsible sections, built purely on client-side\n  JS commands and CSS — no LiveView hook and no server round-trip.\n\n  Each trigger's `phx-click` flips a `data-state` of `\"open\"`/`\"closed\"` (and the\n  matching `aria-expanded`) on both itself and its paired content panel. The\n  panel animates open/closed in pure CSS using the `grid-template-rows`\n  `0fr → 1fr` technique: the content lives inside a grid whose single row\n  transitions between `0fr` (collapsed) and `1fr` (expanded), with the inner\n  wrapper clipping overflow. This animates the panel's height smoothly without\n  anyone having to measure it, and degrades to an instant toggle under\n  `prefers-reduced-motion`.\n\n  Because the collapsed panel stays in the DOM (so it can animate) rather than\n  being `display:none`, it carries `inert` while closed to keep it out of the\n  tab order and the accessibility tree; the same `phx-click` toggles `inert`.\n  The trigger's `data-state` also drives the chevron rotation via the\n  `[&[data-state=open]>svg]:rotate-180` utility lifted from shadcn.\n\n  Ids wire the pieces together: `accordion_item/1` takes a unique `:id`,\n  `accordion_trigger/1` and `accordion_content/1` take that same id and derive\n  `\"#{id}-trigger\"` / `\"#{id}-content\"` so `aria-controls` and the toggle target\n  line up stably.\n\n  NOTE: `type=\"single\"` auto-closing of sibling items is simplified — items\n  behave as independent toggles in this v1, so `type` is currently advisory and\n  rendered as a `data-type` hint on the root.\n  \"\"\"\n  use Phoenix.Component\n\n  alias Phoenix.LiveView.JS\n\n  import Shadix.Cn\n\n  @doc \"\"\"\n  Renders the accordion root, a vertical stack of `accordion_item/1`s.\n\n  `:type` is advisory in this v1 (see moduledoc): both `\"single\"` and\n  `\"multiple\"` render independent toggles and the value is exposed as\n  `data-type` for styling/JS hooks.\n  \"\"\"\n  attr(:type, :string, default: \"single\", values: ~w(single multiple))\n  attr(:class, :string, default: nil)\n  attr(:rest, :global)\n  slot(:inner_block, required: true)\n\n  def accordion(assigns) do\n    ~H\"\"\"\n    <div data-slot=\"accordion\" data-type={@type} class={cn([@class])} {@rest}>\n      {render_slot(@inner_block)}\n    </div>\n    \"\"\"\n  end\n\n  @doc \"\"\"\n  A single accordion item; wraps a trigger and its content.\n\n  `:id` must be unique per item and is reused by `accordion_trigger/1` and\n  `accordion_content/1` to wire `aria-controls` and the toggle target.\n  \"\"\"\n  attr(:id, :string, required: true)\n  attr(:class, :string, default: nil)\n  attr(:rest, :global)\n  slot(:inner_block, required: true)\n\n  def accordion_item(assigns) do\n    ~H\"\"\"\n    <div id={@id} data-slot=\"accordion-item\" class={cn([\"border-b last:border-b-0\", @class])} {@rest}>\n      {render_slot(@inner_block)}\n    </div>\n    \"\"\"\n  end\n\n  @doc ~S\"\"\"\n  The clickable header of an item. `:id` is the *item* id.\n\n  Renders a `<button id=\"#{id}-trigger\">` with `aria-controls=\"#{id}-content\"`,\n  `aria-expanded` and a `data-state` that toggle on click, plus a chevron SVG\n  that rotates when `data-state=\"open\"`. The same `phx-click` flips the paired\n  content panel's `data-state` (which drives the CSS open/close animation) and\n  its `inert` attribute.\n  \"\"\"\n  attr(:id, :string, required: true)\n  attr(:class, :string, default: nil)\n  attr(:rest, :global)\n  slot(:inner_block, required: true)\n\n  def accordion_trigger(assigns) do\n    ~H\"\"\"\n    <h3 class=\"flex\" data-slot=\"accordion-header\">\n      <button\n        type=\"button\"\n        id={\"#{@id}-trigger\"}\n        data-slot=\"accordion-trigger\"\n        data-state=\"closed\"\n        aria-expanded=\"false\"\n        aria-controls={\"#{@id}-content\"}\n        phx-click={\n          JS.toggle_attribute({\"aria-expanded\", \"true\", \"false\"}, to: \"##{@id}-trigger\")\n          |> JS.toggle_attribute({\"data-state\", \"open\", \"closed\"}, to: \"##{@id}-trigger\")\n          |> JS.toggle_attribute({\"data-state\", \"open\", \"closed\"}, to: \"##{@id}-content\")\n          |> JS.toggle_attribute({\"inert\", \"true\"}, to: \"##{@id}-content\")\n        }\n        class={\n          cn([\n            \"flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180\",\n            @class\n          ])\n        }\n        {@rest}\n      >\n        {render_slot(@inner_block)}\n        <svg\n          xmlns=\"http://www.w3.org/2000/svg\"\n          width=\"24\"\n          height=\"24\"\n          viewBox=\"0 0 24 24\"\n          fill=\"none\"\n          stroke=\"currentColor\"\n          stroke-width=\"2\"\n          stroke-linecap=\"round\"\n          stroke-linejoin=\"round\"\n          aria-hidden=\"true\"\n          class=\"pointer-events-none size-4 shrink-0 translate-y-0.5 text-muted-foreground transition-transform duration-200 motion-reduce:transition-none\"\n        >\n          <path d=\"m6 9 6 6 6-6\" />\n        </svg>\n      </button>\n    </h3>\n    \"\"\"\n  end\n\n  @doc ~S\"\"\"\n  The collapsible body of an item. `:id` is the *item* id.\n\n  Renders `<div id=\"#{id}-content\" role=\"region\">`, collapsed (`data-state=\"closed\"`\n  and `inert`) by default. The panel animates open/closed in pure CSS via the\n  `grid-template-rows` `0fr → 1fr` trick keyed off `data-state`; the inner\n  `overflow-hidden` wrapper clips the content as the row collapses. Under\n  `prefers-reduced-motion` the transition is dropped and it toggles instantly.\n  \"\"\"\n  attr(:id, :string, required: true)\n  attr(:class, :string, default: nil)\n  attr(:rest, :global)\n  slot(:inner_block, required: true)\n\n  def accordion_content(assigns) do\n    ~H\"\"\"\n    <div\n      id={\"#{@id}-content\"}\n      data-slot=\"accordion-content\"\n      data-state=\"closed\"\n      role=\"region\"\n      aria-labelledby={\"#{@id}-trigger\"}\n      inert\n      class=\"grid grid-rows-[0fr] text-sm transition-[grid-template-rows] duration-200 ease-[cubic-bezier(0.22,1,0.36,1)] data-[state=open]:grid-rows-[1fr] motion-reduce:transition-none\"\n    >\n      <div class=\"overflow-hidden\">\n        <div class={cn([\"pt-0 pb-4\", @class])}>\n          {render_slot(@inner_block)}\n        </div>\n      </div>\n    </div>\n    \"\"\"\n  end\nend\n",
      "path": "accordion.ex"
    }
  ],
  "hooks": [],
  "name": "accordion",
  "npm_deps": [],
  "registry_deps": [
    "cn"
  ]
}