@proyecto-viviana/solidaria 0.2.4 → 0.2.8

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 (219) hide show
  1. package/LICENSE +21 -0
  2. package/dist/actiongroup/createActionGroup.d.ts +29 -0
  3. package/dist/actiongroup/createActionGroup.d.ts.map +1 -0
  4. package/dist/actiongroup/index.d.ts +2 -0
  5. package/dist/actiongroup/index.d.ts.map +1 -0
  6. package/dist/autocomplete/createAutocomplete.d.ts +6 -2
  7. package/dist/autocomplete/createAutocomplete.d.ts.map +1 -1
  8. package/dist/breadcrumbs/createBreadcrumbs.d.ts +2 -0
  9. package/dist/breadcrumbs/createBreadcrumbs.d.ts.map +1 -1
  10. package/dist/button/createToggleButtonGroup.d.ts +32 -0
  11. package/dist/button/createToggleButtonGroup.d.ts.map +1 -0
  12. package/dist/button/index.d.ts +2 -0
  13. package/dist/button/index.d.ts.map +1 -1
  14. package/dist/calendar/createCalendarCell.d.ts +2 -0
  15. package/dist/calendar/createCalendarCell.d.ts.map +1 -1
  16. package/dist/calendar/createCalendarGrid.d.ts.map +1 -1
  17. package/dist/calendar/createRangeCalendarCell.d.ts +3 -1
  18. package/dist/calendar/createRangeCalendarCell.d.ts.map +1 -1
  19. package/dist/checkbox/createCheckboxGroup.d.ts +5 -1
  20. package/dist/checkbox/createCheckboxGroup.d.ts.map +1 -1
  21. package/dist/collections/index.d.ts +56 -0
  22. package/dist/collections/index.d.ts.map +1 -0
  23. package/dist/color/createColorArea.d.ts.map +1 -1
  24. package/dist/color/createColorSlider.d.ts.map +1 -1
  25. package/dist/color/createColorWheel.d.ts.map +1 -1
  26. package/dist/combobox/createComboBox.d.ts +6 -0
  27. package/dist/combobox/createComboBox.d.ts.map +1 -1
  28. package/dist/datepicker/createDatePicker.d.ts +6 -0
  29. package/dist/datepicker/createDatePicker.d.ts.map +1 -1
  30. package/dist/datepicker/createDateRangePicker.d.ts +40 -0
  31. package/dist/datepicker/createDateRangePicker.d.ts.map +1 -0
  32. package/dist/datepicker/createDateSegment.d.ts +1 -1
  33. package/dist/datepicker/createDateSegment.d.ts.map +1 -1
  34. package/dist/datepicker/createTimeSegment.d.ts +29 -0
  35. package/dist/datepicker/createTimeSegment.d.ts.map +1 -0
  36. package/dist/datepicker/index.d.ts +2 -0
  37. package/dist/datepicker/index.d.ts.map +1 -1
  38. package/dist/disclosure/createDisclosureGroup.d.ts +2 -1
  39. package/dist/disclosure/createDisclosureGroup.d.ts.map +1 -1
  40. package/dist/dnd/createDrag.d.ts.map +1 -1
  41. package/dist/dnd/createDraggableCollection.d.ts +4 -0
  42. package/dist/dnd/createDraggableCollection.d.ts.map +1 -1
  43. package/dist/dnd/createDraggableItem.d.ts.map +1 -1
  44. package/dist/dnd/createDrop.d.ts.map +1 -1
  45. package/dist/dnd/createDroppableCollection.d.ts +32 -1
  46. package/dist/dnd/createDroppableCollection.d.ts.map +1 -1
  47. package/dist/dnd/createDroppableItem.d.ts.map +1 -1
  48. package/dist/dnd/index.d.ts +1 -1
  49. package/dist/dnd/index.d.ts.map +1 -1
  50. package/dist/grid/createGrid.d.ts.map +1 -1
  51. package/dist/gridlist/createGridList.d.ts.map +1 -1
  52. package/dist/index.d.ts +6 -4
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +4659 -3452
  55. package/dist/index.js.map +1 -7
  56. package/dist/index.ssr.js +4659 -3452
  57. package/dist/index.ssr.js.map +1 -7
  58. package/dist/interactions/createFocus.d.ts.map +1 -1
  59. package/dist/interactions/createFocusWithin.d.ts.map +1 -1
  60. package/dist/link/createLink.d.ts +10 -0
  61. package/dist/link/createLink.d.ts.map +1 -1
  62. package/dist/listbox/createListBox.d.ts +1 -0
  63. package/dist/listbox/createListBox.d.ts.map +1 -1
  64. package/dist/listbox/createOption.d.ts.map +1 -1
  65. package/dist/menu/createMenu.d.ts +1 -0
  66. package/dist/menu/createMenu.d.ts.map +1 -1
  67. package/dist/meter/createMeter.d.ts.map +1 -1
  68. package/dist/numberfield/createNumberField.d.ts +18 -0
  69. package/dist/numberfield/createNumberField.d.ts.map +1 -1
  70. package/dist/overlays/createModal.d.ts +16 -0
  71. package/dist/overlays/createModal.d.ts.map +1 -1
  72. package/dist/overlays/createOverlay.d.ts.map +1 -1
  73. package/dist/overlays/index.d.ts +1 -1
  74. package/dist/overlays/index.d.ts.map +1 -1
  75. package/dist/popover/createOverlayPosition.d.ts.map +1 -1
  76. package/dist/popover/createPopover.d.ts.map +1 -1
  77. package/dist/progress/createProgressBar.d.ts.map +1 -1
  78. package/dist/radio/createRadioGroup.d.ts +2 -2
  79. package/dist/radio/createRadioGroup.d.ts.map +1 -1
  80. package/dist/searchfield/createSearchField.d.ts.map +1 -1
  81. package/dist/select/createHiddenSelect.d.ts.map +1 -1
  82. package/dist/select/createSelect.d.ts.map +1 -1
  83. package/dist/slider/createSlider.d.ts.map +1 -1
  84. package/dist/table/createTable.d.ts.map +1 -1
  85. package/dist/tabs/createTabs.d.ts +1 -1
  86. package/dist/tabs/createTabs.d.ts.map +1 -1
  87. package/dist/tag/createTag.d.ts.map +1 -1
  88. package/dist/tag/createTagGroup.d.ts.map +1 -1
  89. package/dist/toast/createToast.d.ts +4 -0
  90. package/dist/toast/createToast.d.ts.map +1 -1
  91. package/dist/toast/createToastRegion.d.ts.map +1 -1
  92. package/dist/toolbar/createToolbar.d.ts.map +1 -1
  93. package/dist/tooltip/createTooltipTrigger.d.ts.map +1 -1
  94. package/dist/tree/createTree.d.ts.map +1 -1
  95. package/dist/tree/createTreeItem.d.ts.map +1 -1
  96. package/dist/tree/types.d.ts +4 -0
  97. package/dist/tree/types.d.ts.map +1 -1
  98. package/dist/utils/env.d.ts +1 -1
  99. package/dist/utils/env.d.ts.map +1 -1
  100. package/dist/utils/platform.d.ts.map +1 -1
  101. package/dist/visually-hidden/createVisuallyHidden.d.ts.map +1 -1
  102. package/package.json +8 -6
  103. package/src/actiongroup/createActionGroup.ts +324 -0
  104. package/src/actiongroup/index.ts +8 -0
  105. package/src/autocomplete/createAutocomplete.ts +32 -9
  106. package/src/breadcrumbs/createBreadcrumbs.ts +10 -15
  107. package/src/button/createButton.ts +1 -1
  108. package/src/button/createToggleButtonGroup.ts +128 -0
  109. package/src/button/index.ts +9 -0
  110. package/src/calendar/createCalendarCell.ts +6 -4
  111. package/src/calendar/createCalendarGrid.ts +27 -18
  112. package/src/calendar/createRangeCalendarCell.ts +26 -9
  113. package/src/checkbox/createCheckboxGroup.ts +21 -4
  114. package/src/collections/index.ts +242 -0
  115. package/src/color/createColorArea.ts +380 -314
  116. package/src/color/createColorField.ts +137 -137
  117. package/src/color/createColorSlider.ts +286 -197
  118. package/src/color/createColorSwatch.ts +40 -40
  119. package/src/color/createColorWheel.ts +218 -208
  120. package/src/color/index.ts +24 -24
  121. package/src/color/types.ts +116 -116
  122. package/src/combobox/createComboBox.ts +670 -647
  123. package/src/combobox/index.ts +6 -6
  124. package/src/datepicker/createDatePicker.ts +54 -16
  125. package/src/datepicker/createDateRangePicker.ts +246 -0
  126. package/src/datepicker/createDateSegment.ts +185 -31
  127. package/src/datepicker/createTimeSegment.ts +370 -0
  128. package/src/datepicker/index.ts +14 -0
  129. package/src/dialog/createDialog.ts +120 -120
  130. package/src/dialog/index.ts +2 -2
  131. package/src/dialog/types.ts +19 -19
  132. package/src/disclosure/createDisclosureGroup.ts +5 -2
  133. package/src/dnd/createDrag.ts +224 -209
  134. package/src/dnd/createDraggableCollection.ts +96 -63
  135. package/src/dnd/createDraggableItem.ts +259 -243
  136. package/src/dnd/createDrop.ts +322 -321
  137. package/src/dnd/createDroppableCollection.ts +682 -293
  138. package/src/dnd/createDroppableItem.ts +215 -213
  139. package/src/dnd/index.ts +55 -47
  140. package/src/dnd/types.ts +89 -89
  141. package/src/dnd/utils.ts +294 -294
  142. package/src/focus/createAutoFocus.ts +321 -321
  143. package/src/focus/createFocusRestore.ts +313 -313
  144. package/src/focus/createVirtualFocus.ts +396 -396
  145. package/src/form/createFormValidation.ts +224 -224
  146. package/src/form/index.ts +11 -11
  147. package/src/grid/createGrid.ts +3 -1
  148. package/src/gridlist/createGridList.ts +16 -0
  149. package/src/gridlist/createGridListItem.ts +1 -1
  150. package/src/i18n/NumberFormatter.ts +266 -266
  151. package/src/i18n/createCollator.ts +79 -79
  152. package/src/i18n/createDateFormatter.ts +83 -83
  153. package/src/i18n/createFilter.ts +131 -131
  154. package/src/i18n/createNumberFormatter.ts +52 -52
  155. package/src/i18n/index.ts +40 -40
  156. package/src/i18n/locale.tsx +188 -188
  157. package/src/i18n/utils.ts +99 -99
  158. package/src/index.ts +51 -0
  159. package/src/interactions/createFocus.ts +6 -5
  160. package/src/interactions/createFocusWithin.ts +6 -5
  161. package/src/interactions/createLongPress.ts +174 -174
  162. package/src/interactions/createMove.ts +289 -289
  163. package/src/interactions/createPress.ts +5 -5
  164. package/src/landmark/createLandmark.ts +377 -377
  165. package/src/landmark/index.ts +8 -8
  166. package/src/link/createLink.ts +23 -8
  167. package/src/listbox/createListBox.ts +308 -269
  168. package/src/listbox/createOption.ts +162 -151
  169. package/src/listbox/index.ts +12 -12
  170. package/src/live-announcer/announce.ts +322 -322
  171. package/src/live-announcer/index.ts +9 -9
  172. package/src/menu/createMenu.ts +405 -396
  173. package/src/menu/createMenuItem.ts +149 -149
  174. package/src/menu/createMenuTrigger.ts +88 -88
  175. package/src/menu/index.ts +18 -18
  176. package/src/meter/createMeter.ts +1 -6
  177. package/src/numberfield/createNumberField.ts +311 -268
  178. package/src/numberfield/index.ts +5 -5
  179. package/src/overlays/ariaHideOutside.ts +219 -219
  180. package/src/overlays/createInteractOutside.ts +149 -149
  181. package/src/overlays/createModal.tsx +238 -202
  182. package/src/overlays/createOverlay.ts +165 -155
  183. package/src/overlays/createOverlayTrigger.ts +85 -85
  184. package/src/overlays/createPreventScroll.ts +266 -266
  185. package/src/overlays/index.ts +48 -44
  186. package/src/popover/calculatePosition.ts +6 -6
  187. package/src/popover/createOverlayPosition.ts +7 -4
  188. package/src/popover/createPopover.ts +21 -7
  189. package/src/progress/createProgressBar.ts +6 -1
  190. package/src/radio/createRadioGroup.ts +88 -14
  191. package/src/searchfield/createSearchField.ts +241 -186
  192. package/src/searchfield/index.ts +2 -2
  193. package/src/select/createHiddenSelect.tsx +263 -236
  194. package/src/select/createSelect.ts +373 -395
  195. package/src/select/index.ts +14 -14
  196. package/src/slider/createSlider.ts +364 -349
  197. package/src/slider/index.ts +2 -2
  198. package/src/ssr/index.tsx +370 -370
  199. package/src/table/createTable.ts +3 -1
  200. package/src/table/createTableColumnHeader.ts +1 -1
  201. package/src/table/createTableRow.ts +1 -1
  202. package/src/tabs/createTabs.ts +80 -51
  203. package/src/tag/createTag.ts +135 -6
  204. package/src/tag/createTagGroup.ts +7 -2
  205. package/src/toast/createToast.ts +8 -2
  206. package/src/toast/createToastRegion.ts +0 -1
  207. package/src/toolbar/createToolbar.ts +75 -1
  208. package/src/tooltip/createTooltip.ts +79 -79
  209. package/src/tooltip/createTooltipTrigger.ts +226 -222
  210. package/src/tooltip/index.ts +6 -6
  211. package/src/tree/createTree.ts +261 -246
  212. package/src/tree/createTreeItem.ts +282 -233
  213. package/src/tree/createTreeSelectionCheckbox.ts +68 -68
  214. package/src/tree/index.ts +16 -16
  215. package/src/tree/types.ts +91 -87
  216. package/src/utils/env.ts +55 -54
  217. package/src/utils/platform.ts +16 -6
  218. package/src/visually-hidden/createVisuallyHidden.ts +139 -124
  219. package/src/visually-hidden/index.ts +6 -6
