@proyecto-viviana/solidaria-components 0.2.5 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (225) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +39 -272
  3. package/dist/ActionBar.d.ts +79 -0
  4. package/dist/ActionBar.d.ts.map +1 -0
  5. package/dist/ActionGroup.d.ts +74 -0
  6. package/dist/ActionGroup.d.ts.map +1 -0
  7. package/dist/Alert.d.ts +70 -0
  8. package/dist/Alert.d.ts.map +1 -0
  9. package/dist/Autocomplete.d.ts +5 -5
  10. package/dist/Autocomplete.d.ts.map +1 -1
  11. package/dist/Breadcrumbs.d.ts +27 -8
  12. package/dist/Breadcrumbs.d.ts.map +1 -1
  13. package/dist/Button.d.ts +28 -5
  14. package/dist/Button.d.ts.map +1 -1
  15. package/dist/Calendar.d.ts +51 -7
  16. package/dist/Calendar.d.ts.map +1 -1
  17. package/dist/Checkbox.d.ts +33 -8
  18. package/dist/Checkbox.d.ts.map +1 -1
  19. package/dist/Collection.d.ts +130 -0
  20. package/dist/Collection.d.ts.map +1 -0
  21. package/dist/Color.d.ts +210 -9
  22. package/dist/Color.d.ts.map +1 -1
  23. package/dist/ColorEditor.d.ts +42 -0
  24. package/dist/ColorEditor.d.ts.map +1 -0
  25. package/dist/ComboBox.d.ts +146 -16
  26. package/dist/ComboBox.d.ts.map +1 -1
  27. package/dist/ContextualHelpTrigger.d.ts +40 -0
  28. package/dist/ContextualHelpTrigger.d.ts.map +1 -0
  29. package/dist/DateField.d.ts +35 -8
  30. package/dist/DateField.d.ts.map +1 -1
  31. package/dist/DatePicker.d.ts +101 -5
  32. package/dist/DatePicker.d.ts.map +1 -1
  33. package/dist/DateRangePickerContext.d.ts +30 -0
  34. package/dist/DateRangePickerContext.d.ts.map +1 -0
  35. package/dist/Dialog.d.ts +5 -5
  36. package/dist/Dialog.d.ts.map +1 -1
  37. package/dist/Disclosure.d.ts +25 -5
  38. package/dist/Disclosure.d.ts.map +1 -1
  39. package/dist/DragAndDrop.d.ts +80 -0
  40. package/dist/DragAndDrop.d.ts.map +1 -0
  41. package/dist/DragPreview.d.ts +14 -0
  42. package/dist/DragPreview.d.ts.map +1 -0
  43. package/dist/DropZone.d.ts +27 -0
  44. package/dist/DropZone.d.ts.map +1 -0
  45. package/dist/FieldError.d.ts +27 -0
  46. package/dist/FieldError.d.ts.map +1 -0
  47. package/dist/FileTrigger.d.ts +26 -0
  48. package/dist/FileTrigger.d.ts.map +1 -0
  49. package/dist/Focusable.d.ts +27 -0
  50. package/dist/Focusable.d.ts.map +1 -0
  51. package/dist/Form.d.ts +41 -0
  52. package/dist/Form.d.ts.map +1 -0
  53. package/dist/GridList.d.ts +69 -10
  54. package/dist/GridList.d.ts.map +1 -1
  55. package/dist/HiddenDateInput.d.ts +26 -0
  56. package/dist/HiddenDateInput.d.ts.map +1 -0
  57. package/dist/HiddenTimeInput.d.ts +25 -0
  58. package/dist/HiddenTimeInput.d.ts.map +1 -0
  59. package/dist/Icon.d.ts +57 -0
  60. package/dist/Icon.d.ts.map +1 -0
  61. package/dist/Keyboard.d.ts +13 -0
  62. package/dist/Keyboard.d.ts.map +1 -0
  63. package/dist/Landmark.d.ts +3 -3
  64. package/dist/Landmark.d.ts.map +1 -1
  65. package/dist/Link.d.ts +10 -4
  66. package/dist/Link.d.ts.map +1 -1
  67. package/dist/ListBox.d.ts +73 -11
  68. package/dist/ListBox.d.ts.map +1 -1
  69. package/dist/ListDropTargetDelegate.d.ts +38 -0
  70. package/dist/ListDropTargetDelegate.d.ts.map +1 -0
  71. package/dist/Menu.d.ts +79 -10
  72. package/dist/Menu.d.ts.map +1 -1
  73. package/dist/Meter.d.ts +4 -4
  74. package/dist/Meter.d.ts.map +1 -1
  75. package/dist/Modal.d.ts +6 -4
  76. package/dist/Modal.d.ts.map +1 -1
  77. package/dist/NumberField.d.ts +10 -12
  78. package/dist/NumberField.d.ts.map +1 -1
  79. package/dist/Popover.d.ts +32 -7
  80. package/dist/Popover.d.ts.map +1 -1
  81. package/dist/Pressable.d.ts +27 -0
  82. package/dist/Pressable.d.ts.map +1 -0
  83. package/dist/ProgressBar.d.ts +6 -4
  84. package/dist/ProgressBar.d.ts.map +1 -1
  85. package/dist/RadioGroup.d.ts +43 -9
  86. package/dist/RadioGroup.d.ts.map +1 -1
  87. package/dist/RangeCalendar.d.ts +39 -7
  88. package/dist/RangeCalendar.d.ts.map +1 -1
  89. package/dist/RouterProvider.d.ts +75 -0
  90. package/dist/RouterProvider.d.ts.map +1 -0
  91. package/dist/SearchField.d.ts +23 -21
  92. package/dist/SearchField.d.ts.map +1 -1
  93. package/dist/Select.d.ts +48 -7
  94. package/dist/Select.d.ts.map +1 -1
  95. package/dist/SelectionIndicator.d.ts +30 -0
  96. package/dist/SelectionIndicator.d.ts.map +1 -0
  97. package/dist/Separator.d.ts +9 -3
  98. package/dist/Separator.d.ts.map +1 -1
  99. package/dist/SharedElementTransition.d.ts +41 -0
  100. package/dist/SharedElementTransition.d.ts.map +1 -0
  101. package/dist/Slider.d.ts +15 -8
  102. package/dist/Slider.d.ts.map +1 -1
  103. package/dist/StepList.d.ts +90 -0
  104. package/dist/StepList.d.ts.map +1 -0
  105. package/dist/Switch.d.ts +11 -5
  106. package/dist/Switch.d.ts.map +1 -1
  107. package/dist/Table.d.ts +222 -19
  108. package/dist/Table.d.ts.map +1 -1
  109. package/dist/Tabs.d.ts +47 -10
  110. package/dist/Tabs.d.ts.map +1 -1
  111. package/dist/TagGroup.d.ts +22 -10
  112. package/dist/TagGroup.d.ts.map +1 -1
  113. package/dist/Text.d.ts +10 -0
  114. package/dist/Text.d.ts.map +1 -0
  115. package/dist/TextField.d.ts +19 -11
  116. package/dist/TextField.d.ts.map +1 -1
  117. package/dist/TimeField.d.ts +32 -7
  118. package/dist/TimeField.d.ts.map +1 -1
  119. package/dist/Toast.d.ts +29 -14
  120. package/dist/Toast.d.ts.map +1 -1
  121. package/dist/ToggleButton.d.ts +36 -0
  122. package/dist/ToggleButton.d.ts.map +1 -0
  123. package/dist/ToggleButtonGroup.d.ts +33 -0
  124. package/dist/ToggleButtonGroup.d.ts.map +1 -0
  125. package/dist/Toolbar.d.ts +7 -3
  126. package/dist/Toolbar.d.ts.map +1 -1
  127. package/dist/Tooltip.d.ts +58 -7
  128. package/dist/Tooltip.d.ts.map +1 -1
  129. package/dist/Tree.d.ts +102 -11
  130. package/dist/Tree.d.ts.map +1 -1
  131. package/dist/Virtualizer.d.ts +61 -0
  132. package/dist/Virtualizer.d.ts.map +1 -0
  133. package/dist/VirtualizerLayouts.d.ts +82 -0
  134. package/dist/VirtualizerLayouts.d.ts.map +1 -0
  135. package/dist/VisuallyHidden.d.ts +4 -2
  136. package/dist/VisuallyHidden.d.ts.map +1 -1
  137. package/dist/contexts.d.ts +6 -1
  138. package/dist/contexts.d.ts.map +1 -1
  139. package/dist/index.d.ts +73 -39
  140. package/dist/index.d.ts.map +1 -1
  141. package/dist/index.js +23342 -10644
  142. package/dist/index.js.map +1 -7
  143. package/dist/index.jsx +18110 -0
  144. package/dist/index.jsx.map +1 -0
  145. package/dist/useDragAndDrop.d.ts +93 -0
  146. package/dist/useDragAndDrop.d.ts.map +1 -0
  147. package/dist/utils.d.ts +8 -2
  148. package/dist/utils.d.ts.map +1 -1
  149. package/dist/virtualizer/Layout.d.ts +79 -0
  150. package/dist/virtualizer/Layout.d.ts.map +1 -0
  151. package/package.json +33 -32
  152. package/src/ActionBar.tsx +251 -0
  153. package/src/ActionGroup.tsx +277 -0
  154. package/src/Alert.tsx +152 -0
  155. package/src/Autocomplete.tsx +39 -44
  156. package/src/Breadcrumbs.tsx +227 -72
  157. package/src/Button.tsx +315 -74
  158. package/src/Calendar.tsx +347 -141
  159. package/src/Checkbox.tsx +414 -123
  160. package/src/Collection.tsx +350 -0
  161. package/src/Color.tsx +1325 -284
  162. package/src/ColorEditor.tsx +213 -0
  163. package/src/ComboBox.tsx +644 -245
  164. package/src/ContextualHelpTrigger.tsx +195 -0
  165. package/src/DateField.tsx +274 -106
  166. package/src/DatePicker.tsx +892 -111
  167. package/src/DateRangePickerContext.tsx +44 -0
  168. package/src/Dialog.tsx +173 -104
  169. package/src/Disclosure.tsx +158 -105
  170. package/src/DragAndDrop.tsx +340 -0
  171. package/src/DragPreview.tsx +47 -0
  172. package/src/DropZone.tsx +233 -0
  173. package/src/FieldError.tsx +89 -0
  174. package/src/FileTrigger.tsx +83 -0
  175. package/src/Focusable.tsx +103 -0
  176. package/src/Form.tsx +140 -0
  177. package/src/GridList.tsx +542 -128
  178. package/src/HiddenDateInput.tsx +153 -0
  179. package/src/HiddenTimeInput.tsx +133 -0
  180. package/src/Icon.tsx +133 -0
  181. package/src/Keyboard.tsx +26 -0
  182. package/src/Landmark.tsx +37 -63
  183. package/src/Link.tsx +132 -69
  184. package/src/ListBox.tsx +656 -106
  185. package/src/ListDropTargetDelegate.ts +283 -0
  186. package/src/Menu.tsx +1234 -132
  187. package/src/Meter.tsx +44 -58
  188. package/src/Modal.tsx +262 -166
  189. package/src/NumberField.tsx +267 -151
  190. package/src/Popover.tsx +452 -343
  191. package/src/Pressable.tsx +108 -0
  192. package/src/ProgressBar.tsx +54 -59
  193. package/src/RadioGroup.tsx +533 -121
  194. package/src/RangeCalendar.tsx +249 -150
  195. package/src/RouterProvider.tsx +223 -0
  196. package/src/SearchField.tsx +460 -133
  197. package/src/Select.tsx +804 -233
  198. package/src/SelectionIndicator.tsx +108 -0
  199. package/src/Separator.tsx +47 -49
  200. package/src/SharedElementTransition.tsx +264 -0
  201. package/src/Slider.tsx +148 -98
  202. package/src/StepList.tsx +272 -0
  203. package/src/Switch.tsx +93 -46
  204. package/src/Table.tsx +1551 -225
  205. package/src/Tabs.tsx +377 -123
  206. package/src/TagGroup.tsx +233 -135
  207. package/src/Text.tsx +18 -0
  208. package/src/TextField.tsx +413 -86
  209. package/src/TimeField.tsx +232 -222
  210. package/src/Toast.tsx +306 -160
  211. package/src/ToggleButton.tsx +169 -0
  212. package/src/ToggleButtonGroup.tsx +141 -0
  213. package/src/Toolbar.tsx +61 -70
  214. package/src/Tooltip.tsx +473 -116
  215. package/src/Tree.tsx +1514 -175
  216. package/src/Virtualizer.tsx +730 -0
  217. package/src/VirtualizerLayouts.ts +280 -0
  218. package/src/VisuallyHidden.tsx +32 -38
  219. package/src/contexts.ts +29 -36
  220. package/src/index.ts +972 -620
  221. package/src/useDragAndDrop.ts +367 -0
  222. package/src/utils.tsx +69 -50
  223. package/src/virtualizer/Layout.ts +192 -0
  224. package/dist/index.ssr.js +0 -9785
  225. package/dist/index.ssr.js.map +0 -7
