@proyecto-viviana/solidaria-components 0.2.9 → 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 (222) hide show
  1. package/README.md +39 -272
  2. package/dist/ActionBar.d.ts +21 -13
  3. package/dist/ActionBar.d.ts.map +1 -1
  4. package/dist/ActionGroup.d.ts +8 -8
  5. package/dist/ActionGroup.d.ts.map +1 -1
  6. package/dist/Alert.d.ts +5 -5
  7. package/dist/Alert.d.ts.map +1 -1
  8. package/dist/Autocomplete.d.ts +5 -5
  9. package/dist/Autocomplete.d.ts.map +1 -1
  10. package/dist/Breadcrumbs.d.ts +18 -7
  11. package/dist/Breadcrumbs.d.ts.map +1 -1
  12. package/dist/Button.d.ts +24 -5
  13. package/dist/Button.d.ts.map +1 -1
  14. package/dist/Calendar.d.ts +38 -7
  15. package/dist/Calendar.d.ts.map +1 -1
  16. package/dist/Checkbox.d.ts +32 -7
  17. package/dist/Checkbox.d.ts.map +1 -1
  18. package/dist/Collection.d.ts +19 -14
  19. package/dist/Collection.d.ts.map +1 -1
  20. package/dist/Color.d.ts +103 -14
  21. package/dist/Color.d.ts.map +1 -1
  22. package/dist/ColorEditor.d.ts +6 -6
  23. package/dist/ColorEditor.d.ts.map +1 -1
  24. package/dist/ComboBox.d.ts +85 -19
  25. package/dist/ComboBox.d.ts.map +1 -1
  26. package/dist/ContextualHelpTrigger.d.ts +2 -2
  27. package/dist/ContextualHelpTrigger.d.ts.map +1 -1
  28. package/dist/DateField.d.ts +8 -6
  29. package/dist/DateField.d.ts.map +1 -1
  30. package/dist/DatePicker.d.ts +53 -22
  31. package/dist/DatePicker.d.ts.map +1 -1
  32. package/dist/DateRangePickerContext.d.ts +30 -0
  33. package/dist/DateRangePickerContext.d.ts.map +1 -0
  34. package/dist/Dialog.d.ts +5 -5
  35. package/dist/Dialog.d.ts.map +1 -1
  36. package/dist/Disclosure.d.ts +23 -5
  37. package/dist/Disclosure.d.ts.map +1 -1
  38. package/dist/DragAndDrop.d.ts +6 -6
  39. package/dist/DragAndDrop.d.ts.map +1 -1
  40. package/dist/DragPreview.d.ts +2 -2
  41. package/dist/DragPreview.d.ts.map +1 -1
  42. package/dist/DropZone.d.ts +4 -4
  43. package/dist/DropZone.d.ts.map +1 -1
  44. package/dist/FieldError.d.ts +9 -5
  45. package/dist/FieldError.d.ts.map +1 -1
  46. package/dist/FileTrigger.d.ts +3 -3
  47. package/dist/FileTrigger.d.ts.map +1 -1
  48. package/dist/Focusable.d.ts +2 -2
  49. package/dist/Focusable.d.ts.map +1 -1
  50. package/dist/Form.d.ts +18 -4
  51. package/dist/Form.d.ts.map +1 -1
  52. package/dist/GridList.d.ts +32 -12
  53. package/dist/GridList.d.ts.map +1 -1
  54. package/dist/HiddenDateInput.d.ts +26 -0
  55. package/dist/HiddenDateInput.d.ts.map +1 -0
  56. package/dist/HiddenTimeInput.d.ts +25 -0
  57. package/dist/HiddenTimeInput.d.ts.map +1 -0
  58. package/dist/Icon.d.ts +5 -5
  59. package/dist/Icon.d.ts.map +1 -1
  60. package/dist/Keyboard.d.ts +1 -1
  61. package/dist/Landmark.d.ts +3 -3
  62. package/dist/Landmark.d.ts.map +1 -1
  63. package/dist/Link.d.ts +10 -4
  64. package/dist/Link.d.ts.map +1 -1
  65. package/dist/ListBox.d.ts +32 -12
  66. package/dist/ListBox.d.ts.map +1 -1
  67. package/dist/ListDropTargetDelegate.d.ts +6 -6
  68. package/dist/ListDropTargetDelegate.d.ts.map +1 -1
  69. package/dist/Menu.d.ts +65 -14
  70. package/dist/Menu.d.ts.map +1 -1
  71. package/dist/Meter.d.ts +3 -3
  72. package/dist/Meter.d.ts.map +1 -1
  73. package/dist/Modal.d.ts +5 -5
  74. package/dist/Modal.d.ts.map +1 -1
  75. package/dist/NumberField.d.ts +8 -12
  76. package/dist/NumberField.d.ts.map +1 -1
  77. package/dist/Popover.d.ts +28 -5
  78. package/dist/Popover.d.ts.map +1 -1
  79. package/dist/Pressable.d.ts +2 -2
  80. package/dist/Pressable.d.ts.map +1 -1
  81. package/dist/ProgressBar.d.ts +5 -3
  82. package/dist/ProgressBar.d.ts.map +1 -1
  83. package/dist/RadioGroup.d.ts +43 -9
  84. package/dist/RadioGroup.d.ts.map +1 -1
  85. package/dist/RangeCalendar.d.ts +34 -7
  86. package/dist/RangeCalendar.d.ts.map +1 -1
  87. package/dist/RouterProvider.d.ts +2 -2
  88. package/dist/RouterProvider.d.ts.map +1 -1
  89. package/dist/SearchField.d.ts +23 -20
  90. package/dist/SearchField.d.ts.map +1 -1
  91. package/dist/Select.d.ts +41 -11
  92. package/dist/Select.d.ts.map +1 -1
  93. package/dist/SelectionIndicator.d.ts +3 -3
  94. package/dist/SelectionIndicator.d.ts.map +1 -1
  95. package/dist/Separator.d.ts +9 -3
  96. package/dist/Separator.d.ts.map +1 -1
  97. package/dist/SharedElementTransition.d.ts +6 -4
  98. package/dist/SharedElementTransition.d.ts.map +1 -1
  99. package/dist/Slider.d.ts +12 -8
  100. package/dist/Slider.d.ts.map +1 -1
  101. package/dist/StepList.d.ts +90 -0
  102. package/dist/StepList.d.ts.map +1 -0
  103. package/dist/Switch.d.ts +11 -5
  104. package/dist/Switch.d.ts.map +1 -1
  105. package/dist/Table.d.ts +187 -23
  106. package/dist/Table.d.ts.map +1 -1
  107. package/dist/Tabs.d.ts +45 -9
  108. package/dist/Tabs.d.ts.map +1 -1
  109. package/dist/TagGroup.d.ts +12 -10
  110. package/dist/TagGroup.d.ts.map +1 -1
  111. package/dist/Text.d.ts +2 -2
  112. package/dist/TextField.d.ts +15 -11
  113. package/dist/TextField.d.ts.map +1 -1
  114. package/dist/TimeField.d.ts +6 -6
  115. package/dist/TimeField.d.ts.map +1 -1
  116. package/dist/Toast.d.ts +29 -14
  117. package/dist/Toast.d.ts.map +1 -1
  118. package/dist/ToggleButton.d.ts +11 -5
  119. package/dist/ToggleButton.d.ts.map +1 -1
  120. package/dist/ToggleButtonGroup.d.ts +7 -7
  121. package/dist/ToggleButtonGroup.d.ts.map +1 -1
  122. package/dist/Toolbar.d.ts +7 -3
  123. package/dist/Toolbar.d.ts.map +1 -1
  124. package/dist/Tooltip.d.ts +50 -8
  125. package/dist/Tooltip.d.ts.map +1 -1
  126. package/dist/Tree.d.ts +66 -17
  127. package/dist/Tree.d.ts.map +1 -1
  128. package/dist/Virtualizer.d.ts +12 -12
  129. package/dist/Virtualizer.d.ts.map +1 -1
  130. package/dist/VirtualizerLayouts.d.ts +2 -2
  131. package/dist/VirtualizerLayouts.d.ts.map +1 -1
  132. package/dist/VisuallyHidden.d.ts +1 -1
  133. package/dist/VisuallyHidden.d.ts.map +1 -1
  134. package/dist/contexts.d.ts +5 -1
  135. package/dist/contexts.d.ts.map +1 -1
  136. package/dist/index.d.ts +73 -71
  137. package/dist/index.d.ts.map +1 -1
  138. package/dist/index.js +23247 -18564
  139. package/dist/index.js.map +1 -1
  140. package/dist/index.jsx +18110 -0
  141. package/dist/index.jsx.map +1 -0
  142. package/dist/useDragAndDrop.d.ts +13 -13
  143. package/dist/useDragAndDrop.d.ts.map +1 -1
  144. package/dist/utils.d.ts +2 -2
  145. package/dist/utils.d.ts.map +1 -1
  146. package/dist/virtualizer/Layout.d.ts +1 -1
  147. package/dist/virtualizer/Layout.d.ts.map +1 -1
  148. package/package.json +31 -32
  149. package/src/ActionBar.tsx +75 -72
  150. package/src/ActionGroup.tsx +53 -61
  151. package/src/Alert.tsx +17 -42
  152. package/src/Autocomplete.tsx +39 -44
  153. package/src/Breadcrumbs.tsx +149 -80
  154. package/src/Button.tsx +267 -70
  155. package/src/Calendar.tsx +218 -138
  156. package/src/Checkbox.tsx +413 -121
  157. package/src/Collection.tsx +67 -58
  158. package/src/Color.tsx +803 -380
  159. package/src/ColorEditor.tsx +131 -149
  160. package/src/ComboBox.tsx +414 -249
  161. package/src/ContextualHelpTrigger.tsx +86 -74
  162. package/src/DateField.tsx +185 -91
  163. package/src/DatePicker.tsx +524 -213
  164. package/src/DateRangePickerContext.tsx +44 -0
  165. package/src/Dialog.tsx +156 -118
  166. package/src/Disclosure.tsx +127 -80
  167. package/src/DragAndDrop.tsx +60 -54
  168. package/src/DragPreview.tsx +13 -11
  169. package/src/DropZone.tsx +42 -22
  170. package/src/FieldError.tsx +45 -23
  171. package/src/FileTrigger.tsx +19 -19
  172. package/src/Focusable.tsx +21 -24
  173. package/src/Form.tsx +71 -16
  174. package/src/GridList.tsx +273 -197
  175. package/src/HiddenDateInput.tsx +153 -0
  176. package/src/HiddenTimeInput.tsx +133 -0
  177. package/src/Icon.tsx +22 -43
  178. package/src/Keyboard.tsx +3 -3
  179. package/src/Landmark.tsx +37 -63
  180. package/src/Link.tsx +125 -75
  181. package/src/ListBox.tsx +332 -233
  182. package/src/ListDropTargetDelegate.ts +81 -80
  183. package/src/Menu.tsx +1023 -274
  184. package/src/Meter.tsx +38 -56
  185. package/src/Modal.tsx +243 -175
  186. package/src/NumberField.tsx +139 -143
  187. package/src/Popover.tsx +386 -233
  188. package/src/Pressable.tsx +21 -21
  189. package/src/ProgressBar.tsx +48 -57
  190. package/src/RadioGroup.tsx +524 -122
  191. package/src/RangeCalendar.tsx +157 -90
  192. package/src/RouterProvider.tsx +30 -47
  193. package/src/SearchField.tsx +362 -143
  194. package/src/Select.tsx +656 -233
  195. package/src/SelectionIndicator.tsx +18 -15
  196. package/src/Separator.tsx +47 -49
  197. package/src/SharedElementTransition.tsx +103 -97
  198. package/src/Slider.tsx +138 -98
  199. package/src/StepList.tsx +272 -0
  200. package/src/Switch.tsx +93 -46
  201. package/src/Table.tsx +1308 -342
  202. package/src/Tabs.tsx +324 -103
  203. package/src/TagGroup.tsx +139 -126
  204. package/src/Text.tsx +3 -3
  205. package/src/TextField.tsx +389 -79
  206. package/src/TimeField.tsx +136 -76
  207. package/src/Toast.tsx +209 -157
  208. package/src/ToggleButton.tsx +47 -37
  209. package/src/ToggleButtonGroup.tsx +39 -34
  210. package/src/Toolbar.tsx +54 -69
  211. package/src/Tooltip.tsx +387 -119
  212. package/src/Tree.tsx +651 -368
  213. package/src/Virtualizer.tsx +208 -180
  214. package/src/VirtualizerLayouts.ts +45 -30
  215. package/src/VisuallyHidden.tsx +19 -19
  216. package/src/contexts.ts +29 -37
  217. package/src/index.ts +110 -195
  218. package/src/useDragAndDrop.ts +87 -71
  219. package/src/utils.tsx +40 -55
  220. package/src/virtualizer/Layout.ts +14 -22
  221. package/dist/index.ssr.js +0 -16996
  222. package/dist/index.ssr.js.map +0 -1
