@@ -16,17 +16,70 @@ defmodule Prima.Dropdown do
1616
1717 attr :class , :string , default: ""
1818 attr :as , :any , default: nil
19+ attr :rest , :global
1920 slot :inner_block , required: true
2021
22+ @ doc """
23+ The trigger button/element for a dropdown menu.
24+
25+ This component renders the clickable element that opens and closes the dropdown
26+ menu. By default, it renders as a `button` element, but can be customized to
27+ render as any component using the `as` attribute.
28+
29+ ## Attributes
30+
31+ * `class` - CSS classes for styling the trigger
32+ * `as` - Custom function component to render instead of the default button element
33+
34+ ## Examples
35+
36+ # Basic trigger
37+ <.dropdown_trigger>
38+ Open Menu
39+ </.dropdown_trigger>
40+
41+ # With custom component
42+ <.dropdown_trigger as={&my_custom_button/1}>
43+ Open Menu
44+ </.dropdown_trigger>
45+
46+ ## Custom Component Requirements
47+
48+ When using the `as` attribute, the custom component receives accessibility
49+ attributes including `aria-haspopup="menu"` and `aria-expanded`.
50+
51+ **IMPORTANT**: Custom components must accept and pass through global attributes
52+ using the `:global` attribute type (commonly via `@rest`). This ensures that
53+ Prima's accessibility attributes are properly applied to the rendered element.
54+
55+ Example of a properly configured custom component:
56+
57+ attr :rest, :global
58+ slot :inner_block, required: true
59+
60+ def my_custom_button(assigns) do
61+ ~H\" \" \"
62+ <button type="button" {@rest}>
63+ {\r ender_slot(@inner_block)}
64+ </button>
65+ \" \" \"
66+ end
67+
68+ Without `{@rest}`, accessibility attributes will not be applied and the dropdown
69+ will not function correctly with keyboard navigation and screen readers.
70+ """
2171 def dropdown_trigger ( assigns ) do
2272 assigns =
2373 assign ( assigns , % {
2474 "aria-haspopup": "menu" ,
2575 "aria-expanded": "false"
2676 } )
2777
28- if assigns [ :as ] do
29- { as , assigns } = Map . pop ( assigns , :as )
78+ { as , assigns } = Map . pop ( assigns , :as )
79+ { rest , assigns } = Map . pop ( assigns , :rest , % { } )
80+ assigns = Map . merge ( assigns , rest )
81+
82+ if as do
3083 as . ( assigns )
3184 else
3285 dynamic_tag (
@@ -86,22 +139,96 @@ defmodule Prima.Dropdown do
86139
87140 attr :class , :string , default: ""
88141 attr :disabled , :boolean , default: false
142+ attr :as , :any , default: nil
143+
144+ # Workaround - unfortunately there seems to be no way to pass through arbitrary assigns without emitting compile warnings
145+ # Since dropdown items are often rendered as links, we add the <.link> attributes here as well.
146+ attr :rest , :global , include: ~w( navigate patch href)
89147 slot :inner_block , required: true
90148
149+ @ doc """
150+ A menu item component for use within a dropdown menu.
151+
152+ This component represents an individual item in a dropdown menu with proper
153+ ARIA attributes and keyboard navigation support. By default, it renders as a
154+ `div` element, but can be customized to render as any component using the `as`
155+ attribute.
156+
157+ ## Attributes
158+
159+ * `class` - CSS classes for styling the menu item
160+ * `disabled` - Boolean to mark the item as disabled (default: false)
161+ * `as` - Custom function component to render instead of the default div element
162+
163+ ## Examples
164+
165+ # Basic menu item
166+ <.dropdown_item>
167+ Save
168+ </.dropdown_item>
169+
170+ # Disabled menu item
171+ <.dropdown_item disabled={true}>
172+ Delete (unavailable)
173+ </.dropdown_item>
174+
175+ # With custom component (e.g., a link)
176+ <.dropdown_item as={&my_link_component/1}>
177+ View Profile
178+ </.dropdown_item>
179+
180+ # With Phoenix.Component.link
181+ <.dropdown_item as={&link/1} navigate={~p"/profile"}>
182+ View Profile
183+ </.dropdown_item>
184+
185+ ## Custom Component Requirements
186+
187+ When using the `as` attribute, the custom component receives all the standard
188+ attributes including `role="menuitem"`, `tabindex="-1"`, and accessibility
189+ attributes like `aria-disabled` when appropriate.
190+
191+ **IMPORTANT**: Custom components must accept and pass through global attributes
192+ using the `:global` attribute type (commonly via `@rest`). This ensures that
193+ Prima's accessibility attributes are properly applied to the rendered element.
194+
195+ Example of a properly configured custom component:
196+
197+ attr :rest, :global
198+ slot :inner_block, required: true
199+
200+ def my_custom_item(assigns) do
201+ ~H\" \" \"
202+ <a {@rest}>
203+ {\r ender_slot(@inner_block)}
204+ </a>
205+ \" \" \"
206+ end
207+
208+ Without `{@rest}`, accessibility attributes will not be applied and the component
209+ will not function correctly with keyboard navigation and screen readers.
210+ """
91211 def dropdown_item ( assigns ) do
92- assigns = assign ( assigns , :aria_disabled , if ( assigns . disabled , do: "true" , else: nil ) )
93- assigns = assign ( assigns , :data_disabled , if ( assigns . disabled , do: "true" , else: nil ) )
212+ { as , assigns } = Map . pop ( assigns , :as )
213+ { rest , assigns } = Map . pop ( assigns , :rest , % { } )
214+ assigns = Map . merge ( assigns , rest )
94215
95- ~H"""
96- < div
97- class = { @ class }
98- role = "menuitem "
99- tabindex = "-1 "
100- aria-disabled = { @ aria_disabled }
101- data-disabled = { @ data_disabled }
102- >
103- { render_slot ( @ inner_block ) }
104- </ div >
105- """
216+ assigns =
217+ assign ( assigns , % {
218+ role: "menuitem" ,
219+ tabindex: "-1" ,
220+ "aria-disabled": if ( assigns . disabled , do: "true" , else: nil ) ,
221+ "data-disabled": if ( assigns . disabled , do: "true" , else: nil )
222+ } )
223+
224+ if as do
225+ as . ( assigns )
226+ else
227+ dynamic_tag (
228+ Map . merge ( assigns , % {
229+ tag_name: "div"
230+ } )
231+ )
232+ end
106233 end
107234end
0 commit comments