@proyecto-viviana/solidaria-components 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (226) 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 +18 -18
  19. package/dist/Breadcrumbs.js.map +1 -1
  20. package/dist/Breadcrumbs.jsx +21 -26
  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/ElementTag.d.ts +29 -0
  60. package/dist/ElementTag.d.ts.map +1 -0
  61. package/dist/ElementTag.js +220 -0
  62. package/dist/ElementTag.js.map +1 -0
  63. package/dist/ElementTag.jsx +67 -0
  64. package/dist/ElementTag.jsx.map +1 -0
  65. package/dist/GridList.d.ts.map +1 -1
  66. package/dist/GridList.js +2 -2
  67. package/dist/GridList.js.map +1 -1
  68. package/dist/GridList.jsx +2 -2
  69. package/dist/GridList.jsx.map +1 -1
  70. package/dist/Landmark.js +4 -3
  71. package/dist/Landmark.js.map +1 -1
  72. package/dist/Landmark.jsx +3 -3
  73. package/dist/Landmark.jsx.map +1 -1
  74. package/dist/Link.js +3 -2
  75. package/dist/Link.js.map +1 -1
  76. package/dist/Link.jsx +3 -3
  77. package/dist/Link.jsx.map +1 -1
  78. package/dist/ListBox.d.ts +2 -2
  79. package/dist/ListBox.d.ts.map +1 -1
  80. package/dist/ListBox.js +327 -166
  81. package/dist/ListBox.js.map +1 -1
  82. package/dist/ListBox.jsx +161 -25
  83. package/dist/ListBox.jsx.map +1 -1
  84. package/dist/Modal.d.ts.map +1 -1
  85. package/dist/Modal.js +7 -5
  86. package/dist/Modal.js.map +1 -1
  87. package/dist/Modal.jsx +5 -5
  88. package/dist/Modal.jsx.map +1 -1
  89. package/dist/Popover.d.ts.map +1 -1
  90. package/dist/Popover.js +16 -9
  91. package/dist/Popover.js.map +1 -1
  92. package/dist/Popover.jsx +25 -4
  93. package/dist/Popover.jsx.map +1 -1
  94. package/dist/RangeCalendar.d.ts.map +1 -1
  95. package/dist/RangeCalendar.js +182 -46
  96. package/dist/RangeCalendar.js.map +1 -1
  97. package/dist/RangeCalendar.jsx +72 -2
  98. package/dist/RangeCalendar.jsx.map +1 -1
  99. package/dist/SearchField.d.ts.map +1 -1
  100. package/dist/SearchField.js +26 -2
  101. package/dist/SearchField.js.map +1 -1
  102. package/dist/SearchField.jsx +26 -2
  103. package/dist/SearchField.jsx.map +1 -1
  104. package/dist/Select.d.ts +7 -0
  105. package/dist/Select.d.ts.map +1 -1
  106. package/dist/Select.js +64 -51
  107. package/dist/Select.js.map +1 -1
  108. package/dist/Select.jsx +35 -20
  109. package/dist/Select.jsx.map +1 -1
  110. package/dist/Separator.js +3 -2
  111. package/dist/Separator.js.map +1 -1
  112. package/dist/Separator.jsx +2 -2
  113. package/dist/Separator.jsx.map +1 -1
  114. package/dist/SharedElementTransition.d.ts.map +1 -1
  115. package/dist/SharedElementTransition.js +35 -33
  116. package/dist/SharedElementTransition.js.map +1 -1
  117. package/dist/SharedElementTransition.jsx +35 -33
  118. package/dist/SharedElementTransition.jsx.map +1 -1
  119. package/dist/StepList.d.ts.map +1 -1
  120. package/dist/StepList.js +2 -9
  121. package/dist/StepList.js.map +1 -1
  122. package/dist/StepList.jsx +1 -6
  123. package/dist/StepList.jsx.map +1 -1
  124. package/dist/Table.d.ts +2 -0
  125. package/dist/Table.d.ts.map +1 -1
  126. package/dist/Table.js +10 -3
  127. package/dist/Table.js.map +1 -1
  128. package/dist/Table.jsx +10 -3
  129. package/dist/Table.jsx.map +1 -1
  130. package/dist/Tabs.d.ts.map +1 -1
  131. package/dist/Tabs.js +3 -0
  132. package/dist/Tabs.js.map +1 -1
  133. package/dist/Tabs.jsx +1 -1
  134. package/dist/Tabs.jsx.map +1 -1
  135. package/dist/TagGroup.d.ts +3 -1
  136. package/dist/TagGroup.d.ts.map +1 -1
  137. package/dist/TagGroup.js +12 -8
  138. package/dist/TagGroup.js.map +1 -1
  139. package/dist/TagGroup.jsx +11 -8
  140. package/dist/TagGroup.jsx.map +1 -1
  141. package/dist/Text.d.ts.map +1 -1
  142. package/dist/Text.js +9 -8
  143. package/dist/Text.js.map +1 -1
  144. package/dist/Text.jsx +3 -3
  145. package/dist/Text.jsx.map +1 -1
  146. package/dist/TextField.js +4 -1
  147. package/dist/TextField.js.map +1 -1
  148. package/dist/TextField.jsx +4 -1
  149. package/dist/TextField.jsx.map +1 -1
  150. package/dist/TimeField.d.ts +14 -50
  151. package/dist/TimeField.d.ts.map +1 -1
  152. package/dist/TimeField.js +111 -205
  153. package/dist/TimeField.js.map +1 -1
  154. package/dist/TimeField.jsx +76 -82
  155. package/dist/TimeField.jsx.map +1 -1
  156. package/dist/Tree.d.ts.map +1 -1
  157. package/dist/Tree.js +3 -3
  158. package/dist/Tree.js.map +1 -1
  159. package/dist/Tree.jsx +3 -3
  160. package/dist/Tree.jsx.map +1 -1
  161. package/dist/VisuallyHidden.d.ts.map +1 -1
  162. package/dist/VisuallyHidden.js +3 -11
  163. package/dist/VisuallyHidden.js.map +1 -1
  164. package/dist/VisuallyHidden.jsx +3 -4
  165. package/dist/VisuallyHidden.jsx.map +1 -1
  166. package/dist/index.d.ts +3 -3
  167. package/dist/index.d.ts.map +1 -1
  168. package/dist/index.js +6 -6
  169. package/dist/index.jsx +6 -6
  170. package/dist/useDragAndDrop.d.ts +0 -8
  171. package/dist/useDragAndDrop.d.ts.map +1 -1
  172. package/dist/useDragAndDrop2.js +6 -2
  173. package/dist/useDragAndDrop2.js.map +1 -1
  174. package/dist/useDragAndDrop2.jsx +6 -2
  175. package/dist/useDragAndDrop2.jsx.map +1 -1
  176. package/dist/utils.d.ts +1 -0
  177. package/dist/utils.d.ts.map +1 -1
  178. package/dist/utils.js +2 -2
  179. package/dist/utils.js.map +1 -1
  180. package/dist/utils.jsx +2 -2
  181. package/dist/utils.jsx.map +1 -1
  182. package/package.json +3 -9
  183. package/src/ActionBar.tsx +10 -12
  184. package/src/ActionGroup.tsx +5 -3
  185. package/src/Autocomplete.tsx +14 -9
  186. package/src/Breadcrumbs.tsx +57 -42
  187. package/src/Calendar.tsx +30 -1
  188. package/src/Color.tsx +42 -27
  189. package/src/ComboBox.tsx +49 -17
  190. package/src/DateField.tsx +285 -52
  191. package/src/DatePicker.tsx +345 -57
  192. package/src/DateRangePickerContext.tsx +2 -0
  193. package/src/Disclosure.tsx +4 -0
  194. package/src/ElementTag.tsx +116 -0
  195. package/src/GridList.tsx +8 -2
  196. package/src/Landmark.tsx +4 -4
  197. package/src/Link.tsx +4 -4
  198. package/src/ListBox.tsx +323 -21
  199. package/src/Modal.tsx +21 -5
  200. package/src/Popover.tsx +28 -7
  201. package/src/RangeCalendar.tsx +120 -1
  202. package/src/SearchField.tsx +53 -1
  203. package/src/Select.tsx +114 -22
  204. package/src/Separator.tsx +3 -3
  205. package/src/SharedElementTransition.tsx +92 -36
  206. package/src/StepList.tsx +0 -7
  207. package/src/Table.tsx +19 -4
  208. package/src/Tabs.tsx +1 -0
  209. package/src/TagGroup.tsx +26 -6
  210. package/src/Text.tsx +5 -4
  211. package/src/TextField.tsx +1 -1
  212. package/src/TimeField.tsx +113 -196
  213. package/src/Tree.tsx +14 -4
  214. package/src/VisuallyHidden.tsx +8 -12
  215. package/src/index.ts +3 -8
  216. package/src/useDragAndDrop.ts +8 -10
  217. package/src/utils.tsx +3 -3
  218. package/dist/DatePicker.js.map +0 -1
  219. package/dist/DatePicker.jsx.map +0 -1
  220. package/dist/HiddenTimeInput.d.ts +0 -25
  221. package/dist/HiddenTimeInput.d.ts.map +0 -1
  222. package/dist/HiddenTimeInput.js +0 -104
  223. package/dist/HiddenTimeInput.js.map +0 -1
  224. package/dist/HiddenTimeInput.jsx +0 -69
  225. package/dist/HiddenTimeInput.jsx.map +0 -1
  226. package/src/HiddenTimeInput.tsx +0 -133
