@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
@@ -0,0 +1,324 @@
1
+ import { onCleanup, type JSX, type Accessor } from 'solid-js';
2
+ import { createButton } from '../button';
3
+ import { filterDOMProps, getEventTarget, mergeProps, nodeContains, isFocusable, focusSafely } from '../utils';
4
+ import { useLocale } from '../i18n';
5
+ import type { Orientation } from '../toolbar';
6
+ import type { Key, ListState } from '@proyecto-viviana/solid-stately';
7
+
8
+ export interface AriaActionGroupProps<T = unknown> {
9
+ /** List items (optional, parity with React Aria prop shape). */
10
+ items?: T[];
11
+ /** Whether the whole action group is disabled. */
12
+ isDisabled?: boolean;
13
+ /** Group orientation. */
14
+ orientation?: Orientation;
15
+ /** Accessible label. */
16
+ 'aria-label'?: string;
17
+ /** Labelled-by id. */
18
+ 'aria-labelledby'?: string;
19
+ /** Handler called when an item action is triggered. */
20
+ onAction?: (key: Key) => void;
21
+ }
22
+
23
+ export interface ActionGroupAria {
24
+ actionGroupProps: JSX.HTMLAttributes<HTMLElement>;
25
+ }
26
+
27
+ export interface AriaActionGroupItemProps {
28
+ key: Key;
29
+ }
30
+
31
+ export interface ActionGroupItemAria {
32
+ buttonProps: JSX.HTMLAttributes<HTMLElement>;
33
+ }
34
+
35
+ interface ActionGroupData {
36
+ onAction?: (key: Key) => void;
37
+ }
38
+
39
+ const actionGroupData = new WeakMap<object, ActionGroupData>();
40
+
41
+ const GROUP_ROLE_BY_MODE = {
42
+ none: 'toolbar',
43
+ single: 'radiogroup',
44
+ multiple: 'toolbar',
45
+ } as const;
46
+
47
+ const ITEM_ROLE_BY_MODE = {
48
+ none: undefined,
49
+ single: 'radio',
50
+ multiple: 'checkbox',
51
+ } as const;
52
+
53
+ function isActionGroupDisabled<T>(props: AriaActionGroupProps<T>, state: ListState<T>): boolean {
54
+ if (props.isDisabled) return true;
55
+ const keys = [...state.collection().getKeys()];
56
+ if (keys.length === 0) return true;
57
+ return !keys.some((key) => !state.isDisabled(key));
58
+ }
59
+
60
+ export function createActionGroup<T>(
61
+ props: AriaActionGroupProps<T>,
62
+ state: ListState<T>,
63
+ _ref?: Accessor<HTMLElement | null>
64
+ ): ActionGroupAria {
65
+ const locale = useLocale();
66
+ let groupRef: HTMLElement | undefined;
67
+ const applyRoleAttributes = (): void => {
68
+ if (!groupRef) return;
69
+ const selectionMode = state.selectionMode();
70
+ const mappedRole = GROUP_ROLE_BY_MODE[selectionMode];
71
+ const nestedToolbar = Boolean(groupRef.parentElement?.closest('[role="toolbar"]'));
72
+ const role = mappedRole === 'toolbar' && nestedToolbar ? 'group' : mappedRole;
73
+ groupRef.setAttribute('role', role);
74
+ if (mappedRole === 'toolbar' && !nestedToolbar) {
75
+ groupRef.setAttribute('aria-orientation', props.orientation ?? 'horizontal');
76
+ } else {
77
+ groupRef.removeAttribute('aria-orientation');
78
+ }
79
+ };
80
+
81
+ const getFocusableItems = (root: HTMLElement): HTMLElement[] => {
82
+ const out: HTMLElement[] = [];
83
+ const pushIfFocusable = (el: Element | null | undefined): void => {
84
+ if (!el || !(el instanceof HTMLElement)) return;
85
+ if (isFocusable(el) && el.getAttribute('aria-disabled') !== 'true') {
86
+ out.push(el);
87
+ }
88
+ };
89
+
90
+ pushIfFocusable(root);
91
+ for (const node of root.querySelectorAll('*')) {
92
+ pushIfFocusable(node);
93
+ }
94
+ return out;
95
+ };
96
+
97
+ const focusRelative = (root: HTMLElement, direction: 'next' | 'previous' | 'first' | 'last'): HTMLElement | null => {
98
+ const focusables = getFocusableItems(root);
99
+ if (focusables.length === 0) return null;
100
+
101
+ if (direction === 'first') {
102
+ const first = focusables[0];
103
+ focusSafely(first);
104
+ return first;
105
+ }
106
+
107
+ if (direction === 'last') {
108
+ const last = focusables[focusables.length - 1];
109
+ focusSafely(last);
110
+ return last;
111
+ }
112
+
113
+ const active = root.ownerDocument.activeElement as HTMLElement | null;
114
+ const currentIndex = active ? focusables.indexOf(active) : -1;
115
+ const delta = direction === 'next' ? 1 : -1;
116
+ const nextIndex = currentIndex === -1
117
+ ? (direction === 'next' ? 0 : focusables.length - 1)
118
+ : (currentIndex + delta + focusables.length) % focusables.length;
119
+ const next = focusables[nextIndex];
120
+ focusSafely(next);
121
+ return next;
122
+ };
123
+
124
+ const resolveKeyFromElement = (element: HTMLElement | null): Key | null => {
125
+ if (!element) return null;
126
+ const keyedElement = element.closest('[data-key]');
127
+ if (!(keyedElement instanceof HTMLElement)) return null;
128
+ const rawKey = keyedElement.getAttribute('data-key');
129
+ if (!rawKey) return null;
130
+ for (const item of state.collection()) {
131
+ if (String(item.key) === rawKey) {
132
+ return item.key;
133
+ }
134
+ }
135
+ return null;
136
+ };
137
+
138
+ const handleFocusMove = (movedTo: HTMLElement | null): void => {
139
+ const key = resolveKeyFromElement(movedTo);
140
+ if (key == null) return;
141
+ state.setFocusedKey(key);
142
+ if (state.selectionMode() === 'single') {
143
+ state.replaceSelection(key);
144
+ }
145
+ };
146
+
147
+ const onKeyDown: JSX.EventHandler<HTMLElement, KeyboardEvent> = (e) => {
148
+ const root = groupRef;
149
+ if (!root || isActionGroupDisabled(props, state)) return;
150
+ if (!nodeContains(e.currentTarget, getEventTarget(e))) return;
151
+
152
+ const orientation = props.orientation ?? 'horizontal';
153
+ const isHorizontal = orientation === 'horizontal';
154
+ const isRTL = locale().direction === 'rtl' && isHorizontal;
155
+
156
+ switch (e.key) {
157
+ case 'ArrowRight':
158
+ case 'ArrowDown': {
159
+ if (e.key === 'ArrowRight' && isHorizontal && isRTL) {
160
+ e.preventDefault();
161
+ e.stopPropagation();
162
+ handleFocusMove(focusRelative(root, 'previous'));
163
+ return;
164
+ }
165
+ if ((e.key === 'ArrowRight' && isHorizontal) || (e.key === 'ArrowDown' && !isHorizontal)) {
166
+ e.preventDefault();
167
+ e.stopPropagation();
168
+ handleFocusMove(focusRelative(root, 'next'));
169
+ }
170
+ return;
171
+ }
172
+ case 'ArrowLeft':
173
+ case 'ArrowUp': {
174
+ if (e.key === 'ArrowLeft' && isHorizontal && isRTL) {
175
+ e.preventDefault();
176
+ e.stopPropagation();
177
+ handleFocusMove(focusRelative(root, 'next'));
178
+ return;
179
+ }
180
+ if ((e.key === 'ArrowLeft' && isHorizontal) || (e.key === 'ArrowUp' && !isHorizontal)) {
181
+ e.preventDefault();
182
+ e.stopPropagation();
183
+ handleFocusMove(focusRelative(root, 'previous'));
184
+ }
185
+ return;
186
+ }
187
+ case 'Home': {
188
+ e.preventDefault();
189
+ e.stopPropagation();
190
+ handleFocusMove(focusRelative(root, 'first'));
191
+ return;
192
+ }
193
+ case 'End': {
194
+ e.preventDefault();
195
+ e.stopPropagation();
196
+ handleFocusMove(focusRelative(root, 'last'));
197
+ return;
198
+ }
199
+ }
200
+ };
201
+
202
+ const actionGroupProps: JSX.HTMLAttributes<HTMLElement> = mergeProps(
203
+ filterDOMProps(props as Record<string, unknown>, { labelable: true }),
204
+ {
205
+ ref: (el: HTMLElement) => {
206
+ groupRef = el;
207
+ applyRoleAttributes();
208
+ queueMicrotask(() => {
209
+ if (!groupRef) return;
210
+ applyRoleAttributes();
211
+ });
212
+ },
213
+ onKeyDown,
214
+ get 'aria-label'() {
215
+ return props['aria-label'];
216
+ },
217
+ get 'aria-labelledby'() {
218
+ return props['aria-label'] ? undefined : props['aria-labelledby'];
219
+ },
220
+ get 'aria-disabled'() {
221
+ return isActionGroupDisabled(props, state) || undefined;
222
+ },
223
+ }
224
+ );
225
+
226
+ actionGroupData.set(state, {
227
+ get onAction() {
228
+ return props.onAction;
229
+ },
230
+ });
231
+
232
+ onCleanup(() => {
233
+ actionGroupData.delete(state);
234
+ });
235
+
236
+ return { actionGroupProps };
237
+ }
238
+
239
+ export function createActionGroupItem<T>(
240
+ props: AriaActionGroupItemProps,
241
+ state: ListState<T>
242
+ ): ActionGroupItemAria {
243
+ const button = createButton({
244
+ elementType: 'button',
245
+ isDisabled: state.isDisabled(props.key),
246
+ onPress: () => {
247
+ state.setFocusedKey(props.key);
248
+ actionGroupData.get(state)?.onAction?.(props.key);
249
+ if (state.selectionMode() !== 'none') {
250
+ state.select(props.key);
251
+ }
252
+ },
253
+ });
254
+
255
+ const isFocused = () => props.key === state.focusedKey();
256
+ const getFirstEnabledKey = (): Key | null => {
257
+ const collection = state.collection();
258
+ let key = collection.getFirstKey();
259
+ while (key != null && state.isDisabled(key)) {
260
+ key = collection.getKeyAfter(key);
261
+ }
262
+ return key;
263
+ };
264
+
265
+ const getDefaultTabStopKey = (): Key | null => {
266
+ const selectionMode = state.selectionMode();
267
+ if (selectionMode !== 'none') {
268
+ const selectedKeys = state.selectedKeys();
269
+ if (selectedKeys === 'all') {
270
+ return getFirstEnabledKey();
271
+ }
272
+
273
+ for (const item of state.collection()) {
274
+ if (!state.isDisabled(item.key) && selectedKeys.has(item.key)) {
275
+ return item.key;
276
+ }
277
+ }
278
+ }
279
+
280
+ return getFirstEnabledKey();
281
+ };
282
+
283
+ onCleanup(() => {
284
+ if (isFocused()) {
285
+ state.setFocusedKey(null);
286
+ }
287
+ });
288
+
289
+ const buttonProps: JSX.HTMLAttributes<HTMLElement> = mergeProps(
290
+ button.buttonProps,
291
+ {
292
+ get role() {
293
+ return ITEM_ROLE_BY_MODE[state.selectionMode()];
294
+ },
295
+ get 'aria-checked'() {
296
+ const mode = state.selectionMode();
297
+ if (mode === 'none') return undefined;
298
+ return state.isSelected(props.key);
299
+ },
300
+ get tabIndex() {
301
+ if (state.isDisabled(props.key)) {
302
+ return -1;
303
+ }
304
+
305
+ if (isFocused()) {
306
+ return 0;
307
+ }
308
+
309
+ if (state.focusedKey() != null) {
310
+ return -1;
311
+ }
312
+
313
+ const defaultTabStopKey = getDefaultTabStopKey();
314
+ return defaultTabStopKey === props.key ? 0 : -1;
315
+ },
316
+ 'data-key': String(props.key),
317
+ onFocus: () => {
318
+ state.setFocusedKey(props.key);
319
+ },
320
+ }
321
+ );
322
+
323
+ return { buttonProps };
324
+ }
@@ -0,0 +1,8 @@
1
+ export {
2
+ createActionGroup,
3
+ createActionGroupItem,
4
+ type AriaActionGroupProps,
5
+ type ActionGroupAria,
6
+ type AriaActionGroupItemProps,
7
+ type ActionGroupItemAria,
8
+ } from './createActionGroup';
@@ -58,11 +58,15 @@ export interface AutocompleteInputProps {
58
58
  autoComplete: 'off'
59
59
  }
