Packages
phoenix_duskmoon
9.6.3
9.9.2
9.9.1
9.9.0
9.8.0
9.7.1
9.7.0
9.6.4
9.6.3
9.6.2
9.6.1
9.6.0
9.5.4
9.5.3
9.5.2
9.5.1
9.5.0
9.4.3
9.4.2
9.4.1
9.4.0
9.3.0
9.2.0
9.1.4
9.1.3
9.1.1
9.1.1-rc.1
retired
9.0.1
9.0.0-rc.3
9.0.0-rc.2
9.0.0-rc.1
8.0.0
7.2.1
7.2.0
7.1.3
7.1.2
7.1.1
6.3.2
6.3.1
6.3.0
6.2.0
6.1.3
6.1.2
6.1.1
6.1.0
6.0.10
6.0.9
6.0.8
6.0.7
6.0.6
6.0.5
6.0.4
6.0.3
6.0.2
6.0.1
6.0.0
5.2.0-beta.5
5.2.0-beta.4
5.2.0-beta.3
5.2.0-beta.2
5.2.0-beta.1
5.1.0
5.0.6
5.0.5
5.0.4
5.0.3
5.0.2
5.0.1
5.0.0
4.6.6
4.6.5
4.6.4
4.6.3
4.6.2
4.6.1
4.6.0
4.5.2
4.5.1
4.5.0
4.4.4
4.4.3
4.4.2
4.4.1
4.4.0
4.3.2
4.3.1
4.3.0
4.2.0
4.1.1
4.1.0
4.0.0
Duskmoon UI component library for Phoenix LiveView
Current section
Files
Jump to
Current section
Files
lib/phoenix_duskmoon/component/action/dropdown.ex
defmodule PhoenixDuskmoon.Component.Action.Dropdown do
@moduledoc """
Dropdown menu component using the `popover` CSS from `@duskmoon-dev/core`.
Uses the native HTML Popover API with CSS anchor positioning for
click-to-toggle behavior without requiring JavaScript.
## Examples
<.dm_dropdown>
<:trigger>Menu</:trigger>
<:content>
<button type="button" class="popover-menu-item">Profile</button>
<button type="button" class="popover-menu-item">Settings</button>
<div class="popover-menu-divider"></div>
<button type="button" class="popover-menu-item">Logout</button>
</:content>
</.dm_dropdown>
<.dm_dropdown position="right" color="primary">
<:trigger>Actions</:trigger>
<:content>
<button type="button" class="popover-menu-item" phx-click="edit">Edit</button>
<button type="button" class="popover-menu-item" phx-click="delete">Delete</button>
</:content>
</.dm_dropdown>
"""
use Phoenix.Component
import PhoenixDuskmoon.Component.Helpers, only: [css_color: 1]
@doc """
Renders a dropdown menu using the native Popover API with
`@duskmoon-dev/core` popover CSS classes.
The trigger is rendered as a `<button>` with `popovertarget`,
providing native click-to-toggle and click-outside-to-dismiss
behavior without JavaScript.
## Examples
<.dm_dropdown>
<:trigger>Click me</:trigger>
<:content>
<button type="button" class="popover-menu-item">Item 1</button>
</:content>
</.dm_dropdown>
"""
@doc type: :component
attr(:id, :any, default: nil, doc: "HTML id attribute")
attr(:position, :string,
default: "bottom",
values: ["left", "right", "top", "bottom"],
doc: "Popover position relative to trigger"
)
attr(:color, :string,
default: nil,
doc: "Popover color variant (primary, secondary, tertiary)"
)
attr(:class, :any, default: nil, doc: "Additional CSS classes on the wrapper")
attr(:dropdown_class, :any, default: nil, doc: "Additional CSS classes on the popover panel")
attr(:rest, :global)
slot :trigger, required: true, doc: "Trigger content that toggles the dropdown" do
attr(:class, :any, doc: "trigger wrapper CSS classes")
end
slot :content, required: true, doc: "Dropdown panel content" do
attr(:class, :any, doc: "content panel CSS classes")
end
def dm_dropdown(assigns) do
assigns =
assigns
|> assign(:color, css_color(assigns.color))
|> assign_new(:rid, fn -> System.unique_integer([:positive]) end)
popover_id =
if assigns[:id] && assigns[:id] != false,
do: "#{assigns[:id]}-popover",
else: "dropdown-#{assigns.rid}"
anchor_name = "--anchor-#{popover_id}"
assigns =
assigns
|> assign(:popover_id, popover_id)
|> assign(:anchor_name, anchor_name)
~H"""
<div class={@class} {@rest}>
<button
:for={trigger <- @trigger}
id={"#{@popover_id}-trigger"}
type="button"
popovertarget={@popover_id}
style={"anchor-name: #{@anchor_name}"}
class={["appearance-none bg-transparent border-0 p-0 m-0 cursor-pointer inline-flex text-inherit", trigger[:class]]}
aria-haspopup="menu"
aria-expanded="false"
aria-controls={@popover_id}
>
{render_slot(trigger)}
</button>
<div
:for={content <- @content}
id={@popover_id}
popover
ontoggle="this.classList.toggle('popover-show', this.matches(':popover-open')); var b=this.previousElementSibling; if(b) b.setAttribute('aria-expanded', this.matches(':popover-open'))"
class={[
"popover popover-menu",
popover_position(@position),
popover_color(@color),
content[:class],
@dropdown_class
]}
style={"position-anchor: #{@anchor_name}"}
role="menu"
aria-labelledby={"#{@popover_id}-trigger"}
>
{render_slot(content)}
</div>
</div>
"""
end
defp popover_position("bottom"), do: "popover-bottom"
defp popover_position("top"), do: "popover-top"
defp popover_position("left"), do: "popover-left"
defp popover_position("right"), do: "popover-right"
defp popover_position(_), do: "popover-bottom"
defp popover_color("primary"), do: "popover-primary"
defp popover_color("secondary"), do: "popover-secondary"
defp popover_color("tertiary"), do: "popover-tertiary"
defp popover_color(_), do: nil
end