@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.
- package/package.json +8 -3
- package/src/Accordion/Accordion.native.tsx +23 -2
- package/src/Accordion/Accordion.web.tsx +73 -2
- package/src/Accordion/types.ts +2 -1
- package/src/ActivityIndicator/ActivityIndicator.native.tsx +15 -1
- package/src/ActivityIndicator/ActivityIndicator.web.tsx +19 -2
- package/src/ActivityIndicator/types.ts +2 -1
- package/src/Avatar/Avatar.native.tsx +19 -2
- package/src/Avatar/Avatar.web.tsx +19 -2
- package/src/Avatar/types.ts +2 -1
- package/src/Breadcrumb/types.ts +3 -2
- package/src/Button/Button.native.tsx +48 -1
- package/src/Button/Button.styles.tsx +3 -5
- package/src/Button/Button.web.tsx +61 -2
- package/src/Button/types.ts +2 -1
- package/src/Card/Card.native.tsx +21 -5
- package/src/Card/Card.web.tsx +21 -4
- package/src/Card/types.ts +2 -6
- package/src/Checkbox/Checkbox.native.tsx +46 -5
- package/src/Checkbox/Checkbox.web.tsx +80 -4
- package/src/Checkbox/types.ts +2 -6
- package/src/Chip/Chip.native.tsx +5 -0
- package/src/Chip/Chip.web.tsx +5 -1
- package/src/Chip/types.ts +2 -1
- package/src/Dialog/Dialog.native.tsx +20 -3
- package/src/Dialog/Dialog.web.tsx +29 -4
- package/src/Dialog/types.ts +2 -1
- package/src/Image/Image.native.tsx +1 -1
- package/src/Image/Image.web.tsx +2 -0
- package/src/Input/Input.native.tsx +37 -1
- package/src/Input/Input.web.tsx +75 -8
- package/src/Input/types.ts +2 -1
- package/src/List/List.native.tsx +18 -2
- package/src/List/ListItem.native.tsx +44 -8
- package/src/List/ListItem.web.tsx +16 -0
- package/src/List/types.ts +6 -3
- package/src/Menu/Menu.native.tsx +21 -2
- package/src/Menu/Menu.web.tsx +110 -3
- package/src/Menu/MenuItem.web.tsx +12 -3
- package/src/Menu/types.ts +2 -1
- package/src/Popover/Popover.native.tsx +17 -1
- package/src/Popover/Popover.web.tsx +31 -2
- package/src/Popover/types.ts +2 -1
- package/src/RadioButton/RadioButton.native.tsx +41 -3
- package/src/RadioButton/RadioButton.web.tsx +45 -6
- package/src/RadioButton/RadioGroup.native.tsx +20 -2
- package/src/RadioButton/RadioGroup.web.tsx +24 -3
- package/src/RadioButton/types.ts +3 -2
- package/src/Select/types.ts +2 -6
- package/src/Skeleton/Skeleton.native.tsx +15 -1
- package/src/Skeleton/Skeleton.web.tsx +20 -1
- package/src/Skeleton/types.ts +2 -1
- package/src/Slider/Slider.native.tsx +42 -2
- package/src/Slider/Slider.web.tsx +81 -7
- package/src/Slider/types.ts +2 -1
- package/src/Switch/Switch.native.tsx +41 -3
- package/src/Switch/Switch.web.tsx +45 -5
- package/src/Switch/types.ts +2 -1
- package/src/TabBar/TabBar.native.tsx +23 -2
- package/src/TabBar/TabBar.web.tsx +71 -2
- package/src/TabBar/types.ts +2 -1
- package/src/Table/Table.native.tsx +17 -1
- package/src/Table/Table.web.tsx +20 -3
- package/src/Table/types.ts +3 -2
- package/src/TextArea/TextArea.native.tsx +50 -1
- package/src/TextArea/TextArea.web.tsx +82 -6
- package/src/TextArea/types.ts +2 -1
- package/src/Tooltip/Tooltip.native.tsx +19 -2
- package/src/Tooltip/Tooltip.web.tsx +54 -2
- package/src/Tooltip/types.ts +2 -1
- package/src/Video/Video.native.tsx +18 -3
- package/src/Video/Video.web.tsx +17 -1
- package/src/Video/types.ts +2 -1
- package/src/examples/InputExamples.tsx +53 -0
- package/src/examples/ListExamples.tsx +34 -0
- package/src/internal/index.ts +2 -0
- package/src/utils/accessibility/ariaHelpers.ts +393 -0
- package/src/utils/accessibility/index.ts +210 -0
- package/src/utils/accessibility/keyboardPatterns.ts +263 -0
- package/src/utils/accessibility/types.ts +223 -0
- package/src/utils/accessibility/useAnnounce.ts +210 -0
- package/src/utils/accessibility/useFocusTrap.ts +265 -0
- package/src/utils/accessibility/useKeyboardNavigation.ts +292 -0
- 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;
|