@@ -504,7 +504,9 @@ export function createTable<T extends object>(
504
504
  'aria-labelledby': p['aria-labelledby'],
505
505
  'aria-describedby': p['aria-describedby'],
506
506
  'aria-multiselectable': s.selectionMode === 'multiple' ? 'true' : undefined,
507
- tabIndex: s.collection.size === 0 ? 0 : -1,
507
+ // Keep the grid itself tabbable so keyboard users can enter
508
+ // row/cell navigation without requiring a prior pointer interaction.
509
+ tabIndex: 0,
508
510
  onKeyDown,
509
511
  onFocus,
510
512
  onBlur,
@@ -39,7 +39,7 @@ export function createTableColumnHeader<T extends object>(
39
39
  const p = props();
40
40
  const s = state();
41
41
 
42
- if (p.allowsSorting && (e.key === 'Enter' || e.key === ' ')) {
42
+ if (p.allowsSorting && (e.key === 'Enter' || e.key === ' ' || e.key === 'Space' || e.key === 'Spacebar')) {
43
43
  e.preventDefault();
44
44
  s.sort(p.node.key);
45
45
  }
@@ -76,7 +76,7 @@ export function createTableRow<T extends object>(
76
76
 
77
77
  if (isDisabled()) return;
78
78
 
79
- if (e.key === 'Enter' || e.key === ' ') {
79
+ if (e.key === 'Enter' || e.key === ' ' || e.key === 'Space' || e.key === 'Spacebar') {
80
80
  e.preventDefault();
81
81
 
82
82
  // Get table metadata for actions
@@ -3,11 +3,12 @@
3
3
  * Based on @react-aria/tabs.
4
4
  */
5
5
 
6
- import { type Accessor, createMemo, onMount } from 'solid-js';
6
+ import { type Accessor, createEffect, createMemo } from 'solid-js';
7
7
  import { createFocusRing } from '../interactions';
8
8
  import { createPress } from '../interactions';
9
9
  import { createHover } from '../interactions';
10
10
  import { createId } from '../ssr';
11
+ import { useLocale } from '../i18n';
11
12
  import type { Key, Collection, CollectionNode } from '@proyecto-viviana/solid-stately';
12
13
 
13
14
  // ============================================
@@ -117,7 +118,7 @@ export interface TabPanelAria {
117
118
  tabPanelProps: {
118
119
  id: string;
119
120
  role: 'tabpanel';
120
- 'aria-labelledby': string;
121
+ 'aria-labelledby'?: string;
121
122
  'aria-label'?: string;
122
123
  'aria-describedby'?: string;
123
124
  tabIndex: number;
@@ -227,6 +228,7 @@ export function createTabList<T>(
227
228
  props: AriaTabListProps,
228
229
  state: TabListState<T>
229
230
  ): TabListAria {
231
+ const locale = useLocale();
230
232
  const orientation = () => props.orientation ?? state.orientation() ?? 'horizontal';
231
233
  const keyboardActivation = () => props.keyboardActivation ?? state.keyboardActivation() ?? 'automatic';
232
234
 
@@ -238,17 +240,17 @@ export function createTabList<T>(
238
240
 
239
241
  let nextKey: Key | null = null;
240
242
  const isHorizontal = orientation() === 'horizontal';
243
+ const isRTL = locale().direction === 'rtl';
241
244
 
242
245
  switch (e.key) {
243
246
  case 'ArrowLeft':
244
247
  if (isHorizontal) {
245
- // TODO: RTL support
246
- nextKey = getPreviousKey(state, currentKey);
248
+ nextKey = isRTL ? getNextKey(state, currentKey) : getPreviousKey(state, currentKey);
247
249
  }
248
250
  break;
249
251
  case 'ArrowRight':
250
252
  if (isHorizontal) {
251
- nextKey = getNextKey(state, currentKey);
253
+ nextKey = isRTL ? getPreviousKey(state, currentKey) : getNextKey(state, currentKey);
252
254
  }
253
255
  break;
254
256
  case 'ArrowUp':
@@ -282,11 +284,6 @@ export function createTabList<T>(
282
284
  if (nextKey !== null) {
283
285
  e.preventDefault();
284
286
  state.setFocusedKey(nextKey);
285
-
286
- // In automatic mode, selection follows focus
287
- if (keyboardActivation() === 'automatic') {
288
- state.setSelectedKey(nextKey);
289
- }
290
287
  }
291
288
  };
292
289
 
@@ -348,8 +345,14 @@ export function createTab<T>(
348
345
  return isDisabled();
349
346
  },
350
347
  onPress: () => {
351
- state.setSelectedKey(key());
352
- state.setFocusedKey(key());
348
+ const tabKey = key();
349
+ const wasSelected = state.selectedKey() === tabKey;
350
+
351
+ state.setFocusedKey(tabKey);
352
+
353
+ if (state.keyboardActivation() === 'manual' || wasSelected) {
354
+ state.setSelectedKey(tabKey);
355
+ }
353
356
  },
354
357
  });
355
358
 
@@ -365,60 +368,71 @@ export function createTab<T>(
365
368
  const tabPanelId = generateTabPanelId(state, key());
366
369
 
367
370
  // Helper to safely call event handlers that may be bound tuples
368
- const callHandler = <E extends Event>(
369
- handler: ((e: E) => void) | [object, (e: E) => void] | undefined,
370
- event: E
371
- ) => {
372
- if (!handler) return;
373
- if (Array.isArray(handler)) {
374
- handler[1].call(handler[0], event);
375
- } else {
376
- handler(event);
371
+ const callHandler = <E extends Event>(handler: unknown, event: E) => {
372
+ if (typeof handler === 'function') {
373
+ (handler as (e: E) => void)(event);
374
+ return;
375
+ }
376
+ if (
377
+ Array.isArray(handler) &&
378
+ handler.length >= 2 &&
379
+ typeof handler[1] === 'function'
380
+ ) {
381
+ (handler[1] as (this: unknown, e: E) => void).call(handler[0], event);
377
382
  }
378
383
  };
379
384
 
380
385
  // Focus management
381
386
  const handleFocus = (e: FocusEvent) => {
382
387
  state.setFocusedKey(key());
383
- callHandler(focusProps.onFocus as any, e);
388
+ callHandler(focusProps.onFocus, e);
384
389
  };
385
390
 
386
391
  // Combine all handlers
387
392
  const handleKeyDown = (e: KeyboardEvent) => {
388
- callHandler(pressProps.onKeyDown as any, e);
393
+ callHandler(pressProps.onKeyDown, e);
389
394
  };
390
395
 
391
396
  const handleMouseDown = (e: MouseEvent) => {
392
- callHandler(pressProps.onMouseDown as any, e);
397
+ callHandler(pressProps.onMouseDown, e);
393
398
  };
394
399
 
395
400
  const handlePointerDown = (e: PointerEvent) => {
396
- callHandler(pressProps.onPointerDown as any, e);
401
+ callHandler(pressProps.onPointerDown, e);
397
402
  };
398
403
 
399
404
  const handleClick = (e: MouseEvent) => {
400
- callHandler(pressProps.onClick as any, e);
405
+ callHandler(pressProps.onClick, e);
401
406
  };
402
407
 
403
- // Focus this tab when it becomes selected and focused
404
- onMount(() => {
405
- const cleanup = createMemo(() => {
406
- if (isFocused() && ref?.()) {
407
- ref()?.focus();
408
- }
409
- });
410
- return cleanup;
408
+ // Keep DOM focus aligned with focusedKey updates from keyboard delegate.
409
+ createEffect(() => {
410
+ const element = ref?.();
411
+ if (!isFocused() || !element) return;
412
+
413
+ const activeElement = element.ownerDocument?.activeElement;
414
+ if (activeElement !== element) {
415
+ element.focus();
416
+ }
411
417
  });
412
418
 
413
419
  return {
414
420
  tabProps: {
415
421
  id: tabId,
416
422
  role: 'tab',
417
- 'aria-selected': isSelected(),
418
- 'aria-disabled': isDisabled() || undefined,
419
- 'aria-controls': isSelected() ? tabPanelId : undefined,
423
+ get 'aria-selected'() {
424
+ return isSelected();
425
+ },
426
+ get 'aria-disabled'() {
427
+ return isDisabled() || undefined;
428
+ },
429
+ get 'aria-controls'() {
430
+ return isSelected() ? tabPanelId : undefined;
431
+ },
420
432
  'aria-label': props['aria-label'],
421
- tabIndex: isSelected() && !isDisabled() ? 0 : -1,
433
+ get tabIndex() {
434
+ return isSelected() && !isDisabled() ? 0 : -1;
435
+ },
422
436
  onKeyDown: handleKeyDown,
423
437
  onMouseDown: handleMouseDown,
424
438
  onPointerDown: handlePointerDown,
@@ -441,27 +455,42 @@ export function createTabPanel<T>(
441
455
  props: AriaTabPanelProps,
442
456
  state: TabListState<T> | null
443
457
  ): TabPanelAria {
444
- // If state is null, the panel is always visible (for SSR scenarios)
458
+ const fallbackId = createId();
459
+
460
+ // Shared panel pattern: if no explicit id is provided, associate the panel
461
+ // with the currently selected tab.
462
+ const associatedKey = createMemo<Key | null>(() => {
463
+ if (state === null) return null;
464
+ return props.id ?? state.selectedKey();
465
+ });
466
+
467
+ // If state is null, the panel is always visible (SSR fallback).
445
468
  const isSelected = createMemo(() => {
446
469
  if (state === null) return true;
447
- if (props.id === undefined) return false;
470
+ if (props.id === undefined) {
471
+ return state.selectedKey() !== null;
472
+ }
448
473
  return state.selectedKey() === props.id;
449
474
  });
450
475
 
451
- // Generate IDs based on the associated tab key
452
- const tabPanelId = state && props.id !== undefined
453
- ? generateTabPanelId(state, props.id)
454
- : createId();
455
-
456
- const tabId = state && props.id !== undefined
457
- ? generateTabId(state, props.id)
458
- : '';
459
-
460
476
  return {
461
477
  tabPanelProps: {
462
- id: tabPanelId,
478
+ get id() {
479
+ const key = associatedKey();
480
+ if (state && key !== null) {
481
+ return generateTabPanelId(state, key);
482
+ }
483
+ return fallbackId;
484
+ },
463
485
  role: 'tabpanel',
464
- 'aria-labelledby': props['aria-labelledby'] ?? tabId,
486
+ get 'aria-labelledby'() {
487
+ if (props['aria-labelledby']) return props['aria-labelledby'];
488
+ const key = associatedKey();
489
+ if (state && key !== null) {
490
+ return generateTabId(state, key);
491
+ }
492
+ return undefined;
493
+ },
465
494
  'aria-label': props['aria-label'],
466
495
  'aria-describedby': props['aria-describedby'],
467
496
  // Make panel focusable if no tabbable children
@@ -83,15 +83,92 @@ export function createTag<T>(
83
83
  return state.isSelected(key());
84
84
  });
85
85
 
86
+ const isSelectable = createMemo(() => state.selectionMode() !== 'none');
87
+
86
88
  const isFocused = createMemo(() => {
87
89
  return state.focusedKey() === key();
88
90
  });
89
91
 
92
+ const getFirstFocusableKey = (): Key | null => {
93
+ const collection = state.collection();
94
+ let candidate = collection.getFirstKey();
95
+ while (candidate != null && state.isDisabled(candidate)) {
96
+ candidate = collection.getKeyAfter(candidate);
97
+ }
98
+ return candidate;
99
+ };
100
+
101
+ const getLastFocusableKey = (): Key | null => {
102
+ const collection = state.collection();
103
+ let candidate = collection.getLastKey();
104
+ while (candidate != null && state.isDisabled(candidate)) {
105
+ candidate = collection.getKeyBefore(candidate);
106
+ }
107
+ return candidate;
108
+ };
109
+
110
+ const getNextFocusableKey = (fromKey: Key): Key | null => {
111
+ const collection = state.collection();
112
+ let candidate = collection.getKeyAfter(fromKey);
113
+ while (candidate != null && state.isDisabled(candidate)) {
114
+ candidate = collection.getKeyAfter(candidate);
115
+ }
116
+
117
+ if (candidate != null) {
118
+ return candidate;
119
+ }
120
+
121
+ return getFirstFocusableKey();
122
+ };
123
+
124
+ const getPreviousFocusableKey = (fromKey: Key): Key | null => {
125
+ const collection = state.collection();
126
+ let candidate = collection.getKeyBefore(fromKey);
127
+ while (candidate != null && state.isDisabled(candidate)) {
128
+ candidate = collection.getKeyBefore(candidate);
129
+ }
130
+
131
+ if (candidate != null) {
132
+ return candidate;
133
+ }
134
+
135
+ return getLastFocusableKey();
136
+ };
137
+
138
+ const focusKey = (nextKey: Key | null) => {
139
+ if (nextKey == null) {
140
+ return;
141
+ }
142
+
143
+ state.setFocusedKey(nextKey);
144
+ const currentElement = ref();
145
+
146
+ if (!currentElement) {
147
+ return;
148
+ }
149
+
150
+ if (nextKey === key()) {
151
+ currentElement.focus();
152
+ return;
153
+ }
154
+
155
+ const tagList = currentElement.parentElement;
156
+ if (!tagList) {
157
+ return;
158
+ }
159
+
160
+ const nextTag = Array.from(tagList.querySelectorAll<HTMLElement>('[role="option"]'))
161
+ .find((el) => el.getAttribute('data-key') === String(nextKey));
162
+
163
+ nextTag?.focus();
164
+ };
165
+
90
166
  // Handle press for selection
91
167
  const { pressProps, isPressed } = createPress({
92
168
  isDisabled,
93
169
  onPress: () => {
94
170
  if (!isDisabled()) {
171
+ state.setFocusedKey(key());
95
172
  state.toggleSelection(key());
96
173
  }
97
174
  },
@@ -100,12 +177,38 @@ export function createTag<T>(
100
177
  // Handle focusable
101
178
  const { focusableProps } = createFocusable({
102
179
  isDisabled,
180
+ onFocus: () => {
181
+ state.setFocusedKey(key());
182
+ },
103
183
  }, ref);
104
184
 
105
- // Handle keyboard for removal
185
+ // Handle keyboard for navigation and removal
106
186
  const handleKeyDown = (e: KeyboardEvent) => {
107
187
  if (isDisabled()) return;
108
188
 
189
+ switch (e.key) {
190
+ case 'ArrowRight':
191
+ case 'ArrowDown':
192
+ e.preventDefault();
193
+ focusKey(getNextFocusableKey(key()));
194
+ return;
195
+ case 'ArrowLeft':
196
+ case 'ArrowUp':
197
+ e.preventDefault();
198
+ focusKey(getPreviousFocusableKey(key()));
199
+ return;
200
+ case 'Home':
201
+ e.preventDefault();
202
+ focusKey(getFirstFocusableKey());
203
+ return;
204
+ case 'End':
205
+ e.preventDefault();
206
+ focusKey(getLastFocusableKey());
207
+ return;
208
+ default:
209
+ break;
210
+ }
211
+
109
212
  if (e.key === 'Delete' || e.key === 'Backspace') {
110
213
  e.preventDefault();
111
214
  const data = getData();
@@ -127,10 +230,35 @@ export function createTag<T>(
127
230
  // Compute tabIndex
128
231
  const tabIndex = createMemo(() => {
129
232
  if (isDisabled()) return -1;
130
- // If this is the focused item, or if nothing is focused yet
131
- if (isFocused() || state.focusedKey() === null) {
233
+
234
+ if (isFocused()) {
235
+ return 0;
236
+ }
237
+
238
+ if (state.focusedKey() !== null) {
239
+ return -1;
240
+ }
241
+
242
+ const collection = state.collection();
243
+ let defaultTabStop: Key | null = null;
244
+
245
+ if (state.selectionMode() !== 'none') {
246
+ for (const item of collection) {
247
+ if (!state.isDisabled(item.key) && state.isSelected(item.key)) {
248
+ defaultTabStop = item.key;
249
+ break;
250
+ }
251
+ }
252
+ }
253
+
254
+ if (defaultTabStop == null) {
255
+ defaultTabStop = getFirstFocusableKey();
256
+ }
257
+
258
+ if (key() === defaultTabStop) {
132
259
  return 0;
133
260
  }
261
+
134
262
  return -1;
135
263
  });
136
264
 
@@ -147,9 +275,10 @@ export function createTag<T>(
147
275
  get rowProps() {
148
276
  return mergeProps(domProps(), focusableProps as Record<string, unknown>, pressProps as Record<string, unknown>, {
149
277
  id: rowId,
150
- role: 'row',
278
+ role: 'option',
151
279
  tabIndex: tabIndex(),
152
- 'aria-selected': isSelected(),
280
+ 'data-key': String(key()),
281
+ 'aria-selected': isSelectable() ? isSelected() : undefined,
153
282
  'aria-disabled': isDisabled() || undefined,
154
283
  onKeyDown: handleKeyDown,
155
284
  });
@@ -157,7 +286,7 @@ export function createTag<T>(
157
286
  get gridCellProps() {
158
287
  return {
159
288
  id: cellId,
160
- role: 'gridcell',
289
+ role: 'presentation',
161
290
  'aria-describedby': allowsRemoving() ? removeButtonId : undefined,
162
291
  };
163
292
  },
@@ -82,6 +82,10 @@ export function createTagGroup<T>(
82
82
  const id = createId(getProps().id);
83
83
  const descriptionId = createId();
84
84
  const errorMessageId = createId();
85
+ const getFallbackAriaLabel = () => {
86
+ const p = getProps();
87
+ return !p.label && !p['aria-label'] && !p['aria-labelledby'] ? 'Tag list' : undefined;
88
+ };
85
89
 
86
90
  // Filter DOM props
87
91
  const domProps = () => filterDOMProps(getProps() as unknown as Record<string, unknown>, { labelable: true });
@@ -89,7 +93,7 @@ export function createTagGroup<T>(
89
93
  // Create label handling
90
94
  const { labelProps, fieldProps } = createLabel({
91
95
  get label() { return getProps().label; },
92
- get 'aria-label'() { return getProps()['aria-label']; },
96
+ get 'aria-label'() { return getProps()['aria-label'] ?? getFallbackAriaLabel(); },
93
97
  get 'aria-labelledby'() { return getProps()['aria-labelledby']; },
94
98
  labelElementType: 'span',
95
99
  });
@@ -130,7 +134,8 @@ export function createTagGroup<T>(
130
134
 
131
135
  return mergeProps(domProps(), fieldProps as Record<string, unknown>, {
132
136
  id,
133
- role: hasItems ? 'grid' : 'group',
137
+ role: hasItems ? 'listbox' : 'group',
138
+ 'aria-multiselectable': hasItems && state.selectionMode() === 'multiple' ? true : undefined,
134
139
  'aria-atomic': false,
135
140
  'aria-relevant': 'additions',
136
141
  'aria-describedby': getAriaDescribedBy(),
@@ -19,6 +19,10 @@ export interface AriaToastProps<T> {
19
19
  toast: QueuedToast<T>;
20
20
  /** The toast state from createToastState. */
21
21
  state: ToastState<T>;
22
+ /** Whether the rendered toast includes a title element. */
23
+ hasTitle?: boolean;
24
+ /** Whether the rendered toast includes a description element. */
25
+ hasDescription?: boolean;
22
26
  }
23
27
 
24
28
  export interface ToastAria {
@@ -70,6 +74,8 @@ export interface ToastAria {
70
74
  export function createToast<T>(props: AriaToastProps<T>): ToastAria {
71
75
  const titleId = createId();
72
76
  const descriptionId = createId();
77
+ const hasTitle = props.hasTitle ?? true;
78
+ const hasDescription = props.hasDescription ?? true;
73
79
 
74
80
  const close = () => {
75
81
  props.state.close(props.toast.key);
@@ -79,8 +85,8 @@ export function createToast<T>(props: AriaToastProps<T>): ToastAria {
79
85
  const toastProps = createMemo<JSX.HTMLAttributes<HTMLElement>>(() => ({
80
86
  role: 'alertdialog',
81
87
  'aria-modal': 'false',
82
- 'aria-labelledby': titleId,
83
- 'aria-describedby': descriptionId,
88
+ 'aria-labelledby': hasTitle ? titleId : undefined,
89
+ 'aria-describedby': hasDescription ? descriptionId : undefined,
84
90
  'data-animation': props.toast.animation,
85
91
  'data-key': props.toast.key,
86
92
  }));
@@ -9,7 +9,6 @@
9
9
 
10
10
  import { type JSX, createMemo } from 'solid-js';
11
11
  import { type ToastState } from '@proyecto-viviana/solid-stately';
12
- import { mergeProps } from '../utils';
13
12
  import { createHover } from '../interactions/createHover';
14
13
 
15
14
  // ============================================
@@ -7,7 +7,6 @@
7
7
 
8
8
  import {
9
9
  createSignal,
10
- createEffect,
11
10
  onMount,
12
11
  onCleanup,
13
12
  type Accessor,
@@ -104,6 +103,47 @@ function getActiveElement(doc: Document): Element | null {
104
103
  return activeElement
105
104
  }
106
105
 
106
+ const TEXT_INPUT_TYPES = new Set([
107
+ '',
108
+ 'text',
109
+ 'search',
110
+ 'url',
111
+ 'tel',
112
+ 'password',
113
+ 'email',
114
+ 'number',
115
+ 'date',
116
+ 'datetime-local',
117
+ 'month',
118
+ 'time',
119
+ 'week',
120
+ ])
121
+
122
+ function isTextInputLikeElement(target: EventTarget | null): boolean {
123
+ if (!(target instanceof HTMLElement)) {
124
+ return false
125
+ }
126
+
127
+ if (target.isContentEditable || !!target.closest('[contenteditable="true"]')) {
128
+ return true
129
+ }
130
+
131
+ if (target.getAttribute('role') === 'textbox') {
132
+ return true
133
+ }
134
+
135
+ if (target instanceof HTMLTextAreaElement || target instanceof HTMLSelectElement) {
136
+ return true
137
+ }
138
+
139
+ if (target instanceof HTMLInputElement) {
140
+ const type = target.type.toLowerCase()
141
+ return TEXT_INPUT_TYPES.has(type)
142
+ }
143
+
144
+ return false
145
+ }
146
+
107
147
  function createFocusManager(ref: Accessor<HTMLElement | undefined>): FocusManager {
108
148
  return {
109
149
  focusNext(opts: FocusManagerOptions = {}) {
@@ -258,9 +298,35 @@ export function createToolbar(props: AriaToolbarProps = {}): ToolbarAria {
258
298
 
259
299
  // Keyboard event handler
260
300
  const onKeyDown = (e: KeyboardEvent) => {
301
+ const root = toolbarRef
302
+ if (!root) return
303
+
261
304
  // Don't handle if nested toolbar (parent handles navigation)
262
305
  if (isInToolbar()) return
263
306
 
307
+ const target = e.target
308
+ if (!(target instanceof Element) || !root.contains(target)) {
309
+ return
310
+ }
311
+
312
+ // Let modified shortcuts pass through.
313
+ if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) {
314
+ return
315
+ }
316
+
317
+ // Text entry controls should keep arrow/home/end for caret/value navigation.
318
+ if (isTextInputLikeElement(target)) {
319
+ switch (e.key) {
320
+ case 'ArrowRight':
321
+ case 'ArrowLeft':
322
+ case 'ArrowDown':
323
+ case 'ArrowUp':
324
+ case 'Home':
325
+ case 'End':
326
+ return
327
+ }
328
+ }
329
+
264
330
  const dir = locale().direction
265
331
  const isRTL = dir === 'rtl'
266
332
  const isHorizontal = orientation() === 'horizontal'
@@ -300,6 +366,14 @@ export function createToolbar(props: AriaToolbarProps = {}): ToolbarAria {
300
366
  handled = true
301
367
  }
302
368
  break
369
+ case 'Home':
370
+ focusManager.focusFirst({ tabbable: true })
371
+ handled = true
372
+ break
373
+ case 'End':
374
+ focusManager.focusLast({ tabbable: true })
375
+ handled = true
376
+ break
303
377
  case 'Tab':
304
378
  // Store the last focused element for re-entry
305
379
  lastFocusedElement = e.target as Element