@@ -26,6 +26,7 @@ import {
26
26
  createAutocompleteState,
27
27
  type AutocompleteState,
28
28
  type AutocompleteStateOptions,
29
+ type CollectionNode,
29
30
  } from "@proyecto-viviana/solid-stately";
30
31
  import { type SlotProps } from "./utils";
31
32
 
@@ -44,7 +45,7 @@ export interface AutocompleteContextValue {
44
45
  export interface AutocompleteCollectionContextValue {
45
46
  collectionProps: CollectionOptions;
46
47
  collectionRef: (el: HTMLElement) => void;
47
- filter?: (textValue: string) => boolean;
48
+ filter?: (nodeTextValue: string, node: CollectionNode<unknown>) => boolean;
48
49
  }
49
50
 
50
51
  export const AutocompleteContext = createContext<AutocompleteContextValue | null>(null);
@@ -126,16 +127,18 @@ export function Autocomplete<T = unknown>(props: AutocompleteProps<T>): JSX.Elem
126
127
 
127
128
  const state = createAutocompleteState(stateProps);
128
129
 
129
- // Create refs
130
- let inputRef: HTMLInputElement | undefined;
131
- let collectionRef: HTMLElement | undefined;
130
+ // Refs are signals so createAutocomplete's effects (the beforeinput listener
131
+ // and the reverse-path focusin listener that mirrors aria-activedescendant)
132
+ // bind reactively once the input/collection elements actually mount.
133
+ const [inputRef, setInputRef] = createSignal<HTMLInputElement>();
134
+ const [collectionRef, setCollectionRef] = createSignal<HTMLElement>();
132
135
 
133
136
  // Create autocomplete aria
134
137
  const autocomplete = createAutocomplete<T>(
135
138
  {
136
139
  ...ariaProps,
137
- inputRef: () => inputRef,
138
- collectionRef: () => collectionRef,
140
+ inputRef,
141
+ collectionRef,
139
142
  },
140
143
  state,
141
144
  );
@@ -144,7 +147,7 @@ export function Autocomplete<T = unknown>(props: AutocompleteProps<T>): JSX.Elem
144
147
  const inputContextValue = createMemo<AutocompleteContextValue>(() => ({
145
148
  inputProps: autocomplete.inputProps,
146
149
  inputRef: (el: HTMLInputElement) => {
147
- inputRef = el;
150
+ setInputRef(el);
148
151
  },
149
152
  }));
150
153
 
@@ -152,9 +155,11 @@ export function Autocomplete<T = unknown>(props: AutocompleteProps<T>): JSX.Elem
152
155
  const collectionContextValue = createMemo<AutocompleteCollectionContextValue>(() => ({
153
156
  collectionProps: autocomplete.collectionProps,
154
157
  collectionRef: (el: HTMLElement) => {
155
- collectionRef = el;
158
+ setCollectionRef(el);
156
159
  },
157
- filter: autocomplete.filter,
160
+ filter: autocomplete.filter as
161
+ | ((nodeTextValue: string, node: CollectionNode<unknown>) => boolean)
162
+ | undefined,
158
163
  }));
159
164
 
160
165
  return (
@@ -17,7 +17,7 @@ import {
17
17
  For,
18
18
  Show,
19
19
  } from "solid-js";
20
- import { Dynamic } from "solid-js/web";
20
+ import { ElementTag } from "./ElementTag";
21
21
  import {
22
22
  createBreadcrumbs,
23
23
  createBreadcrumbItem,
@@ -196,7 +196,20 @@ export function Breadcrumbs<T>(props: BreadcrumbsProps<T>): JSX.Element {
196
196
  setStaticItemCount,
197
197
  }}
198
198
  >
199
- <nav
199
+ {/*
200
+ Faithful to react-aria-components Breadcrumbs: the accessible name (navProps'
201
+ aria-label) sits directly on the <ol> (role="list"), with NO wrapping <nav>
202
+ landmark (react-spectrum/packages/react-aria-components/src/Breadcrumbs.tsx
203
+ renders `<dom.ol {...mergeProps(DOMProps, navProps)} style={props.style}>`).
204
+ The <ol> carries ONLY the consumer's class/style — no hard-coded inline
205
+ layout reset. An inline reset would beat the styled layer's class (inline
206
+ wins over class), which is exactly what broke S2 parity: the solid-spectrum
207
+ `wrapperStyles` class supplies display:flex / list-style:none / padding:0 /
208
+ marginStart and deliberately leaves align-items at its `normal` default, so
209
+ a hard-coded inline `align-items:center` + `margin:0` clobbered the
210
+ wrapper's margin and diverged from S2 (whose <ol> has neither).
211
+ */}
212
+ <ol
200
213
  {...navProps}
201
214
  {...domProps()}
202
215
  ref={(element) => assignRef(local.ref, element)}
@@ -204,39 +217,27 @@ export function Breadcrumbs<T>(props: BreadcrumbsProps<T>): JSX.Element {
204
217
  style={renderProps.style()}
205
218
  data-disabled={isDisabled() || undefined}
206
219
  >
207
- <ol
208
- style={{
209
- display: "flex",
210
- "align-items": "center",
211
- "list-style": "none",
212
- margin: 0,
213
- padding: 0,
214
- }}
220
+ <Show
221
+ when={hasCollectionItems()}
222
+ fallback={<StaticBreadcrumbItems>{local.children as JSX.Element}</StaticBreadcrumbItems>}
215
223
  >
216
- <Show
217
- when={hasCollectionItems()}
218
- fallback={
219
- <StaticBreadcrumbItems>{local.children as JSX.Element}</StaticBreadcrumbItems>
220
- }
221
- >
222
- <For each={items()}>
223
- {(item, index) => {
224
- const itemKey = getItemKey(item, index());
225
- const isLast = () => index() === items().length - 1;
226
- const renderItem = local.children as ((item: T) => JSX.Element) | undefined;
227
-
228
- return (
229
- <li style={{ display: "flex", "align-items": "center" }}>
230
- <BreadcrumbItemContext.Provider value={{ itemKey, isLast }}>
231
- {renderItem?.(item)}
232
- </BreadcrumbItemContext.Provider>
233
- </li>
234
- );
235
- }}
236
- </For>
237
- </Show>
238
- </ol>
239
- </nav>
224
+ <For each={items()}>
225
+ {(item, index) => {
226
+ const itemKey = getItemKey(item, index());
227
+ const isLast = () => index() === items().length - 1;
228
+ const renderItem = local.children as ((item: T) => JSX.Element) | undefined;
229
+
230
+ return (
231
+ <li style={{ display: "flex", "align-items": "center" }}>
232
+ <BreadcrumbItemContext.Provider value={{ itemKey, isLast }}>
233
+ {renderItem?.(item)}
234
+ </BreadcrumbItemContext.Provider>
235
+ </li>
236
+ );
237
+ }}
238
+ </For>
239
+ </Show>
240
+ </ol>
240
241
  </BreadcrumbsContext.Provider>
241
242
  );
242
243
  }
@@ -277,6 +278,18 @@ export function BreadcrumbItem(props: BreadcrumbItemProps): JSX.Element {
277
278
  staticIndex === context.staticItemCount() - 1);
278
279
  const itemKey = () => itemContext?.itemKey ?? ariaProps.id ?? null;
279
280
 
281
+ // Mirror react-aria-components' Link: render an <a> only when there is an
282
+ // href on an enabled, non-current item; otherwise fall back to a <span
283
+ // role="link"> (via createLink). A styled layer may force the element type
284
+ // (S2 renders the current item as a plain <div>), which takes precedence.
285
+ const elementType = () =>
286
+ ariaProps.elementType ?? (ariaProps.href && !isDisabled() && !isCurrent() ? "a" : "span");
287
+ // The current breadcrumb is the current page and is non-interactive: S2
288
+ // renders it as a plain element with no link role. createLink only omits
289
+ // role="link" for an <a>, so treat the current item as an "a" purely for the
290
+ // role decision while still rendering the resolved element type above.
291
+ const linkElementType = () => (isCurrent() ? "a" : elementType());
292
+
280
293
  const handlePress = (e: PressEvent) => {
281
294
  ariaProps.onPress?.(e);
282
295
  const key = itemKey();
@@ -285,7 +298,7 @@ export function BreadcrumbItem(props: BreadcrumbItemProps): JSX.Element {
285
298
  }
286
299
  };
287
300
 
288
- const { itemProps, isPressed } = createBreadcrumbItem({
301
+ const itemAria = createBreadcrumbItem({
289
302
  get id() {
290
303
  return ariaProps.id;
291
304
  },
@@ -305,7 +318,7 @@ export function BreadcrumbItem(props: BreadcrumbItemProps): JSX.Element {
305
318
  return ariaProps.rel;
306
319
  },
307
320
  get elementType() {
308
- return ariaProps.elementType;
321
+ return linkElementType();
309
322
  },
310
323
  get onPress() {
311
324
  return handlePress;
@@ -354,20 +367,23 @@ export function BreadcrumbItem(props: BreadcrumbItemProps): JSX.Element {
354
367
  },
355
368
  });
356
369
 
370
+ const isPressed = itemAria.isPressed;
357
371
  const { isFocused, isFocusVisible, focusProps } = createFocusRing();
358
372
  const { isHovered, hoverProps } = createHover({
359
373
  get isDisabled() {
360
374
  return isDisabled();
361
375
  },
362
376
  });
377
+ // Read itemAria.itemProps fresh inside the memo (do NOT destructure it above):
378
+ // the reactive getter re-runs createBreadcrumbItem → createLink each time this
379
+ // memo re-evaluates, so role/tabIndex track elementType once isCurrent settles.
363
380
  const mergedItemProps = createMemo(() =>
364
381
  mergeProps(
365
- itemProps as Record<string, unknown>,
382
+ itemAria.itemProps as Record<string, unknown>,
366
383
  focusProps as Record<string, unknown>,
367
384
  hoverProps as Record<string, unknown>,
368
385
  ),
369
386
  );
370
- const elementType = () => ariaProps.elementType ?? "a";
371
387
 
372
388
  const renderValues = createMemo<BreadcrumbItemRenderProps>(() => ({
373
389
  isCurrent: isCurrent(),
@@ -397,11 +413,10 @@ export function BreadcrumbItem(props: BreadcrumbItemProps): JSX.Element {
397
413
  };
398
414
 
399
415
  return (
400
- <Dynamic
401
- component={elementType()}
416
+ <ElementTag
417
+ tag={elementType()}
402
418
  {...mergedItemProps()}
403
419
  ref={(element: HTMLElement) => assignRef(local.ref, element)}
404
- href={isCurrent() ? undefined : (mergedItemProps() as { href?: string }).href}
405
420
  aria-current={isCurrent() ? (ariaProps["aria-current"] ?? "page") : undefined}
406
421
  aria-disabled={isDisabled() || isCurrent() || undefined}
407
422
  class={renderProps.class()}
@@ -414,7 +429,7 @@ export function BreadcrumbItem(props: BreadcrumbItemProps): JSX.Element {
414
429
  data-hovered={isHovered() || undefined}
415
430
  >
416
431
  {renderProps.renderChildren()}
417
- </Dynamic>
432
+ </ElementTag>
418
433
  );
419
434
  }
420
435
 
package/src/Calendar.tsx CHANGED
@@ -305,6 +305,17 @@ function CalendarWithState<
305
305
  <h2>{String(calendarAria.calendarProps["aria-label"] ?? "")}</h2>
306
306
  </VisuallyHidden>
307
307
  {props.children}
308
+ {/* For touch screen readers, a visually hidden next button after the month
309
+ * grid so it's easy to navigate after reaching the end without going all
310
+ * the way back to the start. Mirrors react-aria-components Calendar. */}
311
+ <VisuallyHidden>
312
+ <button
313
+ aria-label={String(calendarAria.nextButtonProps["aria-label"] ?? "")}
314
+ disabled={Boolean(calendarAria.nextButtonProps.disabled)}
315
+ onClick={() => state().focusNextPage()}
316
+ tabIndex={-1}
317
+ />
318
+ </VisuallyHidden>
308
319
  </div>
309
320
  );
310
321
  }
@@ -387,6 +398,18 @@ function CalendarInner<
387
398
  <h2>{String(calendarAria.calendarProps["aria-label"] ?? "")}</h2>
388
399
  </VisuallyHidden>
389
400
  {props.children}
401
+ {/* For touch screen readers, a visually hidden next button after the
402
+ * month grid so it's easy to navigate after reaching the end without
403
+ * going all the way back to the start. Mirrors react-aria-components
404
+ * Calendar. */}
405
+ <VisuallyHidden>
406
+ <button
407
+ aria-label={String(calendarAria.nextButtonProps["aria-label"] ?? "")}
408
+ disabled={Boolean(calendarAria.nextButtonProps.disabled)}
409
+ onClick={() => state.focusNextPage()}
410
+ tabIndex={-1}
411
+ />
412
+ </VisuallyHidden>
390
413
  </div>
391
414
  </CalendarContext.Provider>
392
415
  );
@@ -455,12 +478,18 @@ export function CalendarButton(props: CalendarButtonProps): JSX.Element {
455
478
  return calendarAria.nextButtonProps;
456
479
  });
457
480
 
481
+ const isDisabled = () => props.isDisabled || state.isDisabled();
482
+
458
483
  return (
459
484
  <button
460
485
  {...buttonProps()}
461
486
  class={props.class ?? "solidaria-CalendarButton"}
462
487
  style={props.style}
463
- disabled={props.isDisabled || state.isDisabled()}
488
+ disabled={isDisabled()}
489
+ // Mirror useFocusable: always set an explicit tabIndex on an enabled
490
+ // focusable button (0), and drop it entirely when disabled — so the
491
+ // nav buttons appear in the roving tab order exactly as upstream does.
492
+ tabindex={isDisabled() ? undefined : 0}
464
493
  >
465
494
  {props.children}
466
495
  </button>
package/src/Color.tsx CHANGED
@@ -28,6 +28,7 @@ import {
28
28
  createFocusRing,
29
29
  createHover,
30
30
  mergeProps,
31
+ useLocale,
31
32
  type AriaColorSliderOptions,
32
33
  type AriaColorAreaOptions,
33
34
  type AriaColorWheelOptions,
@@ -233,7 +234,11 @@ export function ColorSlider(props: ColorSliderProps): JSX.Element {
233
234
  ],
234
235
  );
235
236
 
236
- // Create color slider state
237
+ const locale = useLocale();
238
+
239
+ // Create color slider state. The locale drives the thumb value label's
240
+ // channel-value formatting (e.g. hue "50°" vs ar-AE "50 درجة"), mirroring
241
+ // RAC ColorSlider passing `useLocale().locale` into useColorSliderState.
237
242
  const state = createColorSliderState(() => ({
238
243
  value: stateProps.value ?? pickerContext?.value,
239
244
  defaultValue: stateProps.defaultValue,
@@ -243,6 +248,7 @@ export function ColorSlider(props: ColorSliderProps): JSX.Element {
243
248
  colorSpace: stateProps.colorSpace,
244
249
  orientation: ariaProps.orientation,
245
250
  isDisabled: ariaProps.isDisabled,
251
+ locale: locale().locale,
246
252
  }));
247
253
 
248
254
  let trackRef: HTMLDivElement | undefined;
@@ -2115,11 +2121,10 @@ export function ColorSwatchPicker(props: ColorSwatchPickerProps): JSX.Element {
2115
2121
  () => ({
2116
2122
  id: local.id,
2117
2123
  "aria-label":
2118
- local["aria-label"] ?? (!local["aria-labelledby"] ? "Color swatch picker" : undefined),
2124
+ local["aria-label"] ?? (!local["aria-labelledby"] ? "Color swatches" : undefined),
2119
2125
  "aria-labelledby": local["aria-labelledby"],
2120
2126
  "aria-describedby": local["aria-describedby"],
2121
2127
  "aria-details": local["aria-details"],
2122
- shouldFocusWrap: true,
2123
2128
  }),
2124
2129
  state,
2125
2130
  );
@@ -2247,32 +2252,28 @@ export function ColorSwatchPicker(props: ColorSwatchPickerProps): JSX.Element {
2247
2252
  let nextKey: Key | null = null;
2248
2253
  switch (e.key) {
2249
2254
  case "ArrowDown":
2250
- nextKey = getGridKeyBelow(listbox, initialKey) ?? getBoundaryEnabledKey("next");
2255
+ nextKey = getGridKeyBelow(listbox, initialKey);
2251
2256
  break;
2252
2257
  case "ArrowUp":
2253
- nextKey = getGridKeyAbove(listbox, initialKey) ?? getBoundaryEnabledKey("prev");
2258
+ nextKey = getGridKeyAbove(listbox, initialKey);
2254
2259
  break;
2255
2260
  case "ArrowRight":
2256
- nextKey =
2257
- getGridKeyRightOf(initialKey) ??
2258
- (resolveDirection() === "rtl"
2259
- ? getBoundaryEnabledKey("prev")
2260
- : getBoundaryEnabledKey("next"));
2261
+ nextKey = getGridKeyRightOf(initialKey);
2261
2262
  break;
2262
2263
  case "ArrowLeft":
2263
- nextKey =
2264
- getGridKeyLeftOf(initialKey) ??
2265
- (resolveDirection() === "rtl"
2266
- ? getBoundaryEnabledKey("next")
2267
- : getBoundaryEnabledKey("prev"));
2264
+ nextKey = getGridKeyLeftOf(initialKey);
2268
2265
  break;
2269
2266
  }
2270
2267
 
2271
- if (nextKey == null) return false;
2272
-
2273
- state.setFocusedKey(nextKey);
2274
- if (state.selectionMode() === "single") {
2275
- state.replaceSelection(nextKey);
2268
+ // Mirror react-aria's grid keyboard delegate: an arrow in grid layout is
2269
+ // always consumed (blocking the linear ListBox handler and page scroll),
2270
+ // but focus only moves when the delegate yields a next key. With
2271
+ // shouldFocusWrap unset (upstream default false) there is no wrap, so arrows
2272
+ // stop dead at the grid's edges. Selection is not mutated on arrow — under
2273
+ // the default 'toggle' selection behavior focus moves without selecting, and
2274
+ // Enter/Space commits (handled by createSelectableItem).
2275
+ if (nextKey != null) {
2276
+ state.setFocusedKey(nextKey);
2276
2277
  }
2277
2278
 
2278
2279
  e.preventDefault();
@@ -2292,12 +2293,18 @@ export function ColorSwatchPicker(props: ColorSwatchPickerProps): JSX.Element {
2292
2293
  getListBoxKeyDown()?.(e);
2293
2294
  };
2294
2295
 
2295
- createEffect(() => {
2296
- const key = selectedKey();
2297
- if (key) {
2298
- state.setFocusedKey(key);
2299
- }
2300
- });
2296
+ // NB: we deliberately do NOT seed `state.setFocusedKey(selectedKey())` at rest.
2297
+ // Upstream's RAC `<ListBox>` (which `AriaColorSwatchPicker` is) keeps
2298
+ // `focusedKey == null` until the collection is focused — the listbox CONTAINER
2299
+ // is the roving tabstop (`tabIndex 0`), and `useSelectableCollection`'s onFocus
2300
+ // navigates to `firstSelectedKey` on entry (mirrored by `createListBox`'s
2301
+ // `onListBoxFocus`). Pre-seeding focusedKey to the selected swatch made the
2302
+ // selected option the rest tabstop instead (container `tabIndex -1`) — a
2303
+ // self-inflicted divergence — and, because our `createFocusWithin` only flips
2304
+ // `manager.setFocused(true)` on CONTAINER focus (Solid's `onFocus` is
2305
+ // non-bubbling), it also routed entry through a direct option focus that never
2306
+ // set `isFocused`, so arrow keys could not pull real DOM focus. Letting the
2307
+ // container be the tabstop restores both.
2301
2308
 
2302
2309
  const { isFocused, isFocusVisible, focusProps } = createFocusRing({ within: true });
2303
2310
  const domProps = createMemo(() =>
@@ -2387,13 +2394,20 @@ export function ColorSwatchPickerItem(props: ColorSwatchPickerItemProps): JSX.El
2387
2394
  onCleanup(() => context.unregisterItem(itemKey));
2388
2395
  });
2389
2396
 
2397
+ const [optionRef, setOptionRef] = createSignal<HTMLElement | null>(null);
2398
+
2390
2399
  const optionAria = createOption(
2391
2400
  () => ({
2392
2401
  key: key(),
2393
2402
  isDisabled: ariaProps.isDisabled,
2394
- "aria-label": ariaProps["aria-label"] ?? textValue(),
2403
+ // No aria-label: the option is named by its content (the child swatch's
2404
+ // role="img" aria-label), mirroring upstream ColorSwatchPickerItem which
2405
+ // sets only textValue (typeahead, threaded via registerItem above) and
2406
+ // never an aria-label.
2407
+ "aria-label": ariaProps["aria-label"],
2395
2408
  }),
2396
2409
  context.state,
2410
+ optionRef,
2397
2411
  );
2398
2412
 
2399
2413
  const renderValues = createMemo<ColorSwatchPickerItemRenderProps>(() => ({
@@ -2427,6 +2441,7 @@ export function ColorSwatchPickerItem(props: ColorSwatchPickerItemProps): JSX.El
2427
2441
 
2428
2442
  return (
2429
2443
  <div
2444
+ ref={setOptionRef}
2430
2445
  {...mergeProps(domProps(), cleanOptionProps())}
2431
2446
  class={renderProps.class()}
2432
2447
  style={renderProps.style()}
package/src/ComboBox.tsx CHANGED
@@ -28,6 +28,7 @@ import {
28
28
  createInteractOutside,
29
29
  createScrollIntoViewOnFocus,
30
30
  mergeProps,
31
+ isFocusVisible as isGlobalFocusVisible,
31
32
  type AriaComboBoxProps,
32
33
  type AriaListBoxProps,
33
34
  type AriaOptionProps,
@@ -239,6 +240,8 @@ export interface ComboBoxValueProps extends SlotProps {
239
240
  }
240
241
 
241
242
  export interface ComboBoxButtonProps extends SlotProps {
243
+ /** A ref to the underlying button element. */
244
+ ref?: RefLike<HTMLButtonElement>;
242
245
  /** The children of the button. */
243
246
  children?: RenderChildren<ComboBoxButtonRenderProps>;
244
247
  /** The CSS className for the element. */
@@ -278,6 +281,8 @@ export interface ComboBoxOptionRenderProps {
278
281
 
279
282
  export interface ComboBoxOptionProps<T>
280
283
  extends Omit<AriaOptionProps, "children" | "key">, SlotProps {
284
+ /** A ref to the underlying option element. */
285
+ ref?: RefLike<HTMLElement>;
281
286
  /** The unique key for the option. */
282
287
  id: Key;
283
288
  /** The item value. */
@@ -706,10 +711,13 @@ export function ComboBoxDescription(props: ComboBoxDescriptionProps): JSX.Elemen
706
711
  return rest;
707
712
  };
708
713
 
714
+ // Upstream renders the description via `<Text slot="description">`, and RAC's
715
+ // `Text` defaults to `elementType="span"` (Text.tsx:24) — so the help text is a
716
+ // `<span>`, not a `<div>`. Match it (mirrors the TextField port's same revert).
709
717
  return (
710
- <div {...domProps} {...cleanDescriptionProps()} class={local.class} style={local.style}>
718
+ <span {...domProps} {...cleanDescriptionProps()} class={local.class} style={local.style}>
711
719
  {local.children}
712
- </div>
720
+ </span>
713
721
  );
714
722
  }
715
723
 
@@ -729,10 +737,12 @@ export function ComboBoxErrorMessage(props: ComboBoxErrorMessageProps): JSX.Elem
729
737
  return rest;
730
738
  };
731
739
 
740
+ // Same as the description: upstream's error is `<Text slot="errorMessage">`,
741
+ // a `<span>` (RAC Text default elementType). Match it.
732
742
  return (
733
- <div {...domProps} {...cleanErrorMessageProps()} class={local.class} style={local.style}>
743
+ <span {...domProps} {...cleanErrorMessageProps()} class={local.class} style={local.style}>
734
744
  {local.children}
735
- </div>
745
+ </span>
736
746
  );
737
747
  }
738
748
 
@@ -865,7 +875,7 @@ export function ComboBoxValue(props: ComboBoxValueProps): JSX.Element {
865
875
  * The trigger button for a combobox.
866
876
  */
867
877
  export function ComboBoxButton(props: ComboBoxButtonProps): JSX.Element {
868
- const [local, domProps] = splitProps(props, ["class", "style", "slot", "children"]);
878
+ const [local, domProps] = splitProps(props, ["class", "style", "slot", "children", "ref"]);
869
879
 
870
880
  const context = useContext(ComboBoxContext);
871
881
  if (!context) {
@@ -911,7 +921,10 @@ export function ComboBoxButton(props: ComboBoxButtonProps): JSX.Element {
911
921
  return (
912
922
  <button
913
923
  {...domProps}
914
- ref={(el) => setButtonRef(el)}
924
+ ref={(el) => {
925
+ setButtonRef(el);
926
+ assignRef(local.ref, el);
927
+ }}
915
928
  {...cleanButtonProps()}
916
929
  {...cleanHoverProps()}
917
930
  class={renderProps.class()}
@@ -941,7 +954,7 @@ export function ComboBoxListBox<T>(props: ComboBoxListBoxProps<T>): JSX.Element
941
954
  const { state: comboBoxState, listState, isOpen, inputRef, buttonRef, setListBoxRef } = context;
942
955
  const state = comboBoxState;
943
956
 
944
- let listBoxRef: HTMLUListElement | undefined;
957
+ let listBoxRef: HTMLElement | undefined;
945
958
 
946
959
  // Reveal the activedescendant-focused option on keyboard navigation while the
947
960
  // listbox is open. Real DOM focus stays on the input, so the browser won't
@@ -1017,7 +1030,7 @@ export function ComboBoxListBox<T>(props: ComboBoxListBoxProps<T>): JSX.Element
1017
1030
  );
1018
1031
  };
1019
1032
 
1020
- const setListBoxElement = (el: HTMLUListElement) => {
1033
+ const setListBoxElement = (el: HTMLElement) => {
1021
1034
  listBoxRef = el;
1022
1035
  setListBoxRef(el);
1023
1036
  };
@@ -1028,7 +1041,10 @@ export function ComboBoxListBox<T>(props: ComboBoxListBoxProps<T>): JSX.Element
1028
1041
 
1029
1042
  return (
1030
1043
  <Show when={isOpen()}>
1031
- <ul
1044
+ {/* `<div role="listbox">`, not `<ul>` — upstream RAC ListBox (and our own
1045
+ Select/ListBox) render div-over-div rows; the `role` comes from the
1046
+ spread listbox aria props (createListBox), not the element tag. */}
1047
+ <div
1032
1048
  {...domProps}
1033
1049
  ref={setListBoxElement}
1034
1050
  {...cleanContextProps()}
@@ -1052,7 +1068,7 @@ export function ComboBoxListBox<T>(props: ComboBoxListBoxProps<T>): JSX.Element
1052
1068
  }}
1053
1069
  </For>
1054
1070
  </Show>
1055
- </ul>
1071
+ </div>
1056
1072
  </Show>
1057
1073
  );
1058
1074
  }
@@ -1069,6 +1085,7 @@ export function ComboBoxOption<T>(props: ComboBoxOptionProps<T>): JSX.Element {
1069
1085
  "item",
1070
1086
  "textValue",
1071
1087
  "onAction",
1088
+ "ref",
1072
1089
  ]);
1073
1090
 
1074
1091
  const stateContext = useContext(ComboBoxStateContext);
@@ -1078,7 +1095,7 @@ export function ComboBoxOption<T>(props: ComboBoxOptionProps<T>): JSX.Element {
1078
1095
  }
1079
1096
  const state = stateContext as ComboBoxState<T>;
1080
1097
  const listState = (comboBoxContext as ComboBoxContextValue<T>).listState;
1081
- const [ref, setRef] = createSignal<HTMLLIElement | null>(null);
1098
+ const [ref, setRef] = createSignal<HTMLElement | null>(null);
1082
1099
  const optionId = () => {
1083
1100
  const listBoxId = getComboBoxData(state as ComboBoxState<unknown>)?.listBoxId;
1084
1101
  return listBoxId ? `${listBoxId}-option-${local.id}` : String(local.id);
@@ -1126,9 +1143,19 @@ export function ComboBoxOption<T>(props: ComboBoxOptionProps<T>): JSX.Element {
1126
1143
  () => ref(),
1127
1144
  );
1128
1145
 
1129
- const isOptionFocusVisible = () =>
1130
- optionAria.isFocusVisible() ||
1131
- (optionAria.isFocused() && (comboBoxContext?.isFocusVisible() ?? false));
1146
+ // Mirror upstream `useOption` exactly (useOption.ts:172):
1147
+ // isFocusVisible = isFocused && selectionManager.isFocused && isFocusVisible()
1148
+ // where `isFocusVisible()` is the GLOBAL interaction-modality singleton read as
1149
+ // a render-time snapshot (NOT the input's focus ring, and NOT the option's own
1150
+ // createFocusRing — the option div never receives real DOM focus under virtual
1151
+ // focus, so its ring never lights). `optionAria.isFocused()` already folds in
1152
+ // `selectionManager.isFocused()` (createSelectableItem.ts:239), so the upstream
1153
+ // three-term product collapses to these two. Reading the global singleton off
1154
+ // the `isFocused` dependency gives the same non-reactive snapshot semantics as
1155
+ // React (mouse/hover focus → pointer modality → no ring; keyboard nav → keyboard
1156
+ // modality → ring), which the earlier input-ring OR-branch got wrong (it lit the
1157
+ // option ring whenever the input ring was on).
1158
+ const isOptionFocusVisible = () => optionAria.isFocused() && isGlobalFocusVisible();
1132
1159
 
1133
1160
  const renderValues = createMemo<ComboBoxOptionRenderProps>(() => ({
1134
1161
  isSelected: optionAria.isSelected(),
@@ -1162,8 +1189,13 @@ export function ComboBoxOption<T>(props: ComboBoxOptionProps<T>): JSX.Element {
1162
1189
 
1163
1190
  return (
1164
1191
  <SelectionIndicatorContext.Provider value={selectionIndicatorContext()}>
1165
- <li
1166
- ref={setRef}
1192
+ {/* `<div role="option">`, not `<li>` — see the ComboBoxListBox note; the
1193
+ `role` comes from the spread option aria props (createOption). */}
1194
+ <div
1195
+ ref={(el) => {
1196
+ setRef(el);
1197
+ assignRef(local.ref, el);
1198
+ }}
1167
1199
  {...cleanOptionProps()}
1168
1200
  class={renderProps.class()}
1169
1201
  style={renderProps.style()}
@@ -1175,7 +1207,7 @@ export function ComboBoxOption<T>(props: ComboBoxOptionProps<T>): JSX.Element {
1175
1207
  data-disabled={optionAria.isDisabled() || undefined}
1176
1208
  >
1177
1209
  {renderProps.renderChildren()}
1178
- </li>
1210
+ </div>
1179
1211
  </SelectionIndicatorContext.Provider>
1180
1212
  );
1181
1213
  }