package/src/Menu.tsx CHANGED
@@ -11,11 +11,13 @@ import {
11
11
  createEffect,
12
12
  createMemo,
13
13
  createSignal,
14
+ createUniqueId,
15
+ onCleanup,
14
16
  splitProps,
15
17
  useContext,
16
18
  For,
17
19
  Show,
18
- } from 'solid-js';
20
+ } from "solid-js";
19
21
  import {
20
22
  createMenu,
21
23
  createMenuItem,
@@ -29,24 +31,29 @@ import {
29
31
  type AriaMenuProps,
30
32
  type AriaMenuItemProps,
31
33
  type AriaMenuTriggerProps,
32
- } from '@proyecto-viviana/solidaria';
34
+ } from "@proyecto-viviana/solidaria";
33
35
  import {
36
+ createSelectionState,
34
37
  createMenuState,
35
38
  createMenuTriggerState,
36
39
  type MenuState,
40
+ type MenuStateProps,
37
41
  type OverlayTriggerState,
38
42
  type Key,
39
43
  type DropTarget,
40
- } from '@proyecto-viviana/solid-stately';
44
+ type SelectionMode,
45
+ type SelectionStateProps,
46
+ } from "@proyecto-viviana/solid-stately";
41
47
  import {
42
48
  type RenderChildren,
43
49
  type ClassNameOrFunction,
44
50
  type StyleOrFunction,
45
51
  type SlotProps,
46
52
  useRenderProps,
47
- } from './utils';
48
- import { SharedElementTransition } from './SharedElementTransition';
49
- import { type DragAndDropHooks } from './useDragAndDrop';
53
+ filterDOMProps,
54
+ } from "./utils";
55
+ import { SharedElementTransition } from "./SharedElementTransition";
56
+ import { type DragAndDropHooks } from "./useDragAndDrop";
50
57
  import {
51
58
  CollectionRendererContext,
52
59
  Section,
@@ -58,31 +65,41 @@ import {
58
65
  useCollectionRenderer,
59
66
  flattenCollectionEntries,
60
67
  isCollectionSection,
61
- } from './Collection';
62
- import { useVirtualizerContext } from './Virtualizer';
68
+ } from "./Collection";
69
+ import { useVirtualizerContext } from "./Virtualizer";
63
70
  import {
64
71
  getNormalizedDropTargetKey,
65
72
  mergePersistedKeysIntoVirtualRange,
66
73
  useDndPersistedKeys,
67
74
  useRenderDropIndicator,
68
- } from './DragAndDrop';
69
-
70
- // ============================================
71
- // TYPES
72
- // ============================================
75
+ } from "./DragAndDrop";
76
+ import { PopoverTriggerContext } from "./contexts";
73
77
 
