@proyecto-viviana/solidaria-components 0.4.0 → 0.4.1

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 (192) hide show
  1. package/dist/ActionBar.d.ts.map +1 -1
  2. package/dist/ActionBar.js +2 -11
  3. package/dist/ActionBar.js.map +1 -1
  4. package/dist/ActionBar.jsx +2 -11
  5. package/dist/ActionBar.jsx.map +1 -1
  6. package/dist/ActionGroup.d.ts.map +1 -1
  7. package/dist/ActionGroup.js +1 -1
  8. package/dist/ActionGroup.js.map +1 -1
  9. package/dist/ActionGroup.jsx +1 -1
  10. package/dist/ActionGroup.jsx.map +1 -1
  11. package/dist/Autocomplete.d.ts +2 -2
  12. package/dist/Autocomplete.d.ts.map +1 -1
  13. package/dist/Autocomplete.js +7 -7
  14. package/dist/Autocomplete.js.map +1 -1
  15. package/dist/Autocomplete.jsx +7 -7
  16. package/dist/Autocomplete.jsx.map +1 -1
  17. package/dist/Breadcrumbs.d.ts.map +1 -1
  18. package/dist/Breadcrumbs.js +15 -16
  19. package/dist/Breadcrumbs.js.map +1 -1
  20. package/dist/Breadcrumbs.jsx +19 -24
  21. package/dist/Breadcrumbs.jsx.map +1 -1
  22. package/dist/Calendar.d.ts.map +1 -1
  23. package/dist/Calendar.js +100 -65
  24. package/dist/Calendar.js.map +1 -1
  25. package/dist/Calendar.jsx +10 -1
  26. package/dist/Calendar.jsx.map +1 -1
  27. package/dist/Color.d.ts.map +1 -1
  28. package/dist/Color.js +15 -18
  29. package/dist/Color.js.map +1 -1
  30. package/dist/Color.jsx +15 -19
  31. package/dist/Color.jsx.map +1 -1
  32. package/dist/ComboBox.d.ts +4 -0
  33. package/dist/ComboBox.d.ts.map +1 -1
  34. package/dist/ComboBox.js +21 -13
  35. package/dist/ComboBox.js.map +1 -1
  36. package/dist/ComboBox.jsx +23 -13
  37. package/dist/ComboBox.jsx.map +1 -1
  38. package/dist/DateField.d.ts +11 -2
  39. package/dist/DateField.d.ts.map +1 -1
  40. package/dist/DateField2.js +237 -234
  41. package/dist/DateField2.js.map +1 -1
  42. package/dist/DateField2.jsx +134 -97
  43. package/dist/DateField2.jsx.map +1 -1
  44. package/dist/DatePicker.d.ts +42 -3
  45. package/dist/DatePicker.d.ts.map +1 -1
  46. package/dist/DatePicker.js +6 -990
  47. package/dist/DatePicker.jsx +6 -681
  48. package/dist/DatePicker2.js +1171 -0
  49. package/dist/DatePicker2.js.map +1 -0
  50. package/dist/DatePicker2.jsx +820 -0
  51. package/dist/DatePicker2.jsx.map +1 -0
  52. package/dist/DateRangePickerContext.d.ts +2 -0
  53. package/dist/DateRangePickerContext.d.ts.map +1 -1
  54. package/dist/Disclosure.d.ts.map +1 -1
  55. package/dist/Disclosure.js +3 -0
  56. package/dist/Disclosure.js.map +1 -1
  57. package/dist/Disclosure.jsx +1 -1
  58. package/dist/Disclosure.jsx.map +1 -1
  59. package/dist/GridList.d.ts.map +1 -1
  60. package/dist/GridList.js +2 -2
  61. package/dist/GridList.js.map +1 -1
  62. package/dist/GridList.jsx +2 -2
  63. package/dist/GridList.jsx.map +1 -1
  64. package/dist/ListBox.d.ts +2 -2
  65. package/dist/ListBox.d.ts.map +1 -1
  66. package/dist/ListBox.js +327 -166
  67. package/dist/ListBox.js.map +1 -1
  68. package/dist/ListBox.jsx +161 -25
  69. package/dist/ListBox.jsx.map +1 -1
  70. package/dist/Modal.d.ts.map +1 -1
  71. package/dist/Modal.js +7 -5
  72. package/dist/Modal.js.map +1 -1
  73. package/dist/Modal.jsx +5 -5
  74. package/dist/Modal.jsx.map +1 -1
  75. package/dist/Popover.d.ts.map +1 -1
  76. package/dist/Popover.js +16 -9
  77. package/dist/Popover.js.map +1 -1
  78. package/dist/Popover.jsx +25 -4
  79. package/dist/Popover.jsx.map +1 -1
  80. package/dist/RangeCalendar.d.ts.map +1 -1
  81. package/dist/RangeCalendar.js +182 -46
  82. package/dist/RangeCalendar.js.map +1 -1
  83. package/dist/RangeCalendar.jsx +72 -2
  84. package/dist/RangeCalendar.jsx.map +1 -1
  85. package/dist/SearchField.d.ts.map +1 -1
  86. package/dist/SearchField.js +26 -2
  87. package/dist/SearchField.js.map +1 -1
  88. package/dist/SearchField.jsx +26 -2
  89. package/dist/SearchField.jsx.map +1 -1
  90. package/dist/Select.d.ts +7 -0
  91. package/dist/Select.d.ts.map +1 -1
  92. package/dist/Select.js +62 -49
  93. package/dist/Select.js.map +1 -1
  94. package/dist/Select.jsx +33 -18
  95. package/dist/Select.jsx.map +1 -1
  96. package/dist/SharedElementTransition.d.ts.map +1 -1
  97. package/dist/SharedElementTransition.js +35 -33
  98. package/dist/SharedElementTransition.js.map +1 -1
  99. package/dist/SharedElementTransition.jsx +35 -33
  100. package/dist/SharedElementTransition.jsx.map +1 -1
  101. package/dist/StepList.d.ts.map +1 -1
  102. package/dist/StepList.js +2 -9
  103. package/dist/StepList.js.map +1 -1
  104. package/dist/StepList.jsx +1 -6
  105. package/dist/StepList.jsx.map +1 -1
  106. package/dist/Table.d.ts +2 -0
  107. package/dist/Table.d.ts.map +1 -1
  108. package/dist/Table.js +10 -3
  109. package/dist/Table.js.map +1 -1
  110. package/dist/Table.jsx +10 -3
  111. package/dist/Table.jsx.map +1 -1
  112. package/dist/Tabs.d.ts.map +1 -1
  113. package/dist/Tabs.js +3 -0
  114. package/dist/Tabs.js.map +1 -1
  115. package/dist/Tabs.jsx +1 -1
  116. package/dist/Tabs.jsx.map +1 -1
  117. package/dist/TagGroup.d.ts +3 -1
  118. package/dist/TagGroup.d.ts.map +1 -1
  119. package/dist/TagGroup.js +12 -8
  120. package/dist/TagGroup.js.map +1 -1
  121. package/dist/TagGroup.jsx +11 -8
  122. package/dist/TagGroup.jsx.map +1 -1
  123. package/dist/TextField.js +4 -1
  124. package/dist/TextField.js.map +1 -1
  125. package/dist/TextField.jsx +4 -1
  126. package/dist/TextField.jsx.map +1 -1
  127. package/dist/TimeField.d.ts +14 -50
  128. package/dist/TimeField.d.ts.map +1 -1
  129. package/dist/TimeField.js +111 -205
  130. package/dist/TimeField.js.map +1 -1
  131. package/dist/TimeField.jsx +76 -82
  132. package/dist/TimeField.jsx.map +1 -1
  133. package/dist/Tree.d.ts.map +1 -1
  134. package/dist/Tree.js +3 -3
  135. package/dist/Tree.js.map +1 -1
  136. package/dist/Tree.jsx +3 -3
  137. package/dist/Tree.jsx.map +1 -1
  138. package/dist/index.d.ts +2 -3
  139. package/dist/index.d.ts.map +1 -1
  140. package/dist/index.js +5 -6
  141. package/dist/index.jsx +5 -6
  142. package/dist/useDragAndDrop.d.ts +0 -8
  143. package/dist/useDragAndDrop.d.ts.map +1 -1
  144. package/dist/useDragAndDrop2.js +6 -2
  145. package/dist/useDragAndDrop2.js.map +1 -1
  146. package/dist/useDragAndDrop2.jsx +6 -2
  147. package/dist/useDragAndDrop2.jsx.map +1 -1
  148. package/dist/utils.d.ts +1 -0
  149. package/dist/utils.d.ts.map +1 -1
  150. package/dist/utils.js +2 -2
  151. package/dist/utils.js.map +1 -1
  152. package/dist/utils.jsx +2 -2
  153. package/dist/utils.jsx.map +1 -1
  154. package/package.json +3 -9
  155. package/src/ActionBar.tsx +10 -12
  156. package/src/ActionGroup.tsx +5 -3
  157. package/src/Autocomplete.tsx +14 -9
  158. package/src/Breadcrumbs.tsx +55 -38
  159. package/src/Calendar.tsx +30 -1
  160. package/src/Color.tsx +42 -27
  161. package/src/ComboBox.tsx +49 -17
  162. package/src/DateField.tsx +285 -52
  163. package/src/DatePicker.tsx +345 -57
  164. package/src/DateRangePickerContext.tsx +2 -0
  165. package/src/Disclosure.tsx +4 -0
  166. package/src/GridList.tsx +8 -2
  167. package/src/ListBox.tsx +323 -21
  168. package/src/Modal.tsx +21 -5
  169. package/src/Popover.tsx +28 -7
  170. package/src/RangeCalendar.tsx +120 -1
  171. package/src/SearchField.tsx +53 -1
  172. package/src/Select.tsx +101 -20
  173. package/src/SharedElementTransition.tsx +92 -36
  174. package/src/StepList.tsx +0 -7
  175. package/src/Table.tsx +19 -4
  176. package/src/Tabs.tsx +1 -0
  177. package/src/TagGroup.tsx +26 -6
  178. package/src/TextField.tsx +1 -1
  179. package/src/TimeField.tsx +113 -196
  180. package/src/Tree.tsx +14 -4
  181. package/src/index.ts +1 -8
  182. package/src/useDragAndDrop.ts +8 -10
  183. package/src/utils.tsx +3 -3
  184. package/dist/DatePicker.js.map +0 -1
  185. package/dist/DatePicker.jsx.map +0 -1
  186. package/dist/HiddenTimeInput.d.ts +0 -25
  187. package/dist/HiddenTimeInput.d.ts.map +0 -1
  188. package/dist/HiddenTimeInput.js +0 -104
  189. package/dist/HiddenTimeInput.js.map +0 -1
  190. package/dist/HiddenTimeInput.jsx +0 -69
  191. package/dist/HiddenTimeInput.jsx.map +0 -1
  192. package/src/HiddenTimeInput.tsx +0 -133
