@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/Popover.tsx CHANGED
@@ -22,6 +22,7 @@ import {
22
22
  createOverlayTrigger,
23
23
  createPopover,
24
24
  FocusScope,
25
+ useLocale,
25
26
  useUNSAFE_PortalContext,
26
27
  visuallyHiddenStyles,
27
28
  type AriaLabelingProps,
@@ -280,12 +281,29 @@ export function Popover(props: PopoverProps): JSX.Element {
280
281
  "isExiting",
281
282
  ]);
282
283
 
283
- let popoverRef!: HTMLDivElement;
284
+ // A reactive ref (not a plain `let`) so the overlay-position effect in
285
+ // createOverlayPosition — which tracks `overlayRef()` as a dependency —
286
+ // re-runs once the portal node actually mounts. React gets this timing for
287
+ // free from useLayoutEffect (the ref is populated before the position effect
288
+ // fires); in Solid the portal node is created lazily on open, AFTER the
289
+ // position effect first ran with a null ref, so a non-reactive ref left the
290
+ // popover stranded at the createOverlayPosition fallback (position:fixed;
291
+ // top:0; left:0) whenever no other dependency happened to re-fire the effect
292
+ // after mount. Mirrors the sibling groupRef signal below.
293
+ const [popoverRef, setPopoverRef] = createSignal<HTMLDivElement | null>(null);
284
294
  const [groupRef, setGroupRef] = createSignal<HTMLDivElement | null>(null);
285
295
  // False on the server and during hydration; true after onMount. Gates the Portal
286
296
  // so overlay content only ever renders client-side, post-hydration.
287
297
  const isHydrated = useIsHydrated();
288
298
 
299
+ // Mirrors upstream S2 Popover (Popover.mjs: `el.lang = locale; el.dir =
300
+ // direction`): the overlay portals out of the app root, so it must carry both
301
+ // the locale `lang` AND writing `direction` — otherwise the portaled
302
+ // listbox/dialog stays LTR and keeps the Latin font under an RTL Provider (the
303
+ // `:lang(ar)` font swap needs a `lang` ancestor the portal would otherwise
304
+ // escape). RAC's own Popover only threads `dir`; S2 adds `lang` on top.
305
+ const locale = useLocale();
306
+
289
307
  const triggerContext = useContext(PopoverTriggerContext);
290
308
  const dialogTriggerContext = useContext(DialogTriggerContext);
291
309
  const popoverGroupContext = useContext(PopoverGroupContext);