60
60
 
61
- export interface AriaAutocompleteOptions<T = unknown> {
61
+ export interface AriaAutocompleteOptions<_T = unknown> {
62
62
  /** Ref accessor for the input element. */
63
63
  inputRef: Accessor<HTMLInputElement | undefined>
64
64
  /** Ref accessor for the collection element. */
65
65
  collectionRef: Accessor<HTMLElement | undefined>
66
+ /** Optional id override for the controlled collection element. */
67
+ collectionId?: string
68
+ /** Optional accessible name for the controlled collection element. */
69
+ collectionAriaLabel?: string
66
70
  /**
67
71
  * An optional filter function used to determine if an option should be included.
68
72
  * @param textValue - The text value of the item
@@ -81,7 +85,7 @@ export interface AriaAutocompleteOptions<T = unknown> {
81
85
  disableVirtualFocus?: boolean
82
86
  }
83
87
 
84
- export interface AutocompleteAria<T = unknown> {
88
+ export interface AutocompleteAria<_T = unknown> {
85
89
  /** Props for the autocomplete input element. */
86
90
  inputProps: AutocompleteInputProps
87
91
  /** Props for the collection (ListBox/Menu). */
@@ -90,6 +94,22 @@ export interface AutocompleteAria<T = unknown> {
90
94
  filter?: (textValue: string) => boolean
91
95
  }
92
96
 
97
+ function toKeyboardEventInit(e: KeyboardEvent): KeyboardEventInit {
98
+ return {
99
+ key: e.key,
100
+ code: e.code,
101
+ location: e.location,
102
+ repeat: e.repeat,
103
+ isComposing: e.isComposing,
104
+ ctrlKey: e.ctrlKey,
105
+ shiftKey: e.shiftKey,
106
+ altKey: e.altKey,
107
+ metaKey: e.metaKey,
108
+ bubbles: e.bubbles,
109
+ cancelable: e.cancelable,
110
+ }
111
+ }
112
+
93
113
  // ============================================
94
114
  // CONSTANTS
95
115
  // ============================================
@@ -139,11 +159,13 @@ export function createAutocomplete<T = unknown>(
139
159
  inputRef,
140
160
  collectionRef,
141
161
  filter,
162
+ collectionId: collectionIdProp,
163
+ collectionAriaLabel,
142
164
  disableAutoFocusFirst = false,
143
165
  disableVirtualFocus = false,
144
166
  } = props
145
167
 
146
- const collectionId = createId()
168
+ const collectionId = collectionIdProp ?? createId()
147
169
  const [shouldUseVirtualFocus] = createSignal(!disableVirtualFocus)
148
170
  let lastInputType = ''
149
171
 
@@ -212,12 +234,13 @@ export function createAutocomplete<T = unknown>(
212
234
 
213
235
  // Handle keyboard navigation
214
236
  const onKeyDown = (e: KeyboardEvent) => {
215
- if ((e as any).isComposing) {
237
+ if ('isComposing' in e && e.isComposing) {
216
238
  return
217
239
  }
218
240
 
219
241
  const focusedNodeId = state.focusedNodeId()
220
242
  const collection = collectionRef()
243
+ const ownerDocument = getOwnerDocument(inputRef() ?? collection)
221
244
 
222
245
  switch (e.key) {
223
246
  case 'Escape':
@@ -265,7 +288,7 @@ export function createAutocomplete<T = unknown>(
265
288
  case 'Enter':
266
289
  // Trigger click on focused item
267
290
  if (focusedNodeId) {
268
- const item = document.getElementById(focusedNodeId)
291
+ const item = ownerDocument?.getElementById(focusedNodeId)
269
292
  if (item) {
270
293
  item.click()
271
294
  e.preventDefault()
@@ -279,12 +302,12 @@ export function createAutocomplete<T = unknown>(
279
302
  e.stopPropagation()
280
303
 
281
304
  if (focusedNodeId) {
282
- const item = document.getElementById(focusedNodeId)
305
+ const item = ownerDocument?.getElementById(focusedNodeId)
283
306
  if (item) {
284
- item.dispatchEvent(new KeyboardEvent(e.type, e))
307
+ item.dispatchEvent(new KeyboardEvent(e.type, toKeyboardEventInit(e)))
285
308
  }
286
309
  } else {
287
- collection.dispatchEvent(new KeyboardEvent(e.type, e))
310
+ collection.dispatchEvent(new KeyboardEvent(e.type, toKeyboardEventInit(e)))
288
311
  }
289
312
  }
290
313
  }
@@ -332,7 +355,7 @@ export function createAutocomplete<T = unknown>(
332
355
  },
333
356
  collectionProps: {
334
357
  id: collectionId,
335
- 'aria-label': 'Suggestions',
358
+ 'aria-label': collectionAriaLabel,
336
359
  shouldUseVirtualFocus: shouldUseVirtualFocus(),
337
360
  disallowTypeAhead: shouldUseVirtualFocus(),
338
361
  },
@@ -31,6 +31,8 @@ export interface BreadcrumbsAria {
31
31
  }
32
32
 
33
33
  export interface AriaBreadcrumbItemProps extends Omit<AriaLinkProps, 'aria-current'> {
34
+ /** The DOM id for the breadcrumb item. */
35
+ id?: string;
34
36
  /** Whether this is the current/last item in the breadcrumb trail. */
35
37
  isCurrent?: boolean;
36
38
  /** The type of current location for aria-current. @default 'page' */
@@ -59,8 +61,9 @@ export function createBreadcrumbs(
59
61
  const getNavProps = (): Record<string, unknown> => {
60
62
  const p = getProps();
61
63
 
62
- // Default aria-label to "Breadcrumbs" if not provided
63
- const ariaLabel = p['aria-label'] ?? 'Breadcrumbs';
64
+ // Only apply a default label when no other label source exists.
65
+ const ariaLabel =
66
+ p['aria-label'] ?? (p['aria-labelledby'] ? undefined : 'Breadcrumbs');
64
67
 
65
68
  return mergeProps(
66
69
  filterDOMProps(p as Record<string, unknown>, { labelable: true }),
@@ -89,9 +92,6 @@ export function createBreadcrumbItem(
89
92
  const isDisabled = () => getProps().isDisabled ?? false;
90
93
  const elementType = () => getProps().elementType ?? 'a';
91
94
 
92
- // Check if element is a heading
93
- const isHeading = () => /^h[1-6]$/.test(elementType());
94
-
95
95
  // Use createLink for base link behavior
96
96
  // Current items are treated as disabled (can't navigate to current page)
97
97
  const { linkProps, isPressed } = createLink({
@@ -102,7 +102,7 @@ export function createBreadcrumbItem(
102
102
  return elementType();
103
103
  },
104
104
  get href() {
105
- return getProps().href;
105
+ return isCurrent() ? undefined : getProps().href;
106
106
  },
107
107
  get target() {
108
108
  return getProps().target;
@@ -155,8 +155,10 @@ export function createBreadcrumbItem(
155
155
  const p = getProps();
156
156
  const current = isCurrent();
157
157
 
158
- // Start with link props (unless it's a heading)
159
- let baseProps: Record<string, unknown> = isHeading() ? {} : linkProps;
158
+ // Start with link props, forwarding id if provided
159
+ let baseProps: Record<string, unknown> = p.id
160
+ ? mergeProps(linkProps, { id: p.id })
161
+ : linkProps;
160
162
 
161
163
  // Add aria-current for current page
162
164
  if (current) {
@@ -165,13 +167,6 @@ export function createBreadcrumbItem(
165
167
  'aria-current': ariaCurrent,
166
168
  });
167
169
 
168
- // Adjust tabIndex for current item
169
- // If autoFocus is true, we want the item focusable
170
- if (p.autoFocus) {
171
- baseProps = mergeProps(baseProps, {
172
- tabIndex: -1,
173
- });
174
- }
175
170
  }
176
171
 
177
172
  // Add aria-disabled for disabled items
@@ -93,7 +93,7 @@ export function createButton(props: AriaButtonProps = {}): ButtonAria {
93
93
  };
94
94
 
95
95
  if (isLink) {
96
- additionalProps.href = props.href;
96
+ additionalProps.href = disabled ? undefined : props.href;
97
97
  additionalProps.target = props.target;
98
98
  additionalProps.rel = props.rel;
99
99
  }
@@ -0,0 +1,128 @@
1
+ import type { JSX } from 'solid-js';
2
+ import type {
3
+ Key,
4
+ ToggleGroupProps,
5
+ ToggleGroupState,
6
+ } from '@proyecto-viviana/solid-stately';
7
+ import { createToolbar, type Orientation } from '../toolbar';
8
+ import { mergeProps } from '../utils';
9
+ import {
10
+ createToggleButton,
11
+ type AriaToggleButtonProps,
12
+ type ToggleButtonAria,
13
+ } from './createToggleButton';
14
+
15
+ export interface AriaToggleButtonGroupProps extends ToggleGroupProps {
16
+ /**
17
+ * The orientation of the toggle button group.
18
+ * @default 'horizontal'
19
+ */
20
+ orientation?: Orientation;
21
+ /** Accessible label. */
22
+ 'aria-label'?: string;
23
+ /** Labelled-by id. */
24
+ 'aria-labelledby'?: string;
25
+ }
26
+
27
+ export interface ToggleButtonGroupAria {
28
+ /** Props for the group container. */
29
+ groupProps: JSX.HTMLAttributes<HTMLElement>;
30
+ }
31
+
32
+ export interface AriaToggleButtonGroupItemProps
33
+ extends Omit<AriaToggleButtonProps, 'children'> {
34
+ /** Key used in the group selection state. */
35
+ id: Key;
36
+ }
37
+
38
+ function isDisabledValue(
39
+ isDisabled: AriaToggleButtonProps['isDisabled']
40
+ ): boolean {
41
+ if (typeof isDisabled === 'function') {
42
+ return isDisabled();
43
+ }
44
+ return !!isDisabled;
45
+ }
46
+
47
+ /**
48
+ * Provides ARIA behavior for a toggle button group container.
49
+ */
50
+ export function createToggleButtonGroup(
51
+ props: AriaToggleButtonGroupProps,
52
+ state: ToggleGroupState
53
+ ): ToggleButtonGroupAria {
54
+ const { toolbarProps } = createToolbar({
55
+ get orientation() {
56
+ return props.orientation;
57
+ },
58
+ get 'aria-label'() {
59
+ return props['aria-label'];
60
+ },
61
+ get 'aria-labelledby'() {
62
+ return props['aria-labelledby'];
63
+ },
64
+ });
65
+
66
+ const groupProps = mergeProps(toolbarProps as Record<string, unknown>, {
67
+ get role() {
68
+ return state.selectionMode === 'single' ? 'radiogroup' : toolbarProps.role;
69
+ },
70
+ get 'aria-disabled'() {
71
+ return props.isDisabled || undefined;
72
+ },
73
+ }) as JSX.HTMLAttributes<HTMLElement>;
74
+
75
+ return { groupProps };
76
+ }
77
+
78
+ /**
79
+ * Provides ARIA behavior for an item within a toggle button group.
80
+ */
81
+ export function createToggleButtonGroupItem(
82
+ props: AriaToggleButtonGroupItemProps,
83
+ state: ToggleGroupState
84
+ ): ToggleButtonAria {
85
+ const { id: _id, ...toggleProps } = props;
86
+
87
+ const toggleButton = createToggleButton({
88
+ ...toggleProps,
89
+ get isSelected() {
90
+ return state.selectedKeys.has(props.id);
91
+ },
92
+ onChange(isSelected) {
93
+ state.setSelected(props.id, isSelected);
94
+ props.onChange?.(isSelected);
95
+ },
96
+ get isDisabled() {
97
+ return isDisabledValue(props.isDisabled) || state.isDisabled;
98
+ },
99
+ });
100
+
101
+ const baseButtonProps = toggleButton.buttonProps as Record<string, unknown>;
102
+ const buttonProps: Record<string, unknown> = {
103
+ ...baseButtonProps,
104
+ get role() {
105
+ if (state.selectionMode === 'single') {
106
+ return 'radio';
107
+ }
108
+ return baseButtonProps.role as string | undefined;
109
+ },
110
+ get 'aria-checked'() {
111
+ if (state.selectionMode !== 'single') {
112
+ return undefined;
113
+ }
114
+ return state.selectedKeys.has(props.id);
115
+ },
116
+ get 'aria-pressed'() {
117
+ if (state.selectionMode === 'single') {
118
+ return undefined;
119
+ }
120
+ return baseButtonProps['aria-pressed'];
121
+ },
122
+ };
123
+
124
+ return {
125
+ ...toggleButton,
126
+ buttonProps,
127
+ };
128
+ }
@@ -1,4 +1,13 @@
1
1
  export { createButton } from './createButton';
2
2
  export { createToggleButton } from './createToggleButton';
3
+ export {
4
+ createToggleButtonGroup,
5
+ createToggleButtonGroupItem,
6
+ } from './createToggleButtonGroup';
3
7
  export type { AriaButtonProps, ButtonAria } from './types';
4
8
  export type { AriaToggleButtonProps, ToggleButtonAria } from './createToggleButton';
9
+ export type {
10
+ AriaToggleButtonGroupProps,
11
+ ToggleButtonGroupAria,
12
+ AriaToggleButtonGroupItemProps,
13
+ } from './createToggleButtonGroup';
@@ -5,7 +5,7 @@
5
5
  * Based on @react-aria/calendar useCalendarCell
6
6
  */
7
7
 
8
- import { createSignal, createMemo, onMount } from 'solid-js';
8
+ import { createSignal, createMemo, createEffect } from 'solid-js';
9
9
  import { access, type MaybeAccessor } from '../utils/reactivity';
10
10
  import type { CalendarState, CalendarDate, DateValue } from '@proyecto-viviana/solid-stately';
11
11
  import { isToday as isTodayUtil, DateFormatter, getLocalTimeZone } from '@internationalized/date';
@@ -19,6 +19,8 @@ export interface AriaCalendarCellProps {
19
19
  date: DateValue;
20
20
  /** Whether the cell is disabled. */
21
21
  isDisabled?: boolean;
22
+ /** Whether the date is outside the current month grid. */
23
+ isOutsideMonth?: boolean;
22
24
  }
23
25
 
24
26
  export interface CalendarCellAria {
@@ -70,7 +72,7 @@ export function createCalendarCell<T extends CalendarState>(
70
72
  return getProps().isDisabled || state.isCellDisabled(date());
71
73
  });
72
74
  const isUnavailable = createMemo(() => state.isCellUnavailable(date()));
73
- const isOutsideMonth = createMemo(() => state.isOutsideVisibleRange(date()));
75
+ const isOutsideMonth = createMemo(() => getProps().isOutsideMonth ?? state.isOutsideVisibleRange(date()));
74
76
  const isToday = createMemo(() => isTodayUtil(date(), timeZone));
75
77
 
76
78
  // Format the date for display
@@ -105,8 +107,8 @@ export function createCalendarCell<T extends CalendarState>(
105
107
  setIsPressed(false);
106
108
  };
107
109
 
108
- // Focus the button when it becomes focused in state
109
- onMount(() => {
110
+ // Keep DOM focus synchronized with focused date updates.
111
+ createEffect(() => {
110
112
  const element = ref?.();
111
113
  if (element && isFocused()) {
112
114
  element.focus();