|
| 1 | +defmodule AtomicWeb.Components.Accordion do |
| 2 | + @moduledoc """ |
| 3 | + Provides accordion-related components and helper functions. |
| 4 | + """ |
| 5 | + use AtomicWeb, :component |
| 6 | + |
| 7 | + alias Phoenix.LiveView.JS |
| 8 | + import AtomicWeb.Components.Icon |
| 9 | + |
| 10 | + @doc """ |
| 11 | + Accordion components allows users to show and hide sections of related panel on a page. |
| 12 | +
|
| 13 | + ## Examples |
| 14 | +
|
| 15 | + ```heex |
| 16 | + <.accordion> |
| 17 | + <:trigger>Accordion</:trigger> |
| 18 | + <:panel>Content</:panel> |
| 19 | + </.accordion> |
| 20 | + ``` |
| 21 | + """ |
| 22 | + |
| 23 | + attr :class, :any, doc: "Extend existing component styles" |
| 24 | + attr :controlled, :boolean, default: false |
| 25 | + attr :id, :string, required: true |
| 26 | + attr :rest, :global |
| 27 | + |
| 28 | + slot :trigger, validate_attrs: false |
| 29 | + slot :panel, validate_attrs: false |
| 30 | + |
| 31 | + @spec accordion(Socket.assigns()) :: Rendered.t() |
| 32 | + def accordion(assigns) do |
| 33 | + ~H""" |
| 34 | + <div class={["accordion", assigns[:class]]} id={@id} {@rest}> |
| 35 | + <%= for {{trigger, panel}, idx} <- @trigger |> Enum.zip(@panel) |> Enum.with_index() do %> |
| 36 | + <h3> |
| 37 | + <button |
| 38 | + aria-controls={panel_id(@id, idx)} |
| 39 | + aria-expanded={to_string(panel[:default_expanded] == true)} |
| 40 | + class={[ |
| 41 | + "accordion-trigger relative w-full [&_.accordion-trigger-icon]:aria-expanded:rotate-180", |
| 42 | + trigger[:class] |
| 43 | + ]} |
| 44 | + id={trigger_id(@id, idx)} |
| 45 | + phx-click={handle_click(assigns, idx)} |
| 46 | + type="button" |
| 47 | + {assigns_to_attributes(trigger, [:class, :icon_name])} |
| 48 | + > |
| 49 | + {render_slot(trigger)} |
| 50 | + <.icon class="accordion-trigger-icon absolute top-1/2 right-4 h-5 w-5 -translate-y-1/2 transition-all duration-300 ease-in-out" name={trigger[:icon_name] || "hero-chevron-down"} /> |
| 51 | + </button> |
| 52 | + </h3> |
| 53 | + <div class="accordion-panel grid-rows-[0fr] grid transform transition-all duration-200 ease-in data-[expanded]:grid-rows-[1fr]" data-expanded={panel[:default_expanded]} id={panel_id(@id, idx)} role="region"> |
| 54 | + <div class="overflow-hidden"> |
| 55 | + <div class={["accordion-panel-content", panel[:class]]} {assigns_to_attributes(panel, [:class, :default_expanded ])}> |
| 56 | + {render_slot(panel)} |
| 57 | + </div> |
| 58 | + </div> |
| 59 | + </div> |
| 60 | + <% end %> |
| 61 | + </div> |
| 62 | + """ |
| 63 | + end |
| 64 | + |
| 65 | + defp trigger_id(id, idx), do: "#{id}_trigger#{idx}" |
| 66 | + defp panel_id(id, idx), do: "#{id}_panel#{idx}" |
| 67 | + |
| 68 | + defp handle_click(%{controlled: controlled, id: id}, idx) do |
| 69 | + op = |
| 70 | + {"aria-expanded", "true", "false"} |
| 71 | + |> JS.toggle_attribute(to: "##{trigger_id(id, idx)}") |
| 72 | + |> JS.toggle_attribute({"data-expanded", ""}, to: "##{panel_id(id, idx)}") |
| 73 | + |
| 74 | + if controlled do |
| 75 | + op |
| 76 | + |> JS.set_attribute({"aria-expanded", "false"}, |
| 77 | + to: "##{id} .accordion-trigger:not(##{trigger_id(id, idx)})" |
| 78 | + ) |
| 79 | + |> JS.remove_attribute("data-expanded", |
| 80 | + to: "##{id} .accordion-panel:not(##{panel_id(id, idx)})" |
| 81 | + ) |
| 82 | + else |
| 83 | + op |
| 84 | + end |
| 85 | + end |
| 86 | +end |
0 commit comments