74
78
  export interface MenuRenderProps {
75
79
  /** Whether the menu is focused. */
76
80
  isFocused: boolean;
77
81
  /** Whether the menu is open. */
78
82
  isOpen: boolean;
83
+ /** Whether the menu has no items. */
84
+ isEmpty: boolean;
79
85
  }
80
86
 
81
87
  export interface MenuProps<T>
82
- extends Omit<AriaMenuProps, 'children'>,
83
- 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
+ > {
84
101
  /** The items to render in the menu. */
85
- items: CollectionEntry<T>[];
102
+ items?: CollectionEntry<T>[];
86
103
  /** Function to get the key from an item. */
87
104
  getKey?: (item: T) => Key;
88
105
  /** Function to get the text value from an item. */
@@ -96,11 +113,24 @@ export interface MenuProps<T>
96
113
  /** Handler called when the menu should close. */
97
114
  onClose?: () => void;
98
115
  /** The children of the component. A function may be provided to render each item. */
99
- 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;
100
119
  /** The CSS className for the element. */
101
120
  class?: ClassNameOrFunction<MenuRenderProps>;
102
121
  /** The inline style for the element. */
103
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;
104
134
  /** Drag and drop hooks from `useDragAndDrop`. */
105
135
  dragAndDropHooks?: DragAndDropHooks<T>;
106
136
  }
