@idealyst/components 1.1.4 → 1.1.5

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 (84) hide show
  1. package/package.json +8 -3
  2. package/src/Accordion/Accordion.native.tsx +23 -2
  3. package/src/Accordion/Accordion.web.tsx +73 -2
  4. package/src/Accordion/types.ts +2 -1
  5. package/src/ActivityIndicator/ActivityIndicator.native.tsx +15 -1
  6. package/src/ActivityIndicator/ActivityIndicator.web.tsx +19 -2
  7. package/src/ActivityIndicator/types.ts +2 -1
  8. package/src/Avatar/Avatar.native.tsx +19 -2
  9. package/src/Avatar/Avatar.web.tsx +19 -2
  10. package/src/Avatar/types.ts +2 -1
  11. package/src/Breadcrumb/types.ts +3 -2
  12. package/src/Button/Button.native.tsx +48 -1
  13. package/src/Button/Button.styles.tsx +3 -5
  14. package/src/Button/Button.web.tsx +61 -2
  15. package/src/Button/types.ts +2 -1
  16. package/src/Card/Card.native.tsx +21 -5
  17. package/src/Card/Card.web.tsx +21 -4
  18. package/src/Card/types.ts +2 -6
  19. package/src/Checkbox/Checkbox.native.tsx +46 -5
  20. package/src/Checkbox/Checkbox.web.tsx +80 -4
  21. package/src/Checkbox/types.ts +2 -6
  22. package/src/Chip/Chip.native.tsx +5 -0
  23. package/src/Chip/Chip.web.tsx +5 -1
  24. package/src/Chip/types.ts +2 -1
  25. package/src/Dialog/Dialog.native.tsx +20 -3
  26. package/src/Dialog/Dialog.web.tsx +29 -4
  27. package/src/Dialog/types.ts +2 -1
  28. package/src/Image/Image.native.tsx +1 -1
  29. package/src/Image/Image.web.tsx +2 -0
  30. package/src/Input/Input.native.tsx +37 -1
  31. package/src/Input/Input.web.tsx +75 -8
  32. package/src/Input/types.ts +2 -1
  33. package/src/List/List.native.tsx +18 -2
  34. package/src/List/ListItem.native.tsx +44 -8
  35. package/src/List/ListItem.web.tsx +16 -0
  36. package/src/List/types.ts +6 -3
  37. package/src/Menu/Menu.native.tsx +21 -2
  38. package/src/Menu/Menu.web.tsx +110 -3
  39. package/src/Menu/MenuItem.web.tsx +12 -3
  40. package/src/Menu/types.ts +2 -1
  41. package/src/Popover/Popover.native.tsx +17 -1
  42. package/src/Popover/Popover.web.tsx +31 -2
  43. package/src/Popover/types.ts +2 -1
  44. package/src/RadioButton/RadioButton.native.tsx +41 -3
  45. package/src/RadioButton/RadioButton.web.tsx +45 -6
  46. package/src/RadioButton/RadioGroup.native.tsx +20 -2
  47. package/src/RadioButton/RadioGroup.web.tsx +24 -3
  48. package/src/RadioButton/types.ts +3 -2
  49. package/src/Select/types.ts +2 -6
  50. package/src/Skeleton/Skeleton.native.tsx +15 -1
  51. package/src/Skeleton/Skeleton.web.tsx +20 -1
  52. package/src/Skeleton/types.ts +2 -1
  53. package/src/Slider/Slider.native.tsx +42 -2
  54. package/src/Slider/Slider.web.tsx +81 -7
  55. package/src/Slider/types.ts +2 -1
  56. package/src/Switch/Switch.native.tsx +41 -3
  57. package/src/Switch/Switch.web.tsx +45 -5
  58. package/src/Switch/types.ts +2 -1
  59. package/src/TabBar/TabBar.native.tsx +23 -2
  60. package/src/TabBar/TabBar.web.tsx +71 -2
  61. package/src/TabBar/types.ts +2 -1
  62. package/src/Table/Table.native.tsx +17 -1
  63. package/src/Table/Table.web.tsx +20 -3
  64. package/src/Table/types.ts +3 -2
  65. package/src/TextArea/TextArea.native.tsx +50 -1
  66. package/src/TextArea/TextArea.web.tsx +82 -6
  67. package/src/TextArea/types.ts +2 -1
  68. package/src/Tooltip/Tooltip.native.tsx +19 -2
  69. package/src/Tooltip/Tooltip.web.tsx +54 -2
  70. package/src/Tooltip/types.ts +2 -1
  71. package/src/Video/Video.native.tsx +18 -3
  72. package/src/Video/Video.web.tsx +17 -1
  73. package/src/Video/types.ts +2 -1
  74. package/src/examples/InputExamples.tsx +53 -0
  75. package/src/examples/ListExamples.tsx +34 -0
  76. package/src/internal/index.ts +2 -0
  77. package/src/utils/accessibility/ariaHelpers.ts +393 -0
  78. package/src/utils/accessibility/index.ts +210 -0
  79. package/src/utils/accessibility/keyboardPatterns.ts +263 -0
  80. package/src/utils/accessibility/types.ts +223 -0
  81. package/src/utils/accessibility/useAnnounce.ts +210 -0
  82. package/src/utils/accessibility/useFocusTrap.ts +265 -0
  83. package/src/utils/accessibility/useKeyboardNavigation.ts +292 -0
  84. package/src/utils/index.ts +3 -0