package/src/Menu.tsx CHANGED
@@ -8,12 +8,16 @@
8
8
  import {
9
9
  type JSX,
10
10
  createContext,
11
+ createEffect,
11
12
  createMemo,
13
+ createSignal,
14
+ createUniqueId,
15
+ onCleanup,
12
16
  splitProps,
13
17
  useContext,
14
18
  For,
15
19
  Show,
16
- } from 'solid-js';
20
+ } from "solid-js";
17
21
  import {
18
22
  createMenu,
19
23
  createMenuItem,
@@ -22,42 +26,80 @@ import {
22
26
  createHover,
23
27
  createButton,
24
28
  createInteractOutside,
29
+ mergeProps,
25
30
  FocusScope,
26
31
  type AriaMenuProps,
27
32
  type AriaMenuItemProps,
28
33
  type AriaMenuTriggerProps,
29
- } from '@proyecto-viviana/solidaria';
34
+ } from "@proyecto-viviana/solidaria";
30
35
  import {
36
+ createSelectionState,
31
37
  createMenuState,
32
38
  createMenuTriggerState,
33
39
  type MenuState,
40
+ type MenuStateProps,
34
41
  type OverlayTriggerState,
35
42
  type Key,
36
- } from '@proyecto-viviana/solid-stately';
43
+ type DropTarget,
44
+ type SelectionMode,
45
+ type SelectionStateProps,
46
+ } from "@proyecto-viviana/solid-stately";
37
47
  import {
38
48
  type RenderChildren,
39
49
  type ClassNameOrFunction,
40
50
  type StyleOrFunction,
41
51
  type SlotProps,
42
52
  useRenderProps,
43
- } from './utils';
44
-
45
- // ============================================
46
- // TYPES
47
- // ============================================
53
+ filterDOMProps,
54
+ } from "./utils";
55
+ import { SharedElementTransition } from "./SharedElementTransition";
56
+ import { type DragAndDropHooks } from "./useDragAndDrop";
57
+ import {
58
+ CollectionRendererContext,
59
+ Section,
60
+ Header,
61
+ Group,
62
+ type CollectionEntry,
63
+ type CollectionRendererContextValue,
64
+ type SectionProps,
65
+ useCollectionRenderer,
66
+ flattenCollectionEntries,
67
+ isCollectionSection,
68
+ } from "./Collection";
69
+ import { useVirtualizerContext } from "./Virtualizer";
70
+ import {
71
+ getNormalizedDropTargetKey,
72
+ mergePersistedKeysIntoVirtualRange,
73
+ useDndPersistedKeys,
74
+ useRenderDropIndicator,
75
+ } from "./DragAndDrop";
76
+ import { PopoverTriggerContext } from "./contexts";
48
77
 
49
78
  export interface MenuRenderProps {
50
79
  /** Whether the menu is focused. */
51
80
  isFocused: boolean;
52
81
  /** Whether the menu is open. */
53
82
  isOpen: boolean;
83
+ /** Whether the menu has no items. */
84
+ isEmpty: boolean;
54
85
  }
55
86
 
56
87
  export interface MenuProps<T>