@@ -108,6 +138,8 @@ export interface MenuProps<T>
108
138
  export interface MenuItemRenderProps {
109
139
  /** Whether the item is selected. */
110
140
  isSelected: boolean;
141
+ /** The parent menu selection mode. */
142
+ selectionMode: SelectionMode;
111
143
  /** Whether the item is focused. */
112
144
  isFocused: boolean;
113
145
  /** Whether the item has keyboard focus. */
@@ -118,11 +150,13 @@ export interface MenuItemRenderProps {
118
150
  isHovered: boolean;
119
151
  /** Whether the item is disabled. */
120
152
  isDisabled: boolean;
153
+ /** Whether the item opens a submenu. */
154
+ hasSubmenu: boolean;
155
+ /** Whether the submenu is currently open. */
156
+ isOpen: boolean;
121
157
  }
122
158
 
123
- export interface MenuItemProps<T>
124
- extends Omit<AriaMenuItemProps, 'children' | 'key'>,
125
- SlotProps {
159
+ export interface MenuItemProps<T> extends Omit<AriaMenuItemProps, "children" | "key">, SlotProps {
126
160
  /** The unique key for the item. */
127
161
  id: Key;
128
162
  /** The item value. */
@@ -137,6 +171,27 @@ export interface MenuItemProps<T>
137
171
  textValue?: string;
138
172
  /** Handler called when the item is activated. */
139
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;
140
195
  }
141
196
 
142
197
  export interface MenuTriggerRenderProps {
@@ -154,7 +209,7 @@ export interface MenuTriggerRenderProps {
154
209
  isDisabled: boolean;
155
210
  }
156
211
 
157
- export interface MenuTriggerProps extends Omit<AriaMenuTriggerProps, 'children'>, SlotProps {
212
+ export interface MenuTriggerProps extends Omit<AriaMenuTriggerProps, "children">, SlotProps {
158
213
  /** The children of the trigger (typically a Button and Menu). */
159
214
  children: JSX.Element;
160
215
  /** Whether the menu trigger is disabled. */
@@ -167,11 +222,18 @@ export interface MenuTriggerProps extends Omit<AriaMenuTriggerProps, 'children'>
167
222
  onOpenChange?: (isOpen: boolean) => void;
168
223
  }
169
224
 
170
- export interface SubmenuTriggerProps extends MenuTriggerProps {}
171
-
172
- // ============================================
173
- // CONTEXT
174
- // ============================================
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
+ }
175
237
 
176
238
  interface MenuContextValue<T> {
177
239
  state: MenuState<T>;
@@ -181,28 +243,74 @@ interface MenuContextValue<T> {
181
243
  dropState?: unknown;
182
244
  }
183
245
 
246
+ type MenuSelectionEvent = { shiftKey?: boolean; ctrlKey?: boolean; metaKey?: boolean };
247
+
184
248
  interface MenuTriggerContextValue {
185
249
  state: OverlayTriggerState;
186
250
  triggerProps: JSX.HTMLAttributes<HTMLElement>;
187
251
  menuProps: JSX.HTMLAttributes<HTMLElement>;
188
252
  }
189
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
+
190
286
  export const MenuContext = createContext<MenuContextValue<unknown> | null>(null);
191
287
  export const MenuStateContext = createContext<MenuState<unknown> | null>(null);
192
288
  export const MenuTriggerContext = createContext<MenuTriggerContextValue | null>(null);
193
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;
194
297
 
195
- // ============================================
196
- // COMPONENTS
197
- // ============================================
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
+ }
303
+
304
+ function resolveBoolean(value: unknown): boolean {
305
+ return typeof value === "function" ? Boolean((value as () => unknown)()) : Boolean(value);
306
+ }
198
307
 
199
308
  /**
200
309
  * A menu trigger wraps a button and menu, handling the open/close state.
201
310
  */
202
311
  export function MenuTrigger(props: MenuTriggerProps): JSX.Element {
203
- const [local, stateProps] = splitProps(props, ['slot']);
312
+ const [local, stateProps] = splitProps(props, ["slot"]);
204
313
 
205
- // Create trigger state
206
314
  const state = createMenuTriggerState({
207
315
  get isOpen() {
208
316
  return stateProps.isOpen;
@@ -215,14 +323,13 @@ export function MenuTrigger(props: MenuTriggerProps): JSX.Element {
215
323
  },
216
324
  });
217
325
 
218
- // Create trigger aria props
219
- const { menuTriggerProps, menuProps } = createMenuTrigger(
326
+ const menuTrigger = createMenuTrigger(
220
327
  {
221
328
  get isDisabled() {
222
329
  return stateProps.isDisabled;
223
330
  },
224
331
  },
225
- state
332
+ state,
226
333
  );
227
334
 
228
335
  return (
@@ -230,8 +337,12 @@ export function MenuTrigger(props: MenuTriggerProps): JSX.Element {
230
337
  <MenuTriggerContext.Provider
231
338
  value={{
232
339
  state,
233
- triggerProps: menuTriggerProps,
234
- menuProps,
340
+ get triggerProps() {
341
+ return menuTrigger.menuTriggerProps;
342
+ },
343
+ get menuProps() {
344
+ return menuTrigger.menuProps;
345
+ },
235
346
  }}
236
347
  >
237
348
  {props.children}
@@ -241,13 +352,124 @@ export function MenuTrigger(props: MenuTriggerProps): JSX.Element {
241
352
  }
242
353
 
243
354
  export function SubmenuTrigger(props: SubmenuTriggerProps): JSX.Element {
244
- return <MenuTrigger {...props} />;
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>
465
+ );
245
466
  }
246
467
 
247
468
  /**
248
469
  * A button that opens a menu.
249
470
  */
250
- export interface MenuButtonProps extends SlotProps, Omit<JSX.HTMLAttributes<HTMLButtonElement>, 'class' | 'style' | 'children'> {
471
+ export interface MenuButtonProps
472
+ extends SlotProps, Omit<JSX.HTMLAttributes<HTMLButtonElement>, "class" | "style" | "children"> {
251
473
  /** The children of the button. A function may be provided to receive render props. */
252
474
  children?: RenderChildren<MenuTriggerRenderProps>;
253
475
  /** The CSS className for the element. */
@@ -258,19 +480,34 @@ export interface MenuButtonProps extends SlotProps, Omit<JSX.HTMLAttributes<HTML
258
480
  isDisabled?: boolean;
259
481
  }
260
482
 
261
- export interface MenuSectionProps extends SectionProps {}
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
+ }
262
501
 
263
502
  export function MenuButton(props: MenuButtonProps): JSX.Element {
264
- const [local, domProps] = splitProps(props, ['class', 'style', 'slot', 'isDisabled', 'children']);
503
+ const [local, domProps] = splitProps(props, ["class", "style", "slot", "isDisabled", "children"]);
265
504
 
266
- // Get trigger context
267
505
  const context = useContext(MenuTriggerContext);
268
506
  if (!context) {
269
- throw new Error('MenuButton must be used within a MenuTrigger');
507
+ throw new Error("MenuButton must be used within a MenuTrigger");
270
508
  }
271
- const { state, triggerProps } = context;
509
+ const { state } = context;
272
510
 
273
- // Create button aria props for proper press handling
274
511
  const buttonAria = createButton({
275
512
  get isDisabled() {
276
513
  return local.isDisabled;
@@ -280,17 +517,14 @@ export function MenuButton(props: MenuButtonProps): JSX.Element {
280
517
  },
281
518
  });
282
519
 
283
- // Create focus ring
284
520
  const { isFocused, isFocusVisible, focusProps } = createFocusRing();
285
521
 
286
- // Create hover
287
522
  const { isHovered, hoverProps } = createHover({
288
523
  get isDisabled() {
289
524
  return local.isDisabled;
290
525
  },
291
526
  });
292
527
 
293
- // Render props values
294
528
  const renderValues = createMemo<MenuTriggerRenderProps>(() => ({
295
529
  isOpen: state.isOpen(),
296
530
  isFocused: isFocused(),
@@ -300,20 +534,26 @@ export function MenuButton(props: MenuButtonProps): JSX.Element {
300
534
  isDisabled: !!local.isDisabled,
301
535
  }));
302
536
 
303
- // Resolve render props
304
537
  const renderProps = useRenderProps(
305
538
  {
306
539
  children: props.children,
307
540
  class: local.class,
308
541
  style: local.style,
309
- defaultClassName: 'solidaria-MenuButton',
542
+ defaultClassName: "solidaria-MenuButton",
310
543
  },
311
- renderValues
544
+ renderValues,
312
545
  );
313
546
 
314
- // Remove ref from spread props
547
+ const resolvedTriggerProps = () => context.triggerProps as Record<string, unknown>;
315
548
  const cleanTriggerProps = () => {
316
- 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();
317
557
  return rest;
318
558
  };
319
559
  const cleanButtonProps = () => {
@@ -339,6 +579,10 @@ export function MenuButton(props: MenuButtonProps): JSX.Element {
339
579
  type="button"
340
580
  class={renderProps.class()}
341
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}
342
586
  data-open={state.isOpen() || undefined}
343
587
  data-focused={isFocused() || undefined}
344
588
  data-focus-visible={isFocusVisible() || undefined}
@@ -357,41 +601,147 @@ export function MenuButton(props: MenuButtonProps): JSX.Element {
357
601
  export function Menu<T>(props: MenuProps<T>): JSX.Element {
358
602
  const [local, stateProps, ariaProps] = splitProps(
359
603
  props,
360
- ['children', 'class', 'style', 'slot'],
361
- ['items', 'getKey', 'getTextValue', 'getDisabled', 'disabledKeys', 'onAction', 'onClose', 'dragAndDropHooks']
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
+ ],
362
632
  );
363
633
 
364
- // Get trigger context if available
365
634
  const triggerContext = useContext(MenuTriggerContext);
366
635
 
367
- // Ref for the menu element (for click outside detection)
368
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
+ };
369
689
 
370
690
  const flatItems = createMemo<T[]>(() => {
371
- return flattenCollectionEntries(stateProps.items);
691
+ return flattenCollectionEntries(stateProps.items ?? []);
372
692
  });
373
693
 
374
- const hasSections = createMemo(() => stateProps.items.some((item) => isCollectionSection(item)));
694
+ const hasSections = createMemo(() =>
695
+ (stateProps.items ?? []).some((item) => isCollectionSection(item)),
696
+ );
375
697
 
376
- // Create menu state
377
698
  const state = createMenuState<T>({
378
699
  get items() {
379
- return flatItems();
700
+ return usesStaticChildren() ? (staticItems() as T[]) : flatItems();
380
701
  },
381
702
  get getKey() {
382
- return stateProps.getKey;
703
+ return usesStaticChildren()
704
+ ? (item: T) => (item as StaticMenuCollectionItem).id
705
+ : stateProps.getKey;
383
706
  },
384
707
  get getTextValue() {
385
- return stateProps.getTextValue;
708
+ return usesStaticChildren()
709
+ ? (item: T) =>
710
+ (item as StaticMenuCollectionItem).textValue ??
711
+ String((item as StaticMenuCollectionItem).id)
712
+ : stateProps.getTextValue;
386
713
  },
387
714
  get getDisabled() {
388
- return stateProps.getDisabled;
715
+ return usesStaticChildren()
716
+ ? (item: T) => Boolean((item as StaticMenuCollectionItem).isDisabled)
717
+ : stateProps.getDisabled;
389
718
  },
390
719
  get disabledKeys() {
391
720
  return stateProps.disabledKeys;
392
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
+ },
393
743
  get onAction() {
394
- return stateProps.onAction;
744
+ return handleAction;
395
745
  },
396
746
  get onClose() {
397
747
  return stateProps.onClose ?? (() => triggerContext?.state.close());
@@ -400,13 +750,12 @@ export function Menu<T>(props: MenuProps<T>): JSX.Element {
400
750
 
401
751
  const resolveDisabled = (): boolean => {
402
752
  const disabled = ariaProps.isDisabled;
403
- if (typeof disabled === 'function') {
753
+ if (typeof disabled === "function") {
404
754
  return (disabled as () => boolean)();
405
755
  }
406
756
  return !!disabled;
407
757
  };
408
758
 
409
- // Create menu aria props
410
759
  const { menuProps, labelProps } = createMenu(
411
760
  {
412
761
  get isDisabled() {
@@ -416,28 +765,26 @@ export function Menu<T>(props: MenuProps<T>): JSX.Element {
416
765
  return ariaProps.label;
417
766
  },
418
767
  get onAction() {
419
- return stateProps.onAction;
768
+ return handleAction;
420
769
  },
421
770
  get onClose() {
422
771
  return stateProps.onClose ?? (() => triggerContext?.state.close());
423
772
  },
424
- get 'aria-label'() {
425
- return ariaProps['aria-label'];
773
+ get "aria-label"() {
774
+ return ariaProps["aria-label"];
426
775
  },
427
- get 'aria-labelledby'() {
428
- return ariaProps['aria-labelledby'];
776
+ get "aria-labelledby"() {
777
+ return ariaProps["aria-labelledby"];
429
778
  },
430
- get 'aria-describedby'() {
431
- return ariaProps['aria-describedby'];
779
+ get "aria-describedby"() {
780
+ return ariaProps["aria-describedby"];
432
781
  },
433
782
  },
434
- state
783
+ state,
435
784
  );
436
785
 
437
- // Create focus ring
438
786
  const { isFocused, focusProps } = createFocusRing();
439
787
 
440
- // Handle click outside to close menu
441
788
  createInteractOutside({
442
789
  ref: () => menuRef(),
443
790
  onInteractOutside: () => {
@@ -450,23 +797,21 @@ export function Menu<T>(props: MenuProps<T>): JSX.Element {
450
797
  },
451
798
  });
452
799
 
453
- // Render props values
454
800
  const renderValues = createMemo<MenuRenderProps>(() => ({
455
801
  isFocused: state.isFocused() || isFocused(),
456
802
  isOpen: triggerContext?.state.isOpen() ?? true,
803
+ isEmpty: state.collection().size === 0,
457
804
  }));
458
805
 
459
- // Resolve render props
460
806
  const renderProps = useRenderProps(
461
807
  {
462
808
  class: local.class,
463
809
  style: local.style,
464
- defaultClassName: 'solidaria-Menu',
810
+ defaultClassName: "solidaria-Menu",
465
811
  },
466
- renderValues
812
+ renderValues,
467
813
  );
468
814
 
469
- // Remove ref from spread props
470
815
  const cleanMenuProps = () => {
471
816
  const { ref: _ref1, ...rest } = menuProps as Record<string, unknown>;
472
817
  return rest;
@@ -480,28 +825,42 @@ export function Menu<T>(props: MenuProps<T>): JSX.Element {
480
825
  const { ref: _ref3, ...rest } = focusProps as Record<string, unknown>;
481
826
  return rest;
482
827
  };
828
+ const domProps = createMemo(() =>
829
+ filterDOMProps(ariaProps as Record<string, unknown>, { global: true }),
830
+ );
483
831
  const cleanLabelProps = () => {
484
832
  const { ref: _ref4, ...rest } = labelProps as Record<string, unknown>;
485
833
  return rest;
486
834
  };
835
+ const setResolvedMenuRef = (el: HTMLUListElement): void => {
836
+ setMenuRef(el);
837
+ assignRef(local.ref, el);
838
+ };
487
839
 
488
840
  // If inside a MenuTrigger, only render when open
489
841
  // If standalone (no trigger context), always render
490
- const shouldRender = () => triggerContext ? triggerContext.state.isOpen() : true;
842
+ const shouldRender = () => (triggerContext ? triggerContext.state.isOpen() : true);
491
843
  const parentCollectionRenderer = useCollectionRenderer<unknown>();
492
844
  const virtualizer = useVirtualizerContext();
493
- const getItemNodes = createMemo(() => Array.from(state.collection()).filter((node) => node.type === 'item'));
494
- const getDropTargetByIndex = (index: number, position: 'before' | 'after' | 'on'): DropTarget | null => {
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 => {
495
852
  const node = getItemNodes()[index];
496
853
  if (!node) return null;
497
- return { type: 'item', key: node.key, dropPosition: position };
854
+ return { type: "item", key: node.key, dropPosition: position };
498
855
  };
499
856
  const hasDroppableDnd = createMemo(() => {
500
857
  const hooks = stateProps.dragAndDropHooks;
501
858
  return Boolean(
502
859
  hooks?.useDroppableCollectionState &&
503
860
  hooks.useDroppableCollection &&
504
- (hooks.dropTargetDelegate || parentCollectionRenderer?.dropTargetDelegate || hooks.ListDropTargetDelegate)
861
+ (hooks.dropTargetDelegate ||
862
+ parentCollectionRenderer?.dropTargetDelegate ||
863
+ hooks.ListDropTargetDelegate),
505
864
  );
506
865
  });
507
866
  const hasDraggableDnd = createMemo(() => {
@@ -522,11 +881,12 @@ export function Menu<T>(props: MenuProps<T>): JSX.Element {
522
881
  { focusedKey: state.focusedKey },
523
882
  stateProps.dragAndDropHooks,
524
883
  dropState(),
525
- state.collection()
884
+ state.collection(),
526
885
  );
527
886
  const virtualRange = createMemo(() => {
528
887
  if (!virtualizer || !parentCollectionRenderer?.isVirtualized || hasSections()) return null;
529
- const baseRange = virtualizer.getVisibleRange(stateProps.items.length);
888
+ const dynamicItems = stateProps.items ?? [];
889
+ const baseRange = virtualizer.getVisibleRange(dynamicItems.length);
530
890
  const itemNodes = getItemNodes();
531
891
  const persistedIndexes = Array.from(persistedKeys())
532
892
  .map((key) => itemNodes.findIndex((node) => node.key === key))
@@ -534,21 +894,32 @@ export function Menu<T>(props: MenuProps<T>): JSX.Element {
534
894
  const dropTarget = dropState()?.target;
535
895
  const normalizedDropKey = getNormalizedDropTargetKey(dropTarget, state.collection());
536
896
  const focusedKey = state.focusedKey();
537
- const focusedIndex = focusedKey != null ? itemNodes.findIndex((node) => node.key === focusedKey) : -1;
897
+ const focusedIndex =
898
+ focusedKey != null ? itemNodes.findIndex((node) => node.key === focusedKey) : -1;
538
899
  const forceIncludeIndexes = [
539
- dropTarget?.type === 'item' ? itemNodes.findIndex((node) => node.key === dropTarget.key) : -1,
540
- normalizedDropKey != null ? itemNodes.findIndex((node) => node.key === normalizedDropKey) : -1,
541
- dropTarget?.type === 'item' ? -1 : focusedIndex,
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,
542
905
  ].filter((index) => index >= 0);
543
- return mergePersistedKeysIntoVirtualRange(baseRange, persistedIndexes, stateProps.items.length, virtualizer, 80, {
544
- forceIncludeIndexes,
545
- forceIncludeMaxSpan: 320,
546
- });
906
+ return mergePersistedKeysIntoVirtualRange(
907
+ baseRange,
908
+ persistedIndexes,
909
+ dynamicItems.length,
910
+ virtualizer,
911
+ 80,
912
+ {
913
+ forceIncludeIndexes,
914
+ forceIncludeMaxSpan: 320,
915
+ },
916
+ );
547
917
  });
548
918
  const visibleItems = createMemo(() => {
549
919
  const range = virtualRange();
550
- if (!range) return stateProps.items;
551
- return stateProps.items.slice(range.start, range.end);
920
+ const items = stateProps.items ?? [];
921
+ if (!range) return items;
922
+ return items.slice(range.start, range.end);
552
923
  });
553
924
  createEffect(() => {
554
925
  if (!hasDraggableDnd()) return;
@@ -562,22 +933,27 @@ export function Menu<T>(props: MenuProps<T>): JSX.Element {
562
933
  const hooks = stateProps.dragAndDropHooks;
563
934
  const activeDropState = dropState();
564
935
  if (!hooks?.useDroppableCollection || !activeDropState) return undefined;
565
- const resolveDirection = (): 'ltr' | 'rtl' => {
936
+ const resolveDirection = (): "ltr" | "rtl" => {
566
937
  const menuEl = menuRef();
567
- if (menuEl && typeof window !== 'undefined' && typeof window.getComputedStyle === 'function') {
938
+ if (
939
+ menuEl &&
940
+ typeof window !== "undefined" &&
941
+ typeof window.getComputedStyle === "function"
942
+ ) {
568
943
  const dir = window.getComputedStyle(menuEl).direction;
569
- if (dir === 'rtl') return 'rtl';
944
+ if (dir === "rtl") return "rtl";
570
945
  }
571
- return typeof document !== 'undefined' && document.dir === 'rtl' ? 'rtl' : 'ltr';
946
+ return typeof document !== "undefined" && document.dir === "rtl" ? "rtl" : "ltr";
572
947
  };
573
- const dropTargetDelegate = hooks.dropTargetDelegate
574
- ?? parentCollectionRenderer?.dropTargetDelegate
575
- ?? (hooks.ListDropTargetDelegate
948
+ const dropTargetDelegate =
949
+ hooks.dropTargetDelegate ??
950
+ parentCollectionRenderer?.dropTargetDelegate ??
951
+ (hooks.ListDropTargetDelegate
576
952
  ? new hooks.ListDropTargetDelegate(
577
- () => state.collection(),
578
- () => menuRef(),
579
- { layout: 'stack', orientation: 'vertical', direction: resolveDirection() }
580
- )
953
+ () => state.collection(),
954
+ () => menuRef(),
955
+ { layout: "stack", orientation: "vertical", direction: resolveDirection() },
956
+ )
581
957
  : undefined);
582
958
  if (!dropTargetDelegate) return undefined;
583
959
  return hooks.useDroppableCollection(
@@ -593,28 +969,30 @@ export function Menu<T>(props: MenuProps<T>): JSX.Element {
593
969
  },
594
970
  },
595
971
  activeDropState,
596
- () => menuRef()
972
+ () => menuRef(),
597
973
  );
598
974
  });
599
975
  const isRootDropTarget = createMemo(() => {
600
- return Boolean(dropState()?.target?.type === 'root');
976
+ return Boolean(dropState()?.target?.type === "root");
601
977
  });
602
- const dndRenderDropIndicator = createMemo(() => useRenderDropIndicator(stateProps.dragAndDropHooks, dropState()));
603
- const dndDropIndicator = (index: number, position: 'before' | 'after' | 'on') => {
978
+ const dndRenderDropIndicator = createMemo(() =>
979
+ useRenderDropIndicator(stateProps.dragAndDropHooks, dropState()),
980
+ );
981
+ const dndDropIndicator = (index: number, position: "before" | "after" | "on") => {
604
982
  const target = getDropTargetByIndex(index, position);
605
- if (!target || target.type !== 'item') return undefined;
983
+ if (!target || target.type !== "item") return undefined;
606
984
  return dndRenderDropIndicator()?.(target);
607
985
  };
608
986
  const sectionedRenderEntries = createMemo(() => {
609
987
  let globalIndex = 0;
610
- return stateProps.items.map((entry) => {
988
+ return (stateProps.items ?? []).map((entry) => {
611
989
  if (isCollectionSection(entry)) {
612
990
  const sectionItems = entry.items.map((item) => ({
613
991
  item,
614
992
  index: globalIndex++,
615
993
  }));
616
994
  return {
617
- type: 'section' as const,
995
+ type: "section" as const,
618
996
  section: entry,
619
997
  items: sectionItems,
620
998
  };
@@ -624,120 +1002,196 @@ export function Menu<T>(props: MenuProps<T>): JSX.Element {
624
1002
  index: globalIndex++,
625
1003
  };
626
1004
  return {
627
- type: 'item' as const,
1005
+ type: "item" as const,
628
1006
  item: indexedItem,
629
1007
  };
630
1008
  });
631
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
+ };
632
1024
  const collectionRenderer = createMemo<CollectionRendererContextValue<unknown>>(() => ({
633
1025
  ...parentCollectionRenderer,
634
- renderItem: (item) => props.children(item as T),
1026
+ renderItem: (item) => renderDynamicItem(item as T),
635
1027
  renderDropIndicator: (index, position) =>
636
- dndDropIndicator(index, position) ?? parentCollectionRenderer?.renderDropIndicator?.(index, position),
1028
+ dndDropIndicator(index, position) ??
1029
+ parentCollectionRenderer?.renderDropIndicator?.(index, position),
637
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>;
638
1139
 
639
1140
  // Only use FocusScope when inside a MenuTrigger (for popover behavior)
640
1141
  // Standalone menus don't need focus restoration
641
1142
  const menuContent = () => (
642
1143
  <MenuContext.Provider
643
- value={{
644
- state,
645
- isDisabled: resolveDisabled,
646
- dragAndDropHooks: stateProps.dragAndDropHooks,
647
- dragState: dragState(),
648
- dropState: dropState(),
649
- } as MenuContextValue<unknown>}
1144
+ value={
1145
+ {
1146
+ state,
1147
+ isDisabled: resolveDisabled,
1148
+ dragAndDropHooks: stateProps.dragAndDropHooks,
1149
+ dragState: dragState(),
1150
+ dropState: dropState(),
1151
+ } as MenuContextValue<unknown>
1152
+ }
650
1153
  >
651
1154
  <MenuStateContext.Provider value={state}>
652
- <CollectionRendererContext.Provider value={collectionRenderer()}>
653
- <>
654
- <Show when={ariaProps.label}>
655
- <span {...cleanLabelProps()}>{ariaProps.label as JSX.Element}</span>
656
- </Show>
657
- <ul
658
- ref={setMenuRef}
659
- {...mergeProps(
660
- cleanMenuProps(),
661
- cleanTriggerMenuProps(),
662
- cleanFocusProps(),
663
- (droppableCollection()?.collectionProps as Record<string, unknown> | undefined) ?? {}
664
- )}
665
- class={renderProps.class()}
666
- style={renderProps.style()}
667
- data-focused={state.isFocused() || undefined}
668
- data-disabled={resolveDisabled() || undefined}
669
- data-drop-target={isRootDropTarget() || undefined}
670
- >
671
- <SharedElementTransition>
672
- {hasSections()
673
- ? (
674
- <For each={sectionedRenderEntries()}>
675
- {(entry) =>
676
- entry.type === 'section'
677
- ? (
678
- <li role="presentation" data-section-wrapper>
679
- <Section class="solidaria-Menu-section">
680
- {entry.section.title != null && (
681
- <Header class="solidaria-Menu-sectionHeader">{entry.section.title}</Header>
682
- )}
683
- <Group class="solidaria-Menu-sectionGroup">
684
- <ul role="group" aria-label={entry.section['aria-label']}>
685
- <For each={entry.items}>
686
- {(indexedItem) => (
687
- <>
688
- {collectionRenderer().renderDropIndicator?.(indexedItem.index, 'before')}
689
- {collectionRenderer().renderDropIndicator?.(indexedItem.index, 'on')}
690
- {props.children?.(indexedItem.item)}
691
- {collectionRenderer().renderDropIndicator?.(indexedItem.index, 'after')}
692
- </>
693
- )}
694
- </For>
695
- </ul>
696
- </Group>
697
- </Section>
698
- </li>
699
- )
700
- : (
701
- <>
702
- {collectionRenderer().renderDropIndicator?.(entry.item.index, 'before')}
703
- {collectionRenderer().renderDropIndicator?.(entry.item.index, 'on')}
704
- {props.children?.(entry.item.item)}
705
- {collectionRenderer().renderDropIndicator?.(entry.item.index, 'after')}
706
- </>
707
- )
708
- }
709
- </For>
710
- )
711
- : (
712
- <>
713
- {virtualRange()?.offsetTop
714
- ? <li role="presentation" aria-hidden="true" style={{ height: `${virtualRange()!.offsetTop}px` }} data-virtualizer-spacer="top" />
715
- : null}
716
- <For each={visibleItems()}>
717
- {(item, index) => {
718
- const itemIndex = () => (virtualRange()?.start ?? 0) + index();
719
- const beforeIndicator = () => collectionRenderer().renderDropIndicator?.(itemIndex(), 'before');
720
- const onIndicator = () => collectionRenderer().renderDropIndicator?.(itemIndex(), 'on');
721
- const afterIndicator = () => collectionRenderer().renderDropIndicator?.(itemIndex(), 'after');
722
- return (
723
- <>
724
- {beforeIndicator()}
725
- {onIndicator()}
726
- {props.children?.(item as T)}
727
- {afterIndicator()}
728
- </>
729
- );
730
- }}
731
- </For>
732
- {virtualRange()?.offsetBottom
733
- ? <li role="presentation" aria-hidden="true" style={{ height: `${virtualRange()!.offsetBottom}px` }} data-virtualizer-spacer="bottom" />
734
- : null}
735
- </>
736
- )}
737
- </SharedElementTransition>
738
- </ul>
739
- </>
740
- </CollectionRendererContext.Provider>
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>
741
1195
  </MenuStateContext.Provider>
742
1196
  </MenuContext.Provider>
743
1197
  );
@@ -758,81 +1212,198 @@ export function Menu<T>(props: MenuProps<T>): JSX.Element {
758
1212
  */
759
1213
  export function MenuItem<T>(props: MenuItemProps<T>): JSX.Element {
760
1214
  const [local, ariaProps] = splitProps(props, [
761
- 'class',
762
- 'style',
763
- 'slot',
764
- 'id',
765
- 'item',
766
- 'textValue',
767
- '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",
768
1231
  ]);
769
1232
 
770
- // Get state from context
771
1233
  const context = useContext(MenuStateContext);
772
1234
  if (!context) {
773
- throw new Error('MenuItem must be used within a Menu');
1235
+ throw new Error("MenuItem must be used within a Menu");
774
1236
  }
775
1237
  const state = context as MenuState<T>;
776
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);
777
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);
778
1306
 
779
- // Create menu item aria props
780
1307
  const itemAria = createMenuItem<T>(
781
1308
  {
782
1309
  key: local.id,
783
1310
  get isDisabled() {
784
- return Boolean(ariaProps.isDisabled || menuContext?.isDisabled());
1311
+ return Boolean(
1312
+ ariaProps.isDisabled ||
1313
+ sectionSelection?.isDisabled(local.id) ||
1314
+ menuContext?.isDisabled(),
1315
+ );
785
1316
  },
786
- get 'aria-label'() {
787
- return ariaProps['aria-label'] ?? local.textValue;
1317
+ get "aria-label"() {
1318
+ return ariaProps["aria-label"] ?? local.textValue;
788
1319
  },
789
1320
  get onAction() {
790
- 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;
791
1341
  },
792
1342
  },
793
- state
1343
+ state,
794
1344
  );
795
1345
 
796
- // Create hover
797
1346
  const { isHovered, hoverProps } = createHover({
798
1347
  get isDisabled() {
799
1348
  return itemAria.isDisabled();
800
1349
  },
1350
+ onHoverStart: local.onHoverStart,
1351
+ onHoverEnd: local.onHoverEnd,
1352
+ onHoverChange: local.onHoverChange,
801
1353
  });
802
1354
 
803
- // Render props values
804
- const renderValues = createMemo<MenuItemRenderProps>(() => ({
805
- isSelected: false, // Menu items don't have selection state
806
- isFocused: itemAria.isFocused(),
807
- isFocusVisible: itemAria.isFocusVisible(),
808
- isPressed: itemAria.isPressed(),
809
- isHovered: isHovered(),
810
- isDisabled: itemAria.isDisabled(),
811
- }));
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
+ });
812
1369
 
813
- // Resolve render props
814
1370
  const renderProps = useRenderProps(
815
1371
  {
816
1372
  children: props.children,
817
1373
  class: local.class,
818
1374
  style: local.style,
819
- defaultClassName: 'solidaria-Menu-item',
1375
+ defaultClassName: "solidaria-Menu-item",
820
1376
  },
821
- renderValues
1377
+ renderValues,
822
1378
  );
823
1379
  const hasPrimitiveLabel = () => {
824
- return typeof props.children === 'string' || typeof props.children === 'number';
1380
+ return typeof props.children === "string" || typeof props.children === "number";
825
1381
  };
826
1382
 
827
- // Remove ref from spread props
828
1383
  const cleanItemProps = () => {
829
1384
  const {
830
1385
  ref: _ref1,
831
- 'aria-describedby': _ariaDescribedby,
1386
+ "aria-describedby": _ariaDescribedby,
832
1387
  ...rest
833
1388
  } = itemAria.menuItemProps as Record<string, unknown>;
834
- if (!hasPrimitiveLabel() && rest['aria-label'] == null) {
835
- delete rest['aria-labelledby'];
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;
836
1407
  }
837
1408
  return rest;
838
1409
  };
@@ -840,49 +1411,169 @@ export function MenuItem<T>(props: MenuItemProps<T>): JSX.Element {
840
1411
  const { ref: _ref2, ...rest } = hoverProps as Record<string, unknown>;
841
1412
  return rest;
842
1413
  };
1414
+ const domProps = createMemo(() =>
1415
+ filterDOMProps(ariaProps as Record<string, unknown>, { global: true }),
1416
+ );
843
1417
  const draggableItem = createMemo(() => {
844
- if (!menuContext?.dragAndDropHooks?.useDraggableItem || !menuContext.dragState) return undefined;
1418
+ if (!menuContext?.dragAndDropHooks?.useDraggableItem || !menuContext.dragState)
1419
+ return undefined;
845
1420
  return menuContext.dragAndDropHooks.useDraggableItem(
846
1421
  {
847
1422
  key: local.id as string | number,
848
1423
  },
849
- menuContext.dragState as Parameters<NonNullable<DragAndDropHooks<T>['useDraggableItem']>>[1]
1424
+ menuContext.dragState as Parameters<NonNullable<DragAndDropHooks<T>["useDraggableItem"]>>[1],
850
1425
  );
851
1426
  });
852
1427
  const droppableItem = createMemo(() => {
853
- if (!menuContext?.dragAndDropHooks?.useDroppableItem || !menuContext.dropState) return undefined;
1428
+ if (!menuContext?.dragAndDropHooks?.useDroppableItem || !menuContext.dropState)
1429
+ return undefined;
854
1430
  return menuContext.dragAndDropHooks.useDroppableItem(
855
1431
  {
856
1432
  key: local.id as string | number,
857
1433
  },
858
- menuContext.dropState as Parameters<NonNullable<DragAndDropHooks<T>['useDroppableItem']>>[1],
859
- () => ref()
1434
+ menuContext.dropState as Parameters<NonNullable<DragAndDropHooks<T>["useDroppableItem"]>>[1],
1435
+ () => ref(),
860
1436
  );
861
1437
  });
862
1438
 
863
- return (
864
- <li
865
- ref={setRef}
866
- {...mergeProps(
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(
867
1488
  cleanItemProps(),
1489
+ contextProps() as Record<string, unknown>,
1490
+ domProps(),
868
1491
  cleanHoverProps(),
869
1492
  (draggableItem()?.dragProps as Record<string, unknown> | undefined) ?? {},
870
- (droppableItem()?.dropProps as Record<string, unknown> | undefined) ?? {}
871
- )}
872
- class={renderProps.class()}
873
- style={renderProps.style()}
874
- data-focused={itemAria.isFocused() || undefined}
875
- data-focus-visible={itemAria.isFocusVisible() || undefined}
876
- data-pressed={itemAria.isPressed() || undefined}
877
- data-hovered={isHovered() || undefined}
878
- data-disabled={itemAria.isDisabled() || undefined}
879
- data-dragging={draggableItem()?.isDragging || undefined}
880
- data-drop-target={droppableItem()?.isDropTarget || 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
+ }
1520
+
1521
+ return (
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
+ }
881
1546
  >
882
- {hasPrimitiveLabel()
883
- ? <span {...itemAria.labelProps}>{renderProps.renderChildren()}</span>
884
- : renderProps.renderChildren()}
885
- </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>
886
1577
  );
887
1578
  }
888
1579
 
@@ -890,8 +1581,66 @@ export function MenuItem<T>(props: MenuItemProps<T>): JSX.Element {
890
1581
  * Section primitive alias for Menu composition parity.
891
1582
  */
892
1583
  export function MenuSection(props: MenuSectionProps): JSX.Element {
893
- return <Section {...props} />;
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>
1643
+ );
894
1644
  }
895
1645
 
896
- // Attach Item as a static property
897
1646
  Menu.Item = MenuItem;