@@ -0,0 +1,393 @@
1
+ import type {
2
+ AccessibilityProps,
3
+ InteractiveAccessibilityProps,
4
+ FormAccessibilityProps,
5
+ RangeAccessibilityProps,
6
+ SelectionAccessibilityProps,
7
+ LiveRegionAccessibilityProps,
8
+ SortableAccessibilityProps,
9
+ SelectableAccessibilityProps,
10
+ HeadingAccessibilityProps,
11
+ CurrentAccessibilityProps,
12
+ AriaRole,
13
+ NativeAccessibilityRole,
14
+ NativeAccessibilityState,
15
+ NativeAccessibilityValue,
16
+ } from './types';
17
+
18
+ /**
19
+ * Filter out undefined values from an object.
20
+ * ARIA attributes should not be present if undefined.
21
+ */
22
+ function filterUndefined<T extends Record<string, unknown>>(obj: T): Partial<T> {
23
+ return Object.fromEntries(
24
+ Object.entries(obj).filter(([, value]) => value !== undefined)
25
+ ) as Partial<T>;
26
+ }
27
+
28
+ // =============================================================================
29
+ // WEB ARIA ATTRIBUTE MAPPERS
30
+ // =============================================================================
31
+
32
+ /**
33
+ * Maps base AccessibilityProps to web ARIA attributes.
34
+ */
35
+ export function getWebAriaProps(props: AccessibilityProps): Record<string, unknown> {
36
+ return filterUndefined({
37
+ 'aria-label': props.accessibilityLabel,
38
+ 'aria-hidden': props.accessibilityHidden,
39
+ 'aria-disabled': props.accessibilityDisabled,
40
+ role: props.accessibilityRole,
41
+ });
42
+ }
43
+
44
+ /**
45
+ * Maps InteractiveAccessibilityProps to web ARIA attributes.
46
+ */
47
+ export function getWebInteractiveAriaProps(
48
+ props: InteractiveAccessibilityProps
49
+ ): Record<string, unknown> {
50
+ return filterUndefined({
51
+ ...getWebAriaProps(props),
52
+ 'aria-labelledby': props.accessibilityLabelledBy,
53
+ 'aria-describedby': props.accessibilityDescribedBy,
54
+ 'aria-controls': props.accessibilityControls,
55
+ 'aria-expanded': props.accessibilityExpanded,
56
+ 'aria-pressed': props.accessibilityPressed,
57
+ 'aria-owns': props.accessibilityOwns,
58
+ 'aria-haspopup': props.accessibilityHasPopup,
59
+ });
60
+ }
61
+
62
+ /**
63
+ * Maps FormAccessibilityProps to web ARIA attributes.
64
+ */
65
+ export function getWebFormAriaProps(props: FormAccessibilityProps): Record<string, unknown> {
66
+ return filterUndefined({
67
+ ...getWebInteractiveAriaProps(props),
68
+ 'aria-required': props.accessibilityRequired,
69
+ 'aria-invalid': props.accessibilityInvalid,
70
+ 'aria-errormessage': props.accessibilityErrorMessage,
71
+ 'aria-autocomplete': props.accessibilityAutoComplete,
72
+ });
73
+ }
74
+
75
+ /**
76
+ * Maps RangeAccessibilityProps to web ARIA attributes.
77
+ */
78
+ export function getWebRangeAriaProps(props: RangeAccessibilityProps): Record<string, unknown> {
79
+ return filterUndefined({
80
+ ...getWebAriaProps(props),
81
+ 'aria-valuenow': props.accessibilityValueNow,
82
+ 'aria-valuemin': props.accessibilityValueMin,
83
+ 'aria-valuemax': props.accessibilityValueMax,
84
+ 'aria-valuetext': props.accessibilityValueText,
85
+ });
86
+ }
87
+
88
+ /**
89
+ * Maps SelectionAccessibilityProps to web ARIA attributes.
90
+ */
91
+ export function getWebSelectionAriaProps(
92
+ props: SelectionAccessibilityProps
93
+ ): Record<string, unknown> {
94
+ return filterUndefined({
95
+ ...getWebInteractiveAriaProps(props),
96
+ 'aria-checked': props.accessibilityChecked,
97
+ });
98
+ }
99
+
100
+ /**
101
+ * Maps LiveRegionAccessibilityProps to web ARIA attributes.
102
+ */
103
+ export function getWebLiveRegionAriaProps(
104
+ props: LiveRegionAccessibilityProps
105
+ ): Record<string, unknown> {
106
+ return filterUndefined({
107
+ ...getWebAriaProps(props),
108
+ 'aria-live': props.accessibilityLive,
109
+ 'aria-busy': props.accessibilityBusy,
110
+ 'aria-relevant': props.accessibilityRelevant,
111
+ 'aria-atomic': props.accessibilityAtomic,
112
+ });
113
+ }
114
+
115
+ /**
116
+ * Maps SortableAccessibilityProps to web ARIA attributes.
117
+ */
118
+ export function getWebSortableAriaProps(
119
+ props: SortableAccessibilityProps
120
+ ): Record<string, unknown> {
121
+ return filterUndefined({
122
+ ...getWebAriaProps(props),
123
+ 'aria-sort': props.accessibilitySort,
124
+ });
125
+ }
126
+
127
+ /**
128
+ * Maps SelectableAccessibilityProps to web ARIA attributes.
129
+ */
130
+ export function getWebSelectableAriaProps(
131
+ props: SelectableAccessibilityProps
132
+ ): Record<string, unknown> {
133
+ return filterUndefined({
134
+ ...getWebAriaProps(props),
135
+ 'aria-selected': props.accessibilitySelected,
136
+ 'aria-posinset': props.accessibilityPosInSet,
137
+ 'aria-setsize': props.accessibilitySetSize,
138
+ });
139
+ }
140
+
141
+ /**
142
+ * Maps HeadingAccessibilityProps to web ARIA attributes.
143
+ */
144
+ export function getWebHeadingAriaProps(props: HeadingAccessibilityProps): Record<string, unknown> {
145
+ return filterUndefined({
146
+ ...getWebAriaProps(props),
147
+ 'aria-level': props.accessibilityLevel,
148
+ });
149
+ }
150
+
151
+ /**
152
+ * Maps CurrentAccessibilityProps to web ARIA attributes.
153
+ */
154
+ export function getWebCurrentAriaProps(props: CurrentAccessibilityProps): Record<string, unknown> {
155
+ return filterUndefined({
156
+ ...getWebAriaProps(props),
157
+ 'aria-current': props.accessibilityCurrent,
158
+ });
159
+ }
160
+
161
+ // =============================================================================
162
+ // REACT NATIVE ACCESSIBILITY MAPPERS
163
+ // =============================================================================
164
+
165
+ /**
166
+ * Maps ARIA role to React Native accessibilityRole.
167
+ * Not all ARIA roles have direct RN equivalents.
168
+ */
169
+ export function mapRoleToNative(role?: AriaRole): NativeAccessibilityRole | undefined {
170
+ if (!role) return undefined;
171
+
172
+ const roleMap: Partial<Record<AriaRole, NativeAccessibilityRole>> = {
173
+ button: 'button',
174
+ link: 'link',
175
+ checkbox: 'checkbox',
176
+ radio: 'radio',
177
+ switch: 'switch',
178
+ slider: 'adjustable',
179
+ progressbar: 'progressbar',
180
+ textbox: 'text',
181
+ searchbox: 'search',
182
+ combobox: 'combobox',
183
+ menu: 'menu',
184
+ menuitem: 'menuitem',
185
+ tab: 'tab',
186
+ tablist: 'tablist',
187
+ alert: 'alert',
188
+ img: 'image',
189
+ list: 'list',
190
+ timer: 'timer',
191
+ heading: 'header',
192
+ // Roles without direct mapping return undefined
193
+ // dialog, tooltip, grid, table, etc. don't have RN equivalents
194
+ };
195
+
196
+ return roleMap[role];
197
+ }
198
+
199
+ /**
200
+ * Maps base AccessibilityProps to React Native accessibility props.
201
+ */
202
+ export function getNativeAccessibilityProps(props: AccessibilityProps): Record<string, unknown> {
203
+ const state: NativeAccessibilityState = {};
204
+
205
+ if (props.accessibilityDisabled !== undefined) {
206
+ state.disabled = props.accessibilityDisabled;
207
+ }
208
+
209
+ return filterUndefined({
210
+ accessibilityLabel: props.accessibilityLabel,
211
+ accessibilityHint: props.accessibilityHint,
212
+ accessibilityRole: mapRoleToNative(props.accessibilityRole),
213
+ accessibilityElementsHidden: props.accessibilityHidden,
214
+ importantForAccessibility: props.accessibilityHidden ? 'no-hide-descendants' : undefined,
215
+ accessibilityState: Object.keys(state).length > 0 ? state : undefined,
216
+ });
217
+ }
218
+
219
+ /**
220
+ * Maps InteractiveAccessibilityProps to React Native accessibility props.
221
+ */
222
+ export function getNativeInteractiveAccessibilityProps(
223
+ props: InteractiveAccessibilityProps
224
+ ): Record<string, unknown> {
225
+ const state: NativeAccessibilityState = {};
226
+
227
+ if (props.accessibilityDisabled !== undefined) {
228
+ state.disabled = props.accessibilityDisabled;
229
+ }
230
+ if (props.accessibilityExpanded !== undefined) {
231
+ state.expanded = props.accessibilityExpanded;
232
+ }
233
+
234
+ return filterUndefined({
235
+ accessibilityLabel: props.accessibilityLabel,
236
+ accessibilityHint: props.accessibilityHint,
237
+ accessibilityRole: mapRoleToNative(props.accessibilityRole),
238
+ accessibilityElementsHidden: props.accessibilityHidden,
239
+ importantForAccessibility: props.accessibilityHidden ? 'no-hide-descendants' : undefined,
240
+ accessibilityState: Object.keys(state).length > 0 ? state : undefined,
241
+ });
242
+ }
243
+
244
+ /**
245
+ * Maps FormAccessibilityProps to React Native accessibility props.
246
+ * Note: aria-describedby, aria-invalid, aria-errormessage don't have direct RN equivalents.
247
+ * Error information should be included in accessibilityLabel.
248
+ */
249
+ export function getNativeFormAccessibilityProps(
250
+ props: FormAccessibilityProps
251
+ ): Record<string, unknown> {
252
+ const state: NativeAccessibilityState = {};
253
+
254
+ if (props.accessibilityDisabled !== undefined) {
255
+ state.disabled = props.accessibilityDisabled;
256
+ }
257
+ if (props.accessibilityExpanded !== undefined) {
258
+ state.expanded = props.accessibilityExpanded;
259
+ }
260
+
261
+ // Build label with error info since RN doesn't support aria-describedby
262
+ let label = props.accessibilityLabel;
263
+ if (props.accessibilityRequired && label) {
264
+ label = `${label}, required`;
265
+ }
266
+ if (props.accessibilityInvalid && label) {
267
+ label = `${label}, invalid`;
268
+ }
269
+
270
+ return filterUndefined({
271
+ accessibilityLabel: label,
272
+ accessibilityHint: props.accessibilityHint,
273
+ accessibilityRole: mapRoleToNative(props.accessibilityRole),
274
+ accessibilityElementsHidden: props.accessibilityHidden,
275
+ importantForAccessibility: props.accessibilityHidden ? 'no-hide-descendants' : undefined,
276
+ accessibilityState: Object.keys(state).length > 0 ? state : undefined,
277
+ });
278
+ }
279
+
280
+ /**
281
+ * Maps RangeAccessibilityProps to React Native accessibility props.
282
+ */
283
+ export function getNativeRangeAccessibilityProps(
284
+ props: RangeAccessibilityProps
285
+ ): Record<string, unknown> {
286
+ const value: NativeAccessibilityValue = {};
287
+
288
+ if (props.accessibilityValueNow !== undefined) {
289
+ value.now = props.accessibilityValueNow;
290
+ }
291
+ if (props.accessibilityValueMin !== undefined) {
292
+ value.min = props.accessibilityValueMin;
293
+ }
294
+ if (props.accessibilityValueMax !== undefined) {
295
+ value.max = props.accessibilityValueMax;
296
+ }
297
+ if (props.accessibilityValueText !== undefined) {
298
+ value.text = props.accessibilityValueText;
299
+ }
300
+
301
+ return filterUndefined({
302
+ accessibilityLabel: props.accessibilityLabel,
303
+ accessibilityHint: props.accessibilityHint,
304
+ accessibilityRole: mapRoleToNative(props.accessibilityRole),
305
+ accessibilityElementsHidden: props.accessibilityHidden,
306
+ importantForAccessibility: props.accessibilityHidden ? 'no-hide-descendants' : undefined,
307
+ accessibilityValue: Object.keys(value).length > 0 ? value : undefined,
308
+ });
309
+ }
310
+
311
+ /**
312
+ * Maps SelectionAccessibilityProps to React Native accessibility props.
313
+ */
314
+ export function getNativeSelectionAccessibilityProps(
315
+ props: SelectionAccessibilityProps
316
+ ): Record<string, unknown> {
317
+ const state: NativeAccessibilityState = {};
318
+
319
+ if (props.accessibilityDisabled !== undefined) {
320
+ state.disabled = props.accessibilityDisabled;
321
+ }
322
+ if (props.accessibilityExpanded !== undefined) {
323
+ state.expanded = props.accessibilityExpanded;
324
+ }
325
+ if (props.accessibilityChecked !== undefined) {
326
+ state.checked = props.accessibilityChecked;
327
+ }
328
+
329
+ return filterUndefined({
330
+ accessibilityLabel: props.accessibilityLabel,
331
+ accessibilityHint: props.accessibilityHint,
332
+ accessibilityRole: mapRoleToNative(props.accessibilityRole),
333
+ accessibilityElementsHidden: props.accessibilityHidden,
334
+ importantForAccessibility: props.accessibilityHidden ? 'no-hide-descendants' : undefined,
335
+ accessibilityState: Object.keys(state).length > 0 ? state : undefined,
336
+ });
337
+ }
338
+
339
+ /**
340
+ * Maps LiveRegionAccessibilityProps to React Native accessibility props.
341
+ */
342
+ export function getNativeLiveRegionAccessibilityProps(
343
+ props: LiveRegionAccessibilityProps
344
+ ): Record<string, unknown> {
345
+ const state: NativeAccessibilityState = {};
346
+
347
+ if (props.accessibilityBusy !== undefined) {
348
+ state.busy = props.accessibilityBusy;
349
+ }
350
+
351
+ // Map aria-live to accessibilityLiveRegion
352
+ let liveRegion: 'none' | 'polite' | 'assertive' | undefined;
353
+ if (props.accessibilityLive === 'off') {
354
+ liveRegion = 'none';
355
+ } else if (props.accessibilityLive) {
356
+ liveRegion = props.accessibilityLive;
357
+ }
358
+
359
+ return filterUndefined({
360
+ accessibilityLabel: props.accessibilityLabel,
361
+ accessibilityHint: props.accessibilityHint,
362
+ accessibilityRole: mapRoleToNative(props.accessibilityRole),
363
+ accessibilityElementsHidden: props.accessibilityHidden,
364
+ importantForAccessibility: props.accessibilityHidden ? 'no-hide-descendants' : undefined,
365
+ accessibilityState: Object.keys(state).length > 0 ? state : undefined,
366
+ accessibilityLiveRegion: liveRegion,
367
+ });
368
+ }
369
+
370
+ /**
371
+ * Maps SelectableAccessibilityProps to React Native accessibility props.
372
+ */
373
+ export function getNativeSelectableAccessibilityProps(
374
+ props: SelectableAccessibilityProps
375
+ ): Record<string, unknown> {
376
+ const state: NativeAccessibilityState = {};
377
+
378
+ if (props.accessibilityDisabled !== undefined) {
379
+ state.disabled = props.accessibilityDisabled;
380
+ }
381
+ if (props.accessibilitySelected !== undefined) {
382
+ state.selected = props.accessibilitySelected;
383
+ }
384
+
385
+ return filterUndefined({
386
+ accessibilityLabel: props.accessibilityLabel,
387
+ accessibilityHint: props.accessibilityHint,
388
+ accessibilityRole: mapRoleToNative(props.accessibilityRole),
389
+ accessibilityElementsHidden: props.accessibilityHidden,
390
+ importantForAccessibility: props.accessibilityHidden ? 'no-hide-descendants' : undefined,
391
+ accessibilityState: Object.keys(state).length > 0 ? state : undefined,
392
+ });
393
+ }
@@ -0,0 +1,210 @@
1
+ /**
2
+ * @idealyst/components - Accessibility Utilities
3
+ *
4
+ * This module provides comprehensive accessibility support for building
5
+ * WCAG 2.1 AA compliant React and React Native applications.
6
+ *
7
+ * @example
8
+ * ```tsx
9
+ * import {
10
+ * AccessibilityProps,
11
+ * getWebAriaProps,
12
+ * useKeyboardNavigation,
13
+ * useFocusTrap,
14
+ * MENU_KEYS,
15
+ * } from '@idealyst/components/utils/accessibility';
16
+ * ```
17
+ */
18
+
19
+ // =============================================================================
20
+ // TYPES
21
+ // =============================================================================
22
+
23
+ export type {
24
+ // Role types
25
+ AriaRole,
26
+ NativeAccessibilityRole,
27
+ // Accessibility prop interfaces
28
+ AccessibilityProps,
29
+ InteractiveAccessibilityProps,
30
+ FormAccessibilityProps,
31
+ RangeAccessibilityProps,
32
+ SelectionAccessibilityProps,
33
+ LiveRegionAccessibilityProps,
34
+ SortableAccessibilityProps,
35
+ SelectableAccessibilityProps,
36
+ HeadingAccessibilityProps,
37
+ CurrentAccessibilityProps,
38
+ // Native types
39
+ NativeAccessibilityState,
40
+ NativeAccessibilityValue,
41
+ } from './types';
42
+
43
+ // =============================================================================
44
+ // ARIA HELPERS
45
+ // =============================================================================
46
+
47
+ export {
48
+ // Web ARIA mappers
49
+ getWebAriaProps,
50
+ getWebInteractiveAriaProps,
51
+ getWebFormAriaProps,
52
+ getWebRangeAriaProps,
53
+ getWebSelectionAriaProps,
54
+ getWebLiveRegionAriaProps,
55
+ getWebSortableAriaProps,
56
+ getWebSelectableAriaProps,
57
+ getWebHeadingAriaProps,
58
+ getWebCurrentAriaProps,
59
+ // React Native mappers
60
+ mapRoleToNative,
61
+ getNativeAccessibilityProps,
62
+ getNativeInteractiveAccessibilityProps,
63
+ getNativeFormAccessibilityProps,
64
+ getNativeRangeAccessibilityProps,
65
+ getNativeSelectionAccessibilityProps,
66
+ getNativeLiveRegionAccessibilityProps,
67
+ getNativeSelectableAccessibilityProps,
68
+ } from './ariaHelpers';
69
+
70
+ // =============================================================================
71
+ // HOOKS
72
+ // =============================================================================
73
+
74
+ export { useKeyboardNavigation } from './useKeyboardNavigation';
75
+ export type {
76
+ UseKeyboardNavigationOptions,
77
+ UseKeyboardNavigationReturn,
78
+ } from './useKeyboardNavigation';
79
+
80
+ export { useFocusTrap, useFocusTrapNative } from './useFocusTrap';
81
+ export type { UseFocusTrapOptions, UseFocusTrapReturn } from './useFocusTrap';
82
+
83
+ export { useAnnounce, useAnnounceNative } from './useAnnounce';
84
+ export type { AnnounceMode, UseAnnounceOptions, UseAnnounceReturn } from './useAnnounce';
85
+
86
+ // =============================================================================
87
+ // KEYBOARD PATTERNS
88
+ // =============================================================================
89
+
90
+ export {
91
+ // Key pattern constants
92
+ BUTTON_KEYS,
93
+ LINK_KEYS,
94
+ MENU_KEYS,
95
+ ACCORDION_KEYS,
96
+ TAB_KEYS,
97
+ SLIDER_KEYS,
98
+ LISTBOX_KEYS,
99
+ DIALOG_KEYS,
100
+ CHECKBOX_KEYS,
101
+ RADIO_KEYS,
102
+ SWITCH_KEYS,
103
+ TREE_KEYS,
104
+ GRID_KEYS,
105
+ COMBOBOX_KEYS,
106
+ TOOLTIP_KEYS,
107
+ // Helper functions
108
+ matchesKey,
109
+ matchesKeyPattern,
110
+ } from './keyboardPatterns';
111
+
112
+ // =============================================================================
113
+ // UTILITIES
114
+ // =============================================================================
115
+
116
+ /**
117
+ * Counter for generating unique accessibility IDs.
118
+ */
119
+ let idCounter = 0;
120
+
121
+ /**
122
+ * Generate a unique ID for accessibility attributes.
123
+ * Use this for aria-labelledby, aria-describedby, aria-controls, etc.
124
+ *
125
+ * @param prefix - Optional prefix for the ID (default: 'a11y')
126
+ * @returns A unique string ID
127
+ *
128
+ * @example
129
+ * ```tsx
130
+ * const inputId = generateAccessibilityId('input'); // 'input-1'
131
+ * const errorId = generateAccessibilityId('error'); // 'error-2'
132
+ * const helperId = generateAccessibilityId('helper'); // 'helper-3'
133
+ *
134
+ * <Input
135
+ * id={inputId}
136
+ * aria-describedby={`${helperId} ${errorId}`}
137
+ * />
138
+ * <Text id={helperId}>Enter your email</Text>
139
+ * <Text id={errorId}>Invalid email format</Text>
140
+ * ```
141
+ */
142
+ export function generateAccessibilityId(prefix: string = 'a11y'): string {
143
+ return `${prefix}-${++idCounter}`;
144
+ }
145
+
146
+ /**
147
+ * Reset the ID counter. Useful for testing.
148
+ * @internal
149
+ */
150
+ export function resetAccessibilityIdCounter(): void {
151
+ idCounter = 0;
152
+ }
153
+
154
+ /**
155
+ * Combine multiple accessibility IDs into a space-separated string.
156
+ * Filters out undefined/null values.
157
+ *
158
+ * @param ids - Array of IDs (can include undefined/null)
159
+ * @returns Space-separated string of IDs, or undefined if all are empty
160
+ *
161
+ * @example
162
+ * ```tsx
163
+ * const describedBy = combineIds([helperId, hasError ? errorId : undefined]);
164
+ * // Returns: "helper-1 error-2" or "helper-1" depending on hasError
165
+ * ```
166
+ */
167
+ export function combineIds(ids: (string | undefined | null)[]): string | undefined {
168
+ const filtered = ids.filter((id): id is string => Boolean(id));
169
+ return filtered.length > 0 ? filtered.join(' ') : undefined;
170
+ }
171
+
172
+ /**
173
+ * Check if the user prefers reduced motion.
174
+ * Useful for disabling animations/transitions for accessibility.
175
+ *
176
+ * @returns true if the user prefers reduced motion
177
+ *
178
+ * @example
179
+ * ```tsx
180
+ * const prefersReducedMotion = checkReducedMotion();
181
+ * const transitionDuration = prefersReducedMotion ? 0 : 300;
182
+ * ```
183
+ */
184
+ export function checkReducedMotion(): boolean {
185
+ if (typeof window === 'undefined') return false;
186
+ const mediaQuery = window.matchMedia?.('(prefers-reduced-motion: reduce)');
187
+ return mediaQuery?.matches ?? false;
188
+ }
189
+
190
+ /**
191
+ * Minimum touch target size in pixels (WCAG 2.5.5 Target Size).
192
+ * Interactive elements should be at least 44x44 pixels.
193
+ */
194
+ export const MIN_TOUCH_TARGET_SIZE = 44;
195
+
196
+ /**
197
+ * Minimum contrast ratio for normal text (WCAG 2.1 AA).
198
+ */
199
+ export const MIN_CONTRAST_RATIO_NORMAL = 4.5;
200
+
201
+ /**
202
+ * Minimum contrast ratio for large text (WCAG 2.1 AA).
203
+ * Large text is 18pt+ or 14pt+ bold.
204
+ */
205
+ export const MIN_CONTRAST_RATIO_LARGE = 3;
206
+
207
+ /**
208
+ * Minimum contrast ratio for UI components (WCAG 2.1 AA).
209
+ */
210
+ export const MIN_CONTRAST_RATIO_UI = 3;