57
- extends Omit<AriaMenuProps, 'children'>,
58
- SlotProps {
88
+ extends
89
+ Omit<AriaMenuProps, "children">,
90
+ SlotProps,
91
+ Pick<
92
+ MenuStateProps<T>,
93
+ | "selectionMode"
94
+ | "selectionBehavior"
95
+ | "disallowEmptySelection"
96
+ | "selectedKeys"
97
+ | "defaultSelectedKeys"
98
+ | "onSelectionChange"
99
+ | "allowDuplicateSelectionEvents"
100
+ > {
59
101
  /** The items to render in the menu. */
60
- items: T[];
102
+ items?: CollectionEntry<T>[];
61
103
  /** Function to get the key from an item. */
62
104
  getKey?: (item: T) => Key;
63
105
  /** Function to get the text value from an item. */
@@ -71,16 +113,33 @@ export interface MenuProps<T>
71
113
  /** Handler called when the menu should close. */
72
114
  onClose?: () => void;
73
115
  /** The children of the component. A function may be provided to render each item. */
74
- children: (item: T) => JSX.Element;
116
+ children?: JSX.Element | ((item: T) => JSX.Element);
117
+ /** Internal lazy static children accessor used when collection children need menu context. */
118
+ staticChildren?: () => JSX.Element | undefined;
75
119
  /** The CSS className for the element. */
76
120
  class?: ClassNameOrFunction<MenuRenderProps>;
77
121
  /** The inline style for the element. */
78
122
  style?: StyleOrFunction<MenuRenderProps>;
123
+ /** Content to display when the menu has no items. */
124
+ renderEmptyState?: () => JSX.Element;
125
+ /** Whether the menu should close when an item is selected. */
126
+ shouldCloseOnSelect?: boolean;
127
+ /** Ref for the menu element. */
128
+ ref?: RefLike<HTMLUListElement>;
129
+ /** Custom renderer for the menu element. */
130
+ render?: (
131
+ props: JSX.HTMLAttributes<HTMLUListElement>,
132
+ renderProps: MenuRenderProps,
133
+ ) => JSX.Element;
134
+ /** Drag and drop hooks from `useDragAndDrop`. */
135
+ dragAndDropHooks?: DragAndDropHooks<T>;
79
136
  }
80
137
 
81
138
  export interface MenuItemRenderProps {
82
139
  /** Whether the item is selected. */
83
140
  isSelected: boolean;
141
+ /** The parent menu selection mode. */
142
+ selectionMode: SelectionMode;
84
143
  /** Whether the item is focused. */
85
144
  isFocused: boolean;
86
145
  /** Whether the item has keyboard focus. */
@@ -91,11 +150,13 @@ export interface MenuItemRenderProps {
91
150
  isHovered: boolean;
92
151
  /** Whether the item is disabled. */
93
152
  isDisabled: boolean;
153
+ /** Whether the item opens a submenu. */
154
+ hasSubmenu: boolean;
155
+ /** Whether the submenu is currently open. */
156
+ isOpen: boolean;
94
157
  }
95
158
 
96
- export interface MenuItemProps<T>
97
- extends Omit<AriaMenuItemProps, 'children' | 'key'>,
98
- SlotProps {
159
+ export interface MenuItemProps<T> extends Omit<AriaMenuItemProps, "children" | "key">, SlotProps {
99
160
  /** The unique key for the item. */
100
161
  id: Key;
101
162
  /** The item value. */
@@ -110,6 +171,27 @@ export interface MenuItemProps<T>
110
171
  textValue?: string;
111
172
  /** Handler called when the item is activated. */
112
173
  onAction?: () => void;
174
+ /** A URL to link to. Turns the menu item into a link. */
175
+ href?: string;
176
+ /** The target window for the link. */
177
+ target?: string;
178
+ /** The relationship between the linked resource and the current page. */
179
+ rel?: string;
180
+ /** Causes the browser to download the linked URL. */
181
+ download?: boolean | string;
182
+ /** Handler called when hover starts. */
183
+ onHoverStart?: () => void;
184
+ /** Handler called when hover ends. */
185
+ onHoverEnd?: () => void;
186
+ /** Handler called when hover state changes. */
187
+ onHoverChange?: (isHovered: boolean) => void;
188
+ /** Ref for the menu item element. */
189
+ ref?: RefLike<HTMLLIElement>;
190
+ /** Custom renderer for the menu item element. */
191
+ render?: (
192
+ props: JSX.HTMLAttributes<HTMLLIElement>,
193
+ renderProps: MenuItemRenderProps,
194
+ ) => JSX.Element;
113
195
  }
114
196
 
115
197
  export interface MenuTriggerRenderProps {
@@ -127,7 +209,7 @@ export interface MenuTriggerRenderProps {
127
209
  isDisabled: boolean;
128
210
  }
129
211
 
130
- export interface MenuTriggerProps extends Omit<AriaMenuTriggerProps, 'children'>, SlotProps {
212
+ export interface MenuTriggerProps extends Omit<AriaMenuTriggerProps, "children">, SlotProps {
131
213
  /** The children of the trigger (typically a Button and Menu). */
132
214
  children: JSX.Element;
133
215
  /** Whether the menu trigger is disabled. */
@@ -140,35 +222,95 @@ export interface MenuTriggerProps extends Omit<AriaMenuTriggerProps, 'children'>
140
222
  onOpenChange?: (isOpen: boolean) => void;
141
223
  }
142
224
 
143
- // ============================================
144
- // CONTEXT
145
- // ============================================
225
+ export interface SubmenuTriggerProps extends SlotProps {
226
+ /** The trigger item followed by submenu content. */
227
+ children: JSX.Element;
228
+ /** Delay before opening the submenu on hover. */
229
+ delay?: number;
230
+ /** Whether the submenu is open (controlled). */
231
+ isOpen?: boolean;
232
+ /** Whether the submenu is open by default. */
233
+ defaultOpen?: boolean;
234
+ /** Handler called when the submenu open state changes. */
235
+ onOpenChange?: (isOpen: boolean) => void;
236
+ }
146
237
 
147
238
  interface MenuContextValue<T> {
148
239
  state: MenuState<T>;
240
+ isDisabled: () => boolean;
241
+ dragAndDropHooks?: DragAndDropHooks<T>;
242
+ dragState?: unknown;
243
+ dropState?: unknown;
149
244
  }
150
245
 
246
+ type MenuSelectionEvent = { shiftKey?: boolean; ctrlKey?: boolean; metaKey?: boolean };
247
+
151
248
  interface MenuTriggerContextValue {
152
249
  state: OverlayTriggerState;
153
250
  triggerProps: JSX.HTMLAttributes<HTMLElement>;
154
251
  menuProps: JSX.HTMLAttributes<HTMLElement>;
155
252
  }
156
253
 
254
+ interface MenuItemContextValue {
255
+ props?: () => JSX.HTMLAttributes<HTMLElement>;
256
+ closeOnSelect?: boolean;
257
+ onAction?: () => void;
258
+ setItemRef?: (el: HTMLLIElement | null) => void;
259
+ }
260
+
261
+ interface StaticMenuCollectionItem {
262
+ id: Key;
263
+ textValue?: string;
264
+ isDisabled?: boolean;
265
+ }
266
+
267
+ interface StaticMenuCollectionContextValue {
268
+ registerItem(item: StaticMenuCollectionItem): void;
269
+ unregisterItem(id: Key): void;
270
+ }
271
+
272
+ interface MenuSectionSelectionContextValue {
273
+ selectionMode: () => SelectionMode;
274
+ isSelected(key: Key): boolean;
275
+ isDisabled(key: Key): boolean;
276
+ select(key: Key, event?: MenuSelectionEvent): void;
277
+ shouldCloseOnSelect(): boolean | undefined;
278
+ }
279
+
280
+ interface MenuSectionSelectionRegistryContextValue {
281
+ registerItem(key: Key, selection: MenuSectionSelectionContextValue): void;
282
+ unregisterItem(key: Key, selection: MenuSectionSelectionContextValue): void;
283
+ selectItem(key: Key, event?: MenuSelectionEvent): boolean;
284
+ }
285
+
157
286
  export const MenuContext = createContext<MenuContextValue<unknown> | null>(null);
158
287
  export const MenuStateContext = createContext<MenuState<unknown> | null>(null);
159
288
  export const MenuTriggerContext = createContext<MenuTriggerContextValue | null>(null);
289
+ export const RootMenuTriggerStateContext = createContext<OverlayTriggerState | null>(null);
290
+ const MenuItemContext = createContext<MenuItemContextValue | null>(null);
291
+ const StaticMenuCollectionContext = createContext<StaticMenuCollectionContextValue | null>(null);
292
+ const MenuSectionSelectionContext = createContext<MenuSectionSelectionContextValue | null>(null);
293
+ const MenuSectionSelectionRegistryContext =
294
+ createContext<MenuSectionSelectionRegistryContextValue | null>(null);
295
+
296
+ type RefLike<T> = ((el: T) => void) | { current?: T | null } | undefined;
297
+
298
+ function assignRef<T>(ref: RefLike<T>, el: T): void {
299
+ if (!ref) return;
300
+ if (typeof ref === "function") ref(el);
301
+ else ref.current = el;
302
+ }
160
303
 
161
- // ============================================
162
- // COMPONENTS
163
- // ============================================
304
+ function resolveBoolean(value: unknown): boolean {
305
+ return typeof value === "function" ? Boolean((value as () => unknown)()) : Boolean(value);
306
+ }
164
307
 
165
308
  /**
166
309
  * A menu trigger wraps a button and menu, handling the open/close state.
167
310
  */
168
311
  export function MenuTrigger(props: MenuTriggerProps): JSX.Element {
169
- const [local, stateProps] = splitProps(props, ['slot']);
312
+ const [local, stateProps] = splitProps(props, ["slot"]);
170
313
 
171
- // Create trigger state
172
314
  const state = createMenuTriggerState({
173
315
  get isOpen() {
174
316
  return stateProps.isOpen;
@@ -181,33 +323,153 @@ export function MenuTrigger(props: MenuTriggerProps): JSX.Element {
181
323
  },
182
324
  });
183
325
 
184
- // Create trigger aria props
185
- const { menuTriggerProps, menuProps } = createMenuTrigger(
326
+ const menuTrigger = createMenuTrigger(
186
327
  {
187
328
  get isDisabled() {
188
329
  return stateProps.isDisabled;
189
330
  },
190
331
  },
191
- state
332
+ state,
192
333
  );
193
334
 
194
335
  return (
195
- <MenuTriggerContext.Provider
196
- value={{
197
- state,
198
- triggerProps: menuTriggerProps,
199
- menuProps,
200
- }}
201
- >
202
- {props.children}
203
- </MenuTriggerContext.Provider>
336
+ <RootMenuTriggerStateContext.Provider value={state}>
337
+ <MenuTriggerContext.Provider
338
+ value={{
339
+ state,
340
+ get triggerProps() {
341
+ return menuTrigger.menuTriggerProps;
342
+ },
343
+ get menuProps() {
344
+ return menuTrigger.menuProps;
345
+ },
346
+ }}
347
+ >
348
+ {props.children}
349
+ </MenuTriggerContext.Provider>
350
+ </RootMenuTriggerStateContext.Provider>
351
+ );
352
+ }
353
+
354
+ export function SubmenuTrigger(props: SubmenuTriggerProps): JSX.Element {
355
+ const children = () =>
356
+ (Array.isArray(props.children) ? props.children : [props.children]) as JSX.Element[];
357
+ const trigger = () => children()[0];
358
+ const content = () => children()[1];
359
+ const parentMenuItemContext = useContext(MenuItemContext);
360
+ const state = createMenuTriggerState({
361
+ get isOpen() {
362
+ return props.isOpen;
363
+ },
364
+ get defaultOpen() {
365
+ return props.defaultOpen;
366
+ },
367
+ get onOpenChange() {
368
+ return props.onOpenChange;
369
+ },
370
+ });
371
+
372
+ let triggerRef: HTMLLIElement | null = null;
373
+ const triggerId = createUniqueId();
374
+ const menuId = createUniqueId();
375
+ let hoverTimeout: number | undefined;
376
+ const delay = () => props.delay ?? 200;
377
+
378
+ const clearHoverTimeout = () => {
379
+ if (hoverTimeout != null) {
380
+ window.clearTimeout(hoverTimeout);
381
+ hoverTimeout = undefined;
382
+ }
383
+ };
384
+
385
+ const openSubmenu = () => {
386
+ clearHoverTimeout();
387
+ state.open();
388
+ };
389
+
390
+ const scheduleOpen = () => {
391
+ clearHoverTimeout();
392
+ hoverTimeout = window.setTimeout(() => {
393
+ hoverTimeout = undefined;
394
+ state.open();
395
+ }, delay());
396
+ };
397
+
398
+ onCleanup(clearHoverTimeout);
399
+
400
+ const menuTriggerContext = createMemo<MenuTriggerContextValue>(() => ({
401
+ state,
402
+ triggerProps: {},
403
+ menuProps: {
404
+ id: menuId,
405
+ "aria-labelledby": triggerId,
406
+ },
407
+ }));
408
+
409
+ const popoverTriggerContext = createMemo(() => ({
410
+ state: {
411
+ isOpen: () => state.isOpen(),
412
+ open: () => state.open(),
413
+ close: () => state.close(),
414
+ toggle: () => state.toggle(),
415
+ },
416
+ triggerRef: () => triggerRef,
417
+ setTriggerRef: (el: HTMLElement | null) => {
418
+ triggerRef = el as HTMLLIElement | null;
419
+ },
420
+ triggerId,
421
+ trigger: "SubmenuTrigger",
422
+ }));
423
+
424
+ const itemContext = createMemo<MenuItemContextValue>(() => ({
425
+ closeOnSelect: false,
426
+ onAction: () => openSubmenu(),
427
+ setItemRef: (el) => {
428
+ triggerRef = el;
429
+ },
430
+ props: () => ({
431
+ id: triggerId,
432
+ "aria-haspopup": "menu",
433
+ "aria-expanded": state.isOpen() || undefined,
434
+ "aria-controls": state.isOpen() ? menuId : undefined,
435
+ onPointerEnter: (event: PointerEvent) => {
436
+ if (event.pointerType === "touch") return;
437
+ scheduleOpen();
438
+ },
439
+ onPointerOver: (event: PointerEvent) => {
440
+ if (event.pointerType === "touch") return;
441
+ scheduleOpen();
442
+ },
443
+ onMouseEnter: () => scheduleOpen(),
444
+ onKeyDown: (event: KeyboardEvent) => {
445
+ if (event.key === "ArrowRight" || event.key === "Enter" || event.key === " ") {
446
+ event.preventDefault();
447
+ openSubmenu();
448
+ } else if (event.key === "ArrowLeft" && state.isOpen()) {
449
+ event.preventDefault();
450
+ state.close();
451
+ }
452
+ },
453
+ }),
454
+ }));
455
+
456
+ return (
457
+ <PopoverTriggerContext.Provider value={popoverTriggerContext()}>
458
+ <MenuTriggerContext.Provider value={menuTriggerContext()}>
459
+ <MenuItemContext.Provider value={{ ...parentMenuItemContext, ...itemContext() }}>
460
+ {trigger()}
461
+ </MenuItemContext.Provider>
462
+ {content()}
463
+ </MenuTriggerContext.Provider>
464
+ </PopoverTriggerContext.Provider>
204
465
  );
205
466
  }
206
467
 
207
468
  /**
208
469
  * A button that opens a menu.
209
470
  */
210
- export interface MenuButtonProps extends SlotProps {
471
+ export interface MenuButtonProps
472
+ extends SlotProps, Omit<JSX.HTMLAttributes<HTMLButtonElement>, "class" | "style" | "children"> {
211
473
  /** The children of the button. A function may be provided to receive render props. */
212
474
  children?: RenderChildren<MenuTriggerRenderProps>;
213
475
  /** The CSS className for the element. */
@@ -218,17 +480,34 @@ export interface MenuButtonProps extends SlotProps {
218
480
  isDisabled?: boolean;
219
481
  }
220
482
 
483
+ export interface MenuSectionProps
484
+ extends
485
+ SectionProps,
486
+ Pick<
487
+ SelectionStateProps,
488
+ | "selectionMode"
489
+ | "selectionBehavior"
490
+ | "disallowEmptySelection"
491
+ | "selectedKeys"
492
+ | "defaultSelectedKeys"
493
+ | "onSelectionChange"
494
+ | "disabledKeys"
495
+ | "disabledBehavior"
496
+ | "allowDuplicateSelectionEvents"
497
+ > {
498
+ /** Whether menu items in this section should close the menu when selected. */
499
+ shouldCloseOnSelect?: boolean;
500
+ }
501
+
221
502
  export function MenuButton(props: MenuButtonProps): JSX.Element {
222
- const [local] = splitProps(props, ['class', 'style', 'slot', 'isDisabled']);
503
+ const [local, domProps] = splitProps(props, ["class", "style", "slot", "isDisabled", "children"]);
223
504
 
224
- // Get trigger context
225
505
  const context = useContext(MenuTriggerContext);
226
506
  if (!context) {
227
- throw new Error('MenuButton must be used within a MenuTrigger');
507
+ throw new Error("MenuButton must be used within a MenuTrigger");
228
508
  }
229
- const { state, triggerProps } = context;
509
+ const { state } = context;
230
510
 
231
- // Create button aria props for proper press handling
232
511
  const buttonAria = createButton({
233
512
  get isDisabled() {
234
513
  return local.isDisabled;
@@ -238,17 +517,14 @@ export function MenuButton(props: MenuButtonProps): JSX.Element {
238
517
  },
239
518
  });
240
519
 
241
- // Create focus ring
242
520
  const { isFocused, isFocusVisible, focusProps } = createFocusRing();
243
521
 
244
- // Create hover
245
522
  const { isHovered, hoverProps } = createHover({
246
523
  get isDisabled() {
247
524
  return local.isDisabled;
248
525
  },
249
526
  });
250
527
 
251
- // Render props values
252
528
  const renderValues = createMemo<MenuTriggerRenderProps>(() => ({
253
529
  isOpen: state.isOpen(),
254
530
  isFocused: isFocused(),
@@ -258,20 +534,26 @@ export function MenuButton(props: MenuButtonProps): JSX.Element {
258
534
  isDisabled: !!local.isDisabled,
259
535
  }));
260
536
 
261
- // Resolve render props
262
537
  const renderProps = useRenderProps(
263
538
  {
264
539
  children: props.children,
265
540
  class: local.class,
266
541
  style: local.style,
267
- defaultClassName: 'solidaria-MenuButton',
542
+ defaultClassName: "solidaria-MenuButton",
268
543
  },
269
- renderValues
544
+ renderValues,
270
545
  );
271
546
 
272
- // Remove ref from spread props
547
+ const resolvedTriggerProps = () => context.triggerProps as Record<string, unknown>;
273
548
  const cleanTriggerProps = () => {
274
- const { ref: _ref1, ...rest } = triggerProps as Record<string, unknown>;
549
+ const {
550
+ ref: _ref1,
551
+ "aria-haspopup": _ariaHasPopup,
552
+ "aria-expanded": _ariaExpanded,
553
+ "aria-controls": _ariaControls,
554
+ "aria-disabled": _ariaDisabled,
555
+ ...rest
556
+ } = resolvedTriggerProps();
275
557
  return rest;
276
558
  };
277
559
  const cleanButtonProps = () => {
@@ -289,6 +571,7 @@ export function MenuButton(props: MenuButtonProps): JSX.Element {
289
571
 
290
572
  return (
291
573
  <button
574
+ {...domProps}
292
575
  {...cleanTriggerProps()}
293
576
  {...cleanButtonProps()}
294
577
  {...cleanFocusProps()}
@@ -296,6 +579,10 @@ export function MenuButton(props: MenuButtonProps): JSX.Element {
296
579
  type="button"
297
580
  class={renderProps.class()}
298
581
  style={renderProps.style()}
582
+ aria-haspopup={resolvedTriggerProps()["aria-haspopup"] as "menu" | "listbox" | undefined}
583
+ aria-expanded={resolvedTriggerProps()["aria-expanded"] as boolean | undefined}
584
+ aria-controls={resolvedTriggerProps()["aria-controls"] as string | undefined}
585
+ aria-disabled={resolvedTriggerProps()["aria-disabled"] as boolean | undefined}
299
586
  data-open={state.isOpen() || undefined}
300
587
  data-focused={isFocused() || undefined}
301
588
  data-focus-visible={isFocusVisible() || undefined}
@@ -314,63 +601,192 @@ export function MenuButton(props: MenuButtonProps): JSX.Element {
314
601
  export function Menu<T>(props: MenuProps<T>): JSX.Element {
315
602
  const [local, stateProps, ariaProps] = splitProps(
316
603
  props,
317
- ['children', 'class', 'style', 'slot'],
318
- ['items', 'getKey', 'getTextValue', 'getDisabled', 'disabledKeys', 'onAction', 'onClose']
604
+ [
605
+ "children",
606
+ "class",
607
+ "style",
608
+ "render",
609
+ "slot",
610
+ "renderEmptyState",
611
+ "shouldCloseOnSelect",
612
+ "ref",
613
+ "staticChildren",
614
+ ],
615
+ [
616
+ "items",
617
+ "getKey",
618
+ "getTextValue",
619
+ "getDisabled",
620
+ "disabledKeys",
621
+ "selectionMode",
622
+ "selectionBehavior",
623
+ "disallowEmptySelection",
624
+ "selectedKeys",
625
+ "defaultSelectedKeys",
626
+ "onSelectionChange",
627
+ "allowDuplicateSelectionEvents",
628
+ "onAction",
629
+ "onClose",
630
+ "dragAndDropHooks",
631
+ ],
319
632
  );
320
633
 
321
- // Get trigger context if available
322
634
  const triggerContext = useContext(MenuTriggerContext);
323
635
 
324
- // Ref for the menu element (for click outside detection)
325
- let menuRef: HTMLUListElement | undefined;
636
+ const [menuRef, setMenuRef] = createSignal<HTMLUListElement | null>(null);
637
+ const [staticItems, setStaticItems] = createSignal<StaticMenuCollectionItem[]>([]);
638
+ const staticItemMap = new Map<Key, StaticMenuCollectionItem>();
639
+ const sectionSelectionMap = new Map<Key, MenuSectionSelectionContextValue>();
640
+ const usesStaticChildren = () => local.staticChildren != null || stateProps.items == null;
641
+
642
+ const syncStaticItems = () => {
643
+ setStaticItems(Array.from(staticItemMap.values()));
644
+ };
645
+
646
+ const staticCollectionContext: StaticMenuCollectionContextValue = {
647
+ registerItem(item) {
648
+ const previous = staticItemMap.get(item.id);
649
+ if (
650
+ previous &&
651
+ previous.textValue === item.textValue &&
652
+ previous.isDisabled === item.isDisabled
653
+ ) {
654
+ return;
655
+ }
656
+
657
+ staticItemMap.set(item.id, item);
658
+ syncStaticItems();
659
+ },
660
+ unregisterItem(id) {
661
+ if (staticItemMap.delete(id)) {
662
+ syncStaticItems();
663
+ }
664
+ },
665
+ };
666
+ const sectionSelectionRegistry: MenuSectionSelectionRegistryContextValue = {
667
+ registerItem(key, selection) {
668
+ sectionSelectionMap.set(key, selection);
669
+ },
670
+ unregisterItem(key, selection) {
671
+ if (sectionSelectionMap.get(key) === selection) {
672
+ sectionSelectionMap.delete(key);
673
+ }
674
+ },
675
+ selectItem(key, event) {
676
+ const selection = sectionSelectionMap.get(key);
677
+ if (!selection || selection.selectionMode() === "none") {
678
+ return false;
679
+ }
680
+
681
+ selection.select(key, event);
682
+ return true;
683
+ },
684
+ };
685
+ const handleAction = (key: Key) => {
686
+ sectionSelectionRegistry.selectItem(key);
687
+ stateProps.onAction?.(key);
688
+ };
689
+
690
+ const flatItems = createMemo<T[]>(() => {
691
+ return flattenCollectionEntries(stateProps.items ?? []);
692
+ });
693
+
694
+ const hasSections = createMemo(() =>
695
+ (stateProps.items ?? []).some((item) => isCollectionSection(item)),
696
+ );
326
697
 
327
- // Create menu state
328
698
  const state = createMenuState<T>({
329
699
  get items() {
330
- return stateProps.items;
700
+ return usesStaticChildren() ? (staticItems() as T[]) : flatItems();
331
701
  },
332
702
  get getKey() {
333
- return stateProps.getKey;
703
+ return usesStaticChildren()
704
+ ? (item: T) => (item as StaticMenuCollectionItem).id
705
+ : stateProps.getKey;
334
706
  },
335
707
  get getTextValue() {
336
- return stateProps.getTextValue;
708
+ return usesStaticChildren()
709
+ ? (item: T) =>
710
+ (item as StaticMenuCollectionItem).textValue ??
711
+ String((item as StaticMenuCollectionItem).id)
712
+ : stateProps.getTextValue;
337
713
  },
338
714
  get getDisabled() {
339
- return stateProps.getDisabled;
715
+ return usesStaticChildren()
716
+ ? (item: T) => Boolean((item as StaticMenuCollectionItem).isDisabled)
717
+ : stateProps.getDisabled;
340
718
  },
341
719
  get disabledKeys() {
342
720
  return stateProps.disabledKeys;
343
721
  },
722
+ get selectionMode() {
723
+ return stateProps.selectionMode;
724
+ },
725
+ get selectionBehavior() {
726
+ return stateProps.selectionBehavior;
727
+ },
728
+ get disallowEmptySelection() {
729
+ return stateProps.disallowEmptySelection;
730
+ },
731
+ get selectedKeys() {
732
+ return stateProps.selectedKeys;
733
+ },
734
+ get defaultSelectedKeys() {
735
+ return stateProps.defaultSelectedKeys;
736
+ },
737
+ get onSelectionChange() {
738
+ return stateProps.onSelectionChange;
739
+ },
740
+ get allowDuplicateSelectionEvents() {
741
+ return stateProps.allowDuplicateSelectionEvents;
742
+ },
344
743
  get onAction() {
345
- return stateProps.onAction;
744
+ return handleAction;
346
745
  },
347
746
  get onClose() {
348
747
  return stateProps.onClose ?? (() => triggerContext?.state.close());
349
748
  },
350
749
  });
351
750
 
352
- // Create menu aria props
353
- const { menuProps } = createMenu(
751
+ const resolveDisabled = (): boolean => {
752
+ const disabled = ariaProps.isDisabled;
753
+ if (typeof disabled === "function") {
754
+ return (disabled as () => boolean)();
755
+ }
756
+ return !!disabled;
757
+ };
758
+
759
+ const { menuProps, labelProps } = createMenu(
354
760
  {
761
+ get isDisabled() {
762
+ return resolveDisabled();
763
+ },
764
+ get label() {
765
+ return ariaProps.label;
766
+ },
767
+ get onAction() {
768
+ return handleAction;
769
+ },
355
770
  get onClose() {
356
771
  return stateProps.onClose ?? (() => triggerContext?.state.close());
357
772
  },
358
- get 'aria-label'() {
359
- return ariaProps['aria-label'];
773
+ get "aria-label"() {
774
+ return ariaProps["aria-label"];
775
+ },
776
+ get "aria-labelledby"() {
777
+ return ariaProps["aria-labelledby"];
360
778
  },
361
- get 'aria-labelledby'() {
362
- return ariaProps['aria-labelledby'];
779
+ get "aria-describedby"() {
780
+ return ariaProps["aria-describedby"];
363
781
  },
364
782
  },
365
- state
783
+ state,
366
784
  );
367
785
 
368
- // Create focus ring
369
786
  const { isFocused, focusProps } = createFocusRing();
370
787
 
371
- // Handle click outside to close menu
372
788
  createInteractOutside({
373
- ref: () => menuRef ?? null,
789
+ ref: () => menuRef(),
374
790
  onInteractOutside: () => {
375
791
  if (triggerContext?.state.isOpen()) {
376
792
  triggerContext.state.close();
@@ -381,23 +797,21 @@ export function Menu<T>(props: MenuProps<T>): JSX.Element {
381
797
  },
382
798
  });
383
799
 
384
- // Render props values
385
800
  const renderValues = createMemo<MenuRenderProps>(() => ({
386
801
  isFocused: state.isFocused() || isFocused(),
387
802
  isOpen: triggerContext?.state.isOpen() ?? true,
803
+ isEmpty: state.collection().size === 0,
388
804
  }));
389
805
 
390
- // Resolve render props
391
806
  const renderProps = useRenderProps(
392
807
  {
393
808
  class: local.class,
394
809
  style: local.style,
395
- defaultClassName: 'solidaria-Menu',
810
+ defaultClassName: "solidaria-Menu",
396
811
  },
397
- renderValues
812
+ renderValues,
398
813
  );
399
814
 
400
- // Remove ref from spread props
401
815
  const cleanMenuProps = () => {
402
816
  const { ref: _ref1, ...rest } = menuProps as Record<string, unknown>;
403
817
  return rest;
@@ -411,27 +825,373 @@ export function Menu<T>(props: MenuProps<T>): JSX.Element {
411
825
  const { ref: _ref3, ...rest } = focusProps as Record<string, unknown>;
412
826
  return rest;
413
827
  };
828
+ const domProps = createMemo(() =>
829
+ filterDOMProps(ariaProps as Record<string, unknown>, { global: true }),
830
+ );
831
+ const cleanLabelProps = () => {
832
+ const { ref: _ref4, ...rest } = labelProps as Record<string, unknown>;
833
+ return rest;
834
+ };
835
+ const setResolvedMenuRef = (el: HTMLUListElement): void => {
836
+ setMenuRef(el);
837
+ assignRef(local.ref, el);
838
+ };
414
839
 
415
840
  // If inside a MenuTrigger, only render when open
416
841
  // If standalone (no trigger context), always render
417
- const shouldRender = () => triggerContext ? triggerContext.state.isOpen() : true;
842
+ const shouldRender = () => (triggerContext ? triggerContext.state.isOpen() : true);
843
+ const parentCollectionRenderer = useCollectionRenderer<unknown>();
844
+ const virtualizer = useVirtualizerContext();
845
+ const getItemNodes = createMemo(() =>
846
+ Array.from(state.collection()).filter((node) => node.type === "item"),
847
+ );
848
+ const getDropTargetByIndex = (
849
+ index: number,
850
+ position: "before" | "after" | "on",
851
+ ): DropTarget | null => {
852
+ const node = getItemNodes()[index];
853
+ if (!node) return null;
854
+ return { type: "item", key: node.key, dropPosition: position };
855
+ };
856
+ const hasDroppableDnd = createMemo(() => {
857
+ const hooks = stateProps.dragAndDropHooks;
858
+ return Boolean(
859
+ hooks?.useDroppableCollectionState &&
860
+ hooks.useDroppableCollection &&
861
+ (hooks.dropTargetDelegate ||
862
+ parentCollectionRenderer?.dropTargetDelegate ||
863
+ hooks.ListDropTargetDelegate),
864
+ );
865
+ });
866
+ const hasDraggableDnd = createMemo(() => {
867
+ const hooks = stateProps.dragAndDropHooks;
868
+ return Boolean(hooks?.useDraggableCollectionState && hooks.useDraggableCollection);
869
+ });
870
+ const dragState = createMemo(() => {
871
+ if (!hasDraggableDnd()) return undefined;
872
+ return stateProps.dragAndDropHooks?.useDraggableCollectionState?.({
873
+ items: flatItems(),
874
+ });
875
+ });
876
+ const dropState = createMemo(() => {
877
+ if (!hasDroppableDnd()) return undefined;
878
+ return stateProps.dragAndDropHooks?.useDroppableCollectionState?.({});
879
+ });
880
+ const persistedKeys = useDndPersistedKeys(
881
+ { focusedKey: state.focusedKey },
882
+ stateProps.dragAndDropHooks,
883
+ dropState(),
884
+ state.collection(),
885
+ );
886
+ const virtualRange = createMemo(() => {
887
+ if (!virtualizer || !parentCollectionRenderer?.isVirtualized || hasSections()) return null;
888
+ const dynamicItems = stateProps.items ?? [];
889
+ const baseRange = virtualizer.getVisibleRange(dynamicItems.length);
890
+ const itemNodes = getItemNodes();
891
+ const persistedIndexes = Array.from(persistedKeys())
892
+ .map((key) => itemNodes.findIndex((node) => node.key === key))
893
+ .filter((index) => index >= 0);
894
+ const dropTarget = dropState()?.target;
895
+ const normalizedDropKey = getNormalizedDropTargetKey(dropTarget, state.collection());
896
+ const focusedKey = state.focusedKey();
897
+ const focusedIndex =
898
+ focusedKey != null ? itemNodes.findIndex((node) => node.key === focusedKey) : -1;
899
+ const forceIncludeIndexes = [
900
+ dropTarget?.type === "item" ? itemNodes.findIndex((node) => node.key === dropTarget.key) : -1,
901
+ normalizedDropKey != null
902
+ ? itemNodes.findIndex((node) => node.key === normalizedDropKey)
903
+ : -1,
904
+ dropTarget?.type === "item" ? -1 : focusedIndex,
905
+ ].filter((index) => index >= 0);
906
+ return mergePersistedKeysIntoVirtualRange(
907
+ baseRange,
908
+ persistedIndexes,
909
+ dynamicItems.length,
910
+ virtualizer,
911
+ 80,
912
+ {
913
+ forceIncludeIndexes,
914
+ forceIncludeMaxSpan: 320,
915
+ },
916
+ );
917
+ });
918
+ const visibleItems = createMemo(() => {
919
+ const range = virtualRange();
920
+ const items = stateProps.items ?? [];
921
+ if (!range) return items;
922
+ return items.slice(range.start, range.end);
923
+ });
924
+ createEffect(() => {
925
+ if (!hasDraggableDnd()) return;
926
+ const hooks = stateProps.dragAndDropHooks;
927
+ const activeDragState = dragState();
928
+ if (!hooks?.useDraggableCollection || !activeDragState) return;
929
+ hooks.useDraggableCollection({}, activeDragState, () => menuRef());
930
+ });
931
+ const droppableCollection = createMemo(() => {
932
+ if (!hasDroppableDnd()) return undefined;
933
+ const hooks = stateProps.dragAndDropHooks;
934
+ const activeDropState = dropState();
935
+ if (!hooks?.useDroppableCollection || !activeDropState) return undefined;
936
+ const resolveDirection = (): "ltr" | "rtl" => {
937
+ const menuEl = menuRef();
938
+ if (
939
+ menuEl &&
940
+ typeof window !== "undefined" &&
941
+ typeof window.getComputedStyle === "function"
942
+ ) {
943
+ const dir = window.getComputedStyle(menuEl).direction;
944
+ if (dir === "rtl") return "rtl";
945
+ }
946
+ return typeof document !== "undefined" && document.dir === "rtl" ? "rtl" : "ltr";
947
+ };
948
+ const dropTargetDelegate =
949
+ hooks.dropTargetDelegate ??
950
+ parentCollectionRenderer?.dropTargetDelegate ??
951
+ (hooks.ListDropTargetDelegate
952
+ ? new hooks.ListDropTargetDelegate(
953
+ () => state.collection(),
954
+ () => menuRef(),
955
+ { layout: "stack", orientation: "vertical", direction: resolveDirection() },
956
+ )
957
+ : undefined);
958
+ if (!dropTargetDelegate) return undefined;
959
+ return hooks.useDroppableCollection(
960
+ {
961
+ dropTargetDelegate,
962
+ keyboardDelegate: {
963
+ getFirstKey: () => state.collection().getFirstKey(),
964
+ getLastKey: () => state.collection().getLastKey(),
965
+ getKeyBelow: (key) => state.collection().getKeyAfter(key),
966
+ getKeyAbove: (key) => state.collection().getKeyBefore(key),
967
+ getKeyPageBelow: (key) => state.collection().getKeyAfter(key),
968
+ getKeyPageAbove: (key) => state.collection().getKeyBefore(key),
969
+ },
970
+ },
971
+ activeDropState,
972
+ () => menuRef(),
973
+ );
974
+ });
975
+ const isRootDropTarget = createMemo(() => {
976
+ return Boolean(dropState()?.target?.type === "root");
977
+ });
978
+ const dndRenderDropIndicator = createMemo(() =>
979
+ useRenderDropIndicator(stateProps.dragAndDropHooks, dropState()),
980
+ );
981
+ const dndDropIndicator = (index: number, position: "before" | "after" | "on") => {
982
+ const target = getDropTargetByIndex(index, position);
983
+ if (!target || target.type !== "item") return undefined;
984
+ return dndRenderDropIndicator()?.(target);
985
+ };
986
+ const sectionedRenderEntries = createMemo(() => {
987
+ let globalIndex = 0;
988
+ return (stateProps.items ?? []).map((entry) => {
989
+ if (isCollectionSection(entry)) {
990
+ const sectionItems = entry.items.map((item) => ({
991
+ item,
992
+ index: globalIndex++,
993
+ }));
994
+ return {
995
+ type: "section" as const,
996
+ section: entry,
997
+ items: sectionItems,
998
+ };
999
+ }
1000
+ const indexedItem = {
1001
+ item: entry as T,
1002
+ index: globalIndex++,
1003
+ };
1004
+ return {
1005
+ type: "item" as const,
1006
+ item: indexedItem,
1007
+ };
1008
+ });
1009
+ });
1010
+ const renderDynamicItem = (item: T) =>
1011
+ typeof local.children === "function" ? local.children(item) : undefined;
1012
+ const resolveStaticChild = (child: unknown): JSX.Element | undefined => {
1013
+ return typeof child === "function"
1014
+ ? (child as () => JSX.Element | undefined)()
1015
+ : (child as JSX.Element | undefined);
1016
+ };
1017
+ const renderStaticChildren = () => {
1018
+ const staticChildren = (local.staticChildren?.() ?? local.children) as unknown;
1019
+ if (Array.isArray(staticChildren)) {
1020
+ return staticChildren.map(resolveStaticChild);
1021
+ }
1022
+ return resolveStaticChild(staticChildren);
1023
+ };
1024
+ const collectionRenderer = createMemo<CollectionRendererContextValue<unknown>>(() => ({
1025
+ ...parentCollectionRenderer,
1026
+ renderItem: (item) => renderDynamicItem(item as T),
1027
+ renderDropIndicator: (index, position) =>
1028
+ dndDropIndicator(index, position) ??
1029
+ parentCollectionRenderer?.renderDropIndicator?.(index, position),
1030
+ }));
1031
+ const menuListChildren = () => (
1032
+ <SharedElementTransition>
1033
+ {state.collection().size === 0 && !usesStaticChildren() && local.renderEmptyState ? (
1034
+ <li role="presentation" data-empty-state>
1035
+ <div role="menuitem" style={{ display: "contents" }}>
1036
+ {local.renderEmptyState()}
1037
+ </div>
1038
+ </li>
1039
+ ) : usesStaticChildren() ? (
1040
+ renderStaticChildren()
1041
+ ) : hasSections() ? (
1042
+ <For each={sectionedRenderEntries()}>
1043
+ {(entry) =>
1044
+ entry.type === "section" ? (
1045
+ <li role="presentation" data-section-wrapper>
1046
+ <Section class="solidaria-Menu-section">
1047
+ {entry.section.title != null && (
1048
+ <Header class="solidaria-Menu-sectionHeader">{entry.section.title}</Header>
1049
+ )}
1050
+ <Group class="solidaria-Menu-sectionGroup">
1051
+ <ul role="group" aria-label={entry.section["aria-label"]}>
1052
+ <For each={entry.items}>
1053
+ {(indexedItem) => (
1054
+ <>
1055
+ {collectionRenderer().renderDropIndicator?.(
1056
+ indexedItem.index,
1057
+ "before",
1058
+ )}
1059
+ {collectionRenderer().renderDropIndicator?.(indexedItem.index, "on")}
1060
+ {renderDynamicItem(indexedItem.item)}
1061
+ {collectionRenderer().renderDropIndicator?.(indexedItem.index, "after")}
1062
+ </>
1063
+ )}
1064
+ </For>
1065
+ </ul>
1066
+ </Group>
1067
+ </Section>
1068
+ </li>
1069
+ ) : (
1070
+ <>
1071
+ {collectionRenderer().renderDropIndicator?.(entry.item.index, "before")}
1072
+ {collectionRenderer().renderDropIndicator?.(entry.item.index, "on")}
1073
+ {renderDynamicItem(entry.item.item)}
1074
+ {collectionRenderer().renderDropIndicator?.(entry.item.index, "after")}
1075
+ </>
1076
+ )
1077
+ }
1078
+ </For>
1079
+ ) : (
1080
+ <>
1081
+ {virtualRange()?.offsetTop ? (
1082
+ <li
1083
+ role="presentation"
1084
+ aria-hidden="true"
1085
+ style={{ height: `${virtualRange()!.offsetTop}px` }}
1086
+ data-virtualizer-spacer="top"
1087
+ />
1088
+ ) : null}
1089
+ <For each={visibleItems()}>
1090
+ {(item, index) => {
1091
+ const itemIndex = () => (virtualRange()?.start ?? 0) + index();
1092
+ const beforeIndicator = () =>
1093
+ collectionRenderer().renderDropIndicator?.(itemIndex(), "before");
1094
+ const onIndicator = () =>
1095
+ collectionRenderer().renderDropIndicator?.(itemIndex(), "on");
1096
+ const afterIndicator = () =>
1097
+ collectionRenderer().renderDropIndicator?.(itemIndex(), "after");
1098
+ return (
1099
+ <>
1100
+ {beforeIndicator()}
1101
+ {onIndicator()}
1102
+ {renderDynamicItem(item as T)}
1103
+ {afterIndicator()}
1104
+ </>
1105
+ );
1106
+ }}
1107
+ </For>
1108
+ {virtualRange()?.offsetBottom ? (
1109
+ <li
1110
+ role="presentation"
1111
+ aria-hidden="true"
1112
+ style={{ height: `${virtualRange()!.offsetBottom}px` }}
1113
+ data-virtualizer-spacer="bottom"
1114
+ />
1115
+ ) : null}
1116
+ </>
1117
+ )}
1118
+ </SharedElementTransition>
1119
+ );
1120
+ const menuListProps = () =>
1121
+ ({
1122
+ ref: setResolvedMenuRef,
1123
+ ...mergeProps(
1124
+ domProps(),
1125
+ cleanMenuProps(),
1126
+ cleanTriggerMenuProps(),
1127
+ cleanFocusProps(),
1128
+ (droppableCollection()?.collectionProps as Record<string, unknown> | undefined) ?? {},
1129
+ ),
1130
+ class: renderProps.class(),
1131
+ style: renderProps.style(),
1132
+ slot: local.slot,
1133
+ "data-focused": state.isFocused() || undefined,
1134
+ "data-disabled": resolveDisabled() || undefined,
1135
+ "data-empty": state.collection().size === 0 || undefined,
1136
+ "data-drop-target": isRootDropTarget() || undefined,
1137
+ children: menuListChildren(),
1138
+ }) as JSX.HTMLAttributes<HTMLUListElement>;
418
1139
 
419
1140
  // Only use FocusScope when inside a MenuTrigger (for popover behavior)
420
1141
  // Standalone menus don't need focus restoration
421
1142
  const menuContent = () => (
422
- <MenuContext.Provider value={{ state }}>
1143
+ <MenuContext.Provider
1144
+ value={
1145
+ {
1146
+ state,
1147
+ isDisabled: resolveDisabled,
1148
+ dragAndDropHooks: stateProps.dragAndDropHooks,
1149
+ dragState: dragState(),
1150
+ dropState: dropState(),
1151
+ } as MenuContextValue<unknown>
1152
+ }
1153
+ >
423
1154
  <MenuStateContext.Provider value={state}>
424
- <ul
425
- ref={(el) => (menuRef = el)}
426
- {...cleanMenuProps()}
427
- {...cleanTriggerMenuProps()}
428
- {...cleanFocusProps()}
429
- class={renderProps.class()}
430
- style={renderProps.style()}
431
- data-focused={state.isFocused() || undefined}
432
- >
433
- <For each={stateProps.items}>{(item) => props.children?.(item)}</For>
434
- </ul>
1155
+ <MenuSectionSelectionRegistryContext.Provider value={sectionSelectionRegistry}>
1156
+ <StaticMenuCollectionContext.Provider
1157
+ value={usesStaticChildren() ? staticCollectionContext : null}
1158
+ >
1159
+ <MenuItemContext.Provider value={{ closeOnSelect: local.shouldCloseOnSelect }}>
1160
+ <CollectionRendererContext.Provider value={collectionRenderer()}>
1161
+ <>
1162
+ <Show when={ariaProps.label}>
1163
+ <span {...cleanLabelProps()}>{ariaProps.label as JSX.Element}</span>
1164
+ </Show>
1165
+ {local.render ? (
1166
+ local.render(menuListProps(), renderValues())
1167
+ ) : (
1168
+ <ul
1169
+ ref={setResolvedMenuRef}
1170
+ {...mergeProps(
1171
+ domProps(),
1172
+ cleanMenuProps(),
1173
+ cleanTriggerMenuProps(),
1174
+ cleanFocusProps(),
1175
+ (droppableCollection()?.collectionProps as
1176
+ | Record<string, unknown>
1177
+ | undefined) ?? {},
1178
+ )}
1179
+ class={renderProps.class()}
1180
+ style={renderProps.style()}
1181
+ slot={local.slot}
1182
+ data-focused={state.isFocused() || undefined}
1183
+ data-disabled={resolveDisabled() || undefined}
1184
+ data-empty={state.collection().size === 0 || undefined}
1185
+ data-drop-target={isRootDropTarget() || undefined}
1186
+ >
1187
+ {menuListChildren()}
1188
+ </ul>
1189
+ )}
1190
+ </>
1191
+ </CollectionRendererContext.Provider>
1192
+ </MenuItemContext.Provider>
1193
+ </StaticMenuCollectionContext.Provider>
1194
+ </MenuSectionSelectionRegistryContext.Provider>
435
1195
  </MenuStateContext.Provider>
436
1196
  </MenuContext.Provider>
437
1197
  );
@@ -452,93 +1212,435 @@ export function Menu<T>(props: MenuProps<T>): JSX.Element {
452
1212
  */
453
1213
  export function MenuItem<T>(props: MenuItemProps<T>): JSX.Element {
454
1214
  const [local, ariaProps] = splitProps(props, [
455
- 'class',
456
- 'style',
457
- 'slot',
458
- 'id',
459
- 'item',
460
- 'textValue',
461
- 'onAction',
1215
+ "class",
1216
+ "style",
1217
+ "render",
1218
+ "slot",
1219
+ "id",
1220
+ "item",
1221
+ "textValue",
1222
+ "onAction",
1223
+ "href",
1224
+ "target",
1225
+ "rel",
1226
+ "download",
1227
+ "onHoverStart",
1228
+ "onHoverEnd",
1229
+ "onHoverChange",
1230
+ "ref",
462
1231
  ]);
463
1232
 
464
- // Get state from context
465
1233
  const context = useContext(MenuStateContext);
466
1234
  if (!context) {
467
- throw new Error('MenuItem must be used within a Menu');
1235
+ throw new Error("MenuItem must be used within a Menu");
468
1236
  }
469
1237
  const state = context as MenuState<T>;
1238
+ const menuContext = useContext(MenuContext) as MenuContextValue<T> | null;
1239
+ const itemContext = useContext(MenuItemContext);
1240
+ const staticCollection = useContext(StaticMenuCollectionContext);
1241
+ const sectionSelection = useContext(MenuSectionSelectionContext);
1242
+ const sectionSelectionRegistry = useContext(MenuSectionSelectionRegistryContext);
1243
+ const [ref, setRef] = createSignal<HTMLLIElement | null>(null);
1244
+ const contextProps = () => itemContext?.props?.() ?? {};
1245
+ const combinedOnAction = () => {
1246
+ local.onAction?.();
1247
+ itemContext?.onAction?.();
1248
+ };
1249
+ const activeSectionSelection = () =>
1250
+ sectionSelection && sectionSelection.selectionMode() !== "none" ? sectionSelection : null;
1251
+ let registeredStaticKey: Key | null = null;
1252
+ let registeredSectionSelectionKey: Key | null = null;
1253
+ let registeredSectionSelection: MenuSectionSelectionContextValue | null = null;
1254
+
1255
+ const unregisterSectionSelection = () => {
1256
+ if (registeredSectionSelectionKey != null && registeredSectionSelection) {
1257
+ sectionSelectionRegistry?.unregisterItem(
1258
+ registeredSectionSelectionKey,
1259
+ registeredSectionSelection,
1260
+ );
1261
+ registeredSectionSelectionKey = null;
1262
+ registeredSectionSelection = null;
1263
+ }
1264
+ };
1265
+
1266
+ createEffect(() => {
1267
+ if (!staticCollection) return;
1268
+
1269
+ if (registeredStaticKey != null && registeredStaticKey !== local.id) {
1270
+ staticCollection.unregisterItem(registeredStaticKey);
1271
+ }
1272
+
1273
+ registeredStaticKey = local.id;
1274
+ staticCollection.registerItem({
1275
+ id: local.id,
1276
+ textValue: local.textValue ?? ariaProps["aria-label"],
1277
+ isDisabled:
1278
+ resolveBoolean(ariaProps.isDisabled) || (sectionSelection?.isDisabled(local.id) ?? false),
1279
+ });
1280
+ });
1281
+
1282
+ onCleanup(() => {
1283
+ if (registeredStaticKey != null) {
1284
+ staticCollection?.unregisterItem(registeredStaticKey);
1285
+ }
1286
+ });
1287
+
1288
+ createEffect(() => {
1289
+ const selection = activeSectionSelection();
1290
+ if (!sectionSelectionRegistry || !selection) {
1291
+ unregisterSectionSelection();
1292
+ return;
1293
+ }
1294
+
1295
+ if (registeredSectionSelectionKey === local.id && registeredSectionSelection === selection) {
1296
+ return;
1297
+ }
1298
+
1299
+ unregisterSectionSelection();
1300
+ registeredSectionSelectionKey = local.id;
1301
+ registeredSectionSelection = selection;
1302
+ sectionSelectionRegistry.registerItem(local.id, selection);
1303
+ });
1304
+
1305
+ onCleanup(unregisterSectionSelection);
470
1306
 
471
- // Create menu item aria props
472
1307
  const itemAria = createMenuItem<T>(
473
1308
  {
474
1309
  key: local.id,
475
1310
  get isDisabled() {
476
- return ariaProps.isDisabled;
1311
+ return Boolean(
1312
+ ariaProps.isDisabled ||
1313
+ sectionSelection?.isDisabled(local.id) ||
1314
+ menuContext?.isDisabled(),
1315
+ );
477
1316
  },
478
- get 'aria-label'() {
479
- return ariaProps['aria-label'];
1317
+ get "aria-label"() {
1318
+ return ariaProps["aria-label"] ?? local.textValue;
480
1319
  },
481
1320
  get onAction() {
482
- return local.onAction;
1321
+ return combinedOnAction;
1322
+ },
1323
+ get closeOnSelect() {
1324
+ return (
1325
+ ariaProps.closeOnSelect ??
1326
+ sectionSelection?.shouldCloseOnSelect() ??
1327
+ itemContext?.closeOnSelect
1328
+ );
1329
+ },
1330
+ get href() {
1331
+ return local.href;
1332
+ },
1333
+ get target() {
1334
+ return local.target;
1335
+ },
1336
+ get rel() {
1337
+ return local.rel;
1338
+ },
1339
+ get download() {
1340
+ return local.download;
483
1341
  },
484
1342
  },
485
- state
1343
+ state,
486
1344
  );
487
1345
 
488
- // Create hover
489
1346
  const { isHovered, hoverProps } = createHover({
490
1347
  get isDisabled() {
491
1348
  return itemAria.isDisabled();
492
1349
  },
1350
+ onHoverStart: local.onHoverStart,
1351
+ onHoverEnd: local.onHoverEnd,
1352
+ onHoverChange: local.onHoverChange,
493
1353
  });
494
1354
 
495
- // Render props values
496
- const renderValues = createMemo<MenuItemRenderProps>(() => ({
497
- isSelected: false, // Menu items don't have selection state
498
- isFocused: itemAria.isFocused(),
499
- isFocusVisible: itemAria.isFocusVisible(),
500
- isPressed: itemAria.isPressed(),
501
- isHovered: isHovered(),
502
- isDisabled: itemAria.isDisabled(),
503
- }));
1355
+ const renderValues = createMemo<MenuItemRenderProps>(() => {
1356
+ const selection = activeSectionSelection();
1357
+ return {
1358
+ isSelected: selection?.isSelected(local.id) ?? itemAria.isSelected(),
1359
+ selectionMode: selection?.selectionMode() ?? itemAria.selectionMode(),
1360
+ isFocused: itemAria.isFocused(),
1361
+ isFocusVisible: itemAria.isFocusVisible(),
1362
+ isPressed: itemAria.isPressed(),
1363
+ isHovered: isHovered(),
1364
+ isDisabled: itemAria.isDisabled(),
1365
+ hasSubmenu: Boolean(contextProps()["aria-haspopup"]),
1366
+ isOpen: contextProps()["aria-expanded"] === true,
1367
+ };
1368
+ });
504
1369
 
505
- // Resolve render props
506
1370
  const renderProps = useRenderProps(
507
1371
  {
508
1372
  children: props.children,
509
1373
  class: local.class,
510
1374
  style: local.style,
511
- defaultClassName: 'solidaria-Menu-item',
1375
+ defaultClassName: "solidaria-Menu-item",
512
1376
  },
513
- renderValues
1377
+ renderValues,
514
1378
  );
1379
+ const hasPrimitiveLabel = () => {
1380
+ return typeof props.children === "string" || typeof props.children === "number";
1381
+ };
515
1382
 
516
- // Remove ref from spread props
517
1383
  const cleanItemProps = () => {
518
- const { ref: _ref1, ...rest } = itemAria.menuItemProps as Record<string, unknown>;
1384
+ const {
1385
+ ref: _ref1,
1386
+ "aria-describedby": _ariaDescribedby,
1387
+ ...rest
1388
+ } = itemAria.menuItemProps as Record<string, unknown>;
1389
+ if (!hasPrimitiveLabel() && rest["aria-label"] == null) {
1390
+ delete rest["aria-labelledby"];
1391
+ }
1392
+ const selection = activeSectionSelection();
1393
+ const selectionMode = selection?.selectionMode();
1394
+ if (selectionMode) {
1395
+ rest.role =
1396
+ selectionMode === "single"
1397
+ ? "menuitemradio"
1398
+ : selectionMode === "multiple"
1399
+ ? "menuitemcheckbox"
1400
+ : "menuitem";
1401
+ if (selectionMode !== "none") {
1402
+ rest["aria-checked"] = selection?.isSelected(local.id) ?? false;
1403
+ } else {
1404
+ delete rest["aria-checked"];
1405
+ }
1406
+ rest["data-selected"] = selection?.isSelected(local.id) || undefined;
1407
+ }
519
1408
  return rest;
520
1409
  };
521
1410
  const cleanHoverProps = () => {
522
1411
  const { ref: _ref2, ...rest } = hoverProps as Record<string, unknown>;
523
1412
  return rest;
524
1413
  };
1414
+ const domProps = createMemo(() =>
1415
+ filterDOMProps(ariaProps as Record<string, unknown>, { global: true }),
1416
+ );
1417
+ const draggableItem = createMemo(() => {
1418
+ if (!menuContext?.dragAndDropHooks?.useDraggableItem || !menuContext.dragState)
1419
+ return undefined;
1420
+ return menuContext.dragAndDropHooks.useDraggableItem(
1421
+ {
1422
+ key: local.id as string | number,
1423
+ },
1424
+ menuContext.dragState as Parameters<NonNullable<DragAndDropHooks<T>["useDraggableItem"]>>[1],
1425
+ );
1426
+ });
1427
+ const droppableItem = createMemo(() => {
1428
+ if (!menuContext?.dragAndDropHooks?.useDroppableItem || !menuContext.dropState)
1429
+ return undefined;
1430
+ return menuContext.dragAndDropHooks.useDroppableItem(
1431
+ {
1432
+ key: local.id as string | number,
1433
+ },
1434
+ menuContext.dropState as Parameters<NonNullable<DragAndDropHooks<T>["useDroppableItem"]>>[1],
1435
+ () => ref(),
1436
+ );
1437
+ });
1438
+
1439
+ const isLink = () => !!local.href;
1440
+
1441
+ const cleanItemPropsForLink = () => {
1442
+ const all = cleanItemProps();
1443
+ const { href: _href, target: _target, rel: _rel, download: _download, ...rest } = all;
1444
+ return rest;
1445
+ };
1446
+
1447
+ const linkDomProps = () => {
1448
+ const all = cleanItemProps();
1449
+ const result: Record<string, unknown> = {};
1450
+ if (all.href !== undefined) result.href = all.href;
1451
+ if (all.target !== undefined) result.target = all.target;
1452
+ if (all.rel !== undefined) result.rel = all.rel;
1453
+ if (all.download !== undefined) result.download = all.download;
1454
+ return result;
1455
+ };
1456
+
1457
+ const dataAttrs = () => {
1458
+ const selection = activeSectionSelection();
1459
+ return {
1460
+ "data-focused": itemAria.isFocused() || undefined,
1461
+ "data-focus-visible": itemAria.isFocusVisible() || undefined,
1462
+ "data-pressed": itemAria.isPressed() || undefined,
1463
+ "data-hovered": isHovered() || undefined,
1464
+ "data-disabled": itemAria.isDisabled() || undefined,
1465
+ "data-selected": (selection?.isSelected(local.id) ?? itemAria.isSelected()) || undefined,
1466
+ "data-has-submenu": Boolean(contextProps()["aria-haspopup"]) || undefined,
1467
+ "data-open": contextProps()["aria-expanded"] === true || undefined,
1468
+ "data-dragging": draggableItem()?.isDragging || undefined,
1469
+ "data-drop-target": droppableItem()?.isDropTarget || undefined,
1470
+ };
1471
+ };
1472
+
1473
+ const childContent = () =>
1474
+ hasPrimitiveLabel() ? (
1475
+ <span {...itemAria.labelProps}>{renderProps.renderChildren()}</span>
1476
+ ) : (
1477
+ renderProps.renderChildren()
1478
+ );
1479
+ const setResolvedItemRef = (el: HTMLLIElement | null) => {
1480
+ setRef(el);
1481
+ itemContext?.setItemRef?.(el);
1482
+ if (el) assignRef(local.ref, el);
1483
+ };
1484
+ const menuItemProps = () =>
1485
+ ({
1486
+ ref: setResolvedItemRef,
1487
+ ...mergeProps(
1488
+ cleanItemProps(),
1489
+ contextProps() as Record<string, unknown>,
1490
+ domProps(),
1491
+ cleanHoverProps(),
1492
+ (draggableItem()?.dragProps as Record<string, unknown> | undefined) ?? {},
1493
+ (droppableItem()?.dropProps as Record<string, unknown> | undefined) ?? {},
1494
+ ),
1495
+ class: renderProps.class(),
1496
+ style: renderProps.style(),
1497
+ ...dataAttrs(),
1498
+ children: childContent(),
1499
+ }) as JSX.HTMLAttributes<HTMLLIElement>;
1500
+ const linkMenuItemProps = () =>
1501
+ ({
1502
+ ...mergeProps(
1503
+ cleanItemPropsForLink(),
1504
+ contextProps() as Record<string, unknown>,
1505
+ domProps(),
1506
+ cleanHoverProps(),
1507
+ linkDomProps(),
1508
+ (draggableItem()?.dragProps as Record<string, unknown> | undefined) ?? {},
1509
+ (droppableItem()?.dropProps as Record<string, unknown> | undefined) ?? {},
1510
+ ),
1511
+ class: renderProps.class(),
1512
+ style: renderProps.style(),
1513
+ ...dataAttrs(),
1514
+ children: childContent(),
1515
+ }) as JSX.HTMLAttributes<HTMLLIElement>;
1516
+
1517
+ if (local.render && !isLink()) {
1518
+ return local.render(menuItemProps(), renderValues());
1519
+ }
525
1520
 
526
1521
  return (
527
- <li
528
- {...cleanItemProps()}
529
- {...cleanHoverProps()}
530
- class={renderProps.class()}
531
- style={renderProps.style()}
532
- data-focused={itemAria.isFocused() || undefined}
533
- data-focus-visible={itemAria.isFocusVisible() || undefined}
534
- data-pressed={itemAria.isPressed() || undefined}
535
- data-hovered={isHovered() || undefined}
536
- data-disabled={itemAria.isDisabled() || undefined}
1522
+ <Show
1523
+ when={isLink()}
1524
+ fallback={
1525
+ <li
1526
+ ref={(el) => {
1527
+ setRef(el);
1528
+ itemContext?.setItemRef?.(el);
1529
+ assignRef(local.ref, el);
1530
+ }}
1531
+ {...mergeProps(
1532
+ cleanItemProps(),
1533
+ contextProps() as Record<string, unknown>,
1534
+ domProps(),
1535
+ cleanHoverProps(),
1536
+ (draggableItem()?.dragProps as Record<string, unknown> | undefined) ?? {},
1537
+ (droppableItem()?.dropProps as Record<string, unknown> | undefined) ?? {},
1538
+ )}
1539
+ class={renderProps.class()}
1540
+ style={renderProps.style()}
1541
+ {...dataAttrs()}
1542
+ >
1543
+ {childContent()}
1544
+ </li>
1545
+ }
537
1546
  >
538
- {renderProps.renderChildren()}
539
- </li>
1547
+ <li
1548
+ ref={(el) => {
1549
+ setRef(el);
1550
+ itemContext?.setItemRef?.(el);
1551
+ assignRef(local.ref, el);
1552
+ }}
1553
+ role="presentation"
1554
+ >
1555
+ {local.render ? (
1556
+ local.render(linkMenuItemProps(), renderValues())
1557
+ ) : (
1558
+ <a
1559
+ {...mergeProps(
1560
+ cleanItemPropsForLink(),
1561
+ contextProps() as Record<string, unknown>,
1562
+ domProps(),
1563
+ cleanHoverProps(),
1564
+ linkDomProps(),
1565
+ (draggableItem()?.dragProps as Record<string, unknown> | undefined) ?? {},
1566
+ (droppableItem()?.dropProps as Record<string, unknown> | undefined) ?? {},
1567
+ )}
1568
+ class={renderProps.class()}
1569
+ style={renderProps.style()}
1570
+ {...dataAttrs()}
1571
+ >
1572
+ {childContent()}
1573
+ </a>
1574
+ )}
1575
+ </li>
1576
+ </Show>
1577
+ );
1578
+ }
1579
+
1580
+ /**
1581
+ * Section primitive alias for Menu composition parity.
1582
+ */
1583
+ export function MenuSection(props: MenuSectionProps): JSX.Element {
1584
+ const [selectionProps, sectionProps] = splitProps(props, [
1585
+ "selectionMode",
1586
+ "selectionBehavior",
1587
+ "disallowEmptySelection",
1588
+ "selectedKeys",
1589
+ "defaultSelectedKeys",
1590
+ "onSelectionChange",
1591
+ "disabledKeys",
1592
+ "disabledBehavior",
1593
+ "allowDuplicateSelectionEvents",
1594
+ "shouldCloseOnSelect",
1595
+ ]);
1596
+
1597
+ const selectionState = createSelectionState({
1598
+ get selectionMode() {
1599
+ return selectionProps.selectionMode ?? "none";
1600
+ },
1601
+ get selectionBehavior() {
1602
+ return selectionProps.selectionBehavior;
1603
+ },
1604
+ get disallowEmptySelection() {
1605
+ return selectionProps.disallowEmptySelection;
1606
+ },
1607
+ get selectedKeys() {
1608
+ return selectionProps.selectionMode ? selectionProps.selectedKeys : undefined;
1609
+ },
1610
+ get defaultSelectedKeys() {
1611
+ return selectionProps.selectionMode ? selectionProps.defaultSelectedKeys : undefined;
1612
+ },
1613
+ get onSelectionChange() {
1614
+ return selectionProps.selectionMode ? selectionProps.onSelectionChange : undefined;
1615
+ },
1616
+ get disabledKeys() {
1617
+ return selectionProps.disabledKeys;
1618
+ },
1619
+ get disabledBehavior() {
1620
+ return selectionProps.disabledBehavior;
1621
+ },
1622
+ get allowDuplicateSelectionEvents() {
1623
+ return selectionProps.allowDuplicateSelectionEvents;
1624
+ },
1625
+ });
1626
+
1627
+ const sectionSelection: MenuSectionSelectionContextValue = {
1628
+ selectionMode: selectionState.selectionMode,
1629
+ isSelected: selectionState.isSelected,
1630
+ isDisabled: selectionState.isDisabled,
1631
+ select(key, event) {
1632
+ selectionState.select(key, event);
1633
+ },
1634
+ shouldCloseOnSelect() {
1635
+ return selectionProps.shouldCloseOnSelect;
1636
+ },
1637
+ };
1638
+
1639
+ return (
1640
+ <MenuSectionSelectionContext.Provider value={sectionSelection}>
1641
+ <Section {...sectionProps} />
1642
+ </MenuSectionSelectionContext.Provider>
540
1643
  );
541
1644
  }
542
1645
 
543
- // Attach Item as a static property
544
1646
  Menu.Item = MenuItem;