@@ -333,7 +351,7 @@ export function Popover(props: PopoverProps): JSX.Element {
333
351
  const popoverAria = createPopover(
334
352
  {
335
353
  triggerRef: getTriggerRef,
336
- popoverRef: () => popoverRef ?? null,
354
+ popoverRef: () => popoverRef(),
337
355
  groupRef: () => (isSubPopover() ? (popoverGroupContext?.() ?? null) : groupRef()),
338
356
  get placement() {
339
357
  return local.placement;
@@ -514,7 +532,7 @@ export function Popover(props: PopoverProps): JSX.Element {
514
532
  createEffect(() => {
515
533
  if (!isOpen() || !shouldBeDialog()) return;
516
534
  if ((local.autoFocus ?? true) === false) return;
517
- if (!popoverRef) return;
535
+ if (!popoverRef()) return;
518
536
  if (resolvedTrigger() === "SubmenuTrigger") return;
519
537
 
520
538
  let timeout: number | undefined;
@@ -522,11 +540,12 @@ export function Popover(props: PopoverProps): JSX.Element {
522
540
 
523
541
  const focusIfNeeded = () => {
524
542
  if (!isOpen() || !shouldBeDialog()) return;
525
- if (!popoverRef || resolvedTrigger() === "SubmenuTrigger") return;
526
- if (document.activeElement === popoverRef || popoverRef.contains(document.activeElement)) {
543
+ const node = popoverRef();
544
+ if (!node || resolvedTrigger() === "SubmenuTrigger") return;
545
+ if (document.activeElement === node || node.contains(document.activeElement)) {
527
546
  return;
528
547
  }
529
- popoverRef.focus();
548
+ node.focus();
530
549
  };
531
550
 
532
551
  const scheduleFocus = () => {
@@ -572,12 +591,14 @@ export function Popover(props: PopoverProps): JSX.Element {
572
591
  <div
573
592
  {...domProps()}
574
593
  {...cleanPopoverProps()}
575
- ref={popoverRef}
594
+ ref={setPopoverRef}
576
595
  id={overlayId()}
577
596
  role={shouldBeDialog() ? "dialog" : undefined}
578
597
  tabIndex={shouldBeDialog() ? -1 : undefined}
579
598
  class={renderProps.class()}
580
599
  style={mergedStyle()}
600
+ lang={locale().locale}
601
+ dir={locale().direction}
581
602
  data-trigger={resolvedTrigger()}
582
603
  data-placement={popoverAria.placement()}
583
604
  data-entering={dataAttr(local.isEntering)}
@@ -196,6 +196,13 @@ export function useRangeCalendarContext(): RangeCalendarState<DateValue> {
196
196
  export function RangeCalendar<T extends DateValue = CalendarDate>(
197
197
  props: RangeCalendarProps<T>,
198
198
  ): JSX.Element {
199
+ // When rendered inside a composite that owns the range state (e.g.
200
+ // DateRangePicker provides `RangeCalendarContext`), reuse that shared state
201
+ // instead of minting a fresh one — otherwise the composite's focus/anchor
202
+ // flags (e.g. the popover-open `setFocused(true)`) drive a different state
203
+ // instance than the one this grid renders. Mirrors the single `Calendar`.
204
+ const inheritedState = useContext(RangeCalendarContext);
205
+
199
206
  // Use hydration-safe pattern for client-only rendering
200
207
  const isHydrated = useIsHydrated();
201
208
 
@@ -209,11 +216,106 @@ export function RangeCalendar<T extends DateValue = CalendarDate>(
209
216
  />
210
217
  }
211
218
  >
212
- <RangeCalendarInner {...props} />
219
+ <Show when={inheritedState} fallback={<RangeCalendarInner {...props} />}>
220
+ <RangeCalendarWithState
221
+ state={inheritedState as RangeCalendarState<DateValue>}
222
+ {...props}
223
+ />
224
+ </Show>
213
225
  </Show>
214
226
  );
215
227
  }
216
228
 
229
+ /**
230
+ * RangeCalendar variant that renders against an externally-owned state (from
231
+ * `RangeCalendarContext`) rather than creating its own. Mirrors the single
232
+ * `CalendarWithState`.
233
+ */
234
+ function RangeCalendarWithState<T extends DateValue = CalendarDate>(
235
+ props: RangeCalendarProps<T> & { state: RangeCalendarState<DateValue> },
236
+ ): JSX.Element {
237
+ const [local, , rest] = splitProps(
238
+ props,
239
+ ["children", "class", "style", "slot", "state", "ref"],
240
+ [
241
+ "value",
242
+ "defaultValue",
243
+ "onChange",
244
+ "minValue",
245
+ "maxValue",
246
+ "isDisabled",
247
+ "isReadOnly",
248
+ "focusedValue",
249
+ "defaultFocusedValue",
250
+ "onFocusChange",
251
+ "locale",
252
+ "createCalendar",
253
+ "isDateUnavailable",
254
+ "visibleMonths",
255
+ "pageBehavior",
256
+ "selectionAlignment",
257
+ "isDateDisabled",
258
+ "validationState",
259
+ "allowsNonContiguousRanges",
260
+ "firstDayOfWeek",
261
+ ],
262
+ );
263
+
264
+ const state = () => props.state;
265
+
266
+ const calendarAria = createRangeCalendar(rest, state());
267
+
268
+ const renderValues = createMemo<RangeCalendarRenderProps>(() => ({
269
+ isDisabled: state().isDisabled(),
270
+ isReadOnly: state().isReadOnly(),
271
+ isInvalid: state().isValueInvalid(),
272
+ isDragging: state().isDragging(),
273
+ }));
274
+
275
+ const renderProps = useRenderProps(
276
+ {
277
+ class: local.class,
278
+ style: local.style,
279
+ defaultClassName: "solidaria-RangeCalendar",
280
+ },
281
+ renderValues,
282
+ );
283
+
284
+ return (
285
+ <RangeCalendarStateContext.Provider value={state()}>
286
+ <RangeCalendarContext.Provider value={state()}>
287
+ <div
288
+ {...calendarAria.calendarProps}
289
+ ref={(el) => assignRef(local.ref, el)}
290
+ class={renderProps.class()}
291
+ style={renderProps.style()}
292
+ data-disabled={dataAttr(state().isDisabled())}
293
+ data-readonly={dataAttr(state().isReadOnly())}
294
+ data-invalid={dataAttr(state().isValueInvalid())}
295
+ data-dragging={dataAttr(state().isDragging())}
296
+ >
297
+ <VisuallyHidden>
298
+ <h2>{String(calendarAria.calendarProps["aria-label"] ?? "")}</h2>
299
+ </VisuallyHidden>
300
+ {props.children}
301
+ {/* For touch screen readers, a visually hidden next button after the
302
+ * month grid so it's easy to navigate after reaching the end without
303
+ * going all the way back to the start. Mirrors react-aria-components
304
+ * Calendar (and the headless Calendar). */}
305
+ <VisuallyHidden>
306
+ <button
307
+ aria-label={String(calendarAria.nextButtonProps["aria-label"] ?? "")}
308
+ disabled={Boolean(calendarAria.nextButtonProps.disabled)}
309
+ onClick={() => state().focusNextPage()}
310
+ tabIndex={-1}
311
+ />
312
+ </VisuallyHidden>
313
+ </div>
314
+ </RangeCalendarContext.Provider>
315
+ </RangeCalendarStateContext.Provider>
316
+ );
317
+ }
318
+
217
319
  /**
218
320
  * Internal RangeCalendar component that renders after client mount.
219
321
  */
@@ -284,6 +386,18 @@ function RangeCalendarInner<T extends DateValue = CalendarDate>(
284
386
  <h2>{String(calendarAria.calendarProps["aria-label"] ?? "")}</h2>
285
387
  </VisuallyHidden>
286
388
  {props.children}
389
+ {/* For touch screen readers, a visually hidden next button after the
390
+ * month grid so it's easy to navigate after reaching the end without
391
+ * going all the way back to the start. Mirrors react-aria-components
392
+ * Calendar (and the headless Calendar). */}
393
+ <VisuallyHidden>
394
+ <button
395
+ aria-label={String(calendarAria.nextButtonProps["aria-label"] ?? "")}
396
+ disabled={Boolean(calendarAria.nextButtonProps.disabled)}
397
+ onClick={() => state.focusNextPage()}
398
+ tabIndex={-1}
399
+ />
400
+ </VisuallyHidden>
287
401
  </div>
288
402
  </RangeCalendarContext.Provider>
289
403
  </RangeCalendarStateContext.Provider>
@@ -350,6 +464,11 @@ export function RangeCalendarButton(props: RangeCalendarButtonProps): JSX.Elemen
350
464
  class={props.class ?? "solidaria-RangeCalendarButton"}
351
465
  style={props.style}
352
466
  disabled={isDisabled()}
467
+ // Mirror useFocusable (same as CalendarButton): always set an explicit
468
+ // tabIndex on an enabled focusable nav button (0), and drop it entirely
469
+ // when disabled — so the prev/next buttons appear in the roving tab order
470
+ // exactly as the S2 oracle does.
471
+ tabindex={isDisabled() ? undefined : 0}
353
472
  >
354
473
  {props.children}
355
474
  </button>
@@ -42,6 +42,7 @@ import {
42
42
  Provider,
43
43
  } from "./utils";
44
44
  import { TextContext } from "./Text";
45
+ import { useAutocompleteInput } from "./Autocomplete";
45
46
 
46
47
  export interface SearchFieldRenderProps {
47
48
  /** Whether the search field is empty. */
@@ -247,7 +248,35 @@ export function SearchField(props: SearchFieldProps): JSX.Element {
247
248
  const baseProps = (
248
249
  contextProps ? mergeProps(contextBaseProps(), contextSlotProps ?? {}, props) : props
249
250
  ) as SearchFieldProps;
250
- const mergedProps = withFormValidationBehavior(baseProps, formContext);
251
+
252
+ // When rendered inside an <Autocomplete>, the FieldInputContext (AutocompleteContext)
253
+ // controls the input's value and keyboard handling: value + onChange make the field's
254
+ // state controlled by the shared autocomplete state (which drives the collection
255
+ // filter), and onKeyDown/onFocus/onBlur chain the autocomplete's virtual-focus
256
+ // navigation into the input. This mirrors RAC's
257
+ // useContextProps(props, inputRef, FieldInputContext). The remaining passthroughs
258
+ // (aria-activedescendant/aria-controls/aria-autocomplete) and the input-ref
259
+ // registration are applied at the <input> in SearchFieldInput.
260
+ const autocompleteInput = useAutocompleteInput();
261
+ const autocompleteProps = autocompleteInput
262
+ ? ({
263
+ get value() {
264
+ return autocompleteInput.inputProps.value();
265
+ },
266
+ onChange: autocompleteInput.inputProps.onChange,
267
+ onKeyDown: autocompleteInput.inputProps
268
+ .onKeyDown as unknown as SearchFieldProps["onKeyDown"],
269
+ onFocus: autocompleteInput.inputProps.onFocus as unknown as SearchFieldProps["onFocus"],
270
+ onBlur: autocompleteInput.inputProps.onBlur as unknown as SearchFieldProps["onBlur"],
271
+ } as Partial<SearchFieldProps>)
272
+ : undefined;
273
+
274
+ const mergedProps = withFormValidationBehavior(
275
+ autocompleteProps
276
+ ? (mergeProps(baseProps, autocompleteProps) as SearchFieldProps)
277
+ : baseProps,
278
+ formContext,
279
+ );
251
280
 
252
281
  const [local, stateProps, ariaProps, rest] = splitProps(
253
282
  mergedProps,
@@ -629,6 +658,15 @@ export function SearchFieldInput(props: SearchFieldInputProps): JSX.Element {
629
658
  throw new Error("SearchFieldInput must be used within a SearchField");
630
659
  }
631
660
 
661
+ // Inside an <Autocomplete>, expose the ARIA attributes that identify the
662
+ // controlled collection and its virtually-focused option. value/onChange and the
663
+ // keyboard handlers are already merged upstream in SearchField(); here we only add
664
+ // the passthrough attributes that createTextField doesn't forward, and register the
665
+ // input element as the autocomplete inputRef so the beforeinput + reverse-path
666
+ // (aria-activedescendant mirror) listeners bind. Mirrors RAC's
667
+ // [InputContext, {...inputProps, ref: inputRef}].
668
+ const autocompleteInput = useAutocompleteInput();
669
+
632
670
  const { isFocused, isFocusVisible, focusProps } = createFocusRing();
633
671
 
634
672
  const { isHovered, hoverProps } = createHover({
@@ -667,12 +705,25 @@ export function SearchFieldInput(props: SearchFieldInputProps): JSX.Element {
667
705
  return rest;
668
706
  };
669
707
 
708
+ const autocompleteInputAttrs = (): Record<string, unknown> => {
709
+ if (!autocompleteInput) {
710
+ return {};
711
+ }
712
+ const inputProps = autocompleteInput.inputProps;
713
+ return {
714
+ "aria-controls": inputProps["aria-controls"],
715
+ "aria-autocomplete": inputProps["aria-autocomplete"],
716
+ "aria-activedescendant": inputProps["aria-activedescendant"](),
717
+ };
718
+ };
719
+
670
720
  const mergedInputProps = () =>
671
721
  ({
672
722
  ...domProps,
673
723
  ...cleanInputProps(),
674
724
  ...cleanFocusProps(),
675
725
  ...cleanHoverProps(),
726
+ ...autocompleteInputAttrs(),
676
727
  }) as Record<string, unknown>;
677
728
 
678
729
  onMount(() => {
@@ -723,6 +774,7 @@ export function SearchFieldInput(props: SearchFieldInputProps): JSX.Element {
723
774
  ref={(element) => {
724
775
  inputElement = element;
725
776
  context.setInputRef?.(element);
777
+ autocompleteInput?.inputRef(element);
726
778
  const ref = (domProps as { ref?: unknown }).ref;
727
779
  if (typeof ref === "function") {
728
780
  ref(element);
package/src/Select.tsx CHANGED
@@ -186,11 +186,15 @@ export interface SelectTriggerRenderProps {
186
186
  isFocusVisible: boolean;
187
187
  /** Whether the trigger is hovered. */
188
188
  isHovered: boolean;
189
+ /** Whether the trigger is pressed. */
190
+ isPressed: boolean;
189
191
  /** Whether the trigger is disabled. */
190
192
  isDisabled: boolean;
191
193
  }
192
194
 
193
195
  export interface SelectTriggerProps extends SlotProps {
196
+ /** A ref callback/object for the underlying trigger button element. */
197
+ ref?: RefLike<HTMLButtonElement>;
194
198
  /** The children of the trigger. A function may be provided to receive render props. */
195
199
  children?: RenderChildren<SelectTriggerRenderProps>;
196
200
  /** The CSS className for the element. */
@@ -241,6 +245,8 @@ export interface SelectOptionRenderProps {
241
245
  }
242
246
 
243
247
  export interface SelectOptionProps<T> extends Omit<AriaOptionProps, "children" | "key">, SlotProps {
248
+ /** A ref callback/object for the underlying option element. */
249
+ ref?: RefLike<HTMLDivElement>;
244
250
  /** The unique key for the option. */
245
251
  id: Key;
246
252
  /** The item value. */
@@ -269,6 +275,7 @@ interface SelectContextValue<T> {
269
275
  isOpen: Accessor<boolean>;
270
276
  isFocused: Accessor<boolean>;
271
277
  isFocusVisible: Accessor<boolean>;
278
+ isPressed: Accessor<boolean>;
272
279
  isDisabled: Accessor<boolean>;
273
280
  placeholder?: string;
274
281
  items: T[];
@@ -398,8 +405,16 @@ export function Select<T>(props: SelectProps<T>): JSX.Element {
398
405
  return clean as typeof ariaProps;
399
406
  });
400
407
 
401
- const { labelProps, triggerProps, valueProps, menuProps, isFocused, isFocusVisible, isOpen } =
402
- createSelect<T>(selectAriaProps, state);
408
+ const {
409
+ labelProps,
410
+ triggerProps,
411
+ valueProps,
412
+ menuProps,
413
+ isFocused,
414
+ isFocusVisible,
415
+ isOpen,
416
+ isPressed,
417
+ } = createSelect<T>(selectAriaProps, state);
403
418
 
404
419
  const { isHovered, hoverProps } = createHover({
405
420
  get isDisabled() {
@@ -579,6 +594,17 @@ export function Select<T>(props: SelectProps<T>): JSX.Element {
579
594
 
580
595
  return (
581
596
  <>
597
+ <Show when={ariaProps.label}>
598
+ <span {...cleanLabelProps()}>{ariaProps.label as JSX.Element}</span>
599
+ </Show>
600
+ {selectChildren}
601
+ {/*
602
+ The HiddenSelect (native `<select>` for form autofill/submission) renders
603
+ AFTER the trigger button, mirroring upstream RAC `Select` which emits
604
+ `{renderProps.children}` then `<HiddenSelect>` (Select.tsx:288-289). Order
605
+ matters for the DOM tab/focus trail: the visible button must precede the
606
+ form-only hidden control.
607
+ */}
582
608
  <div {...containerProps}>
583
609
  <select
584
610
  {...hiddenSelectProps}
@@ -652,10 +678,6 @@ export function Select<T>(props: SelectProps<T>): JSX.Element {
652
678
  />
653
679
  </Show>
654
680
  </div>
655
- <Show when={ariaProps.label}>
656
- <span {...cleanLabelProps()}>{ariaProps.label as JSX.Element}</span>
657
- </Show>
658
- {selectChildren}
659
681
  </>
660
682
  );
661
683
  };
@@ -711,6 +733,7 @@ export function Select<T>(props: SelectProps<T>): JSX.Element {
711
733
  isOpen,
712
734
  isFocused,
713
735
  isFocusVisible,
736
+ isPressed,
714
737
  isDisabled: resolveDisabled,
715
738
  placeholder: ariaProps.placeholder,
716
739
  items: stateProps.items,
@@ -731,17 +754,18 @@ export function Select<T>(props: SelectProps<T>): JSX.Element {
731
754
  * The trigger button for a select.
732
755
  */
733
756
  export function SelectTrigger(props: SelectTriggerProps): JSX.Element {
734
- const [local, domProps] = splitProps(props, ["class", "style", "slot", "children"]);
757
+ const [local, domProps] = splitProps(props, ["class", "style", "slot", "children", "ref"]);
735
758
 
736
759
  const context = useContext(SelectContext);
737
760
  if (!context) {
738
761
  throw new Error("SelectTrigger must be used within a Select");
739
762
  }
740
- const { isOpen, isFocused, isFocusVisible, state } = context;
763
+ const { isOpen, isFocused, isFocusVisible, isPressed, state } = context;
741
764
  let triggerRef: HTMLButtonElement | undefined;
742
765
  const setTriggerRef = (el: HTMLButtonElement) => {
743
766
  triggerRef = el;
744
767
  context.setTriggerRef(el);
768
+ assignRef(local.ref, el);
745
769
  };
746
770
 
747
771
  createEffect(() => {
@@ -761,6 +785,7 @@ export function SelectTrigger(props: SelectTriggerProps): JSX.Element {
761
785
  isFocused: isFocused(),
762
786
  isFocusVisible: isFocusVisible(),
763
787
  isHovered: isHovered(),
788
+ isPressed: isPressed(),
764
789
  isDisabled: state.isDisabled,
765
790
  }));
766
791
 
@@ -926,6 +951,14 @@ export function SelectListBox<T>(props: SelectListBoxProps<T>): JSX.Element {
926
951
  if (!isOpen()) {
927
952
  return;
928
953
  }
954
+ // On open the listbox takes collection focus. Upstream `useSelect` sets
955
+ // `menuProps.autoFocus = state.focusStrategy || true`, so the ListBox's
956
+ // `useSelectableCollection` auto-focuses on mount — setting the selection
957
+ // manager's `isFocused` true and focusing the selected option. Our
958
+ // `createListBox` reimplements keyboard nav inline (no `autoFocus` effect),
959
+ // so we set the manager focus here to match. Without it the selected option
960
+ // never paints its focus-visible background (`data-focused` stays unset).
961
+ state.selectionManager.setFocused(true);
929
962
  if (state.focusedKey() != null) {
930
963
  return;
931
964
  }
@@ -935,7 +968,7 @@ export function SelectListBox<T>(props: SelectListBoxProps<T>): JSX.Element {
935
968
  }
936
969
  });
937
970
 
938
- let listBoxRef: HTMLUListElement | undefined;
971
+ let listBoxRef: HTMLDivElement | undefined;
939
972
 
940
973
  createInteractOutside({
941
974
  ref: () => rootRef() ?? listBoxRef ?? null,
@@ -949,7 +982,14 @@ export function SelectListBox<T>(props: SelectListBoxProps<T>): JSX.Element {
949
982
  },
950
983
  });
951
984
 
952
- const { listBoxProps } = createListBox(
985
+ // Keep the hook result intact — destructuring `{ listBoxProps }` invokes the
986
+ // `get listBoxProps()` getter ONCE and freezes it (the Solid "destructuring a
987
+ // get-prop freezes reactivity" gotcha). The listbox `tabIndex` is computed from
988
+ // `focusedKey` (`focusedKey != null ? -1 : 0`); on open the effect above sets a
989
+ // focused key, so a frozen snapshot would leave the listbox tabbable (`0`) when
990
+ // upstream flips it to `-1`. Reading `listBoxHook.listBoxProps` per access keeps
991
+ // it reactive.
992
+ const listBoxHook = createListBox(
953
993
  {
954
994
  ...(menuProps as unknown as AriaListBoxProps),
955
995
  shouldSelectOnPressUp: true,
@@ -982,8 +1022,37 @@ export function SelectListBox<T>(props: SelectListBoxProps<T>): JSX.Element {
982
1022
  return rest;
983
1023
  };
984
1024
  const cleanListBoxProps = () => {
985
- const { ref: _ref2, ...rest } = listBoxProps as Record<string, unknown>;
986
- return rest;
1025
+ const {
1026
+ ref: _ref2,
1027
+ "aria-activedescendant": _activeDescendant,
1028
+ onFocus: _onFocus,
1029
+ ...rest
1030
+ } = listBoxHook.listBoxProps as Record<string, unknown>;
1031
+ // Faithful Select listbox focus model: the focused option receives REAL DOM
1032
+ // focus (the open effect above + `createSelectableItem`'s self-focus move
1033
+ // `document.activeElement` onto it), mirroring upstream `useSelect`. So the
1034
+ // listbox must (a) leave the tab sequence once an option is focused —
1035
+ // `useSelectableCollection` uses `tabIndex = focusedKey == null ? 0 : -1`
1036
+ // (useSelectableCollection.ts:687-690) — and (b) NOT expose
1037
+ // `aria-activedescendant`, which is the virtual-focus channel upstream Select
1038
+ // never uses. These are overridden here, not in the shared `createListBox`,
1039
+ // because the standalone `ListBox` keeps its container-focus model (focus
1040
+ // stays on the listbox, `aria-activedescendant` is its AT channel).
1041
+ //
1042
+ // We also drop `createListBox`'s container-focus TRAMPOLINE (`onFocus`): it
1043
+ // marshals the first key when the container is focused with `focusedKey ==
1044
+ // null`, which is exactly Select's deliberate click-open state (the open
1045
+ // effect sets `isFocused` true but leaves `focusedKey` null when nothing is
1046
+ // selected, so the first arrow enters the first item). Upstream's own
1047
+ // trampoline no-ops in this case because it guards on `manager.isFocused`
1048
+ // (already true); our shared guard uses `focusedKey == null` for the
1049
+ // standalone ListBox, so we strip it here to preserve Select's faithful
1050
+ // model. (Auto-selecting on focus under replace behavior would also toggle
1051
+ // the first item selected on open — the bug this override prevents.)
1052
+ return {
1053
+ ...rest,
1054
+ tabIndex: state.isDisabled ? undefined : state.focusedKey() != null ? -1 : 0,
1055
+ };
987
1056
  };
988
1057
 
989
1058
  const items = () => Array.from(state.collection());
@@ -1003,7 +1072,12 @@ export function SelectListBox<T>(props: SelectListBoxProps<T>): JSX.Element {
1003
1072
  });
1004
1073
 
1005
1074
  const listBox = () => (
1006
- <ul
1075
+ // Upstream RAC ListBox (and our own Menu.tsx) render a `<div role="listbox">`
1076
+ // over `<div role="option">` rows, NOT `<ul>`/`<li>` — the collection is
1077
+ // div-based for virtualization parity. `<ul>`/`<li>` here was a self-inflicted
1078
+ // structural divergence surfaced by the Picker recertification (D5/D6/D8 saw
1079
+ // `li[option]`/`ul[listbox]` where the React oracle sees `div`).
1080
+ <div
1007
1081
  ref={(el) => (listBoxRef = el)}
1008
1082
  {...domProps}
1009
1083
  {...cleanMenuProps()}
@@ -1014,9 +1088,9 @@ export function SelectListBox<T>(props: SelectListBoxProps<T>): JSX.Element {
1014
1088
  data-empty={state.collection().size === 0 || undefined}
1015
1089
  >
1016
1090
  {state.collection().size === 0 && local.renderEmptyState ? (
1017
- <li role="option" style={{ display: "contents" }} data-empty-state>
1091
+ <div role="option" style={{ display: "contents" }} data-empty-state>
1018
1092
  {local.renderEmptyState()}
1019
- </li>
1093
+ </div>
1020
1094
  ) : (
1021
1095
  <Show
1022
1096
  when={local.children}
@@ -1040,7 +1114,7 @@ export function SelectListBox<T>(props: SelectListBoxProps<T>): JSX.Element {
1040
1114
  {local.renderLoadMore?.()}
1041
1115
  </ListBoxLoadMoreItem>
1042
1116
  </Show>
1043
- </ul>
1117
+ </div>
1044
1118
  );
1045
1119
 
1046
1120
  return (
@@ -1070,6 +1144,7 @@ export function SelectOption<T>(props: SelectOptionProps<T>): JSX.Element {
1070
1144
  "id",
1071
1145
  "item",
1072
1146
  "textValue",
1147
+ "ref",
1073
1148
  ]);
1074
1149
 
1075
1150
  const context = useContext(SelectStateContext);
@@ -1078,7 +1153,11 @@ export function SelectOption<T>(props: SelectOptionProps<T>): JSX.Element {
1078
1153
  }
1079
1154
  const state = context as SelectState<T>;
1080
1155
  const selectContext = useContext(SelectContext) as SelectContextValue<T> | null;
1081
- const [ref, setRef] = createSignal<HTMLLIElement | null>(null);
1156
+ const [ref, setRefSignal] = createSignal<HTMLDivElement | null>(null);
1157
+ const setRef = (el: HTMLDivElement | null) => {
1158
+ setRefSignal(el);
1159
+ assignRef(local.ref, el as HTMLDivElement);
1160
+ };
1082
1161
 
1083
1162
  const optionAria = createOption<T>(
1084
1163
  {
@@ -1187,7 +1266,7 @@ export function SelectOption<T>(props: SelectOptionProps<T>): JSX.Element {
1187
1266
  selectOption();
1188
1267
  }
1189
1268
  });
1190
- }) as JSX.EventHandler<HTMLLIElement, MouseEvent>;
1269
+ }) as JSX.EventHandler<HTMLDivElement, MouseEvent>;
1191
1270
  return rest;
1192
1271
  };
1193
1272
  const selectOption = () => {
@@ -1209,7 +1288,9 @@ export function SelectOption<T>(props: SelectOptionProps<T>): JSX.Element {
1209
1288
 
1210
1289
  return (
1211
1290
  <SelectionIndicatorContext.Provider value={selectionIndicatorContext()}>
1212
- <li
1291
+ {/* `<div role="option">`, not `<li>` — see the SelectListBox note; upstream
1292
+ RAC options are div-based. */}
1293
+ <div
1213
1294
  ref={setRef}
1214
1295
  {...cleanOptionProps()}
1215
1296
  class={renderProps.class()}
@@ -1226,7 +1307,7 @@ export function SelectOption<T>(props: SelectOptionProps<T>): JSX.Element {
1226
1307
  ) : (
1227
1308
  renderProps.renderChildren()
1228
1309
  )}
1229
- </li>
1310
+ </div>
1230
1311
  </SelectionIndicatorContext.Provider>
1231
1312
  );
1232
1313
  }