@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/ListBox.tsx CHANGED
@@ -8,24 +8,29 @@
8
8
  import {
9
9
  type JSX,
10
10
  createContext,
11
+ createEffect,
11
12
  createMemo,
13
+ createSignal,
14
+ onCleanup,
12
15
  splitProps,
13
16
  useContext,
14
17
  For,
15
- } from 'solid-js';
18
+ Show,
19
+ } from "solid-js";
16
20
  import {
17
21
  createListBox,
18
22
  createOption,
19
23
  createFocusRing,
20
- createHover,
24
+ mergeProps,
21
25
  type AriaListBoxProps,
22
26
  type AriaOptionProps,
23
- } from '@proyecto-viviana/solidaria';
27
+ } from "@proyecto-viviana/solidaria";
24
28
  import {
25
29
  createListState,
26
30
  type ListState,
27
31
  type Key,
28
- } from '@proyecto-viviana/solid-stately';
32
+ type DropTarget,
33
+ } from "@proyecto-viviana/solid-stately";
29
34
  import {
30
35
  type RenderChildren,
31
36
  type ClassNameOrFunction,
@@ -33,11 +38,32 @@ import {
33
38
  type SlotProps,
34
39
  useRenderProps,
35
40
  filterDOMProps,
36
- } from './utils';
37
-
38
- // ============================================
39
- // TYPES
40
- // ============================================
41
+ } from "./utils";
42
+ import { SharedElementTransition } from "./SharedElementTransition";
43
+ import {
44
+ SelectionIndicatorContext,
45
+ type SelectionIndicatorContextValue,
46
+ } from "./SelectionIndicator";
47
+ import { useVirtualizerContext } from "./Virtualizer";
48
+ import { type DragAndDropHooks } from "./useDragAndDrop";
49
+ import {
50
+ getNormalizedDropTargetKey,
51
+ mergePersistedKeysIntoVirtualRange,
52
+ useDndPersistedKeys,
53
+ useRenderDropIndicator,
54
+ } from "./DragAndDrop";
55
+ import {
56
+ CollectionRendererContext,
57
+ Section,
58
+ Header,
59
+ Group,
60
+ type CollectionEntry,
61
+ type CollectionRendererContextValue,
62
+ type SectionProps,
63
+ useCollectionRenderer,
64
+ isCollectionSection,
65
+ flattenCollectionEntries,
66
+ } from "./Collection";
41
67
 
