@proyecto-viviana/solidaria-components 0.2.5 → 0.2.9

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 (194) hide show
  1. package/LICENSE +21 -0
  2. package/dist/ActionBar.d.ts +71 -0
  3. package/dist/ActionBar.d.ts.map +1 -0
  4. package/dist/ActionGroup.d.ts +74 -0
  5. package/dist/ActionGroup.d.ts.map +1 -0
  6. package/dist/Alert.d.ts +70 -0
  7. package/dist/Alert.d.ts.map +1 -0
  8. package/dist/Breadcrumbs.d.ts +10 -2
  9. package/dist/Breadcrumbs.d.ts.map +1 -1
  10. package/dist/Button.d.ts +4 -0
  11. package/dist/Button.d.ts.map +1 -1
  12. package/dist/Calendar.d.ts +13 -0
  13. package/dist/Calendar.d.ts.map +1 -1
  14. package/dist/Checkbox.d.ts +2 -2
  15. package/dist/Checkbox.d.ts.map +1 -1
  16. package/dist/Collection.d.ts +125 -0
  17. package/dist/Collection.d.ts.map +1 -0
  18. package/dist/Color.d.ts +114 -2
  19. package/dist/Color.d.ts.map +1 -1
  20. package/dist/ColorEditor.d.ts +42 -0
  21. package/dist/ColorEditor.d.ts.map +1 -0
  22. package/dist/ComboBox.d.ts +64 -0
  23. package/dist/ComboBox.d.ts.map +1 -1
  24. package/dist/ContextualHelpTrigger.d.ts +40 -0
  25. package/dist/ContextualHelpTrigger.d.ts.map +1 -0
  26. package/dist/DateField.d.ts +27 -2
  27. package/dist/DateField.d.ts.map +1 -1
  28. package/dist/DatePicker.d.ts +67 -2
  29. package/dist/DatePicker.d.ts.map +1 -1
  30. package/dist/Dialog.d.ts.map +1 -1
  31. package/dist/Disclosure.d.ts +2 -0
  32. package/dist/Disclosure.d.ts.map +1 -1
  33. package/dist/DragAndDrop.d.ts +80 -0
  34. package/dist/DragAndDrop.d.ts.map +1 -0
  35. package/dist/DragPreview.d.ts +14 -0
  36. package/dist/DragPreview.d.ts.map +1 -0
  37. package/dist/DropZone.d.ts +27 -0
  38. package/dist/DropZone.d.ts.map +1 -0
  39. package/dist/FieldError.d.ts +23 -0
  40. package/dist/FieldError.d.ts.map +1 -0
  41. package/dist/FileTrigger.d.ts +26 -0
  42. package/dist/FileTrigger.d.ts.map +1 -0
  43. package/dist/Focusable.d.ts +27 -0
  44. package/dist/Focusable.d.ts.map +1 -0
  45. package/dist/Form.d.ts +27 -0
  46. package/dist/Form.d.ts.map +1 -0
  47. package/dist/GridList.d.ts +40 -1
  48. package/dist/GridList.d.ts.map +1 -1
  49. package/dist/Icon.d.ts +57 -0
  50. package/dist/Icon.d.ts.map +1 -0
  51. package/dist/Keyboard.d.ts +13 -0
  52. package/dist/Keyboard.d.ts.map +1 -0
  53. package/dist/Link.d.ts.map +1 -1
  54. package/dist/ListBox.d.ts +43 -1
  55. package/dist/ListBox.d.ts.map +1 -1
  56. package/dist/ListDropTargetDelegate.d.ts +38 -0
  57. package/dist/ListDropTargetDelegate.d.ts.map +1 -0
  58. package/dist/Menu.d.ts +20 -2
  59. package/dist/Menu.d.ts.map +1 -1
  60. package/dist/Meter.d.ts +2 -2
  61. package/dist/Meter.d.ts.map +1 -1
  62. package/dist/Modal.d.ts +2 -0
  63. package/dist/Modal.d.ts.map +1 -1
  64. package/dist/NumberField.d.ts +2 -0
  65. package/dist/NumberField.d.ts.map +1 -1
  66. package/dist/Popover.d.ts +4 -2
  67. package/dist/Popover.d.ts.map +1 -1
  68. package/dist/Pressable.d.ts +27 -0
  69. package/dist/Pressable.d.ts.map +1 -0
  70. package/dist/ProgressBar.d.ts +2 -2
  71. package/dist/ProgressBar.d.ts.map +1 -1
  72. package/dist/RadioGroup.d.ts.map +1 -1
  73. package/dist/RangeCalendar.d.ts +5 -0
  74. package/dist/RangeCalendar.d.ts.map +1 -1
  75. package/dist/RouterProvider.d.ts +75 -0
  76. package/dist/RouterProvider.d.ts.map +1 -0
  77. package/dist/SearchField.d.ts +2 -3
  78. package/dist/SearchField.d.ts.map +1 -1
  79. package/dist/Select.d.ts +11 -0
  80. package/dist/Select.d.ts.map +1 -1
  81. package/dist/SelectionIndicator.d.ts +30 -0
  82. package/dist/SelectionIndicator.d.ts.map +1 -0
  83. package/dist/SharedElementTransition.d.ts +39 -0
  84. package/dist/SharedElementTransition.d.ts.map +1 -0
  85. package/dist/Slider.d.ts +6 -3
  86. package/dist/Slider.d.ts.map +1 -1
  87. package/dist/Table.d.ts +39 -0
  88. package/dist/Table.d.ts.map +1 -1
  89. package/dist/Tabs.d.ts +4 -3
  90. package/dist/Tabs.d.ts.map +1 -1
  91. package/dist/TagGroup.d.ts +12 -2
  92. package/dist/TagGroup.d.ts.map +1 -1
  93. package/dist/Text.d.ts +10 -0
  94. package/dist/Text.d.ts.map +1 -0
  95. package/dist/TextField.d.ts +4 -0
  96. package/dist/TextField.d.ts.map +1 -1
  97. package/dist/TimeField.d.ts +26 -1
  98. package/dist/TimeField.d.ts.map +1 -1
  99. package/dist/Toast.d.ts.map +1 -1
  100. package/dist/ToggleButton.d.ts +30 -0
  101. package/dist/ToggleButton.d.ts.map +1 -0
  102. package/dist/ToggleButtonGroup.d.ts +33 -0
  103. package/dist/ToggleButtonGroup.d.ts.map +1 -0
  104. package/dist/Toolbar.d.ts.map +1 -1
  105. package/dist/Tooltip.d.ts +9 -0
  106. package/dist/Tooltip.d.ts.map +1 -1
  107. package/dist/Tree.d.ts +44 -2
  108. package/dist/Tree.d.ts.map +1 -1
  109. package/dist/Virtualizer.d.ts +61 -0
  110. package/dist/Virtualizer.d.ts.map +1 -0
  111. package/dist/VirtualizerLayouts.d.ts +82 -0
  112. package/dist/VirtualizerLayouts.d.ts.map +1 -0
  113. package/dist/VisuallyHidden.d.ts +3 -1
  114. package/dist/VisuallyHidden.d.ts.map +1 -1
  115. package/dist/contexts.d.ts +1 -0
  116. package/dist/contexts.d.ts.map +1 -1
  117. package/dist/index.d.ts +57 -25
  118. package/dist/index.d.ts.map +1 -1
  119. package/dist/index.js +13961 -5946
  120. package/dist/index.js.map +1 -7
  121. package/dist/index.ssr.js +9612 -2401
  122. package/dist/index.ssr.js.map +1 -7
  123. package/dist/useDragAndDrop.d.ts +93 -0
  124. package/dist/useDragAndDrop.d.ts.map +1 -0
  125. package/dist/utils.d.ts +7 -1
  126. package/dist/utils.d.ts.map +1 -1
  127. package/dist/virtualizer/Layout.d.ts +79 -0
  128. package/dist/virtualizer/Layout.d.ts.map +1 -0
  129. package/package.json +8 -6
  130. package/src/ActionBar.tsx +248 -0
  131. package/src/ActionGroup.tsx +285 -0
  132. package/src/Alert.tsx +177 -0
  133. package/src/Autocomplete.tsx +1 -1
  134. package/src/Breadcrumbs.tsx +103 -17
  135. package/src/Button.tsx +65 -21
  136. package/src/Calendar.tsx +179 -53
  137. package/src/Checkbox.tsx +1 -2
  138. package/src/Collection.tsx +341 -0
  139. package/src/Color.tsx +652 -34
  140. package/src/ColorEditor.tsx +231 -0
  141. package/src/ComboBox.tsx +315 -81
  142. package/src/ContextualHelpTrigger.tsx +183 -0
  143. package/src/DateField.tsx +93 -19
  144. package/src/DatePicker.tsx +495 -25
  145. package/src/Dialog.tsx +40 -9
  146. package/src/Disclosure.tsx +33 -27
  147. package/src/DragAndDrop.tsx +334 -0
  148. package/src/DragPreview.tsx +45 -0
  149. package/src/DropZone.tsx +213 -0
  150. package/src/FieldError.tsx +67 -0
  151. package/src/FileTrigger.tsx +83 -0
  152. package/src/Focusable.tsx +106 -0
  153. package/src/Form.tsx +85 -0
  154. package/src/GridList.tsx +379 -41
  155. package/src/Icon.tsx +154 -0
  156. package/src/Keyboard.tsx +26 -0
  157. package/src/Link.tsx +14 -1
  158. package/src/ListBox.tsx +484 -33
  159. package/src/ListDropTargetDelegate.ts +282 -0
  160. package/src/Menu.tsx +388 -35
  161. package/src/Meter.tsx +7 -3
  162. package/src/Modal.tsx +32 -4
  163. package/src/NumberField.tsx +163 -43
  164. package/src/Popover.tsx +136 -180
  165. package/src/Pressable.tsx +108 -0
  166. package/src/ProgressBar.tsx +7 -3
  167. package/src/RadioGroup.tsx +35 -25
  168. package/src/RangeCalendar.tsx +100 -68
  169. package/src/RouterProvider.tsx +240 -0
  170. package/src/SearchField.tsx +142 -34
  171. package/src/Select.tsx +221 -73
  172. package/src/SelectionIndicator.tsx +105 -0
  173. package/src/SharedElementTransition.tsx +258 -0
  174. package/src/Slider.tsx +16 -6
  175. package/src/Table.tsx +417 -57
  176. package/src/Tabs.tsx +68 -35
  177. package/src/TagGroup.tsx +121 -36
  178. package/src/Text.tsx +18 -0
  179. package/src/TextField.tsx +25 -8
  180. package/src/TimeField.tsx +101 -151
  181. package/src/Toast.tsx +108 -14
  182. package/src/ToggleButton.tsx +159 -0
  183. package/src/ToggleButtonGroup.tsx +136 -0
  184. package/src/Toolbar.tsx +14 -8
  185. package/src/Tooltip.tsx +108 -19
  186. package/src/Tree.tsx +1143 -87
  187. package/src/Virtualizer.tsx +702 -0
  188. package/src/VirtualizerLayouts.ts +265 -0
  189. package/src/VisuallyHidden.tsx +15 -21
  190. package/src/contexts.ts +1 -0
  191. package/src/index.ts +1057 -620
  192. package/src/useDragAndDrop.ts +351 -0
  193. package/src/utils.tsx +37 -3
  194. package/src/virtualizer/Layout.ts +200 -0
