@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
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import React, { useState, useRef, useCallback, isValidElement, forwardRef } from 'react';
|
|
1
|
+
import React, { useState, useRef, useCallback, isValidElement, forwardRef, useMemo } from 'react';
|
|
2
2
|
import { getWebProps } from 'react-native-unistyles/web';
|
|
3
3
|
import { sliderStyles } from './Slider.styles';
|
|
4
4
|
import type { SliderProps } from './types';
|
|
5
5
|
import { IconSvg } from '../Icon/IconSvg/IconSvg.web';
|
|
6
6
|
import { resolveIconPath, isIconName } from '../Icon/icon-resolver';
|
|
7
7
|
import useMergeRefs from '../hooks/useMergeRefs';
|
|
8
|
+
import { getWebRangeAriaProps, generateAccessibilityId, SLIDER_KEYS } from '../utils/accessibility';
|
|
8
9
|
|
|
9
10
|
const Slider = forwardRef<HTMLDivElement, SliderProps>(({
|
|
10
11
|
value: controlledValue,
|
|
@@ -28,6 +29,16 @@ const Slider = forwardRef<HTMLDivElement, SliderProps>(({
|
|
|
28
29
|
style,
|
|
29
30
|
testID,
|
|
30
31
|
id,
|
|
32
|
+
// Accessibility props
|
|
33
|
+
accessibilityLabel,
|
|
34
|
+
accessibilityHint,
|
|
35
|
+
accessibilityDisabled,
|
|
36
|
+
accessibilityHidden,
|
|
37
|
+
accessibilityRole,
|
|
38
|
+
accessibilityValueNow,
|
|
39
|
+
accessibilityValueMin,
|
|
40
|
+
accessibilityValueMax,
|
|
41
|
+
accessibilityValueText,
|
|
31
42
|
}, ref) => {
|
|
32
43
|
const [internalValue, setInternalValue] = useState(defaultValue);
|
|
33
44
|
const [isDragging, setIsDragging] = useState(false);
|
|
@@ -119,6 +130,72 @@ const Slider = forwardRef<HTMLDivElement, SliderProps>(({
|
|
|
119
130
|
}
|
|
120
131
|
}, [isDragging, value, onValueCommit]);
|
|
121
132
|
|
|
133
|
+
// Handle keyboard navigation for accessibility
|
|
134
|
+
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
|
135
|
+
if (disabled) return;
|
|
136
|
+
|
|
137
|
+
const key = e.key;
|
|
138
|
+
let newValue = value;
|
|
139
|
+
const largeStep = (max - min) / 10; // 10% of range for PageUp/PageDown
|
|
140
|
+
|
|
141
|
+
if (SLIDER_KEYS.increase.includes(key)) {
|
|
142
|
+
e.preventDefault();
|
|
143
|
+
newValue = clampValue(value + step);
|
|
144
|
+
} else if (SLIDER_KEYS.decrease.includes(key)) {
|
|
145
|
+
e.preventDefault();
|
|
146
|
+
newValue = clampValue(value - step);
|
|
147
|
+
} else if (SLIDER_KEYS.min.includes(key)) {
|
|
148
|
+
e.preventDefault();
|
|
149
|
+
newValue = min;
|
|
150
|
+
} else if (SLIDER_KEYS.max.includes(key)) {
|
|
151
|
+
e.preventDefault();
|
|
152
|
+
newValue = max;
|
|
153
|
+
} else if (key === 'PageUp') {
|
|
154
|
+
e.preventDefault();
|
|
155
|
+
newValue = clampValue(value + largeStep);
|
|
156
|
+
} else if (key === 'PageDown') {
|
|
157
|
+
e.preventDefault();
|
|
158
|
+
newValue = clampValue(value - largeStep);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (newValue !== value) {
|
|
162
|
+
updateValue(newValue);
|
|
163
|
+
onValueCommit?.(newValue);
|
|
164
|
+
}
|
|
165
|
+
}, [disabled, value, step, min, max, clampValue, updateValue, onValueCommit]);
|
|
166
|
+
|
|
167
|
+
// Generate unique ID for accessibility
|
|
168
|
+
const sliderId = useMemo(() => id || generateAccessibilityId('slider'), [id]);
|
|
169
|
+
|
|
170
|
+
// Generate ARIA props
|
|
171
|
+
const ariaProps = useMemo(() => {
|
|
172
|
+
return getWebRangeAriaProps({
|
|
173
|
+
accessibilityLabel,
|
|
174
|
+
accessibilityHint,
|
|
175
|
+
accessibilityDisabled: accessibilityDisabled ?? disabled,
|
|
176
|
+
accessibilityHidden,
|
|
177
|
+
accessibilityRole: accessibilityRole ?? 'slider',
|
|
178
|
+
accessibilityValueNow: accessibilityValueNow ?? value,
|
|
179
|
+
accessibilityValueMin: accessibilityValueMin ?? min,
|
|
180
|
+
accessibilityValueMax: accessibilityValueMax ?? max,
|
|
181
|
+
accessibilityValueText,
|
|
182
|
+
});
|
|
183
|
+
}, [
|
|
184
|
+
accessibilityLabel,
|
|
185
|
+
accessibilityHint,
|
|
186
|
+
accessibilityDisabled,
|
|
187
|
+
disabled,
|
|
188
|
+
accessibilityHidden,
|
|
189
|
+
accessibilityRole,
|
|
190
|
+
accessibilityValueNow,
|
|
191
|
+
value,
|
|
192
|
+
accessibilityValueMin,
|
|
193
|
+
min,
|
|
194
|
+
accessibilityValueMax,
|
|
195
|
+
max,
|
|
196
|
+
accessibilityValueText,
|
|
197
|
+
]);
|
|
198
|
+
|
|
122
199
|
React.useEffect(() => {
|
|
123
200
|
if (isDragging) {
|
|
124
201
|
document.addEventListener('mousemove', handleMouseMove);
|
|
@@ -166,7 +243,7 @@ const Slider = forwardRef<HTMLDivElement, SliderProps>(({
|
|
|
166
243
|
const mergedRef = useMergeRefs(ref, containerProps.ref);
|
|
167
244
|
|
|
168
245
|
return (
|
|
169
|
-
<div {...containerProps} ref={mergedRef} id={
|
|
246
|
+
<div {...containerProps} ref={mergedRef} id={sliderId} data-testid={testID}>
|
|
170
247
|
{showValue && (
|
|
171
248
|
<div {...valueLabelProps}>
|
|
172
249
|
{value}
|
|
@@ -176,13 +253,10 @@ const Slider = forwardRef<HTMLDivElement, SliderProps>(({
|
|
|
176
253
|
<div {...wrapperProps}>
|
|
177
254
|
<div
|
|
178
255
|
{...trackProps}
|
|
256
|
+
{...ariaProps}
|
|
179
257
|
ref={trackRef}
|
|
180
258
|
onMouseDown={handleMouseDown}
|
|
181
|
-
|
|
182
|
-
aria-valuenow={value}
|
|
183
|
-
aria-valuemin={min}
|
|
184
|
-
aria-valuemax={max}
|
|
185
|
-
aria-disabled={disabled}
|
|
259
|
+
onKeyDown={handleKeyDown}
|
|
186
260
|
tabIndex={disabled ? -1 : 0}
|
|
187
261
|
>
|
|
188
262
|
{/* Filled track */}
|
package/src/Slider/types.ts
CHANGED
|
@@ -2,12 +2,13 @@ import type { StyleProp, ViewStyle } from 'react-native';
|
|
|
2
2
|
import type { IconName } from '../Icon/icon-types';
|
|
3
3
|
import { Intent, Size } from '@idealyst/theme';
|
|
4
4
|
import { FormInputStyleProps } from '../utils/viewStyleProps';
|
|
5
|
+
import { RangeAccessibilityProps } from '../utils/accessibility';
|
|
5
6
|
|
|
6
7
|
// Component-specific type aliases for future extensibility
|
|
7
8
|
export type SliderIntentVariant = Intent;
|
|
8
9
|
export type SliderSizeVariant = Size;
|
|
9
10
|
|
|
10
|
-
export interface SliderProps extends FormInputStyleProps {
|
|
11
|
+
export interface SliderProps extends FormInputStyleProps, RangeAccessibilityProps {
|
|
11
12
|
value?: number;
|
|
12
13
|
defaultValue?: number;
|
|
13
14
|
min?: number;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import React, { ComponentRef, forwardRef } from 'react';
|
|
1
|
+
import React, { ComponentRef, forwardRef, useMemo } from 'react';
|
|
2
2
|
import { Pressable } from 'react-native';
|
|
3
3
|
import Animated, { useSharedValue, useAnimatedStyle, withSpring } from 'react-native-reanimated';
|
|
4
4
|
import { switchStyles } from './Switch.styles';
|
|
5
5
|
import Text from '../Text';
|
|
6
6
|
import type { SwitchProps } from './types';
|
|
7
|
+
import { getNativeSelectionAccessibilityProps } from '../utils/accessibility';
|
|
7
8
|
|
|
8
9
|
const Switch = forwardRef<ComponentRef<typeof Pressable>, SwitchProps>(({
|
|
9
10
|
checked = false,
|
|
@@ -20,6 +21,15 @@ const Switch = forwardRef<ComponentRef<typeof Pressable>, SwitchProps>(({
|
|
|
20
21
|
style,
|
|
21
22
|
testID,
|
|
22
23
|
id,
|
|
24
|
+
// Accessibility props
|
|
25
|
+
accessibilityLabel,
|
|
26
|
+
accessibilityHint,
|
|
27
|
+
accessibilityDisabled,
|
|
28
|
+
accessibilityHidden,
|
|
29
|
+
accessibilityRole,
|
|
30
|
+
accessibilityLabelledBy,
|
|
31
|
+
accessibilityDescribedBy,
|
|
32
|
+
accessibilityChecked,
|
|
23
33
|
}, ref) => {
|
|
24
34
|
switchStyles.useVariants({
|
|
25
35
|
size,
|
|
@@ -45,6 +55,35 @@ const Switch = forwardRef<ComponentRef<typeof Pressable>, SwitchProps>(({
|
|
|
45
55
|
}
|
|
46
56
|
};
|
|
47
57
|
|
|
58
|
+
// Generate native accessibility props
|
|
59
|
+
const nativeA11yProps = useMemo(() => {
|
|
60
|
+
const computedLabel = accessibilityLabel ?? label;
|
|
61
|
+
const computedChecked = accessibilityChecked ?? checked;
|
|
62
|
+
|
|
63
|
+
return getNativeSelectionAccessibilityProps({
|
|
64
|
+
accessibilityLabel: computedLabel,
|
|
65
|
+
accessibilityHint,
|
|
66
|
+
accessibilityDisabled: accessibilityDisabled ?? disabled,
|
|
67
|
+
accessibilityHidden,
|
|
68
|
+
accessibilityRole: accessibilityRole ?? 'switch',
|
|
69
|
+
accessibilityLabelledBy,
|
|
70
|
+
accessibilityDescribedBy,
|
|
71
|
+
accessibilityChecked: computedChecked,
|
|
72
|
+
});
|
|
73
|
+
}, [
|
|
74
|
+
accessibilityLabel,
|
|
75
|
+
label,
|
|
76
|
+
accessibilityHint,
|
|
77
|
+
accessibilityDisabled,
|
|
78
|
+
disabled,
|
|
79
|
+
accessibilityHidden,
|
|
80
|
+
accessibilityRole,
|
|
81
|
+
accessibilityLabelledBy,
|
|
82
|
+
accessibilityDescribedBy,
|
|
83
|
+
accessibilityChecked,
|
|
84
|
+
checked,
|
|
85
|
+
]);
|
|
86
|
+
|
|
48
87
|
const getThumbDistance = () => {
|
|
49
88
|
if (size === 'sm') return 16;
|
|
50
89
|
if (size === 'lg') return 24;
|
|
@@ -87,8 +126,7 @@ const Switch = forwardRef<ComponentRef<typeof Pressable>, SwitchProps>(({
|
|
|
87
126
|
disabled={disabled}
|
|
88
127
|
style={switchStyles.switchContainer}
|
|
89
128
|
testID={testID}
|
|
90
|
-
|
|
91
|
-
accessibilityState={{ checked, disabled }}
|
|
129
|
+
{...nativeA11yProps}
|
|
92
130
|
>
|
|
93
131
|
<Animated.View style={switchStyles.switchTrack({ checked, intent })}>
|
|
94
132
|
<Animated.View
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import React, { isValidElement, forwardRef } from 'react';
|
|
1
|
+
import React, { isValidElement, forwardRef, useMemo } from 'react';
|
|
2
2
|
import { getWebProps } from 'react-native-unistyles/web';
|
|
3
3
|
import { switchStyles } from './Switch.styles';
|
|
4
4
|
import type { SwitchProps } from './types';
|
|
5
5
|
import { IconSvg } from '../Icon/IconSvg/IconSvg.web';
|
|
6
6
|
import { resolveIconPath, isIconName } from '../Icon/icon-resolver';
|
|
7
7
|
import useMergeRefs from '../hooks/useMergeRefs';
|
|
8
|
+
import { getWebSelectionAriaProps, generateAccessibilityId } from '../utils/accessibility';
|
|
8
9
|
|
|
9
10
|
const Switch = forwardRef<HTMLDivElement | HTMLButtonElement, SwitchProps>(({
|
|
10
11
|
checked = false,
|
|
@@ -23,6 +24,15 @@ const Switch = forwardRef<HTMLDivElement | HTMLButtonElement, SwitchProps>(({
|
|
|
23
24
|
style,
|
|
24
25
|
testID,
|
|
25
26
|
id,
|
|
27
|
+
// Accessibility props
|
|
28
|
+
accessibilityLabel,
|
|
29
|
+
accessibilityHint,
|
|
30
|
+
accessibilityDisabled,
|
|
31
|
+
accessibilityHidden,
|
|
32
|
+
accessibilityRole,
|
|
33
|
+
accessibilityLabelledBy,
|
|
34
|
+
accessibilityDescribedBy,
|
|
35
|
+
accessibilityChecked,
|
|
26
36
|
}, ref) => {
|
|
27
37
|
const handleClick = () => {
|
|
28
38
|
if (!disabled && onCheckedChange) {
|
|
@@ -30,6 +40,38 @@ const Switch = forwardRef<HTMLDivElement | HTMLButtonElement, SwitchProps>(({
|
|
|
30
40
|
}
|
|
31
41
|
};
|
|
32
42
|
|
|
43
|
+
// Generate unique ID for accessibility
|
|
44
|
+
const switchId = useMemo(() => id || generateAccessibilityId('switch'), [id]);
|
|
45
|
+
|
|
46
|
+
// Generate ARIA props
|
|
47
|
+
const ariaProps = useMemo(() => {
|
|
48
|
+
const computedLabel = accessibilityLabel ?? label;
|
|
49
|
+
const computedChecked = accessibilityChecked ?? checked;
|
|
50
|
+
|
|
51
|
+
return getWebSelectionAriaProps({
|
|
52
|
+
accessibilityLabel: computedLabel,
|
|
53
|
+
accessibilityHint,
|
|
54
|
+
accessibilityDisabled: accessibilityDisabled ?? disabled,
|
|
55
|
+
accessibilityHidden,
|
|
56
|
+
accessibilityRole: accessibilityRole ?? 'switch',
|
|
57
|
+
accessibilityLabelledBy,
|
|
58
|
+
accessibilityDescribedBy,
|
|
59
|
+
accessibilityChecked: computedChecked,
|
|
60
|
+
});
|
|
61
|
+
}, [
|
|
62
|
+
accessibilityLabel,
|
|
63
|
+
label,
|
|
64
|
+
accessibilityHint,
|
|
65
|
+
accessibilityDisabled,
|
|
66
|
+
disabled,
|
|
67
|
+
accessibilityHidden,
|
|
68
|
+
accessibilityRole,
|
|
69
|
+
accessibilityLabelledBy,
|
|
70
|
+
accessibilityDescribedBy,
|
|
71
|
+
accessibilityChecked,
|
|
72
|
+
checked,
|
|
73
|
+
]);
|
|
74
|
+
|
|
33
75
|
// Apply variants using the correct Unistyles v3 pattern
|
|
34
76
|
switchStyles.useVariants({
|
|
35
77
|
size: size as 'sm' | 'md' | 'lg',
|
|
@@ -88,14 +130,12 @@ const Switch = forwardRef<HTMLDivElement | HTMLButtonElement, SwitchProps>(({
|
|
|
88
130
|
const switchElement = (
|
|
89
131
|
<button
|
|
90
132
|
{...computedButtonProps}
|
|
133
|
+
{...ariaProps}
|
|
91
134
|
ref={mergedButtonRef}
|
|
92
135
|
onClick={handleClick}
|
|
93
136
|
disabled={disabled}
|
|
94
|
-
id={
|
|
137
|
+
id={switchId}
|
|
95
138
|
data-testid={testID}
|
|
96
|
-
role="switch"
|
|
97
|
-
aria-checked={checked}
|
|
98
|
-
aria-disabled={disabled}
|
|
99
139
|
>
|
|
100
140
|
<div {...trackProps}>
|
|
101
141
|
<div {...thumbProps}>
|
package/src/Switch/types.ts
CHANGED
|
@@ -2,12 +2,13 @@ import type { StyleProp, ViewStyle } from 'react-native';
|
|
|
2
2
|
import type { IconName } from '../Icon/icon-types';
|
|
3
3
|
import { Intent, Size } from '@idealyst/theme';
|
|
4
4
|
import { FormInputStyleProps } from '../utils/viewStyleProps';
|
|
5
|
+
import { SelectionAccessibilityProps } from '../utils/accessibility';
|
|
5
6
|
|
|
6
7
|
// Component-specific type aliases for future extensibility
|
|
7
8
|
export type SwitchIntentVariant = Intent;
|
|
8
9
|
export type SwitchSizeVariant = Size;
|
|
9
10
|
|
|
10
|
-
export interface SwitchProps extends FormInputStyleProps {
|
|
11
|
+
export interface SwitchProps extends FormInputStyleProps, SelectionAccessibilityProps {
|
|
11
12
|
checked?: boolean;
|
|
12
13
|
onCheckedChange?: (checked: boolean) => void;
|
|
13
14
|
disabled?: boolean;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useState, useRef, useEffect, forwardRef, ReactNode } from 'react';
|
|
1
|
+
import React, { useState, useRef, useEffect, forwardRef, ReactNode, useMemo } from 'react';
|
|
2
2
|
import { View, TouchableOpacity, Text, ScrollView } from 'react-native';
|
|
3
3
|
import Animated, { useSharedValue, useAnimatedStyle, withSpring } from 'react-native-reanimated';
|
|
4
4
|
import {
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
tabBarIconStyles
|
|
10
10
|
} from './TabBar.styles';
|
|
11
11
|
import type { TabBarProps, TabBarItem } from './types';
|
|
12
|
+
import { getNativeAccessibilityProps } from '../utils/accessibility';
|
|
12
13
|
|
|
13
14
|
// Icon size mapping based on size variant
|
|
14
15
|
const ICON_SIZES: Record<string, number> = {
|
|
@@ -53,10 +54,27 @@ const TabBar = forwardRef<View, TabBarProps>(({
|
|
|
53
54
|
style,
|
|
54
55
|
testID,
|
|
55
56
|
id,
|
|
57
|
+
// Accessibility props
|
|
58
|
+
accessibilityLabel,
|
|
59
|
+
accessibilityHint,
|
|
60
|
+
accessibilityDisabled,
|
|
61
|
+
accessibilityHidden,
|
|
62
|
+
accessibilityRole,
|
|
56
63
|
}, ref) => {
|
|
57
64
|
const firstItemValue = items[0]?.value || '';
|
|
58
65
|
const [internalValue, setInternalValue] = useState(defaultValue || firstItemValue);
|
|
59
66
|
|
|
67
|
+
// Generate native accessibility props
|
|
68
|
+
const nativeA11yProps = useMemo(() => {
|
|
69
|
+
return getNativeAccessibilityProps({
|
|
70
|
+
accessibilityLabel,
|
|
71
|
+
accessibilityHint,
|
|
72
|
+
accessibilityDisabled,
|
|
73
|
+
accessibilityHidden,
|
|
74
|
+
accessibilityRole: accessibilityRole ?? 'tablist',
|
|
75
|
+
});
|
|
76
|
+
}, [accessibilityLabel, accessibilityHint, accessibilityDisabled, accessibilityHidden, accessibilityRole]);
|
|
77
|
+
|
|
60
78
|
const indicatorPosition = useSharedValue(0);
|
|
61
79
|
const indicatorWidth = useSharedValue(0);
|
|
62
80
|
const tabLayouts = useRef<{ [key: string]: { x: number; width: number } }>({});
|
|
@@ -132,7 +150,7 @@ const TabBar = forwardRef<View, TabBarProps>(({
|
|
|
132
150
|
showsHorizontalScrollIndicator={false}
|
|
133
151
|
contentContainerStyle={{ position: 'relative' }}
|
|
134
152
|
>
|
|
135
|
-
<View ref={ref} nativeID={id} style={[tabBarContainerStyles.container, style]} testID={testID}>
|
|
153
|
+
<View ref={ref} nativeID={id} style={[tabBarContainerStyles.container, style]} testID={testID} {...nativeA11yProps}>
|
|
136
154
|
{/* Animated indicator - render first so it's behind */}
|
|
137
155
|
<Animated.View
|
|
138
156
|
style={[
|
|
@@ -185,6 +203,9 @@ const TabBar = forwardRef<View, TabBarProps>(({
|
|
|
185
203
|
disabled={item.disabled}
|
|
186
204
|
activeOpacity={0.7}
|
|
187
205
|
testID={`${testID}-tab-${item.value}`}
|
|
206
|
+
accessibilityRole="tab"
|
|
207
|
+
accessibilityLabel={item.label}
|
|
208
|
+
accessibilityState={{ selected: isActive, disabled: item.disabled }}
|
|
188
209
|
>
|
|
189
210
|
{icon && <View style={tabBarIconStyles.tabIcon}>{icon}</View>}
|
|
190
211
|
<Text style={tabBarLabelStyles.tabLabel}>{item.label}</Text>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useState, useRef, useEffect, ReactNode } from 'react';
|
|
1
|
+
import React, { useState, useRef, useEffect, ReactNode, useMemo, useCallback } from 'react';
|
|
2
2
|
import { getWebProps } from 'react-native-unistyles/web';
|
|
3
3
|
import {
|
|
4
4
|
tabBarContainerStyles,
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
} from './TabBar.styles';
|
|
10
10
|
import type { TabBarProps, TabBarItem } from './types';
|
|
11
11
|
import useMergeRefs from '../hooks/useMergeRefs';
|
|
12
|
+
import { getWebAriaProps, generateAccessibilityId, TAB_KEYS } from '../utils/accessibility';
|
|
12
13
|
|
|
13
14
|
// Icon size mapping based on size variant
|
|
14
15
|
const ICON_SIZES: Record<string, number> = {
|
|
@@ -36,6 +37,7 @@ interface TabProps {
|
|
|
36
37
|
item: TabBarItem;
|
|
37
38
|
isActive: boolean;
|
|
38
39
|
onClick: () => void;
|
|
40
|
+
onKeyDown: (e: React.KeyboardEvent) => void;
|
|
39
41
|
size: TabBarProps['size'];
|
|
40
42
|
type: TabBarProps['type'];
|
|
41
43
|
pillMode: TabBarProps['pillMode'];
|
|
@@ -43,12 +45,15 @@ interface TabProps {
|
|
|
43
45
|
justify: TabBarProps['justify'];
|
|
44
46
|
testID?: string;
|
|
45
47
|
tabRef: (el: HTMLButtonElement | null) => void;
|
|
48
|
+
tabId: string;
|
|
49
|
+
panelId: string;
|
|
46
50
|
}
|
|
47
51
|
|
|
48
52
|
const Tab: React.FC<TabProps> = ({
|
|
49
53
|
item,
|
|
50
54
|
isActive,
|
|
51
55
|
onClick,
|
|
56
|
+
onKeyDown,
|
|
52
57
|
size,
|
|
53
58
|
type,
|
|
54
59
|
pillMode,
|
|
@@ -56,6 +61,8 @@ const Tab: React.FC<TabProps> = ({
|
|
|
56
61
|
justify,
|
|
57
62
|
testID,
|
|
58
63
|
tabRef,
|
|
64
|
+
tabId,
|
|
65
|
+
panelId,
|
|
59
66
|
}) => {
|
|
60
67
|
const iconSize = ICON_SIZES[size || 'md'] || 18;
|
|
61
68
|
|
|
@@ -99,11 +106,15 @@ const Tab: React.FC<TabProps> = ({
|
|
|
99
106
|
<button
|
|
100
107
|
{...tabProps}
|
|
101
108
|
ref={mergedRef}
|
|
109
|
+
id={tabId}
|
|
102
110
|
onClick={onClick}
|
|
111
|
+
onKeyDown={onKeyDown}
|
|
103
112
|
disabled={item.disabled}
|
|
104
113
|
role="tab"
|
|
105
114
|
aria-selected={isActive}
|
|
115
|
+
aria-controls={panelId}
|
|
106
116
|
aria-disabled={item.disabled}
|
|
117
|
+
tabIndex={isActive ? 0 : -1}
|
|
107
118
|
data-testid={`${testID}-tab-${item.value}`}
|
|
108
119
|
>
|
|
109
120
|
{icon && <span {...iconProps}>{icon}</span>}
|
|
@@ -133,6 +144,12 @@ const TabBar: React.FC<TabBarProps> = ({
|
|
|
133
144
|
style,
|
|
134
145
|
testID,
|
|
135
146
|
id,
|
|
147
|
+
// Accessibility props
|
|
148
|
+
accessibilityLabel,
|
|
149
|
+
accessibilityHint,
|
|
150
|
+
accessibilityDisabled,
|
|
151
|
+
accessibilityHidden,
|
|
152
|
+
accessibilityRole,
|
|
136
153
|
}) => {
|
|
137
154
|
const firstItemValue = items[0]?.value || '';
|
|
138
155
|
const [internalValue, setInternalValue] = useState(defaultValue || firstItemValue);
|
|
@@ -141,6 +158,54 @@ const TabBar: React.FC<TabBarProps> = ({
|
|
|
141
158
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
142
159
|
const tabRefs = useRef<{ [key: string]: HTMLButtonElement | null }>({});
|
|
143
160
|
|
|
161
|
+
// Generate unique ID for the tablist
|
|
162
|
+
const tabListId = useMemo(() => id || generateAccessibilityId('tablist'), [id]);
|
|
163
|
+
|
|
164
|
+
// Generate tab and panel IDs
|
|
165
|
+
const getTabId = useCallback((itemValue: string) => `${tabListId}-tab-${itemValue}`, [tabListId]);
|
|
166
|
+
const getPanelId = useCallback((itemValue: string) => `${tabListId}-panel-${itemValue}`, [tabListId]);
|
|
167
|
+
|
|
168
|
+
// Get enabled items for keyboard navigation
|
|
169
|
+
const enabledItems = useMemo(() => items.filter(item => !item.disabled), [items]);
|
|
170
|
+
|
|
171
|
+
// Keyboard navigation handler
|
|
172
|
+
const handleKeyDown = useCallback((e: React.KeyboardEvent, itemValue: string) => {
|
|
173
|
+
const key = e.key;
|
|
174
|
+
const currentIndex = enabledItems.findIndex(item => item.value === itemValue);
|
|
175
|
+
let nextIndex = -1;
|
|
176
|
+
|
|
177
|
+
if (TAB_KEYS.next.includes(key)) {
|
|
178
|
+
e.preventDefault();
|
|
179
|
+
nextIndex = currentIndex < enabledItems.length - 1 ? currentIndex + 1 : 0;
|
|
180
|
+
} else if (TAB_KEYS.prev.includes(key)) {
|
|
181
|
+
e.preventDefault();
|
|
182
|
+
nextIndex = currentIndex > 0 ? currentIndex - 1 : enabledItems.length - 1;
|
|
183
|
+
} else if (TAB_KEYS.first.includes(key)) {
|
|
184
|
+
e.preventDefault();
|
|
185
|
+
nextIndex = 0;
|
|
186
|
+
} else if (TAB_KEYS.last.includes(key)) {
|
|
187
|
+
e.preventDefault();
|
|
188
|
+
nextIndex = enabledItems.length - 1;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (nextIndex >= 0) {
|
|
192
|
+
const nextItem = enabledItems[nextIndex];
|
|
193
|
+
const tabButton = tabRefs.current[nextItem.value];
|
|
194
|
+
tabButton?.focus();
|
|
195
|
+
}
|
|
196
|
+
}, [enabledItems]);
|
|
197
|
+
|
|
198
|
+
// Generate ARIA props
|
|
199
|
+
const ariaProps = useMemo(() => {
|
|
200
|
+
return getWebAriaProps({
|
|
201
|
+
accessibilityLabel,
|
|
202
|
+
accessibilityHint,
|
|
203
|
+
accessibilityDisabled,
|
|
204
|
+
accessibilityHidden,
|
|
205
|
+
accessibilityRole: accessibilityRole ?? 'tablist',
|
|
206
|
+
});
|
|
207
|
+
}, [accessibilityLabel, accessibilityHint, accessibilityDisabled, accessibilityHidden, accessibilityRole]);
|
|
208
|
+
|
|
144
209
|
const value = controlledValue !== undefined ? controlledValue : internalValue;
|
|
145
210
|
|
|
146
211
|
const updateIndicator = () => {
|
|
@@ -229,9 +294,10 @@ const TabBar: React.FC<TabBarProps> = ({
|
|
|
229
294
|
return (
|
|
230
295
|
<div
|
|
231
296
|
{...containerProps}
|
|
297
|
+
{...ariaProps}
|
|
232
298
|
ref={mergedContainerRef}
|
|
233
299
|
role="tablist"
|
|
234
|
-
id={
|
|
300
|
+
id={tabListId}
|
|
235
301
|
data-testid={testID}
|
|
236
302
|
>
|
|
237
303
|
{/* Sliding indicator */}
|
|
@@ -249,12 +315,15 @@ const TabBar: React.FC<TabBarProps> = ({
|
|
|
249
315
|
item={item}
|
|
250
316
|
isActive={isActive}
|
|
251
317
|
onClick={() => handleTabClick(item.value, item.disabled)}
|
|
318
|
+
onKeyDown={(e) => handleKeyDown(e, item.value)}
|
|
252
319
|
size={size}
|
|
253
320
|
type={type}
|
|
254
321
|
pillMode={pillMode}
|
|
255
322
|
iconPosition={iconPosition}
|
|
256
323
|
justify={justify}
|
|
257
324
|
testID={testID}
|
|
325
|
+
tabId={getTabId(item.value)}
|
|
326
|
+
panelId={getPanelId(item.value)}
|
|
258
327
|
tabRef={(el) => {
|
|
259
328
|
tabRefs.current[item.value] = el;
|
|
260
329
|
// Update indicator when active tab ref is set
|
package/src/TabBar/types.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { Size } from '@idealyst/theme';
|
|
|
2
2
|
import type { ReactNode } from 'react';
|
|
3
3
|
import type { StyleProp, ViewStyle } from 'react-native';
|
|
4
4
|
import { ContainerStyleProps } from '../utils/viewStyleProps';
|
|
5
|
+
import { AccessibilityProps } from '../utils/accessibility';
|
|
5
6
|
|
|
6
7
|
// Component-specific type aliases for future extensibility
|
|
7
8
|
export type TabBarSizeVariant = Size;
|
|
@@ -19,7 +20,7 @@ export interface TabBarItem {
|
|
|
19
20
|
disabled?: boolean;
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
export interface TabBarProps extends ContainerStyleProps {
|
|
23
|
+
export interface TabBarProps extends ContainerStyleProps, AccessibilityProps {
|
|
23
24
|
items: TabBarItem[];
|
|
24
25
|
value?: string;
|
|
25
26
|
defaultValue?: string;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import React, { forwardRef } from 'react';
|
|
1
|
+
import React, { forwardRef, useMemo } from 'react';
|
|
2
2
|
import { View, ScrollView, Text, TouchableOpacity } from 'react-native';
|
|
3
3
|
import { tableStyles } from './Table.styles';
|
|
4
4
|
import type { TableProps, TableColumn } from './types';
|
|
5
|
+
import { getNativeAccessibilityProps } from '../utils/accessibility';
|
|
5
6
|
|
|
6
7
|
function TableInner<T = any>({
|
|
7
8
|
columns,
|
|
@@ -21,7 +22,21 @@ function TableInner<T = any>({
|
|
|
21
22
|
style,
|
|
22
23
|
testID,
|
|
23
24
|
id,
|
|
25
|
+
// Accessibility props
|
|
26
|
+
accessibilityLabel,
|
|
27
|
+
accessibilityHint,
|
|
28
|
+
accessibilityRole,
|
|
29
|
+
accessibilityHidden,
|
|
24
30
|
}: TableProps<T>, ref: React.Ref<ScrollView>) {
|
|
31
|
+
// Generate native accessibility props
|
|
32
|
+
const nativeA11yProps = useMemo(() => {
|
|
33
|
+
return getNativeAccessibilityProps({
|
|
34
|
+
accessibilityLabel,
|
|
35
|
+
accessibilityHint,
|
|
36
|
+
accessibilityRole: accessibilityRole ?? 'grid',
|
|
37
|
+
accessibilityHidden,
|
|
38
|
+
});
|
|
39
|
+
}, [accessibilityLabel, accessibilityHint, accessibilityRole, accessibilityHidden]);
|
|
25
40
|
// Apply variants
|
|
26
41
|
tableStyles.useVariants({
|
|
27
42
|
type,
|
|
@@ -54,6 +69,7 @@ function TableInner<T = any>({
|
|
|
54
69
|
horizontal
|
|
55
70
|
style={[tableStyles.container, style]}
|
|
56
71
|
testID={testID}
|
|
72
|
+
{...nativeA11yProps}
|
|
57
73
|
>
|
|
58
74
|
<View style={tableStyles.table}>
|
|
59
75
|
{/* Header */}
|
package/src/Table/Table.web.tsx
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
2
|
import { getWebProps } from 'react-native-unistyles/web';
|
|
3
3
|
import { tableStyles } from './Table.styles';
|
|
4
4
|
import type { TableProps, TableColumn } from './types';
|
|
5
|
+
import { getWebAriaProps } from '../utils/accessibility';
|
|
5
6
|
|
|
6
7
|
function Table<T = any>({
|
|
7
8
|
columns,
|
|
@@ -21,7 +22,21 @@ function Table<T = any>({
|
|
|
21
22
|
style,
|
|
22
23
|
testID,
|
|
23
24
|
id,
|
|
25
|
+
// Accessibility props
|
|
26
|
+
accessibilityLabel,
|
|
27
|
+
accessibilityHint,
|
|
28
|
+
accessibilityRole,
|
|
29
|
+
accessibilityHidden,
|
|
24
30
|
}: TableProps<T>) {
|
|
31
|
+
// Generate ARIA props
|
|
32
|
+
const ariaProps = useMemo(() => {
|
|
33
|
+
return getWebAriaProps({
|
|
34
|
+
accessibilityLabel,
|
|
35
|
+
accessibilityHint,
|
|
36
|
+
accessibilityRole: accessibilityRole ?? 'table',
|
|
37
|
+
accessibilityHidden,
|
|
38
|
+
});
|
|
39
|
+
}, [accessibilityLabel, accessibilityHint, accessibilityRole, accessibilityHidden]);
|
|
25
40
|
// Apply variants
|
|
26
41
|
tableStyles.useVariants({
|
|
27
42
|
type,
|
|
@@ -50,8 +65,8 @@ function Table<T = any>({
|
|
|
50
65
|
const isClickable = !!onRowPress;
|
|
51
66
|
|
|
52
67
|
return (
|
|
53
|
-
<div {...containerProps} id={id} data-testid={testID}>
|
|
54
|
-
<table {...tableProps}>
|
|
68
|
+
<div {...containerProps} {...ariaProps} id={id} data-testid={testID}>
|
|
69
|
+
<table {...tableProps} role="table">
|
|
55
70
|
<thead {...getWebProps([tableStyles.thead])}>
|
|
56
71
|
<tr>
|
|
57
72
|
{columns.map((column) => {
|
|
@@ -67,6 +82,8 @@ function Table<T = any>({
|
|
|
67
82
|
<th
|
|
68
83
|
key={column.key}
|
|
69
84
|
{...headerCellProps}
|
|
85
|
+
scope="col"
|
|
86
|
+
aria-sort={column.accessibilitySort}
|
|
70
87
|
style={{
|
|
71
88
|
width: column.width,
|
|
72
89
|
}}
|
package/src/Table/types.ts
CHANGED
|
@@ -2,13 +2,14 @@ import type { StyleProp, ViewStyle, TextStyle } from 'react-native';
|
|
|
2
2
|
import type { ReactNode } from 'react';
|
|
3
3
|
import { Size } from '@idealyst/theme';
|
|
4
4
|
import { ContainerStyleProps } from '../utils/viewStyleProps';
|
|
5
|
+
import { AccessibilityProps, SortableAccessibilityProps } from '../utils/accessibility';
|
|
5
6
|
|
|
6
7
|
// Component-specific type aliases for future extensibility
|
|
7
8
|
export type TableSizeVariant = Size;
|
|
8
9
|
export type TableType = 'standard' | 'bordered' | 'striped';
|
|
9
10
|
export type TableAlignVariant = 'left' | 'center' | 'right';
|
|
10
11
|
|
|
11
|
-
export interface TableColumn<T = any> {
|
|
12
|
+
export interface TableColumn<T = any> extends SortableAccessibilityProps {
|
|
12
13
|
key: string;
|
|
13
14
|
title: string;
|
|
14
15
|
dataIndex?: string;
|
|
@@ -17,7 +18,7 @@ export interface TableColumn<T = any> {
|
|
|
17
18
|
align?: TableAlignVariant;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
export interface TableProps<T = any> extends ContainerStyleProps {
|
|
21
|
+
export interface TableProps<T = any> extends ContainerStyleProps, AccessibilityProps {
|
|
21
22
|
columns: TableColumn<T>[];
|
|
22
23
|
data: T[];
|
|
23
24
|
type?: TableType;
|