Files
Rivertalk/lib/river_connect_web/components/ui/collapsible.ex
T
2026-02-12 17:10:03 -06:00

142 lines
3.6 KiB
Elixir

defmodule RiverConnectWeb.Components.UI.Collapsible do
@moduledoc """
Implementation of Collapsible component for SaladUI framework.
This component allows content to be shown or hidden with smooth animations,
accessibility support, and keyboard navigation.
## Examples:
<.collapsible id="collapsible-1" open>
<.collapsible_trigger>
<.button variant="outline">Show content</.button>
</.collapsible_trigger>
<.collapsible_content>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor
incididunt ut labore et dolore magna aliqua.
</p>
</.collapsible_content>
</.collapsible>
"""
use RiverConnectWeb.Components.UI, :component
@doc """
The main collapsible component.
## Options
* `:id` - Required unique identifier for the collapsible.
* `:open` - Whether the collapsible is initially open. Defaults to `false`.
* `:on-open` - Handler for collapsible open event.
* `:on-close` - Handler for collapsible close event.
* `:class` - Additional CSS classes.
"""
attr :id, :string, required: true, doc: "Unique identifier for the collapsible"
attr :open, :boolean, default: false, doc: "Whether the collapsible is initially open"
attr :"on-open", :any, default: nil, doc: "Handler for collapsible open event"
attr :"on-close", :any, default: nil, doc: "Handler for collapsible close event"
attr :class, :string, default: nil
attr :rest, :global
slot :inner_block, required: true
def collapsible(assigns) do
# Collect event mappings
event_map =
%{}
|> add_event_mapping(assigns, "opened", :"on-open")
|> add_event_mapping(assigns, "closed", :"on-close")
assigns =
assigns
|> assign(:event_map, Jason.encode!(event_map))
|> assign(:initial_state, if(assigns.open, do: "open", else: "closed"))
|> assign(
:options,
Jason.encode!(%{
open: assigns.open,
animations: get_animation_config()
})
)
~H"""
<div
id={@id}
class={classes(["relative", @class])}
data-component="collapsible"
data-state={@initial_state}
data-event-mappings={@event_map}
data-options={@options}
data-part="root"
phx-hook="SaladUI"
{@rest}
>
{render_slot(@inner_block)}
</div>
"""
end
@doc """
The trigger element that toggles the collapsible content.
"""
attr :class, :string, default: nil
attr :as, :any, default: "div"
attr :rest, :global
slot :inner_block, required: true
def collapsible_trigger(assigns) do
~H"""
<.dynamic
data-part="trigger"
data-action="toggle"
tag={@as}
class={classes(["cursor-pointer", @class])}
{@rest}
>
{render_slot(@inner_block)}
</.dynamic>
"""
end
@doc """
The collapsible content that appears when triggered.
"""
attr :class, :string, default: nil
attr :rest, :global
slot :inner_block, required: true
def collapsible_content(assigns) do
~H"""
<div
data-part="content"
hidden
class={
classes([
"transition-all duration-200 ease-in-out",
@class
])
}
{@rest}
>
{render_slot(@inner_block)}
</div>
"""
end
defp get_animation_config do
%{
"closed_to_open" => %{
animation: ["ease-out duration-200", "opacity-0", "opacity-100"],
duration: 200,
target_part: "content"
},
"open_to_closed" => %{
animation: ["ease-out duration-200", "opacity-100", "opacity-70"],
duration: 200,
target_part: "content"
}
}
end
end