package/src/GridList.tsx CHANGED
@@ -383,7 +383,13 @@ export function GridList<T extends object>(props: GridListProps<T>): JSX.Element
383
383
  onSelectionChange: stateProps.onSelectionChange,
384
384
  }));
385
385
 
386
- const { gridProps } = createGridList<T, GridCollection<T>>(
386
+ // Keep the aria object (do NOT destructure `gridProps`): its `gridProps` getter
387
+ // returns a memo that recomputes on state changes — chiefly the roving container
388
+ // `tabIndex`, which rolls to -1 once a row takes focus. Destructuring here would
389
+ // freeze the getter at its first value (tabIndex 0) and the roll would never
390
+ // reach the DOM. Read `gridListAria.gridProps` inside `cleanGridProps` instead,
391
+ // mirroring how ListBox consumes `listBoxAria.listBoxProps`.
392
+ const gridListAria = createGridList<T, GridCollection<T>>(
387
393
  () => ({
388
394
  id: ariaProps.id,
389
395
  "aria-label": ariaProps["aria-label"],
@@ -425,7 +431,7 @@ export function GridList<T extends object>(props: GridListProps<T>): JSX.Element
425
431
  });
426
432
 
427
433
  const cleanGridProps = () => {
428
- const { ref: _ref1, ...rest } = gridProps as Record<string, unknown>;
434
+ const { ref: _ref1, ...rest } = gridListAria.gridProps as Record<string, unknown>;
429
435
  return rest;
430
436
  };
431
437
  const cleanFocusProps = () => {
package/src/ListBox.tsx CHANGED
@@ -11,6 +11,7 @@ import {
11
11
  createEffect,
12
12
  createMemo,
13
13
  createSignal,
14
+ on,
14
15
  onCleanup,
15
16
  splitProps,
16
17
  useContext,
@@ -22,17 +23,29 @@ import {
22
23
  createOption,
23
24
  createFocusRing,
24
25
  createScrollIntoViewOnFocus,
26
+ createStringFormatter,
27
+ createDragSession,
28
+ getGlobalDraggingCollectionRef,
29
+ getGlobalDraggingKeys,
30
+ registerDropItem,
31
+ dndIntlStrings,
25
32
  mergeProps,
33
+ moveVirtualFocus,
26
34
  useLocale,
35
+ FOCUS_EVENT,
36
+ CLEAR_FOCUS_EVENT,
27
37
  type AriaListBoxProps,
28
38
  type AriaOptionProps,
29
39
  } from "@proyecto-viviana/solidaria";
30
40
  import {
31
41
  createListState,
42
+ createFilteredListState,
32
43
  type ListState,
44
+ type ListFilterFn,
33
45
  type Key,
34
46
  type DropTarget,
35
47
  } from "@proyecto-viviana/solid-stately";
48
+ import { useAutocompleteCollection } from "./Autocomplete";
36
49
  import {
37
50
  type RenderChildren,
38
51
  type ClassNameOrFunction,
@@ -53,7 +66,10 @@ import {
53
66
  mergePersistedKeysIntoVirtualRange,
54
67
  useDndPersistedKeys,
55
68
  useRenderDropIndicator,
69
+ DropIndicatorContext,
70
+ type DropIndicatorProps,
56
71
  } from "./DragAndDrop";
72
+ import type { ItemDropTarget } from "@proyecto-viviana/solid-stately";
57
73
  import {
58
74
  CollectionRendererContext,
59
75
  Section,
@@ -124,7 +140,7 @@ export interface ListBoxProps<T> extends Omit<AriaListBoxProps, "children">, Slo
124
140
  /** Called when the load more sentinel becomes visible. */
125
141
  onLoadMore?: () => void | Promise<void>;
126
142
  /** Ref for the listbox element. */
127
- ref?: RefLike<HTMLUListElement>;
143
+ ref?: RefLike<HTMLDivElement>;
128
144
  /** Drag and drop hooks from `useDragAndDrop`. */
129
145
  dragAndDropHooks?: DragAndDropHooks<T>;
130
146
  /** Layout hint for styling parity. */
@@ -165,7 +181,7 @@ export interface ListBoxOptionProps<T>
165
181
  /** The text value of the option (for typeahead). */
166
182
  textValue?: string;
167
183
  /** Ref for the option element. */
168
- ref?: RefLike<HTMLLIElement>;
184
+ ref?: RefLike<HTMLDivElement>;
169
185
  }
170
186
 
171
187
  export interface ListBoxLoadMoreItemProps extends SlotProps {
@@ -245,7 +261,7 @@ export function ListBox<T>(props: ListBoxProps<T>): JSX.Element {
245
261
 
246
262
  const hasSections = createMemo(() => stateProps.items.some((item) => isCollectionSection(item)));
247
263
 
248
- const state = createListState<T>({
264
+ const baseState = createListState<T>({
249
265
  get items() {
250
266
  return flatItems();
251
267
  },
@@ -281,6 +297,21 @@ export function ListBox<T>(props: ListBoxProps<T>): JSX.Element {
281
297
  },
282
298
  });
283
299
 
300
+ // When this ListBox is the collection of an Autocomplete, the input and the
301
+ // collection are separate components bridged by context. The collection's
302
+ // state is filtered by the input's predicate (mirrors RAC's
303
+ // UNSTABLE_useFilteredListState), and virtual focus is driven across the
304
+ // component boundary by synthetic DOM events (see the bridge effects below).
305
+ // ComboBox/Picker never provide this context, so their createListBox path is
306
+ // untouched.
307
+ const autocompleteCtx = useAutocompleteCollection();
308
+ const state = autocompleteCtx
309
+ ? createFilteredListState<T>(
310
+ baseState,
311
+ () => autocompleteCtx.filter as ListFilterFn<T> | undefined,
312
+ )
313
+ : baseState;
314
+
284
315
  const resolveDisabled = (): boolean => {
285
316
  const disabled = ariaProps.isDisabled;
286
317
  if (typeof disabled === "function") {
@@ -290,9 +321,33 @@ export function ListBox<T>(props: ListBoxProps<T>): JSX.Element {
290
321
  };
291
322
 
292
323
  const locale = useLocale();
324
+ // A parent Virtualizer publishes `isVirtualized` through the collection
325
+ // renderer context; the base ListBox forwards it into createListBox so each
326
+ // option emits aria-posinset/aria-setsize for the windowed (incomplete) DOM.
327
+ const parentCollectionRenderer = useCollectionRenderer<unknown>();
293
328
  const listBoxAria = createListBox(
294
329
  {
295
330
  ...ariaProps,
331
+ get isVirtualized() {
332
+ return parentCollectionRenderer?.isVirtualized ?? ariaProps.isVirtualized;
333
+ },
334
+ // Under Autocomplete, the input owns the collection's id (its
335
+ // aria-controls target), accessible name, and virtual-focus/type-ahead
336
+ // config; prefer the bridged values over any locally-passed props.
337
+ get id() {
338
+ return autocompleteCtx?.collectionProps.id ?? (ariaProps as { id?: string }).id;
339
+ },
340
+ get "aria-label"() {
341
+ return autocompleteCtx?.collectionProps["aria-label"] ?? ariaProps["aria-label"];
342
+ },
343
+ get shouldUseVirtualFocus() {
344
+ return (
345
+ autocompleteCtx?.collectionProps.shouldUseVirtualFocus ?? ariaProps.shouldUseVirtualFocus
346
+ );
347
+ },
348
+ get disallowTypeAhead() {
349
+ return autocompleteCtx?.collectionProps.disallowTypeAhead ?? ariaProps.disallowTypeAhead;
350
+ },
296
351
  get isDisabled() {
297
352
  return resolveDisabled();
298
353
  },
@@ -353,8 +408,81 @@ export function ListBox<T>(props: ListBoxProps<T>): JSX.Element {
353
408
  ref: () => listRef(),
354
409
  });
355
410
 
411
+ // Autocomplete bridge. createListBox reimplements navigation inline (it does
412
+ // not route through createSelectableCollection), so the virtual-focus event
413
+ // wiring the Autocomplete input relies on lives here, gated on the bridge
414
+ // context. This mirrors createSelectableCollection's FOCUS_EVENT/CLEAR_FOCUS_EVENT
415
+ // handlers and useSelectableItem's per-item moveVirtualFocus, at the component
416
+ // layer, without touching the shared hooks (protecting ComboBox/Picker).
417
+ if (autocompleteCtx) {
418
+ // Forward path: the input dispatches FOCUS_EVENT/CLEAR_FOCUS_EVENT onto the
419
+ // collection element. FOCUS_EVENT marks the collection focused (and, when the
420
+ // user types forward, requests the first item); the actual arrow navigation
421
+ // arrives as a re-dispatched KeyboardEvent handled by createListBox's onKeyDown.
422
+ let shouldVirtualFocusFirst = false;
423
+ createEffect(() => {
424
+ const list = listRef();
425
+ if (!list) return;
426
+ const onFocusEvent = (e: Event) => {
427
+ const detail = (e as CustomEvent).detail;
428
+ e.stopPropagation();
429
+ state.setFocused(true);
430
+ if (detail?.focusStrategy === "first") {
431
+ shouldVirtualFocusFirst = true;
432
+ }
433
+ };
434
+ const onClearFocusEvent = (e: Event) => {
435
+ e.stopPropagation();
436
+ state.setFocused(false);
437
+ if ((e as CustomEvent).detail?.clearFocusKey) {
438
+ state.setFocusedKey(null);
439
+ }
440
+ };
441
+ list.addEventListener(FOCUS_EVENT, onFocusEvent);
442
+ list.addEventListener(CLEAR_FOCUS_EVENT, onClearFocusEvent);
443
+ onCleanup(() => {
444
+ list.removeEventListener(FOCUS_EVENT, onFocusEvent);
445
+ list.removeEventListener(CLEAR_FOCUS_EVENT, onClearFocusEvent);
446
+ });
447
+ });
448
+
449
+ // Focus the first item once the (filtered) collection settles after the user
450
+ // types forward. If nothing survives the filter, clear the input's active
451
+ // descendant by moving virtual focus onto the collection itself (its focusin
452
+ // reaches the input's clear branch). Mirrors useSelectableCollection.
453
+ createEffect(
454
+ on(
455
+ () => [state.collection().getFirstKey?.() ?? null, state.collection().size] as const,
456
+ ([firstKey, size]) => {
457
+ if (!shouldVirtualFocusFirst) return;
458
+ if (firstKey == null) {
459
+ const list = listRef();
460
+ if (list) moveVirtualFocus(list);
461
+ if (size > 0) shouldVirtualFocusFirst = false;
462
+ } else {
463
+ state.setFocusedKey(firstKey);
464
+ shouldVirtualFocusFirst = false;
465
+ }
466
+ },
467
+ { defer: true },
468
+ ),
469
+ );
470
+
471
+ // Reverse path: mirror the focused key onto the option's DOM element via a
472
+ // synthetic, bubbling focusin (moveVirtualFocus). The input's focusin listener
473
+ // reads target.id into its aria-activedescendant. Option ids are String(key)
474
+ // in this path (createOption). Real DOM focus stays on the input.
475
+ createEffect(() => {
476
+ const key = state.focusedKey();
477
+ const list = listRef();
478
+ if (!list || !state.isFocused()) return;
479
+ if (key == null) return;
480
+ const el = list.ownerDocument.getElementById(String(key));
481
+ if (el) moveVirtualFocus(el);
482
+ });
483
+ }
484
+
356
485
  const isEmpty = () => stateProps.items.length === 0;
357
- const parentCollectionRenderer = useCollectionRenderer<unknown>();
358
486
  const getItemNodes = createMemo(() =>
359
487
  Array.from(state.collection()).filter((node) => node.type === "item"),
360
488
  );
@@ -432,6 +560,14 @@ export function ListBox<T>(props: ListBoxProps<T>): JSX.Element {
432
560
  getKeyPageBelow: (key) => state.collection().getKeyAfter(key),
433
561
  getKeyPageAbove: (key) => state.collection().getKeyBefore(key),
434
562
  },
563
+ // The real collection drives keyboard drop-target navigation
564
+ // (`navigate()` walks getKeyAfter/getKeyBefore) and post-drop focus
565
+ // restoration. Reading it here re-registers the drop target when the
566
+ // collection changes (mirrors upstream keying its effect on the state).
567
+ collection: state.collection(),
568
+ selectedKeys: state.selectionManager.selectedKeys,
569
+ setSelectedKeys: (keys) => state.selectionManager.setSelectedKeys(keys),
570
+ setFocusedKey: (key) => state.setFocusedKey(key),
435
571
  },
436
572
  activeDropState,
437
573
  () => listRef(),
@@ -448,6 +584,165 @@ export function ListBox<T>(props: ListBoxProps<T>): JSX.Element {
448
584
  if (!target || target.type !== "item") return undefined;
449
585
  return dndRenderDropIndicator()?.(target);
450
586
  };
587
+
588
+ // ── Drop indicator (Fix B: labeled, self-focusing) ─────────────────────────
589
+ // The port of react-aria-components' ListBoxDropIndicatorWrapper +
590
+ // useDropIndicator. The droppable-collection state layer diverged to a minimal
591
+ // reactive target-holder with no collection reference, so the label (which
592
+ // needs the collection's text values) and the self-focus (which needs the live
593
+ // drag session) are computed here in the host layer, where both are available.
594
+ const dndFormatter = createStringFormatter(dndIntlStrings);
595
+ const dragSession = createDragSession();
596
+ const dropIndicatorTargetsEqual = (
597
+ a: DropTarget | null | undefined,
598
+ b: ItemDropTarget,
599
+ ): boolean =>
600
+ !!a && a.type === "item" && a.key === b.key && a.dropPosition === b.dropPosition;
601
+ // Faithful port of useDropIndicator's label logic: "Drop on X" for an on-item
602
+ // target, else "Insert between X and Y" / "Insert before X" / "Insert after X"
603
+ // resolved from the target's neighbours in the collection.
604
+ const dropIndicatorLabel = (target: ItemDropTarget): string => {
605
+ const collection = state.collection();
606
+ const getText = (key: Key | null): string => {
607
+ if (key == null) return "";
608
+ return collection.getTextValue(key) ?? collection.getItem(key)?.textValue ?? "";
609
+ };
610
+ if (target.dropPosition === "on") {
611
+ return dndFormatter().format("dropOnItem", { itemText: getText(target.key) });
612
+ }
613
+ let before: Key | null;
614
+ let after: Key | null;
615
+ if (target.dropPosition === "before") {
616
+ const prevKey = collection.getKeyBefore(target.key);
617
+ const prevNode = prevKey != null ? collection.getItem(prevKey) : null;
618
+ before = prevNode?.type === "item" ? prevNode.key : null;
619
+ } else {
620
+ before = target.key;
621
+ }
622
+ if (target.dropPosition === "after") {
623
+ const nextKey = collection.getKeyAfter(target.key);
624
+ const nextNode = nextKey != null ? collection.getItem(nextKey) : null;
625
+ after = nextNode?.type === "item" ? nextNode.key : null;
626
+ } else {
627
+ after = target.key;
628
+ }
629
+ if (before != null && after != null) {
630
+ return dndFormatter().format("insertBetween", {
631
+ beforeItemText: getText(before),
632
+ afterItemText: getText(after),
633
+ });
634
+ }
635
+ if (before != null) {
636
+ return dndFormatter().format("insertAfter", { itemText: getText(before) });
637
+ }
638
+ if (after != null) {
639
+ return dndFormatter().format("insertBefore", { itemText: getText(after) });
640
+ }
641
+ return "";
642
+ };
643
+ // Wrap the raw Set<string> the DragManager hands drop items into the DragTypes
644
+ // delegate the port state expects (mirrors createDroppableCollection.wrapTypes).
645
+ const wrapDropTypes = (types: Set<string>) => ({
646
+ has: (type: string | string[]) => {
647
+ if (typeof type === "string") return types.has(type);
648
+ if (Array.isArray(type)) return type.some((t) => typeof t === "string" && types.has(t));
649
+ return false;
650
+ },
651
+ });
652
+ const ListBoxDropIndicator = (props: { target: ItemDropTarget }): JSX.Element => {
653
+ const [indicatorRef, setIndicatorRef] = createSignal<HTMLDivElement | null>(null);
654
+ const isDropTarget = createMemo(() =>
655
+ dropIndicatorTargetsEqual(dropState()?.target, props.target),
656
+ );
657
+ // aria-hidden mirrors useDropIndicator: outside a drag session, or on a
658
+ // non-active target, the indicator is hidden from AT ('true'); the active
659
+ // target during a session is exposed (undefined). isHidden then tells us
660
+ // whether to omit the element entirely (RAC returns null when hidden).
661
+ const ariaHidden = createMemo(() =>
662
+ !dragSession() ? "true" : isDropTarget() ? undefined : "true",
663
+ );
664
+ const isHidden = createMemo(() => !isDropTarget() && !!ariaHidden());
665
+ // Faithful port of useDroppableItem. Registration (of the indicator element as
666
+ // a DragManager droppable item) and the self-focus must happen in one effect,
667
+ // registration first: the DragManager's onFocus keys off the dropItems map, so
668
+ // if the element self-focused before it was registered, onFocus would treat it
669
+ // as an unknown target and redirect focus back to the collection (a pickup
670
+ // bounce). React's two useDroppableItem effects run in guaranteed order after
671
+ // the ref is attached; Solid's independent effects race, so we fuse them. The
672
+ // dropItems map keys off live elements — the ref nulls itself on Show cleanup
673
+ // (below), so a hidden indicator unregisters instead of lingering as a stale
674
+ // detached entry.
675
+ createEffect(() => {
676
+ const el = indicatorRef();
677
+ if (!el) return;
678
+ const unregister = registerDropItem({
679
+ element: el,
680
+ target: props.target,
681
+ getDropOperation: (types, allowedOperations) => {
682
+ const st = dropState();
683
+ if (!st) return "cancel";
684
+ // Thread the same drag identity the collection uses (see
685
+ // createDroppableCollection.isInternalDropOperation): an internal
686
+ // keyboard reorder must report `isInternal` so the state's feature
687
+ // detection keeps before/after indicators valid (and rejects "on" for a
688
+ // reorder-only host). Without it every indicator would return "cancel"
689
+ // and drop out of the DragManager's ariaHideOutside keep-set.
690
+ const isInternal = getGlobalDraggingCollectionRef() === listRef();
691
+ return st.getDropOperation(
692
+ props.target,
693
+ wrapDropTypes(types) as Parameters<typeof st.getDropOperation>[1],
694
+ allowedOperations,
695
+ isInternal,
696
+ getGlobalDraggingKeys(),
697
+ );
698
+ },
699
+ });
700
+ onCleanup(unregister);
701
+ // While a drag session is active, the current drop target moves real DOM
702
+ // focus onto itself, so keyboard drop navigation lands on the labeled
703
+ // insertion point rather than the collection element. The self-focus is
704
+ // deferred to an animation frame to mirror upstream's timing: in
705
+ // useDroppableItem the focus is a *passive* useEffect, so it commits only
706
+ // after the browser's microtask checkpoint. That ordering matters because
707
+ // the DragManager keeps the active drop indicator visible via
708
+ // ariaHideOutside, whose keep-set is `[dragSource, ...registered drop
709
+ // items]` (the collection is dropped from the set once it owns items). A
710
+ // freshly-mounted indicator is momentarily `inert` — it only leaves the
711
+ // keep-set's blind spot once the DragManager's MutationObserver re-runs
712
+ // updateValidDropTargets on a later microtask. Focusing synchronously (or
713
+ // on a microtask) would call .focus() while the element is still inert and
714
+ // silently no-op; the animation frame runs after that refresh, exactly as
715
+ // React's passive effect does.
716
+ if (dragSession() && isDropTarget()) {
717
+ requestAnimationFrame(() => {
718
+ if (dragSession() && isDropTarget() && indicatorRef() === el) {
719
+ el.focus();
720
+ }
721
+ });
722
+ }
723
+ });
724
+ return (
725
+ <Show when={!isHidden()}>
726
+ <div
727
+ ref={(el) => {
728
+ setIndicatorRef(el);
729
+ onCleanup(() => setIndicatorRef(null));
730
+ }}
731
+ role="option"
732
+ class="solidaria-DropIndicator"
733
+ aria-roledescription={dndFormatter().format("dropIndicator")}
734
+ aria-label={dropIndicatorLabel(props.target)}
735
+ aria-hidden={ariaHidden()}
736
+ tabindex={-1}
737
+ data-drop-target={isDropTarget() || undefined}
738
+ />
739
+ </Show>
740
+ );
741
+ };
742
+ const dropIndicatorContextValue = {
743
+ render: (p: DropIndicatorProps) => <ListBoxDropIndicator target={p.target} />,
744
+ };
745
+
451
746
  const virtualizer = useVirtualizerContext();
452
747
  const persistedKeys = useDndPersistedKeys(
453
748
  { focusedKey: state.focusedKey },
@@ -565,11 +860,12 @@ export function ListBox<T>(props: ListBoxProps<T>): JSX.Element {
565
860
  >
566
861
  <ListBoxStateContext.Provider value={state}>
567
862
  <CollectionRendererContext.Provider value={collectionRenderer()}>
863
+ <DropIndicatorContext.Provider value={dropIndicatorContextValue}>
568
864
  <>
569
865
  <Show when={ariaProps.label}>
570
866
  <span {...cleanLabelProps()}>{ariaProps.label as JSX.Element}</span>
571
867
  </Show>
572
- <ul
868
+ <div
573
869
  {...mergeProps(
574
870
  domProps(),
575
871
  cleanListBoxProps(),
@@ -580,6 +876,7 @@ export function ListBox<T>(props: ListBoxProps<T>): JSX.Element {
580
876
  ref={(el) => {
581
877
  setListRef(el);
582
878
  assignRef(local.ref, el);
879
+ autocompleteCtx?.collectionRef(el);
583
880
  }}
584
881
  class={renderProps.class()}
585
882
  style={renderProps.style()}
@@ -594,14 +891,14 @@ export function ListBox<T>(props: ListBoxProps<T>): JSX.Element {
594
891
  >
595
892
  <SharedElementTransition>
596
893
  {isEmpty() && local.renderEmptyState ? (
597
- <li role="option" style={{ display: "contents" }} data-empty-state>
894
+ <div role="option" style={{ display: "contents" }} data-empty-state>
598
895
  {local.renderEmptyState()}
599
- </li>
896
+ </div>
600
897
  ) : hasSections() ? (
601
898
  <For each={sectionedRenderEntries()}>
602
899
  {(entry) =>
603
900
  entry.type === "section" ? (
604
- <li role="presentation" data-section-wrapper>
901
+ <div role="presentation" data-section-wrapper>
605
902
  <Section class="solidaria-ListBox-section">
606
903
  {entry.section.title != null && (
607
904
  <Header class="solidaria-ListBox-sectionHeader">
@@ -609,7 +906,7 @@ export function ListBox<T>(props: ListBoxProps<T>): JSX.Element {
609
906
  </Header>
610
907
  )}
611
908
  <Group class="solidaria-ListBox-sectionGroup">
612
- <ul role="group" aria-label={entry.section["aria-label"]}>
909
+ <div role="group" aria-label={entry.section["aria-label"]}>
613
910
  <For each={entry.items}>
614
911
  {(indexedItem) => (
615
912
  <>
@@ -629,10 +926,10 @@ export function ListBox<T>(props: ListBoxProps<T>): JSX.Element {
629
926
  </>
630
927
  )}
631
928
  </For>
632
- </ul>
929
+ </div>
633
930
  </Group>
634
931
  </Section>
635
- </li>
932
+ </div>
636
933
  ) : (
637
934
  <>
638
935
  {collectionRenderer().renderDropIndicator?.(entry.item.index, "before")}
@@ -646,7 +943,7 @@ export function ListBox<T>(props: ListBoxProps<T>): JSX.Element {
646
943
  ) : (
647
944
  <>
648
945
  {virtualRange()?.offsetTop ? (
649
- <li
946
+ <div
650
947
  role="presentation"
651
948
  aria-hidden="true"
652
949
  style={virtualSpacerStyle(virtualRange()!.offsetTop)}
@@ -673,7 +970,7 @@ export function ListBox<T>(props: ListBoxProps<T>): JSX.Element {
673
970
  }}
674
971
  </For>
675
972
  {virtualRange()?.offsetBottom ? (
676
- <li
973
+ <div
677
974
  role="presentation"
678
975
  aria-hidden="true"
679
976
  style={virtualSpacerStyle(virtualRange()!.offsetBottom)}
@@ -686,8 +983,9 @@ export function ListBox<T>(props: ListBoxProps<T>): JSX.Element {
686
983
  {local.hasMore && local.onLoadMore && (
687
984
  <ListBoxLoadMoreItem onLoadMore={local.onLoadMore} isLoading={local.isLoading} />
688
985
  )}
689
- </ul>
986
+ </div>
690
987
  </>
988
+ </DropIndicatorContext.Provider>
691
989
  </CollectionRendererContext.Provider>
692
990
  </ListBoxStateContext.Provider>
693
991
  </ListBoxContext.Provider>
@@ -714,7 +1012,7 @@ export function ListBoxOption<T>(props: ListBoxOptionProps<T>): JSX.Element {
714
1012
  }
715
1013
  const state = context as ListState<T>;
716
1014
  const listContext = useContext(ListBoxContext) as ListBoxContextValue<T> | null;
717
- const [ref, setRef] = createSignal<HTMLLIElement | null>(null);
1015
+ const [ref, setRef] = createSignal<HTMLDivElement | null>(null);
718
1016
 
719
1017
  const optionAria = createOption<T>(
720
1018
  {
@@ -742,6 +1040,7 @@ export function ListBoxOption<T>(props: ListBoxOptionProps<T>): JSX.Element {
742
1040
  },
743
1041
  },
744
1042
  state,
1043
+ ref,
745
1044
  );
746
1045
 
747
1046
  const renderValues = createMemo<ListBoxOptionRenderProps>(() => ({
@@ -777,6 +1076,9 @@ export function ListBoxOption<T>(props: ListBoxOptionProps<T>): JSX.Element {
777
1076
  return listContext.dragAndDropHooks.useDraggableItem(
778
1077
  {
779
1078
  key: local.id as string | number,
1079
+ // Surfaces the drag-affordance description (aria-describedby) only for
1080
+ // selectable collections, mirroring upstream useDraggableItem.
1081
+ selectionMode: state.selectionManager.selectionMode,
780
1082
  },
781
1083
  listContext.dragState as Parameters<NonNullable<DragAndDropHooks<T>["useDraggableItem"]>>[1],
782
1084
  );
@@ -808,7 +1110,7 @@ export function ListBoxOption<T>(props: ListBoxOptionProps<T>): JSX.Element {
808
1110
 
809
1111
  return (
810
1112
  <SelectionIndicatorContext.Provider value={selectionIndicatorContext()}>
811
- <li
1113
+ <div
812
1114
  ref={(el) => {
813
1115
  setRef(el);
814
1116
  assignRef(local.ref, el);
@@ -836,7 +1138,7 @@ export function ListBoxOption<T>(props: ListBoxOptionProps<T>): JSX.Element {
836
1138
  ) : (
837
1139
  renderProps.renderChildren()
838
1140
  )}
839
- </li>
1141
+ </div>
840
1142
  </SelectionIndicatorContext.Provider>
841
1143
  );
842
1144
  }
@@ -892,10 +1194,10 @@ export function ListBoxLoadMoreItem(props: ListBoxLoadMoreItemProps): JSX.Elemen
892
1194
 
893
1195
  return (
894
1196
  <>
895
- <li style={{ position: "relative", width: 0, height: 0, overflow: "hidden" }} inert>
1197
+ <div style={{ position: "relative", width: 0, height: 0, overflow: "hidden" }} inert>
896
1198
  <div ref={sentinelRef} style={{ position: "absolute", height: "1px", width: "1px" }} />
897
- </li>
898
- <li
1199
+ </div>
1200
+ <div
899
1201
  role="option"
900
1202
  aria-disabled={true}
901
1203
  tabIndex={0}
@@ -907,7 +1209,7 @@ export function ListBoxLoadMoreItem(props: ListBoxLoadMoreItemProps): JSX.Elemen
907
1209
  data-loading={isLoading() || undefined}
908
1210
  >
909
1211
  {renderProps.renderChildren()}
910
- </li>
1212
+ </div>
911
1213
  </>
912
1214
  );
913
1215
  }
package/src/Modal.tsx CHANGED
@@ -264,9 +264,14 @@ export function ModalOverlay(props: ModalOverlayProps): JSX.Element {
264
264
  }
265
265
  };
266
266
 
267
- document.addEventListener("keydown", handleKeyDown, true);
267
+ // Bubble phase, mirroring react-aria's `useOverlay` (its `onKeyDown` is an
268
+ // overlay-element prop, handled on bubble). Capture would preempt inner
269
+ // widgets that dismiss on Escape (a combobox/menu inside the dialog must
270
+ // consume Escape first) and would also fire before the event is recorded by
271
+ // capture-phase listeners, inverting the keydown/onOpenChange order.
272
+ document.addEventListener("keydown", handleKeyDown, false);
268
273
  onCleanup(() => {
269
- document.removeEventListener("keydown", handleKeyDown, true);
274
+ document.removeEventListener("keydown", handleKeyDown, false);
270
275
  });
271
276
  });
272
277
 
@@ -487,9 +492,11 @@ function ModalContent(props: ModalProps): JSX.Element {
487
492
  }
488
493
  };
489
494
 
490
- document.addEventListener("keydown", handleKeyDown, true);
495
+ // Bubble phase — see the ModalOverlay handler above for why (react-aria
496
+ // `useOverlay` parity; inner-widget Escape precedence; event-order fidelity).
497
+ document.addEventListener("keydown", handleKeyDown, false);
491
498
  onCleanup(() => {
492
- document.removeEventListener("keydown", handleKeyDown, true);
499
+ document.removeEventListener("keydown", handleKeyDown, false);
493
500
  });
494
501
  });
495
502
 
@@ -548,7 +555,16 @@ function ModalContent(props: ModalProps): JSX.Element {
548
555
  // No autoFocus: react-aria's Overlay renders FocusScope with restoreFocus +
549
556
  // contain only — initial focus is useDialog's job (it focuses the dialog
550
557
  // element itself when nothing inside holds focus).
551
- <FocusScope contain restoreFocus>
558
+ //
559
+ // `contain` is gated on `!isExiting`, mirroring react-aria's Overlay
560
+ // (react-aria/src/overlays/Overlay.tsx: `contain={... && !isExiting}`). The
561
+ // overlay stays mounted through its exit transition (isOpen || isExiting), so
562
+ // without this gate containment would remain active during the exit and yank
563
+ // focus back into the dialog as restoreFocus moves it to the trigger —
564
+ // producing a focus bounce (focusout→focusin→focusout) that React never
565
+ // shows. Releasing containment the instant the exit begins lets focus settle
566
+ // on the trigger in a single move, matching upstream's event sequence.
567
+ <FocusScope contain={!isModalExiting()} restoreFocus>
552
568
  <div
553
569
  {...domProps()}
554
570
  ref={registerModalRef}