@idealyst/components 1.1.3 → 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,263 @@
1
+ /**
2
+ * WCAG 2.1 keyboard patterns for common widgets.
3
+ * These constants define the standard key bindings for accessible interactions.
4
+ * @see https://www.w3.org/WAI/ARIA/apg/patterns/
5
+ */
6
+
7
+ /**
8
+ * Keys for button activation.
9
+ * WCAG requirement: Buttons must be activatable with Enter and Space.
10
+ */
11
+ export const BUTTON_KEYS = {
12
+ /** Keys that activate the button */
13
+ activate: ['Enter', ' '] as const,
14
+ } as const;
15
+
16
+ /**
17
+ * Keys for link activation.
18
+ * Links only use Enter (Space typically scrolls the page).
19
+ */
20
+ export const LINK_KEYS = {
21
+ /** Keys that activate the link */
22
+ activate: ['Enter'] as const,
23
+ } as const;
24
+
25
+ /**
26
+ * Keys for menu navigation.
27
+ * @see https://www.w3.org/WAI/ARIA/apg/patterns/menu/
28
+ */
29
+ export const MENU_KEYS = {
30
+ /** Keys that open the menu */
31
+ open: ['Enter', ' ', 'ArrowDown', 'ArrowUp'] as const,
32
+ /** Keys that close the menu */
33
+ close: ['Escape', 'Tab'] as const,
34
+ /** Keys for navigating between items */
35
+ navigateVertical: ['ArrowUp', 'ArrowDown'] as const,
36
+ /** Keys for navigating horizontal menus/menubars */
37
+ navigateHorizontal: ['ArrowLeft', 'ArrowRight'] as const,
38
+ /** Keys for selecting an item */
39
+ select: ['Enter', ' '] as const,
40
+ /** Keys for jumping to first item */
41
+ first: ['Home'] as const,
42
+ /** Keys for jumping to last item */
43
+ last: ['End'] as const,
44
+ } as const;
45
+
46
+ /**
47
+ * Keys for accordion navigation.
48
+ * @see https://www.w3.org/WAI/ARIA/apg/patterns/accordion/
49
+ */
50
+ export const ACCORDION_KEYS = {
51
+ /** Keys that toggle the accordion panel */
52
+ toggle: ['Enter', ' '] as const,
53
+ /** Keys for navigating between headers */
54
+ navigate: ['ArrowUp', 'ArrowDown'] as const,
55
+ /** Keys for jumping to first header */
56
+ first: ['Home'] as const,
57
+ /** Keys for jumping to last header */
58
+ last: ['End'] as const,
59
+ } as const;
60
+
61
+ /**
62
+ * Keys for tab navigation.
63
+ * @see https://www.w3.org/WAI/ARIA/apg/patterns/tabs/
64
+ */
65
+ export const TAB_KEYS = {
66
+ /** Keys that select a tab (when autoActivate is true, navigation also selects) */
67
+ select: ['Enter', ' '] as const,
68
+ /** Keys for navigating between tabs */
69
+ navigate: ['ArrowLeft', 'ArrowRight'] as const,
70
+ /** Keys for vertical tab lists */
71
+ navigateVertical: ['ArrowUp', 'ArrowDown'] as const,
72
+ /** Keys for jumping to first tab */
73
+ first: ['Home'] as const,
74
+ /** Keys for jumping to last tab */
75
+ last: ['End'] as const,
76
+ } as const;
77
+
78
+ /**
79
+ * Keys for slider/range control.
80
+ * @see https://www.w3.org/WAI/ARIA/apg/patterns/slider/
81
+ */
82
+ export const SLIDER_KEYS = {
83
+ /** Keys that increase the value by step */
84
+ increase: ['ArrowRight', 'ArrowUp'] as const,
85
+ /** Keys that decrease the value by step */
86
+ decrease: ['ArrowLeft', 'ArrowDown'] as const,
87
+ /** Keys that increase the value by large step (typically 10x) */
88
+ increaseLarge: ['PageUp'] as const,
89
+ /** Keys that decrease the value by large step (typically 10x) */
90
+ decreaseLarge: ['PageDown'] as const,
91
+ /** Keys that set value to maximum */
92
+ max: ['End'] as const,
93
+ /** Keys that set value to minimum */
94
+ min: ['Home'] as const,
95
+ } as const;
96
+
97
+ /**
98
+ * Keys for listbox/select navigation.
99
+ * @see https://www.w3.org/WAI/ARIA/apg/patterns/listbox/
100
+ */
101
+ export const LISTBOX_KEYS = {
102
+ /** Keys for navigating between options */
103
+ navigate: ['ArrowUp', 'ArrowDown'] as const,
104
+ /** Keys for selecting an option */
105
+ select: ['Enter', ' '] as const,
106
+ /** Keys that close the listbox (if popup) */
107
+ close: ['Escape'] as const,
108
+ /** Keys for jumping to first option */
109
+ first: ['Home'] as const,
110
+ /** Keys for jumping to last option */
111
+ last: ['End'] as const,
112
+ /** Keys that select all (multi-select only) */
113
+ selectAll: ['Control+a', 'Meta+a'] as const,
114
+ } as const;
115
+
116
+ /**
117
+ * Keys for dialog/modal.
118
+ * @see https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/
119
+ */
120
+ export const DIALOG_KEYS = {
121
+ /** Keys that close the dialog */
122
+ close: ['Escape'] as const,
123
+ } as const;
124
+
125
+ /**
126
+ * Keys for checkbox control.
127
+ */
128
+ export const CHECKBOX_KEYS = {
129
+ /** Keys that toggle the checkbox */
130
+ toggle: [' '] as const,
131
+ } as const;
132
+
133
+ /**
134
+ * Keys for radio group navigation.
135
+ * @see https://www.w3.org/WAI/ARIA/apg/patterns/radio/
136
+ */
137
+ export const RADIO_KEYS = {
138
+ /** Keys for navigating and selecting in radio groups */
139
+ navigate: ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'] as const,
140
+ } as const;
141
+
142
+ /**
143
+ * Keys for switch/toggle control.
144
+ */
145
+ export const SWITCH_KEYS = {
146
+ /** Keys that toggle the switch */
147
+ toggle: ['Enter', ' '] as const,
148
+ } as const;
149
+
150
+ /**
151
+ * Keys for tree view navigation.
152
+ * @see https://www.w3.org/WAI/ARIA/apg/patterns/treeview/
153
+ */
154
+ export const TREE_KEYS = {
155
+ /** Keys for navigating between items */
156
+ navigate: ['ArrowUp', 'ArrowDown'] as const,
157
+ /** Keys for expanding a collapsed node */
158
+ expand: ['ArrowRight'] as const,
159
+ /** Keys for collapsing an expanded node */
160
+ collapse: ['ArrowLeft'] as const,
161
+ /** Keys for activating/selecting an item */
162
+ select: ['Enter', ' '] as const,
163
+ /** Keys for jumping to first item */
164
+ first: ['Home'] as const,
165
+ /** Keys for jumping to last visible item */
166
+ last: ['End'] as const,
167
+ /** Expand all siblings (Shift + *) */
168
+ expandAll: ['*'] as const,
169
+ } as const;
170
+
171
+ /**
172
+ * Keys for grid/table navigation.
173
+ * @see https://www.w3.org/WAI/ARIA/apg/patterns/grid/
174
+ */
175
+ export const GRID_KEYS = {
176
+ /** Keys for moving between cells */
177
+ navigateCell: ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'] as const,
178
+ /** Keys for jumping to first cell in row */
179
+ rowStart: ['Home'] as const,
180
+ /** Keys for jumping to last cell in row */
181
+ rowEnd: ['End'] as const,
182
+ /** Keys for jumping to first cell in grid */
183
+ gridStart: ['Control+Home', 'Meta+Home'] as const,
184
+ /** Keys for jumping to last cell in grid */
185
+ gridEnd: ['Control+End', 'Meta+End'] as const,
186
+ /** Keys for page up/down in grid */
187
+ page: ['PageUp', 'PageDown'] as const,
188
+ } as const;
189
+
190
+ /**
191
+ * Keys for combobox navigation.
192
+ * @see https://www.w3.org/WAI/ARIA/apg/patterns/combobox/
193
+ */
194
+ export const COMBOBOX_KEYS = {
195
+ /** Keys that open the dropdown */
196
+ open: ['ArrowDown', 'ArrowUp', 'Alt+ArrowDown'] as const,
197
+ /** Keys that close the dropdown */
198
+ close: ['Escape', 'Alt+ArrowUp'] as const,
199
+ /** Keys for navigating options */
200
+ navigate: ['ArrowUp', 'ArrowDown'] as const,
201
+ /** Keys for selecting an option */
202
+ select: ['Enter'] as const,
203
+ /** Keys for autocomplete */
204
+ first: ['Home'] as const,
205
+ last: ['End'] as const,
206
+ } as const;
207
+
208
+ /**
209
+ * Keys for tooltip display.
210
+ * @see https://www.w3.org/WAI/ARIA/apg/patterns/tooltip/
211
+ */
212
+ export const TOOLTIP_KEYS = {
213
+ /** Keys that dismiss the tooltip */
214
+ dismiss: ['Escape'] as const,
215
+ } as const;
216
+
217
+ /**
218
+ * Key code helper - checks if a keyboard event matches any of the specified keys.
219
+ * @param event - The keyboard event
220
+ * @param keys - Array of key names to check
221
+ * @returns true if the event key matches any of the specified keys
222
+ */
223
+ export function matchesKey(event: { key: string }, keys: readonly string[]): boolean {
224
+ return keys.includes(event.key);
225
+ }
226
+
227
+ /**
228
+ * Key code helper with modifier support.
229
+ * @param event - The keyboard event
230
+ * @param keyPattern - Key pattern like 'Control+a' or 'Enter'
231
+ * @returns true if the event matches the pattern
232
+ */
233
+ export function matchesKeyPattern(
234
+ event: { key: string; ctrlKey?: boolean; metaKey?: boolean; shiftKey?: boolean; altKey?: boolean },
235
+ keyPattern: string
236
+ ): boolean {
237
+ const parts = keyPattern.split('+');
238
+ const key = parts[parts.length - 1];
239
+ const modifiers = parts.slice(0, -1);
240
+
241
+ // Check the key
242
+ if (event.key !== key && event.key.toLowerCase() !== key.toLowerCase()) {
243
+ return false;
244
+ }
245
+
246
+ // Check modifiers
247
+ const requiresControl = modifiers.includes('Control');
248
+ const requiresMeta = modifiers.includes('Meta');
249
+ const requiresShift = modifiers.includes('Shift');
250
+ const requiresAlt = modifiers.includes('Alt');
251
+
252
+ if (requiresControl && !event.ctrlKey) return false;
253
+ if (requiresMeta && !event.metaKey) return false;
254
+ if (requiresShift && !event.shiftKey) return false;
255
+ if (requiresAlt && !event.altKey) return false;
256
+
257
+ // Ensure no extra modifiers are pressed (unless we're checking for ControlOrMeta)
258
+ if (!requiresControl && !requiresMeta && (event.ctrlKey || event.metaKey)) return false;
259
+ if (!requiresShift && event.shiftKey) return false;
260
+ if (!requiresAlt && event.altKey) return false;
261
+
262
+ return true;
263
+ }
@@ -0,0 +1,223 @@
1
+ import type { AccessibilityRole as RNAccessibilityRole } from 'react-native';
2
+
3
+ /**
4
+ * ARIA roles supported for cross-platform accessibility.
5
+ * Maps to native roles where possible via ariaHelpers.
6
+ */
7
+ export type AriaRole =
8
+ // Interactive roles
9
+ | 'button'
10
+ | 'link'
11
+ | 'checkbox'
12
+ | 'radio'
13
+ | 'switch'
14
+ | 'slider'
15
+ | 'progressbar'
16
+ | 'spinbutton'
17
+ // Form roles
18
+ | 'textbox'
19
+ | 'searchbox'
20
+ | 'combobox'
21
+ | 'listbox'
22
+ | 'option'
23
+ // Menu roles
24
+ | 'menu'
25
+ | 'menuitem'
26
+ | 'menuitemcheckbox'
27
+ | 'menuitemradio'
28
+ // Tab roles
29
+ | 'tab'
30
+ | 'tablist'
31
+ | 'tabpanel'
32
+ // Dialog roles
33
+ | 'dialog'
34
+ | 'alertdialog'
35
+ | 'tooltip'
36
+ // Grid/table roles
37
+ | 'grid'
38
+ | 'row'
39
+ | 'cell'
40
+ | 'columnheader'
41
+ | 'rowheader'
42
+ | 'table'
43
+ // List roles
44
+ | 'list'
45
+ | 'listitem'
46
+ // Landmark roles
47
+ | 'article'
48
+ | 'region'
49
+ | 'navigation'
50
+ | 'main'
51
+ | 'banner'
52
+ | 'contentinfo'
53
+ | 'complementary'
54
+ | 'form'
55
+ | 'search'
56
+ // Live region roles
57
+ | 'status'
58
+ | 'alert'
59
+ | 'log'
60
+ | 'marquee'
61
+ | 'timer'
62
+ // Other roles
63
+ | 'img'
64
+ | 'figure'
65
+ | 'separator'
66
+ | 'none'
67
+ | 'presentation'
68
+ | 'heading'
69
+ | 'group';
70
+
71
+ /**
72
+ * Base accessibility props shared across all components.
73
+ * These props provide essential screen reader and assistive technology support.
74
+ */
75
+ export interface AccessibilityProps {
76
+ /** Text alternative for screen readers. Required for non-text content. */
77
+ accessibilityLabel?: string;
78
+ /** Additional hint text providing context (e.g., "Double tap to activate") */
79
+ accessibilityHint?: string;
80
+ /** Whether the element is disabled for assistive technology */
81
+ accessibilityDisabled?: boolean;
82
+ /** Whether the element is hidden from assistive technology */
83
+ accessibilityHidden?: boolean;
84
+ /** The semantic role of the element for assistive technology */
85
+ accessibilityRole?: AriaRole;
86
+ }
87
+
88
+ /**
89
+ * Accessibility props for interactive elements that can be activated or controlled.
90
+ * Extends base props with relationship and state attributes.
91
+ */
92
+ export interface InteractiveAccessibilityProps extends AccessibilityProps {
93
+ /** ID of element that labels this element (aria-labelledby) */
94
+ accessibilityLabelledBy?: string;
95
+ /** ID of element that describes this element (aria-describedby) */
96
+ accessibilityDescribedBy?: string;
97
+ /** ID of element controlled by this element (aria-controls) */
98
+ accessibilityControls?: string;
99
+ /** Whether an expandable element is currently expanded */
100
+ accessibilityExpanded?: boolean;
101
+ /** Whether a toggle element is pressed (true, false, or 'mixed' for tri-state) */
102
+ accessibilityPressed?: boolean | 'mixed';
103
+ /** Whether this element owns another element (aria-owns) */
104
+ accessibilityOwns?: string;
105
+ /** Whether this element has a popup (aria-haspopup) */
106
+ accessibilityHasPopup?: boolean | 'menu' | 'listbox' | 'tree' | 'grid' | 'dialog';
107
+ }
108
+
109
+ /**
110
+ * Accessibility props for form controls.
111
+ * Extends interactive props with validation and requirement states.
112
+ */
113
+ export interface FormAccessibilityProps extends InteractiveAccessibilityProps {
114
+ /** Whether the field is required */
115
+ accessibilityRequired?: boolean;
116
+ /** Whether the field has a validation error */
117
+ accessibilityInvalid?: boolean;
118
+ /** ID of element containing the error message (aria-errormessage) */
119
+ accessibilityErrorMessage?: string;
120
+ /** Autocomplete hint for the field */
121
+ accessibilityAutoComplete?: 'none' | 'inline' | 'list' | 'both';
122
+ }
123
+
124
+ /**
125
+ * Accessibility props for range controls (Slider, Progress, Spinbutton).
126
+ * Provides value information for screen readers.
127
+ */
128
+ export interface RangeAccessibilityProps extends AccessibilityProps {
129
+ /** Current numeric value */
130
+ accessibilityValueNow?: number;
131
+ /** Minimum allowed value */
132
+ accessibilityValueMin?: number;
133
+ /** Maximum allowed value */
134
+ accessibilityValueMax?: number;
135
+ /** Human-readable value description (e.g., "50 percent", "Medium") */
136
+ accessibilityValueText?: string;
137
+ }
138
+
139
+ /**
140
+ * Accessibility props for selection controls (Checkbox, Radio, Switch).
141
+ * Extends interactive props with checked state.
142
+ */
143
+ export interface SelectionAccessibilityProps extends InteractiveAccessibilityProps {
144
+ /** Whether the element is checked (true, false, or 'mixed' for indeterminate) */
145
+ accessibilityChecked?: boolean | 'mixed';
146
+ }
147
+
148
+ /**
149
+ * Accessibility props for live regions that announce dynamic content.
150
+ * Used for alerts, status messages, and real-time updates.
151
+ */
152
+ export interface LiveRegionAccessibilityProps extends AccessibilityProps {
153
+ /** How updates should be announced: 'polite' waits, 'assertive' interrupts */
154
+ accessibilityLive?: 'off' | 'polite' | 'assertive';
155
+ /** Whether the region is currently loading/updating */
156
+ accessibilityBusy?: boolean;
157
+ /** Which changes should be announced */
158
+ accessibilityRelevant?: 'additions' | 'removals' | 'text' | 'all' | 'additions text';
159
+ /** Whether to announce the entire region or just changes */
160
+ accessibilityAtomic?: boolean;
161
+ }
162
+
163
+ /**
164
+ * Accessibility props for sortable columns in tables/grids.
165
+ */
166
+ export interface SortableAccessibilityProps extends AccessibilityProps {
167
+ /** Current sort direction */
168
+ accessibilitySort?: 'ascending' | 'descending' | 'none' | 'other';
169
+ }
170
+
171
+ /**
172
+ * Accessibility props for selectable items in lists/grids.
173
+ */
174
+ export interface SelectableAccessibilityProps extends AccessibilityProps {
175
+ /** Whether the item is currently selected */
176
+ accessibilitySelected?: boolean;
177
+ /** Position in the set (1-based) */
178
+ accessibilityPosInSet?: number;
179
+ /** Total number of items in the set */
180
+ accessibilitySetSize?: number;
181
+ }
182
+
183
+ /**
184
+ * Accessibility props for heading elements.
185
+ */
186
+ export interface HeadingAccessibilityProps extends AccessibilityProps {
187
+ /** Heading level (1-6) for aria-level */
188
+ accessibilityLevel?: 1 | 2 | 3 | 4 | 5 | 6;
189
+ }
190
+
191
+ /**
192
+ * Accessibility props for current/active navigation items.
193
+ */
194
+ export interface CurrentAccessibilityProps extends AccessibilityProps {
195
+ /** Indicates the current item in a set (aria-current) */
196
+ accessibilityCurrent?: boolean | 'page' | 'step' | 'location' | 'date' | 'time';
197
+ }
198
+
199
+ /**
200
+ * React Native AccessibilityRole type re-export for convenience.
201
+ */
202
+ export type NativeAccessibilityRole = RNAccessibilityRole;
203
+
204
+ /**
205
+ * React Native accessibility state shape.
206
+ */
207
+ export interface NativeAccessibilityState {
208
+ disabled?: boolean;
209
+ selected?: boolean;
210
+ checked?: boolean | 'mixed';
211
+ busy?: boolean;
212
+ expanded?: boolean;
213
+ }
214
+
215
+ /**
216
+ * React Native accessibility value shape.
217
+ */
218
+ export interface NativeAccessibilityValue {
219
+ min?: number;
220
+ max?: number;
221
+ now?: number;
222
+ text?: string;
223
+ }
@@ -0,0 +1,210 @@
1
+ import { useCallback, useRef, useEffect } from 'react';
2
+
3
+ /**
4
+ * Announcement priority level.
5
+ * - 'polite': Waits for current speech to finish before announcing
6
+ * - 'assertive': Interrupts current speech to announce immediately
7
+ */
8
+ export type AnnounceMode = 'polite' | 'assertive';
9
+
10
+ /**
11
+ * Options for the useAnnounce hook.
12
+ */
13
+ export interface UseAnnounceOptions {
14
+ /** Default announcement mode (default: 'polite') */
15
+ defaultMode?: AnnounceMode;
16
+ /** Time in ms before clearing the announcement (default: 1000) */
17
+ clearDelay?: number;
18
+ }
19
+
20
+ /**
21
+ * Return type for the useAnnounce hook.
22
+ */
23
+ export interface UseAnnounceReturn {
24
+ /** Announce a message with the specified mode */
25
+ announce: (message: string, mode?: AnnounceMode) => void;
26
+ /** Announce a message politely (waits for current speech) */
27
+ announcePolite: (message: string) => void;
28
+ /** Announce a message assertively (interrupts current speech) */
29
+ announceAssertive: (message: string) => void;
30
+ /** Clear any pending announcements */
31
+ clear: () => void;
32
+ }
33
+
34
+ /**
35
+ * Visually hidden styles for the live region.
36
+ * Element is hidden from view but readable by screen readers.
37
+ */
38
+ const VISUALLY_HIDDEN_STYLES = `
39
+ position: absolute;
40
+ width: 1px;
41
+ height: 1px;
42
+ padding: 0;
43
+ margin: -1px;
44
+ overflow: hidden;
45
+ clip: rect(0, 0, 0, 0);
46
+ white-space: nowrap;
47
+ border: 0;
48
+ `;
49
+
50
+ /**
51
+ * Hook for announcing dynamic content to screen readers using ARIA live regions.
52
+ *
53
+ * Creates hidden live regions in the DOM that screen readers will announce
54
+ * when content changes. Useful for:
55
+ * - Form validation feedback
56
+ * - Loading state changes
57
+ * - Action confirmations
58
+ * - Dynamic content updates
59
+ *
60
+ * @example
61
+ * ```tsx
62
+ * const { announce, announceAssertive } = useAnnounce();
63
+ *
64
+ * const handleSubmit = async () => {
65
+ * announce('Submitting form...');
66
+ * try {
67
+ * await submitForm();
68
+ * announce('Form submitted successfully');
69
+ * } catch (error) {
70
+ * announceAssertive('Error submitting form. Please try again.');
71
+ * }
72
+ * };
73
+ * ```
74
+ */
75
+ export function useAnnounce(options: UseAnnounceOptions = {}): UseAnnounceReturn {
76
+ const { defaultMode = 'polite', clearDelay = 1000 } = options;
77
+
78
+ const politeRegionRef = useRef<HTMLDivElement | null>(null);
79
+ const assertiveRegionRef = useRef<HTMLDivElement | null>(null);
80
+ const clearTimeoutRef = useRef<ReturnType<typeof setTimeout>>();
81
+
82
+ /**
83
+ * Create a live region element.
84
+ */
85
+ const createLiveRegion = useCallback((mode: AnnounceMode): HTMLDivElement => {
86
+ const region = document.createElement('div');
87
+ region.setAttribute('aria-live', mode);
88
+ region.setAttribute('aria-atomic', 'true');
89
+ region.setAttribute('role', mode === 'assertive' ? 'alert' : 'status');
90
+ region.style.cssText = VISUALLY_HIDDEN_STYLES;
91
+ region.id = `a11y-live-region-${mode}`;
92
+ return region;
93
+ }, []);
94
+
95
+ // Create live regions on mount, clean up on unmount
96
+ useEffect(() => {
97
+ // Check if regions already exist (e.g., from another instance)
98
+ let politeRegion = document.getElementById('a11y-live-region-polite') as HTMLDivElement | null;
99
+ let assertiveRegion = document.getElementById('a11y-live-region-assertive') as HTMLDivElement | null;
100
+
101
+ if (!politeRegion) {
102
+ politeRegion = createLiveRegion('polite');
103
+ document.body.appendChild(politeRegion);
104
+ }
105
+ politeRegionRef.current = politeRegion;
106
+
107
+ if (!assertiveRegion) {
108
+ assertiveRegion = createLiveRegion('assertive');
109
+ document.body.appendChild(assertiveRegion);
110
+ }
111
+ assertiveRegionRef.current = assertiveRegion;
112
+
113
+ // Don't remove on unmount as other components may still use them
114
+ // The regions are shared across the application
115
+ return () => {
116
+ if (clearTimeoutRef.current) {
117
+ clearTimeout(clearTimeoutRef.current);
118
+ }
119
+ };
120
+ }, [createLiveRegion]);
121
+
122
+ /**
123
+ * Clear the live regions.
124
+ */
125
+ const clear = useCallback(() => {
126
+ if (clearTimeoutRef.current) {
127
+ clearTimeout(clearTimeoutRef.current);
128
+ }
129
+ if (politeRegionRef.current) {
130
+ politeRegionRef.current.textContent = '';
131
+ }
132
+ if (assertiveRegionRef.current) {
133
+ assertiveRegionRef.current.textContent = '';
134
+ }
135
+ }, []);
136
+
137
+ /**
138
+ * Announce a message to screen readers.
139
+ */
140
+ const announce = useCallback(
141
+ (message: string, mode: AnnounceMode = defaultMode) => {
142
+ const region = mode === 'assertive' ? assertiveRegionRef.current : politeRegionRef.current;
143
+ if (!region) return;
144
+
145
+ // Clear any pending timeout
146
+ if (clearTimeoutRef.current) {
147
+ clearTimeout(clearTimeoutRef.current);
148
+ }
149
+
150
+ // Clear first, then set - ensures re-announcement of same message
151
+ region.textContent = '';
152
+
153
+ // Use requestAnimationFrame to ensure the clear is processed first
154
+ requestAnimationFrame(() => {
155
+ region.textContent = message;
156
+ });
157
+
158
+ // Clear after delay to avoid accumulation
159
+ clearTimeoutRef.current = setTimeout(() => {
160
+ region.textContent = '';
161
+ }, clearDelay);
162
+ },
163
+ [defaultMode, clearDelay]
164
+ );
165
+
166
+ /**
167
+ * Announce a message politely (waits for current speech to finish).
168
+ */
169
+ const announcePolite = useCallback(
170
+ (message: string) => announce(message, 'polite'),
171
+ [announce]
172
+ );
173
+
174
+ /**
175
+ * Announce a message assertively (interrupts current speech).
176
+ */
177
+ const announceAssertive = useCallback(
178
+ (message: string) => announce(message, 'assertive'),
179
+ [announce]
180
+ );
181
+
182
+ return {
183
+ announce,
184
+ announcePolite,
185
+ announceAssertive,
186
+ clear,
187
+ };
188
+ }
189
+
190
+ /**
191
+ * React Native version of useAnnounce.
192
+ * Uses AccessibilityInfo.announceForAccessibility on native platforms.
193
+ */
194
+ export function useAnnounceNative(): UseAnnounceReturn {
195
+ const announce = useCallback((message: string, _mode?: AnnounceMode) => {
196
+ // This will be imported dynamically in native files
197
+ // import { AccessibilityInfo } from 'react-native';
198
+ // AccessibilityInfo.announceForAccessibility(message);
199
+
200
+ // For now, this is a placeholder that native components will override
201
+ console.log(`[Accessibility] ${message}`);
202
+ }, []);
203
+
204
+ return {
205
+ announce,
206
+ announcePolite: announce,
207
+ announceAssertive: announce,
208
+ clear: () => {},
209
+ };
210
+ }