42
68
  export interface ListBoxRenderProps {
43
69
  /** Whether the listbox has focus. */
@@ -50,11 +76,17 @@ export interface ListBoxRenderProps {
50
76
  isEmpty: boolean;
51
77
  }
52
78
 
53
- export interface ListBoxProps<T>
54
- extends Omit<AriaListBoxProps, 'children'>,
55
- SlotProps {
79
+ type RefLike<T> = ((el: T) => void) | { current?: T | null } | undefined;
80
+
81
+ function assignRef<T>(ref: RefLike<T>, el: T): void {
82
+ if (!ref) return;
83
+ if (typeof ref === "function") ref(el);
84
+ else ref.current = el;
85
+ }
86
+
87
+ export interface ListBoxProps<T> extends Omit<AriaListBoxProps, "children">, SlotProps {
56
88
  /** The items to render in the listbox. */
57
- items: T[];
89
+ items: CollectionEntry<T>[];
58
90
  /** Function to get the key from an item. */
59
91
  getKey?: (item: T) => Key;
60
92
  /** Function to get the text value from an item. */
@@ -62,15 +94,19 @@ export interface ListBoxProps<T>
62
94
  /** Function to check if an item is disabled. */
63
95
  getDisabled?: (item: T) => boolean;
64
96
  /** The selection mode. */
65
- selectionMode?: 'none' | 'single' | 'multiple';
97
+ selectionMode?: "none" | "single" | "multiple";
98
+ /** The selection behavior (toggle vs replace). */
99
+ selectionBehavior?: "toggle" | "replace";
100
+ /** Whether disabled items can still receive focus. */
101
+ disabledBehavior?: "selection" | "all";
66
102
  /** Keys of disabled items. */
67
103
  disabledKeys?: Iterable<Key>;
68
104
  /** Currently selected keys (controlled). */
69
- selectedKeys?: 'all' | Iterable<Key>;
105
+ selectedKeys?: "all" | Iterable<Key>;
70
106
  /** Default selected keys (uncontrolled). */
71
- defaultSelectedKeys?: 'all' | Iterable<Key>;
107
+ defaultSelectedKeys?: "all" | Iterable<Key>;
72
108
  /** Handler called when selection changes. */
73
- onSelectionChange?: (keys: 'all' | Set<Key>) => void;
109
+ onSelectionChange?: (keys: "all" | Set<Key>) => void;
74
110
  /** The children of the component. A function may be provided to render each item. */
75
111
  children: (item: T) => JSX.Element;
76
112
  /** The CSS className for the element. */
@@ -79,6 +115,22 @@ export interface ListBoxProps<T>
79
115
  style?: StyleOrFunction<ListBoxRenderProps>;
80
116
  /** A function to render when the listbox is empty. */
81
117
  renderEmptyState?: () => JSX.Element;
118
+ /** Whether there are more items to load. */
119
+ hasMore?: boolean;
120
+ /** Whether additional items are currently loading. */
121
+ isLoading?: boolean;
122
+ /** Called when the load more sentinel becomes visible. */
123
+ onLoadMore?: () => void | Promise<void>;
124
+ /** Ref for the listbox element. */
125
+ ref?: RefLike<HTMLUListElement>;
126
+ /** Drag and drop hooks from `useDragAndDrop`. */
127
+ dragAndDropHooks?: DragAndDropHooks<T>;
128
+ /** Layout hint for styling parity. */
129
+ layout?: "stack" | "grid";
130
+ /** Orientation hint for styling parity. */
131
+ orientation?: "vertical" | "horizontal";
132
+ /** Slot definitions provided through ListBoxContext. */
133
+ slots?: Record<string, Partial<ListBoxProps<T>>>;
82
134
  }
83
135
 
84
136
  export interface ListBoxOptionRenderProps {
@@ -97,8 +149,7 @@ export interface ListBoxOptionRenderProps {
97
149
  }
98
150
 
99
151
  export interface ListBoxOptionProps<T>
100
- extends Omit<AriaOptionProps, 'children' | 'key'>,
101
- SlotProps {
152
+ extends Omit<AriaOptionProps, "children" | "key">, SlotProps {
102
153
  /** The unique key for the option. */
103
154
  id: Key;
104
155
  /** The item value. */
@@ -111,37 +162,90 @@ export interface ListBoxOptionProps<T>
111
162
  style?: StyleOrFunction<ListBoxOptionRenderProps>;
112
163
  /** The text value of the option (for typeahead). */
113
164
  textValue?: string;
165
+ /** Ref for the option element. */
166
+ ref?: RefLike<HTMLLIElement>;
167
+ }
168
+
169
+ export interface ListBoxLoadMoreItemProps extends SlotProps {
170
+ /** Called when the sentinel becomes visible. */
171
+ onLoadMore: () => void | Promise<void>;
172
+ /** Whether additional items are currently loading. */
173
+ isLoading?: boolean;
174
+ /** Scroll offset multiplier for early loading trigger (default: 1 = 100% of viewport height). */
175
+ scrollOffset?: number;
176
+ /** Content for the load more row. */
177
+ children?: JSX.Element;
178
+ /** The CSS className for the element. */
179
+ class?: ClassNameOrFunction<{ isLoading: boolean }>;
180
+ /** The inline style for the element. */
181
+ style?: StyleOrFunction<{ isLoading: boolean }>;
114
182
  }
115
183
 
116
- // ============================================
117
- // CONTEXT
118
- // ============================================
184
+ export interface ListBoxSectionProps extends SectionProps {}
119
185
 
120
186
  interface ListBoxContextValue<T> {
121
187
  state: ListState<T>;
188
+ isDisabled: () => boolean;
189
+ dragAndDropHooks?: DragAndDropHooks<unknown>;
190
+ dragState?: unknown;
191
+ dropState?: unknown;
192
+ slots?: Record<string, Partial<ListBoxProps<T>>>;
122
193
  }
123
194
 
124
195
  export const ListBoxContext = createContext<ListBoxContextValue<unknown> | null>(null);
125
196
  export const ListBoxStateContext = createContext<ListState<unknown> | null>(null);
126
-
127
- // ============================================
128
- // COMPONENTS
129
- // ============================================
197
+ export const ListStateContext = ListBoxStateContext;
130
198
 
131
199
  /**
132
200
  * A listbox displays a list of options and allows a user to select one or more of them.
133
201
  */
134
202
  export function ListBox<T>(props: ListBoxProps<T>): JSX.Element {
203
+ const parentContext = useContext(ListBoxContext) as ListBoxContextValue<T> | null;
204
+ const contextSlotProps = parentContext?.slots?.[props.slot ?? "default"];
205
+ const mergedListBoxProps = contextSlotProps
206
+ ? (mergeProps(contextSlotProps, props) as ListBoxProps<T>)
207
+ : props;
135
208
  const [local, stateProps, ariaProps] = splitProps(
136
- props,
137
- ['children', 'class', 'style', 'slot', 'renderEmptyState'],
138
- ['items', 'getKey', 'getTextValue', 'getDisabled', 'disabledKeys', 'selectionMode', 'selectedKeys', 'defaultSelectedKeys', 'onSelectionChange']
209
+ mergedListBoxProps,
210
+ [
211
+ "children",
212
+ "class",
213
+ "style",
214
+ "slot",
215
+ "renderEmptyState",
216
+ "hasMore",
217
+ "isLoading",
218
+ "onLoadMore",
219
+ "dragAndDropHooks",
220
+ "slots",
221
+ "ref",
222
+ ],
223
+ [
224
+ "items",
225
+ "getKey",
226
+ "getTextValue",
227
+ "getDisabled",
228
+ "disabledKeys",
229
+ "disabledBehavior",
230
+ "selectionMode",
231
+ "selectionBehavior",
232
+ "selectedKeys",
233
+ "defaultSelectedKeys",
234
+ "onSelectionChange",
235
+ "layout",
236
+ "orientation",
237
+ ],
139
238
  );
140
239
 
141
- // Create list state
240
+ const flatItems = createMemo<T[]>(() => {
241
+ return flattenCollectionEntries(stateProps.items);
242
+ });
243
+
244
+ const hasSections = createMemo(() => stateProps.items.some((item) => isCollectionSection(item)));
245
+
142
246
  const state = createListState<T>({
143
247
  get items() {
144
- return stateProps.items;
248
+ return flatItems();
145
249
  },
146
250
  get getKey() {
147
251
  return stateProps.getKey;
@@ -158,6 +262,12 @@ export function ListBox<T>(props: ListBoxProps<T>): JSX.Element {
158
262
  get selectionMode() {
159
263
  return stateProps.selectionMode;
160
264
  },
265
+ get selectionBehavior() {
266
+ return stateProps.selectionBehavior;
267
+ },
268
+ get disabledBehavior() {
269
+ return stateProps.disabledBehavior;
270
+ },
161
271
  get selectedKeys() {
162
272
  return stateProps.selectedKeys;
163
273
  },
@@ -169,30 +279,26 @@ export function ListBox<T>(props: ListBoxProps<T>): JSX.Element {
169
279
  },
170
280
  });
171
281
 
172
- // Helper to resolve isDisabled
173
282
  const resolveDisabled = (): boolean => {
174
283
  const disabled = ariaProps.isDisabled;
175
- if (typeof disabled === 'function') {
284
+ if (typeof disabled === "function") {
176
285
  return (disabled as () => boolean)();
177
286
  }
178
287
  return !!disabled;
179
288
  };
180
289
 
181
- // Create listbox aria props
182
- const { listBoxProps } = createListBox(
290
+ const listBoxAria = createListBox(
183
291
  {
184
292
  ...ariaProps,
185
293
  get isDisabled() {
186
294
  return resolveDisabled();
187
295
  },
188
296
  },
189
- state
297
+ state,
190
298
  );
191
299
 
192
- // Create focus ring
193
300
  const { isFocused, isFocusVisible, focusProps } = createFocusRing();
194
301
 
195
- // Render props values
196
302
  const renderValues = createMemo<ListBoxRenderProps>(() => ({
197
303
  isFocused: state.isFocused() || isFocused(),
198
304
  isFocusVisible: isFocusVisible(),
@@ -200,53 +306,366 @@ export function ListBox<T>(props: ListBoxProps<T>): JSX.Element {
200
306
  isEmpty: state.collection().size === 0,
201
307
  }));
202
308
 
203
- // Resolve render props
204
309
  const renderProps = useRenderProps(
205
310
  {
206
311
  class: local.class,
207
312
  style: local.style,
208
- defaultClassName: 'solidaria-ListBox',
313
+ defaultClassName: "solidaria-ListBox",
209
314
  },
210
- renderValues
315
+ renderValues,
211
316
  );
212
317
 
213
- // Filter DOM props
214
318
  const domProps = createMemo(() => {
215
319
  const filtered = filterDOMProps(ariaProps as Record<string, unknown>, { global: true });
216
320
  return filtered;
217
321
  });
218
322
 
219
- // Remove ref from spread props
220
323
  const cleanListBoxProps = () => {
221
- const { ref: _ref1, ...rest } = listBoxProps as Record<string, unknown>;
324
+ const { ref: _ref1, ...rest } = listBoxAria.listBoxProps as Record<string, unknown>;
222
325
  return rest;
223
326
  };
224
327
  const cleanFocusProps = () => {
225
328
  const { ref: _ref2, ...rest } = focusProps as Record<string, unknown>;
226
329
  return rest;
227
330
  };
331
+ const cleanLabelProps = () => {
332
+ const { ref: _ref3, ...rest } = listBoxAria.labelProps as Record<string, unknown>;
333
+ return rest;
334
+ };
335
+ const [listRef, setListRef] = createSignal<HTMLElement | null>(null);
228
336
 
229
337
  const isEmpty = () => stateProps.items.length === 0;
338
+ const parentCollectionRenderer = useCollectionRenderer<unknown>();
339
+ const getItemNodes = createMemo(() =>
340
+ Array.from(state.collection()).filter((node) => node.type === "item"),
341
+ );
342
+ const getDropTargetByIndex = (
343
+ index: number,
344
+ position: "before" | "after" | "on",
345
+ ): DropTarget | null => {
346
+ const node = getItemNodes()[index];
347
+ if (!node) return null;
348
+ return { type: "item", key: node.key, dropPosition: position };
349
+ };
350
+ const hasDroppableDnd = createMemo(() => {
351
+ const hooks = local.dragAndDropHooks;
352
+ return Boolean(
353
+ hooks?.useDroppableCollectionState &&
354
+ hooks.useDroppableCollection &&
355
+ (hooks.dropTargetDelegate ||
356
+ parentCollectionRenderer?.dropTargetDelegate ||
357
+ hooks.ListDropTargetDelegate),
358
+ );
359
+ });
360
+ const dropState = createMemo(() => {
361
+ if (!hasDroppableDnd()) return undefined;
362
+ return local.dragAndDropHooks?.useDroppableCollectionState?.({});
363
+ });
364
+ const hasDraggableDnd = createMemo(() => {
365
+ const hooks = local.dragAndDropHooks;
366
+ return Boolean(hooks?.useDraggableCollectionState && hooks.useDraggableCollection);
367
+ });
368
+ const dragState = createMemo(() => {
369
+ if (!hasDraggableDnd()) return undefined;
370
+ return local.dragAndDropHooks?.useDraggableCollectionState?.({
371
+ items: flatItems(),
372
+ });
373
+ });
374
+ createEffect(() => {
375
+ if (!hasDraggableDnd()) return;
376
+ const hooks = local.dragAndDropHooks;
377
+ const activeDragState = dragState();
378
+ if (!hooks?.useDraggableCollection || !activeDragState) return;
379
+ hooks.useDraggableCollection({}, activeDragState, () => listRef());
380
+ });
381
+ const droppableCollection = createMemo(() => {
382
+ if (!hasDroppableDnd()) return undefined;
383
+ const hooks = local.dragAndDropHooks;
384
+ const activeDropState = dropState();
385
+ if (!hooks?.useDroppableCollection || !activeDropState) return undefined;
386
+ const resolveDirection = (): "ltr" | "rtl" => {
387
+ const el = listRef();
388
+ if (el && typeof window !== "undefined" && typeof window.getComputedStyle === "function") {
389
+ const dir = window.getComputedStyle(el).direction;
390
+ if (dir === "rtl") return "rtl";
391
+ }
392
+ return typeof document !== "undefined" && document.dir === "rtl" ? "rtl" : "ltr";
393
+ };
394
+ const dropTargetDelegate =
395
+ hooks.dropTargetDelegate ??
396
+ parentCollectionRenderer?.dropTargetDelegate ??
397
+ (hooks.ListDropTargetDelegate
398
+ ? new hooks.ListDropTargetDelegate(
399
+ () => state.collection(),
400
+ () => listRef(),
401
+ { layout: "stack", orientation: "vertical", direction: resolveDirection() },
402
+ )
403
+ : undefined);
404
+ if (!dropTargetDelegate) return undefined;
405
+ return hooks.useDroppableCollection(
406
+ {
407
+ dropTargetDelegate,
408
+ keyboardDelegate: {
409
+ getFirstKey: () => state.collection().getFirstKey(),
410
+ getLastKey: () => state.collection().getLastKey(),
411
+ getKeyBelow: (key) => state.collection().getKeyAfter(key),
412
+ getKeyAbove: (key) => state.collection().getKeyBefore(key),
413
+ getKeyPageBelow: (key) => state.collection().getKeyAfter(key),
414
+ getKeyPageAbove: (key) => state.collection().getKeyBefore(key),
415
+ },
416
+ },
417
+ activeDropState,
418
+ () => listRef(),
419
+ );
420
+ });
421
+ const isRootDropTarget = createMemo(() => {
422
+ return Boolean(dropState()?.target?.type === "root");
423
+ });
424
+ const dndRenderDropIndicator = createMemo(() =>
425
+ useRenderDropIndicator(local.dragAndDropHooks, dropState()),
426
+ );
427
+ const dndDropIndicator = (index: number, position: "before" | "after" | "on") => {
428
+ const target = getDropTargetByIndex(index, position);
429
+ if (!target || target.type !== "item") return undefined;
430
+ return dndRenderDropIndicator()?.(target);
431
+ };
432
+ const virtualizer = useVirtualizerContext();
433
+ const persistedKeys = useDndPersistedKeys(
434
+ { focusedKey: state.focusedKey },
435
+ local.dragAndDropHooks,
436
+ dropState(),
437
+ state.collection(),
438
+ );
439
+ const virtualRange = createMemo(() => {
440
+ if (!virtualizer || !parentCollectionRenderer?.isVirtualized || hasSections()) return null;
441
+ const baseRange = virtualizer.getVisibleRange(stateProps.items.length);
442
+ const itemNodes = getItemNodes();
443
+ const persistedIndexes = Array.from(persistedKeys())
444
+ .map((key) => itemNodes.findIndex((node) => node.key === key))
445
+ .filter((index) => index >= 0);
446
+ const dropTarget = dropState()?.target;
447
+ const normalizedDropKey = getNormalizedDropTargetKey(dropTarget, state.collection());
448
+ const focusedKey = state.focusedKey();
449
+ const focusedIndex =
450
+ focusedKey != null ? itemNodes.findIndex((node) => node.key === focusedKey) : -1;
451
+ const forceIncludeIndexes = [
452
+ dropTarget?.type === "item" ? itemNodes.findIndex((node) => node.key === dropTarget.key) : -1,
453
+ normalizedDropKey != null
454
+ ? itemNodes.findIndex((node) => node.key === normalizedDropKey)
455
+ : -1,
456
+ dropTarget?.type === "item" ? -1 : focusedIndex,
457
+ ].filter((index) => index >= 0);
458
+ return mergePersistedKeysIntoVirtualRange(
459
+ baseRange,
460
+ persistedIndexes,
461
+ stateProps.items.length,
462
+ virtualizer,
463
+ 80,
464
+ {
465
+ forceIncludeIndexes,
466
+ forceIncludeMaxSpan: 320,
467
+ },
468
+ );
469
+ });
470
+ createEffect(() => {
471
+ if (!virtualizer || !parentCollectionRenderer?.isVirtualized) return;
472
+ const getItemNodes = () =>
473
+ Array.from(state.collection()).filter((node) => node.type === "item");
474
+ virtualizer.setDropTargetItemCountResolver(() => getItemNodes().length);
475
+ virtualizer.setDropTargetIndexResolver((key) => {
476
+ const index = getItemNodes().findIndex((node) => node.key === key);
477
+ return index >= 0 ? index : null;
478
+ });
479
+ virtualizer.setDropTargetResolver((target) => {
480
+ const node = getItemNodes()[target.index];
481
+ if (!node) return target;
482
+ return {
483
+ ...target,
484
+ key: typeof node.key === "string" || typeof node.key === "number" ? node.key : undefined,
485
+ };
486
+ });
487
+ onCleanup(() => {
488
+ virtualizer.setDropTargetIndexResolver(undefined);
489
+ virtualizer.setDropTargetItemCountResolver(undefined);
490
+ virtualizer.setDropTargetResolver(undefined);
491
+ });
492
+ });
493
+ const visibleItems = createMemo(() => {
494
+ const range = virtualRange();
495
+ if (!range) return stateProps.items;
496
+ return stateProps.items.slice(range.start, range.end);
497
+ });
498
+ const sectionedRenderEntries = createMemo(() => {
499
+ let globalIndex = 0;
500
+ return stateProps.items.map((entry) => {
501
+ if (isCollectionSection(entry)) {
502
+ const sectionItems = entry.items.map((item) => ({
503
+ item,
504
+ index: globalIndex++,
505
+ }));
506
+ return {
507
+ type: "section" as const,
508
+ section: entry,
509
+ items: sectionItems,
510
+ };
511
+ }
512
+ const indexedItem = {
513
+ item: entry as T,
514
+ index: globalIndex++,
515
+ };
516
+ return {
517
+ type: "item" as const,
518
+ item: indexedItem,
519
+ };
520
+ });
521
+ });
522
+ const collectionRenderer = createMemo<CollectionRendererContextValue<unknown>>(() => ({
523
+ ...parentCollectionRenderer,
524
+ renderItem: (item) => local.children(item as T),
525
+ renderDropIndicator: (index, position) =>
526
+ dndDropIndicator(index, position) ??
527
+ parentCollectionRenderer?.renderDropIndicator?.(index, position),
528
+ }));
230
529
 
231
530
  return (
232
- <ListBoxContext.Provider value={{ state }}>
531
+ <ListBoxContext.Provider
532
+ value={
533
+ {
534
+ state,
535
+ isDisabled: resolveDisabled,
536
+ dragAndDropHooks: local.dragAndDropHooks as DragAndDropHooks<unknown> | undefined,
537
+ dragState: dragState(),
538
+ dropState: dropState(),
539
+ slots: local.slots,
540
+ } as ListBoxContextValue<unknown>
541
+ }
542
+ >
233
543
  <ListBoxStateContext.Provider value={state}>
234
- <ul
235
- {...domProps()}
236
- {...cleanListBoxProps()}
237
- {...cleanFocusProps()}
238
- class={renderProps.class()}
239
- style={renderProps.style()}
240
- data-focused={state.isFocused() || undefined}
241
- data-focus-visible={isFocusVisible() || undefined}
242
- data-disabled={resolveDisabled() || undefined}
243
- data-empty={isEmpty() || undefined}
244
- >
245
- {isEmpty() && local.renderEmptyState
246
- ? local.renderEmptyState()
247
- : <For each={stateProps.items}>{(item) => props.children(item)}</For>
248
- }
249
- </ul>
544
+ <CollectionRendererContext.Provider value={collectionRenderer()}>
545
+ <>
546
+ <Show when={ariaProps.label}>
547
+ <span {...cleanLabelProps()}>{ariaProps.label as JSX.Element}</span>
548
+ </Show>
549
+ <ul
550
+ {...mergeProps(
551
+ domProps(),
552
+ cleanListBoxProps(),
553
+ cleanFocusProps(),
554
+ (droppableCollection()?.collectionProps as Record<string, unknown> | undefined) ??
555
+ {},
556
+ )}
557
+ ref={(el) => {
558
+ setListRef(el);
559
+ assignRef(local.ref, el);
560
+ }}
561
+ class={renderProps.class()}
562
+ style={renderProps.style()}
563
+ data-focused={state.isFocused() || undefined}
564
+ data-focus-visible={isFocusVisible() || undefined}
565
+ data-disabled={resolveDisabled() || undefined}
566
+ data-empty={isEmpty() || undefined}
567
+ data-layout={stateProps.layout}
568
+ data-orientation={stateProps.orientation}
569
+ data-drop-target={isRootDropTarget() || undefined}
570
+ slot={local.slot}
571
+ >
572
+ <SharedElementTransition>
573
+ {isEmpty() && local.renderEmptyState ? (
574
+ <li role="option" style={{ display: "contents" }} data-empty-state>
575
+ {local.renderEmptyState()}
576
+ </li>
577
+ ) : hasSections() ? (
578
+ <For each={sectionedRenderEntries()}>
579
+ {(entry) =>
580
+ entry.type === "section" ? (
581
+ <li role="presentation" data-section-wrapper>
582
+ <Section class="solidaria-ListBox-section">
583
+ {entry.section.title != null && (
584
+ <Header class="solidaria-ListBox-sectionHeader">
585
+ {entry.section.title}
586
+ </Header>
587
+ )}
588
+ <Group class="solidaria-ListBox-sectionGroup">
589
+ <ul role="group" aria-label={entry.section["aria-label"]}>
590
+ <For each={entry.items}>
591
+ {(indexedItem) => (
592
+ <>
593
+ {collectionRenderer().renderDropIndicator?.(
594
+ indexedItem.index,
595
+ "before",
596
+ )}
597
+ {collectionRenderer().renderDropIndicator?.(
598
+ indexedItem.index,
599
+ "on",
600
+ )}
601
+ {local.children(indexedItem.item)}
602
+ {collectionRenderer().renderDropIndicator?.(
603
+ indexedItem.index,
604
+ "after",
605
+ )}
606
+ </>
607
+ )}
608
+ </For>
609
+ </ul>
610
+ </Group>
611
+ </Section>
612
+ </li>
613
+ ) : (
614
+ <>
615
+ {collectionRenderer().renderDropIndicator?.(entry.item.index, "before")}
616
+ {collectionRenderer().renderDropIndicator?.(entry.item.index, "on")}
617
+ {local.children(entry.item.item)}
618
+ {collectionRenderer().renderDropIndicator?.(entry.item.index, "after")}
619
+ </>
620
+ )
621
+ }
622
+ </For>
623
+ ) : (
624
+ <>
625
+ {virtualRange()?.offsetTop ? (
626
+ <li
627
+ role="presentation"
628
+ aria-hidden="true"
629
+ style={{ height: `${virtualRange()!.offsetTop}px` }}
630
+ data-virtualizer-spacer="top"
631
+ />
632
+ ) : null}
633
+ <For each={visibleItems()}>
634
+ {(item, index) => {
635
+ const itemIndex = () => (virtualRange()?.start ?? 0) + index();
636
+ const beforeIndicator = () =>
637
+ collectionRenderer().renderDropIndicator?.(itemIndex(), "before");
638
+ const onIndicator = () =>
639
+ collectionRenderer().renderDropIndicator?.(itemIndex(), "on");
640
+ const afterIndicator = () =>
641
+ collectionRenderer().renderDropIndicator?.(itemIndex(), "after");
642
+ return (
643
+ <>
644
+ {beforeIndicator()}
645
+ {onIndicator()}
646
+ {local.children(item as T)}
647
+ {afterIndicator()}
648
+ </>
649
+ );
650
+ }}
651
+ </For>
652
+ {virtualRange()?.offsetBottom ? (
653
+ <li
654
+ role="presentation"
655
+ aria-hidden="true"
656
+ style={{ height: `${virtualRange()!.offsetBottom}px` }}
657
+ data-virtualizer-spacer="bottom"
658
+ />
659
+ ) : null}
660
+ </>
661
+ )}
662
+ </SharedElementTransition>
663
+ {local.hasMore && local.onLoadMore && (
664
+ <ListBoxLoadMoreItem onLoadMore={local.onLoadMore} isLoading={local.isLoading} />
665
+ )}
666
+ </ul>
667
+ </>
668
+ </CollectionRendererContext.Provider>
250
669
  </ListBoxStateContext.Provider>
251
670
  </ListBoxContext.Provider>
252
671
  );
@@ -257,90 +676,221 @@ export function ListBox<T>(props: ListBoxProps<T>): JSX.Element {
257
676
  */
258
677
  export function ListBoxOption<T>(props: ListBoxOptionProps<T>): JSX.Element {
259
678
  const [local, ariaProps] = splitProps(props, [
260
- 'class',
261
- 'style',
262
- 'slot',
263
- 'id',
264
- 'item',
265
- 'textValue',
679
+ "class",
680
+ "style",
681
+ "slot",
682
+ "id",
683
+ "item",
684
+ "textValue",
685
+ "ref",
266
686
  ]);
267
687
 
268
- // Get state from context
269
688
  const context = useContext(ListBoxStateContext);
270
689
  if (!context) {
271
- throw new Error('ListBoxOption must be used within a ListBox');
690
+ throw new Error("ListBoxOption must be used within a ListBox");
272
691
  }
273
692
  const state = context as ListState<T>;
693
+ const listContext = useContext(ListBoxContext) as ListBoxContextValue<T> | null;
694
+ const [ref, setRef] = createSignal<HTMLLIElement | null>(null);
274
695
 
275
- // Create option aria props
276
696
  const optionAria = createOption<T>(
277
697
  {
278
698
  key: local.id,
279
699
  get isDisabled() {
280
- return ariaProps.isDisabled;
700
+ return Boolean(ariaProps.isDisabled || listContext?.isDisabled());
701
+ },
702
+ get "aria-label"() {
703
+ return ariaProps["aria-label"] ?? local.textValue;
704
+ },
705
+ get shouldSelectOnPressUp() {
706
+ return ariaProps.shouldSelectOnPressUp;
281
707
  },
282
- get 'aria-label'() {
283
- return ariaProps['aria-label'];
708
+ get shouldFocusOnHover() {
709
+ return ariaProps.shouldFocusOnHover;
710
+ },
711
+ get onHoverStart() {
712
+ return ariaProps.onHoverStart;
713
+ },
714
+ get onHoverEnd() {
715
+ return ariaProps.onHoverEnd;
716
+ },
717
+ get onHoverChange() {
718
+ return ariaProps.onHoverChange;
284
719
  },
285
720
  },
286
- state
721
+ state,
287
722
  );
288
723
 
289
- // Create hover
290
- const { isHovered, hoverProps } = createHover({
291
- get isDisabled() {
292
- return optionAria.isDisabled();
293
- },
294
- });
295
-
296
- // Render props values
297
724
  const renderValues = createMemo<ListBoxOptionRenderProps>(() => ({
298
725
  isSelected: optionAria.isSelected(),
299
726
  isFocused: optionAria.isFocused(),
300
727
  isFocusVisible: optionAria.isFocusVisible(),
301
728
  isPressed: optionAria.isPressed(),
302
- isHovered: isHovered(),
729
+ isHovered: optionAria.isHovered(),
303
730
  isDisabled: optionAria.isDisabled(),
304
731
  }));
305
732
 
306
- // Resolve render props
307
733
  const renderProps = useRenderProps(
308
734
  {
309
735
  children: props.children,
310
736
  class: local.class,
311
737
  style: local.style,
312
- defaultClassName: 'solidaria-ListBox-option',
738
+ defaultClassName: "solidaria-ListBox-option",
313
739
  },
314
- renderValues
740
+ renderValues,
315
741
  );
742
+ const hasPrimitiveLabel = () => {
743
+ return typeof props.children === "string" || typeof props.children === "number";
744
+ };
745
+
746
+ const selectionIndicatorContext = createMemo<SelectionIndicatorContextValue>(() => ({
747
+ isSelected: optionAria.isSelected,
748
+ }));
749
+ const draggableItem = createMemo(() => {
750
+ if (!listContext?.dragAndDropHooks?.useDraggableItem || !listContext.dragState)
751
+ return undefined;
752
+ return listContext.dragAndDropHooks.useDraggableItem(
753
+ {
754
+ key: local.id as string | number,
755
+ },
756
+ listContext.dragState as Parameters<NonNullable<DragAndDropHooks<T>["useDraggableItem"]>>[1],
757
+ );
758
+ });
759
+ const droppableItem = createMemo(() => {
760
+ if (!listContext?.dragAndDropHooks?.useDroppableItem || !listContext.dropState)
761
+ return undefined;
762
+ return listContext.dragAndDropHooks.useDroppableItem(
763
+ {
764
+ key: local.id as string | number,
765
+ },
766
+ listContext.dropState as Parameters<NonNullable<DragAndDropHooks<T>["useDroppableItem"]>>[1],
767
+ () => ref(),
768
+ );
769
+ });
316
770
 
317
- // Remove ref from spread props
318
771
  const cleanOptionProps = () => {
319
- const { ref: _ref1, ...rest } = optionAria.optionProps as Record<string, unknown>;
772
+ const {
773
+ ref: _ref1,
774
+ "aria-describedby": _ariaDescribedby,
775
+ ...rest
776
+ } = optionAria.optionProps as Record<string, unknown>;
777
+ if (!hasPrimitiveLabel() && rest["aria-label"] == null) {
778
+ delete rest["aria-labelledby"];
779
+ }
320
780
  return rest;
321
781
  };
322
- const cleanHoverProps = () => {
323
- const { ref: _ref2, ...rest } = hoverProps as Record<string, unknown>;
324
- return rest;
782
+ const domProps = () => filterDOMProps(ariaProps as Record<string, unknown>, { global: true });
783
+
784
+ return (
785
+ <SelectionIndicatorContext.Provider value={selectionIndicatorContext()}>
786
+ <li
787
+ ref={(el) => {
788
+ setRef(el);
789
+ assignRef(local.ref, el);
790
+ }}
791
+ {...mergeProps(
792
+ domProps(),
793
+ cleanOptionProps(),
794
+ (draggableItem()?.dragProps as Record<string, unknown> | undefined) ?? {},
795
+ (droppableItem()?.dropProps as Record<string, unknown> | undefined) ?? {},
796
+ )}
797
+ class={renderProps.class()}
798
+ style={renderProps.style()}
799
+ data-selected={optionAria.isSelected() || undefined}
800
+ data-focused={optionAria.isFocused() || undefined}
801
+ data-focus-visible={optionAria.isFocusVisible() || undefined}
802
+ data-pressed={optionAria.isPressed() || undefined}
803
+ data-hovered={optionAria.isHovered() || undefined}
804
+ data-disabled={optionAria.isDisabled() || undefined}
805
+ data-dragging={draggableItem()?.isDragging || undefined}
806
+ data-drop-target={droppableItem()?.isDropTarget || undefined}
807
+ slot={local.slot}
808
+ >
809
+ {hasPrimitiveLabel() ? (
810
+ <span {...optionAria.labelProps}>{renderProps.renderChildren()}</span>
811
+ ) : (
812
+ renderProps.renderChildren()
813
+ )}
814
+ </li>
815
+ </SelectionIndicatorContext.Provider>
816
+ );
817
+ }
818
+
819
+ /**
820
+ * Load more sentinel item for listbox collections.
821
+ */
822
+ export function ListBoxLoadMoreItem(props: ListBoxLoadMoreItemProps): JSX.Element {
823
+ let sentinelRef: HTMLDivElement | undefined;
824
+ const [isPending, setIsPending] = createSignal(false);
825
+
826
+ const isLoading = () => !!props.isLoading || isPending();
827
+
828
+ const triggerLoadMore = async () => {
829
+ if (isLoading()) return;
830
+ setIsPending(true);
831
+ try {
832
+ await props.onLoadMore();
833
+ } finally {
834
+ setIsPending(false);
835
+ }
325
836
  };
326
837
 
838
+ createEffect(() => {
839
+ if (!sentinelRef || typeof IntersectionObserver !== "function") return;
840
+
841
+ const offset = props.scrollOffset ?? 1;
842
+ const margin = `0px 0px ${100 * offset}% 0px`;
843
+ const observer = new IntersectionObserver(
844
+ (entries) => {
845
+ if (entries[0]?.isIntersecting) {
846
+ void triggerLoadMore();
847
+ }
848
+ },
849
+ { rootMargin: margin },
850
+ );
851
+
852
+ observer.observe(sentinelRef);
853
+ return () => observer.disconnect();
854
+ });
855
+
856
+ const renderProps = useRenderProps(
857
+ {
858
+ children: props.children ?? (() => (isLoading() ? "Loading more..." : "Load more")),
859
+ class: props.class,
860
+ style: props.style,
861
+ defaultClassName: "solidaria-ListBox-loadMore",
862
+ },
863
+ () => ({ isLoading: isLoading() }),
864
+ );
865
+
327
866
  return (
328
- <li
329
- {...cleanOptionProps()}
330
- {...cleanHoverProps()}
331
- class={renderProps.class()}
332
- style={renderProps.style()}
333
- data-selected={optionAria.isSelected() || undefined}
334
- data-focused={optionAria.isFocused() || undefined}
335
- data-focus-visible={optionAria.isFocusVisible() || undefined}
336
- data-pressed={optionAria.isPressed() || undefined}
337
- data-hovered={isHovered() || undefined}
338
- data-disabled={optionAria.isDisabled() || undefined}
339
- >
340
- {renderProps.renderChildren()}
341
- </li>
867
+ <>
868
+ <li style={{ position: "relative", width: 0, height: 0, overflow: "hidden" }} inert>
869
+ <div ref={sentinelRef} style={{ position: "absolute", height: "1px", width: "1px" }} />
870
+ </li>
871
+ <li
872
+ role="option"
873
+ aria-disabled={true}
874
+ tabIndex={0}
875
+ onFocus={() => {
876
+ void triggerLoadMore();
877
+ }}
878
+ class={renderProps.class()}
879
+ style={renderProps.style()}
880
+ data-loading={isLoading() || undefined}
881
+ >
882
+ {renderProps.renderChildren()}
883
+ </li>
884
+ </>
342
885
  );
343
886
  }
344
887
 
345
- // Attach Option as a static property
888
+ /**
889
+ * Section primitive alias for ListBox composition parity.
890
+ */
891
+ export function ListBoxSection(props: ListBoxSectionProps): JSX.Element {
892
+ return <Section {...props} />;
893
+ }
894
+
346
895
  ListBox.Option = ListBoxOption;
896
+ ListBox.LoadMoreItem = ListBoxLoadMoreItem;