package/src/ListBox.tsx CHANGED
@@ -8,16 +8,21 @@
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,
18
+ Show,
15
19
  } from 'solid-js';
16
20
  import {
17
21
  createListBox,
18
22
  createOption,
19
23
  createFocusRing,
20
24
  createHover,
25
+ mergeProps,
21
26
  type AriaListBoxProps,
22
27
  type AriaOptionProps,
23
28
  } from '@proyecto-viviana/solidaria';
@@ -25,6 +30,7 @@ import {
25
30
  createListState,
26
31
  type ListState,
27
32
  type Key,
33
+ type DropTarget,
28
34
  } from '@proyecto-viviana/solid-stately';
29
35
  import {
30
36
  type RenderChildren,
@@ -34,6 +40,31 @@ import {
34
40
  useRenderProps,
35
41
  filterDOMProps,
36
42
  } from './utils';
43
+ import { SharedElementTransition } from './SharedElementTransition';
44
+ import {
45
+ SelectionIndicatorContext,
46
+ type SelectionIndicatorContextValue,
47
+ } from './SelectionIndicator';
48
+ import { useVirtualizerContext } from './Virtualizer';
49
+ import { type DragAndDropHooks } from './useDragAndDrop';
50
+ import {
51
+ getNormalizedDropTargetKey,
52
+ mergePersistedKeysIntoVirtualRange,
53
+ useDndPersistedKeys,
54
+ useRenderDropIndicator,
55
+ } from './DragAndDrop';
56
+ import {
57
+ CollectionRendererContext,
58
+ Section,
59
+ Header,
60
+ Group,
61
+ type CollectionEntry,
62
+ type CollectionRendererContextValue,
63
+ type SectionProps,
64
+ useCollectionRenderer,
65
+ isCollectionSection,
66
+ flattenCollectionEntries,
67
+ } from './Collection';
37
68
 
38
69
  // ============================================
39
70
  // TYPES
@@ -54,7 +85,7 @@ export interface ListBoxProps<T>
54
85
  extends Omit<AriaListBoxProps, 'children'>,
55
86
  SlotProps {
56
87
  /** The items to render in the listbox. */
57
- items: T[];
88
+ items: CollectionEntry<T>[];
58
89
  /** Function to get the key from an item. */
59
90
  getKey?: (item: T) => Key;
60
91
  /** Function to get the text value from an item. */
@@ -79,6 +110,14 @@ export interface ListBoxProps<T>
79
110
  style?: StyleOrFunction<ListBoxRenderProps>;
80
111
  /** A function to render when the listbox is empty. */
81
112
  renderEmptyState?: () => JSX.Element;
113
+ /** Whether there are more items to load. */
114
+ hasMore?: boolean;
115
+ /** Whether additional items are currently loading. */
116
+ isLoading?: boolean;
117
+ /** Called when the load more sentinel becomes visible. */
118
+ onLoadMore?: () => void | Promise<void>;
119
+ /** Drag and drop hooks from `useDragAndDrop`. */
120
+ dragAndDropHooks?: DragAndDropHooks<T>;
82
121
  }
83
122
 
84
123
  export interface ListBoxOptionRenderProps {
@@ -113,16 +152,36 @@ export interface ListBoxOptionProps<T>
113
152
  textValue?: string;
114
153
  }
115
154
 
155
+ export interface ListBoxLoadMoreItemProps extends SlotProps {
156
+ /** Called when the sentinel becomes visible. */
157
+ onLoadMore: () => void | Promise<void>;
158
+ /** Whether additional items are currently loading. */
159
+ isLoading?: boolean;
160
+ /** Content for the load more row. */
161
+ children?: JSX.Element;
162
+ /** The CSS className for the element. */
163
+ class?: ClassNameOrFunction<{ isLoading: boolean }>;
164
+ /** The inline style for the element. */
165
+ style?: StyleOrFunction<{ isLoading: boolean }>;
166
+ }
167
+
168
+ export interface ListBoxSectionProps extends SectionProps {}
169
+
116
170
  // ============================================
117
171
  // CONTEXT
118
172
  // ============================================
119
173
 
120
174
  interface ListBoxContextValue<T> {
121
175
  state: ListState<T>;
176
+ isDisabled: () => boolean;
177
+ dragAndDropHooks?: DragAndDropHooks<unknown>;
178
+ dragState?: unknown;
179
+ dropState?: unknown;
122
180
  }
123
181
 
124
182
  export const ListBoxContext = createContext<ListBoxContextValue<unknown> | null>(null);
125
183
  export const ListBoxStateContext = createContext<ListState<unknown> | null>(null);
184
+ export const ListStateContext = ListBoxStateContext;
126
185
 
127
186
  // ============================================
128
187
  // COMPONENTS
@@ -134,14 +193,20 @@ export const ListBoxStateContext = createContext<ListState<unknown> | null>(null
134
193
  export function ListBox<T>(props: ListBoxProps<T>): JSX.Element {
135
194
  const [local, stateProps, ariaProps] = splitProps(
136
195
  props,
137
- ['children', 'class', 'style', 'slot', 'renderEmptyState'],
196
+ ['children', 'class', 'style', 'slot', 'renderEmptyState', 'hasMore', 'isLoading', 'onLoadMore', 'dragAndDropHooks'],
138
197
  ['items', 'getKey', 'getTextValue', 'getDisabled', 'disabledKeys', 'selectionMode', 'selectedKeys', 'defaultSelectedKeys', 'onSelectionChange']
139
198
  );
140
199
 
200
+ const flatItems = createMemo<T[]>(() => {
201
+ return flattenCollectionEntries(stateProps.items);
202
+ });
203
+
204
+ const hasSections = createMemo(() => stateProps.items.some((item) => isCollectionSection(item)));
205
+
141
206
  // Create list state
142
207
  const state = createListState<T>({
143
208
  get items() {
144
- return stateProps.items;
209
+ return flatItems();
145
210
  },
146
211
  get getKey() {
147
212
  return stateProps.getKey;
@@ -179,7 +244,7 @@ export function ListBox<T>(props: ListBoxProps<T>): JSX.Element {
179
244
  };
180
245
 
181
246
  // Create listbox aria props
182
- const { listBoxProps } = createListBox(
247
+ const listBoxAria = createListBox(
183
248
  {
184
249
  ...ariaProps,
185
250
  get isDisabled() {
@@ -218,35 +283,303 @@ export function ListBox<T>(props: ListBoxProps<T>): JSX.Element {
218
283
 
219
284
  // Remove ref from spread props
220
285
  const cleanListBoxProps = () => {
221
- const { ref: _ref1, ...rest } = listBoxProps as Record<string, unknown>;
286
+ const { ref: _ref1, ...rest } = listBoxAria.listBoxProps as Record<string, unknown>;
222
287
  return rest;
223
288
  };
224
289
  const cleanFocusProps = () => {
225
290
  const { ref: _ref2, ...rest } = focusProps as Record<string, unknown>;
226
291
  return rest;
227
292
  };
293
+ const cleanLabelProps = () => {
294
+ const { ref: _ref3, ...rest } = listBoxAria.labelProps as Record<string, unknown>;
295
+ return rest;
296
+ };
297
+ const [listRef, setListRef] = createSignal<HTMLElement | null>(null);
228
298
 
229
299
  const isEmpty = () => stateProps.items.length === 0;
300
+ const parentCollectionRenderer = useCollectionRenderer<unknown>();
301
+ const getItemNodes = createMemo(() => Array.from(state.collection()).filter((node) => node.type === 'item'));
302
+ const getDropTargetByIndex = (index: number, position: 'before' | 'after' | 'on'): DropTarget | null => {
303
+ const node = getItemNodes()[index];
304
+ if (!node) return null;
305
+ return { type: 'item', key: node.key, dropPosition: position };
306
+ };
307
+ const hasDroppableDnd = createMemo(() => {
308
+ const hooks = local.dragAndDropHooks;
309
+ return Boolean(
310
+ hooks?.useDroppableCollectionState &&
311
+ hooks.useDroppableCollection &&
312
+ (hooks.dropTargetDelegate || parentCollectionRenderer?.dropTargetDelegate || hooks.ListDropTargetDelegate)
313
+ );
314
+ });
315
+ const dropState = createMemo(() => {
316
+ if (!hasDroppableDnd()) return undefined;
317
+ return local.dragAndDropHooks?.useDroppableCollectionState?.({});
318
+ });
319
+ const hasDraggableDnd = createMemo(() => {
320
+ const hooks = local.dragAndDropHooks;
321
+ return Boolean(hooks?.useDraggableCollectionState && hooks.useDraggableCollection);
322
+ });
323
+ const dragState = createMemo(() => {
324
+ if (!hasDraggableDnd()) return undefined;
325
+ return local.dragAndDropHooks?.useDraggableCollectionState?.({
326
+ items: flatItems(),
327
+ });
328
+ });
329
+ createEffect(() => {
330
+ if (!hasDraggableDnd()) return;
331
+ const hooks = local.dragAndDropHooks;
332
+ const activeDragState = dragState();
333
+ if (!hooks?.useDraggableCollection || !activeDragState) return;
334
+ hooks.useDraggableCollection({}, activeDragState, () => listRef());
335
+ });
336
+ const droppableCollection = createMemo(() => {
337
+ if (!hasDroppableDnd()) return undefined;
338
+ const hooks = local.dragAndDropHooks;
339
+ const activeDropState = dropState();
340
+ if (!hooks?.useDroppableCollection || !activeDropState) return undefined;
341
+ const resolveDirection = (): 'ltr' | 'rtl' => {
342
+ const el = listRef();
343
+ if (el && typeof window !== 'undefined' && typeof window.getComputedStyle === 'function') {
344
+ const dir = window.getComputedStyle(el).direction;
345
+ if (dir === 'rtl') return 'rtl';
346
+ }
347
+ return typeof document !== 'undefined' && document.dir === 'rtl' ? 'rtl' : 'ltr';
348
+ };
349
+ const dropTargetDelegate = hooks.dropTargetDelegate
350
+ ?? parentCollectionRenderer?.dropTargetDelegate
351
+ ?? (hooks.ListDropTargetDelegate
352
+ ? new hooks.ListDropTargetDelegate(
353
+ () => state.collection(),
354
+ () => listRef(),
355
+ { layout: 'stack', orientation: 'vertical', direction: resolveDirection() }
356
+ )
357
+ : undefined);
358
+ if (!dropTargetDelegate) return undefined;
359
+ return hooks.useDroppableCollection(
360
+ {
361
+ dropTargetDelegate,
362
+ keyboardDelegate: {
363
+ getFirstKey: () => state.collection().getFirstKey(),
364
+ getLastKey: () => state.collection().getLastKey(),
365
+ getKeyBelow: (key) => state.collection().getKeyAfter(key),
366
+ getKeyAbove: (key) => state.collection().getKeyBefore(key),
367
+ getKeyPageBelow: (key) => state.collection().getKeyAfter(key),
368
+ getKeyPageAbove: (key) => state.collection().getKeyBefore(key),
369
+ },
370
+ },
371
+ activeDropState,
372
+ () => listRef()
373
+ );
374
+ });
375
+ const isRootDropTarget = createMemo(() => {
376
+ return Boolean(dropState()?.target?.type === 'root');
377
+ });
378
+ const dndRenderDropIndicator = createMemo(() => useRenderDropIndicator(local.dragAndDropHooks, dropState()));
379
+ const dndDropIndicator = (index: number, position: 'before' | 'after' | 'on') => {
380
+ const target = getDropTargetByIndex(index, position);
381
+ if (!target || target.type !== 'item') return undefined;
382
+ return dndRenderDropIndicator()?.(target);
383
+ };
384
+ const virtualizer = useVirtualizerContext();
385
+ const persistedKeys = useDndPersistedKeys(
386
+ { focusedKey: state.focusedKey },
387
+ local.dragAndDropHooks,
388
+ dropState(),
389
+ state.collection()
390
+ );
391
+ const virtualRange = createMemo(() => {
392
+ if (!virtualizer || !parentCollectionRenderer?.isVirtualized || hasSections()) return null;
393
+ const baseRange = virtualizer.getVisibleRange(stateProps.items.length);
394
+ const itemNodes = getItemNodes();
395
+ const persistedIndexes = Array.from(persistedKeys())
396
+ .map((key) => itemNodes.findIndex((node) => node.key === key))
397
+ .filter((index) => index >= 0);
398
+ const dropTarget = dropState()?.target;
399
+ const normalizedDropKey = getNormalizedDropTargetKey(dropTarget, state.collection());
400
+ const focusedKey = state.focusedKey();
401
+ const focusedIndex = focusedKey != null ? itemNodes.findIndex((node) => node.key === focusedKey) : -1;
402
+ const forceIncludeIndexes = [
403
+ dropTarget?.type === 'item' ? itemNodes.findIndex((node) => node.key === dropTarget.key) : -1,
404
+ normalizedDropKey != null ? itemNodes.findIndex((node) => node.key === normalizedDropKey) : -1,
405
+ dropTarget?.type === 'item' ? -1 : focusedIndex,
406
+ ].filter((index) => index >= 0);
407
+ return mergePersistedKeysIntoVirtualRange(baseRange, persistedIndexes, stateProps.items.length, virtualizer, 80, {
408
+ forceIncludeIndexes,
409
+ forceIncludeMaxSpan: 320,
410
+ });
411
+ });
412
+ createEffect(() => {
413
+ if (!virtualizer || !parentCollectionRenderer?.isVirtualized) return;
414
+ const getItemNodes = () => Array.from(state.collection()).filter((node) => node.type === 'item');
415
+ virtualizer.setDropTargetItemCountResolver(() => getItemNodes().length);
416
+ virtualizer.setDropTargetIndexResolver((key) => {
417
+ const index = getItemNodes().findIndex((node) => node.key === key);
418
+ return index >= 0 ? index : null;
419
+ });
420
+ virtualizer.setDropTargetResolver((target) => {
421
+ const node = getItemNodes()[target.index];
422
+ if (!node) return target;
423
+ return {
424
+ ...target,
425
+ key: typeof node.key === 'string' || typeof node.key === 'number' ? node.key : undefined,
426
+ };
427
+ });
428
+ onCleanup(() => {
429
+ virtualizer.setDropTargetIndexResolver(undefined);
430
+ virtualizer.setDropTargetItemCountResolver(undefined);
431
+ virtualizer.setDropTargetResolver(undefined);
432
+ });
433
+ });
434
+ const visibleItems = createMemo(() => {
435
+ const range = virtualRange();
436
+ if (!range) return stateProps.items;
437
+ return stateProps.items.slice(range.start, range.end);
438
+ });
439
+ const sectionedRenderEntries = createMemo(() => {
440
+ let globalIndex = 0;
441
+ return stateProps.items.map((entry) => {
442
+ if (isCollectionSection(entry)) {
443
+ const sectionItems = entry.items.map((item) => ({
444
+ item,
445
+ index: globalIndex++,
446
+ }));
447
+ return {
448
+ type: 'section' as const,
449
+ section: entry,
450
+ items: sectionItems,
451
+ };
452
+ }
453
+ const indexedItem = {
454
+ item: entry as T,
455
+ index: globalIndex++,
456
+ };
457
+ return {
458
+ type: 'item' as const,
459
+ item: indexedItem,
460
+ };
461
+ });
462
+ });
463
+ const collectionRenderer = createMemo<CollectionRendererContextValue<unknown>>(() => ({
464
+ ...parentCollectionRenderer,
465
+ renderItem: (item) => props.children(item as T),
466
+ renderDropIndicator: (index, position) =>
467
+ dndDropIndicator(index, position) ?? parentCollectionRenderer?.renderDropIndicator?.(index, position),
468
+ }));
230
469
 
231
470
  return (
232
- <ListBoxContext.Provider value={{ state }}>
471
+ <ListBoxContext.Provider
472
+ value={{
473
+ state,
474
+ isDisabled: resolveDisabled,
475
+ dragAndDropHooks: local.dragAndDropHooks as DragAndDropHooks<unknown> | undefined,
476
+ dragState: dragState(),
477
+ dropState: dropState(),
478
+ }}
479
+ >
233
480
  <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>
481
+ <CollectionRendererContext.Provider value={collectionRenderer()}>
482
+ <>
483
+ <Show when={ariaProps.label}>
484
+ <span {...cleanLabelProps()}>{ariaProps.label as JSX.Element}</span>
485
+ </Show>
486
+ <ul
487
+ {...mergeProps(
488
+ domProps(),
489
+ cleanListBoxProps(),
490
+ cleanFocusProps(),
491
+ (droppableCollection()?.collectionProps as Record<string, unknown> | undefined) ?? {}
492
+ )}
493
+ ref={(el) => {
494
+ setListRef(el);
495
+ }}
496
+ class={renderProps.class()}
497
+ style={renderProps.style()}
498
+ data-focused={state.isFocused() || undefined}
499
+ data-focus-visible={isFocusVisible() || undefined}
500
+ data-disabled={resolveDisabled() || undefined}
501
+ data-empty={isEmpty() || undefined}
502
+ data-drop-target={isRootDropTarget() || undefined}
503
+ >
504
+ <SharedElementTransition>
505
+ {isEmpty() && local.renderEmptyState
506
+ ? local.renderEmptyState()
507
+ : hasSections()
508
+ ? (
509
+ <For each={sectionedRenderEntries()}>
510
+ {(entry) =>
511
+ entry.type === 'section'
512
+ ? (
513
+ <li role="presentation" data-section-wrapper>
514
+ <Section class="solidaria-ListBox-section">
515
+ {entry.section.title != null && (
516
+ <Header class="solidaria-ListBox-sectionHeader">{entry.section.title}</Header>
517
+ )}
518
+ <Group class="solidaria-ListBox-sectionGroup">
519
+ <ul role="group" aria-label={entry.section['aria-label']}>
520
+ <For each={entry.items}>
521
+ {(indexedItem) => (
522
+ <>
523
+ {collectionRenderer().renderDropIndicator?.(indexedItem.index, 'before')}
524
+ {collectionRenderer().renderDropIndicator?.(indexedItem.index, 'on')}
525
+ {props.children(indexedItem.item)}
526
+ {collectionRenderer().renderDropIndicator?.(indexedItem.index, 'after')}
527
+ </>
528
+ )}
529
+ </For>
530
+ </ul>
531
+ </Group>
532
+ </Section>
533
+ </li>
534
+ )
535
+ : (
536
+ <>
537
+ {collectionRenderer().renderDropIndicator?.(entry.item.index, 'before')}
538
+ {collectionRenderer().renderDropIndicator?.(entry.item.index, 'on')}
539
+ {props.children(entry.item.item)}
540
+ {collectionRenderer().renderDropIndicator?.(entry.item.index, 'after')}
541
+ </>
542
+ )
543
+ }
544
+ </For>
545
+ )
546
+ : (
547
+ <>
548
+ {virtualRange()?.offsetTop
549
+ ? <li role="presentation" aria-hidden="true" style={{ height: `${virtualRange()!.offsetTop}px` }} data-virtualizer-spacer="top" />
550
+ : null}
551
+ <For each={visibleItems()}>
552
+ {(item, index) => {
553
+ const itemIndex = () => (virtualRange()?.start ?? 0) + index();
554
+ const beforeIndicator = () => collectionRenderer().renderDropIndicator?.(itemIndex(), 'before');
555
+ const onIndicator = () => collectionRenderer().renderDropIndicator?.(itemIndex(), 'on');
556
+ const afterIndicator = () => collectionRenderer().renderDropIndicator?.(itemIndex(), 'after');
557
+ return (
558
+ <>
559
+ {beforeIndicator()}
560
+ {onIndicator()}
561
+ {props.children(item as T)}
562
+ {afterIndicator()}
563
+ </>
564
+ );
565
+ }}
566
+ </For>
567
+ {virtualRange()?.offsetBottom
568
+ ? <li role="presentation" aria-hidden="true" style={{ height: `${virtualRange()!.offsetBottom}px` }} data-virtualizer-spacer="bottom" />
569
+ : null}
570
+ </>
571
+ )
572
+ }
573
+ </SharedElementTransition>
574
+ {local.hasMore && local.onLoadMore && (
575
+ <ListBoxLoadMoreItem
576
+ onLoadMore={local.onLoadMore}
577
+ isLoading={local.isLoading}
578
+ />
579
+ )}
580
+ </ul>
581
+ </>
582
+ </CollectionRendererContext.Provider>
250
583
  </ListBoxStateContext.Provider>
251
584
  </ListBoxContext.Provider>
252
585
  );
@@ -271,16 +604,21 @@ export function ListBoxOption<T>(props: ListBoxOptionProps<T>): JSX.Element {
271
604
  throw new Error('ListBoxOption must be used within a ListBox');
272
605
  }
273
606
  const state = context as ListState<T>;
607
+ const listContext = useContext(ListBoxContext) as ListBoxContextValue<T> | null;
608
+ const [ref, setRef] = createSignal<HTMLLIElement | null>(null);
274
609
 
275
610
  // Create option aria props
276
611
  const optionAria = createOption<T>(
277
612
  {
278
613
  key: local.id,
279
614
  get isDisabled() {
280
- return ariaProps.isDisabled;
615
+ return Boolean(ariaProps.isDisabled || listContext?.isDisabled());
281
616
  },
282
617
  get 'aria-label'() {
283
- return ariaProps['aria-label'];
618
+ return ariaProps['aria-label'] ?? local.textValue;
619
+ },
620
+ get shouldSelectOnPressUp() {
621
+ return ariaProps.shouldSelectOnPressUp;
284
622
  },
285
623
  },
286
624
  state
@@ -313,10 +651,43 @@ export function ListBoxOption<T>(props: ListBoxOptionProps<T>): JSX.Element {
313
651
  },
314
652
  renderValues
315
653
  );
654
+ const hasPrimitiveLabel = () => {
655
+ return typeof props.children === 'string' || typeof props.children === 'number';
656
+ };
657
+
658
+ const selectionIndicatorContext = createMemo<SelectionIndicatorContextValue>(() => ({
659
+ isSelected: optionAria.isSelected,
660
+ }));
661
+ const draggableItem = createMemo(() => {
662
+ if (!listContext?.dragAndDropHooks?.useDraggableItem || !listContext.dragState) return undefined;
663
+ return listContext.dragAndDropHooks.useDraggableItem(
664
+ {
665
+ key: local.id as string | number,
666
+ },
667
+ listContext.dragState as Parameters<NonNullable<DragAndDropHooks<T>['useDraggableItem']>>[1]
668
+ );
669
+ });
670
+ const droppableItem = createMemo(() => {
671
+ if (!listContext?.dragAndDropHooks?.useDroppableItem || !listContext.dropState) return undefined;
672
+ return listContext.dragAndDropHooks.useDroppableItem(
673
+ {
674
+ key: local.id as string | number,
675
+ },
676
+ listContext.dropState as Parameters<NonNullable<DragAndDropHooks<T>['useDroppableItem']>>[1],
677
+ () => ref()
678
+ );
679
+ });
316
680
 
317
681
  // Remove ref from spread props
318
682
  const cleanOptionProps = () => {
319
- const { ref: _ref1, ...rest } = optionAria.optionProps as Record<string, unknown>;
683
+ const {
684
+ ref: _ref1,
685
+ 'aria-describedby': _ariaDescribedby,
686
+ ...rest
687
+ } = optionAria.optionProps as Record<string, unknown>;
688
+ if (!hasPrimitiveLabel() && rest['aria-label'] == null) {
689
+ delete rest['aria-labelledby'];
690
+ }
320
691
  return rest;
321
692
  };
322
693
  const cleanHoverProps = () => {
@@ -324,23 +695,103 @@ export function ListBoxOption<T>(props: ListBoxOptionProps<T>): JSX.Element {
324
695
  return rest;
325
696
  };
326
697
 
698
+ return (
699
+ <SelectionIndicatorContext.Provider value={selectionIndicatorContext()}>
700
+ <li
701
+ ref={setRef}
702
+ {...mergeProps(
703
+ cleanOptionProps(),
704
+ cleanHoverProps(),
705
+ (draggableItem()?.dragProps as Record<string, unknown> | undefined) ?? {},
706
+ (droppableItem()?.dropProps as Record<string, unknown> | undefined) ?? {}
707
+ )}
708
+ class={renderProps.class()}
709
+ style={renderProps.style()}
710
+ data-selected={optionAria.isSelected() || undefined}
711
+ data-focused={optionAria.isFocused() || undefined}
712
+ data-focus-visible={optionAria.isFocusVisible() || undefined}
713
+ data-pressed={optionAria.isPressed() || undefined}
714
+ data-hovered={isHovered() || undefined}
715
+ data-disabled={optionAria.isDisabled() || undefined}
716
+ data-dragging={draggableItem()?.isDragging || undefined}
717
+ data-drop-target={droppableItem()?.isDropTarget || undefined}
718
+ >
719
+ {hasPrimitiveLabel()
720
+ ? <span {...optionAria.labelProps}>{renderProps.renderChildren()}</span>
721
+ : renderProps.renderChildren()}
722
+ </li>
723
+ </SelectionIndicatorContext.Provider>
724
+ );
725
+ }
726
+
727
+ /**
728
+ * Load more sentinel item for listbox collections.
729
+ */
730
+ export function ListBoxLoadMoreItem(props: ListBoxLoadMoreItemProps): JSX.Element {
731
+ let ref: HTMLLIElement | undefined;
732
+ const [isPending, setIsPending] = createSignal(false);
733
+
734
+ const isLoading = () => !!props.isLoading || isPending();
735
+
736
+ const triggerLoadMore = async () => {
737
+ if (isLoading()) return;
738
+ setIsPending(true);
739
+ try {
740
+ await props.onLoadMore();
741
+ } finally {
742
+ setIsPending(false);
743
+ }
744
+ };
745
+
746
+ createEffect(() => {
747
+ if (!ref || typeof IntersectionObserver !== 'function') return;
748
+
749
+ const observer = new IntersectionObserver((entries) => {
750
+ const entry = entries[0];
751
+ if (entry?.isIntersecting) {
752
+ void triggerLoadMore();
753
+ }
754
+ });
755
+
756
+ observer.observe(ref);
757
+ return () => observer.disconnect();
758
+ });
759
+
760
+ const renderProps = useRenderProps(
761
+ {
762
+ children: props.children ?? (() => (isLoading() ? 'Loading more...' : 'Load more')),
763
+ class: props.class,
764
+ style: props.style,
765
+ defaultClassName: 'solidaria-ListBox-loadMore',
766
+ },
767
+ () => ({ isLoading: isLoading() })
768
+ );
769
+
327
770
  return (
328
771
  <li
329
- {...cleanOptionProps()}
330
- {...cleanHoverProps()}
772
+ ref={ref}
773
+ role="option"
774
+ aria-disabled={true}
775
+ tabIndex={0}
776
+ onFocus={() => {
777
+ void triggerLoadMore();
778
+ }}
331
779
  class={renderProps.class()}
332
780
  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}
781
+ data-loading={isLoading() || undefined}
339
782
  >
340
783
  {renderProps.renderChildren()}
341
784
  </li>
342
785
  );
343
786
  }
344
787
 
788
+ /**
789
+ * Section primitive alias for ListBox composition parity.
790
+ */
791
+ export function ListBoxSection(props: ListBoxSectionProps): JSX.Element {
792
+ return <Section {...props} />;
793
+ }
794
+
345
795
  // Attach Option as a static property
346
796
  ListBox.Option = ListBoxOption;
797
+ ListBox.LoadMoreItem = ListBoxLoadMoreItem;