@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,9 +1,10 @@
|
|
|
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 { ButtonProps } from './types';
|
|
4
4
|
import { buttonStyles } from './Button.styles';
|
|
5
5
|
import { IconSvg } from '../Icon/IconSvg/IconSvg.web';
|
|
6
6
|
import useMergeRefs from '../hooks/useMergeRefs';
|
|
7
|
+
import { getWebInteractiveAriaProps, generateAccessibilityId } from '../utils/accessibility';
|
|
7
8
|
|
|
8
9
|
// Extended props to include path props added by Babel plugin
|
|
9
10
|
interface InternalButtonProps extends ButtonProps {
|
|
@@ -29,6 +30,19 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>((props: InternalButton
|
|
|
29
30
|
style,
|
|
30
31
|
testID,
|
|
31
32
|
id,
|
|
33
|
+
// Accessibility props
|
|
34
|
+
accessibilityLabel,
|
|
35
|
+
accessibilityHint,
|
|
36
|
+
accessibilityDisabled,
|
|
37
|
+
accessibilityHidden,
|
|
38
|
+
accessibilityRole,
|
|
39
|
+
accessibilityLabelledBy,
|
|
40
|
+
accessibilityDescribedBy,
|
|
41
|
+
accessibilityControls,
|
|
42
|
+
accessibilityExpanded,
|
|
43
|
+
accessibilityPressed,
|
|
44
|
+
accessibilityOwns,
|
|
45
|
+
accessibilityHasPopup,
|
|
32
46
|
} = props;
|
|
33
47
|
|
|
34
48
|
buttonStyles.useVariants({
|
|
@@ -47,6 +61,50 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>((props: InternalButton
|
|
|
47
61
|
}
|
|
48
62
|
};
|
|
49
63
|
|
|
64
|
+
// Generate unique ID for accessibility
|
|
65
|
+
const buttonId = useMemo(() => id || generateAccessibilityId('button'), [id]);
|
|
66
|
+
|
|
67
|
+
// Generate ARIA props - especially important for icon-only buttons
|
|
68
|
+
const ariaProps = useMemo(() => {
|
|
69
|
+
// For icon-only buttons, accessibilityLabel is critical
|
|
70
|
+
const buttonContent = children || title;
|
|
71
|
+
const isIconOnly = !buttonContent && (leftIcon || rightIcon);
|
|
72
|
+
const computedLabel = accessibilityLabel ?? (isIconOnly && typeof leftIcon === 'string' ? leftIcon : undefined);
|
|
73
|
+
|
|
74
|
+
return getWebInteractiveAriaProps({
|
|
75
|
+
accessibilityLabel: computedLabel,
|
|
76
|
+
accessibilityHint,
|
|
77
|
+
accessibilityDisabled: accessibilityDisabled ?? disabled,
|
|
78
|
+
accessibilityHidden,
|
|
79
|
+
accessibilityRole: accessibilityRole ?? 'button',
|
|
80
|
+
accessibilityLabelledBy,
|
|
81
|
+
accessibilityDescribedBy,
|
|
82
|
+
accessibilityControls,
|
|
83
|
+
accessibilityExpanded,
|
|
84
|
+
accessibilityPressed,
|
|
85
|
+
accessibilityOwns,
|
|
86
|
+
accessibilityHasPopup,
|
|
87
|
+
});
|
|
88
|
+
}, [
|
|
89
|
+
accessibilityLabel,
|
|
90
|
+
children,
|
|
91
|
+
title,
|
|
92
|
+
leftIcon,
|
|
93
|
+
rightIcon,
|
|
94
|
+
accessibilityHint,
|
|
95
|
+
accessibilityDisabled,
|
|
96
|
+
disabled,
|
|
97
|
+
accessibilityHidden,
|
|
98
|
+
accessibilityRole,
|
|
99
|
+
accessibilityLabelledBy,
|
|
100
|
+
accessibilityDescribedBy,
|
|
101
|
+
accessibilityControls,
|
|
102
|
+
accessibilityExpanded,
|
|
103
|
+
accessibilityPressed,
|
|
104
|
+
accessibilityOwns,
|
|
105
|
+
accessibilityHasPopup,
|
|
106
|
+
]);
|
|
107
|
+
|
|
50
108
|
// Compute dynamic styles
|
|
51
109
|
const buttonStyleArray = [
|
|
52
110
|
buttonStyles.button,
|
|
@@ -95,8 +153,9 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>((props: InternalButton
|
|
|
95
153
|
return (
|
|
96
154
|
<button
|
|
97
155
|
{...webProps}
|
|
156
|
+
{...ariaProps}
|
|
98
157
|
ref={mergedRef}
|
|
99
|
-
id={
|
|
158
|
+
id={buttonId}
|
|
100
159
|
onClick={handleClick}
|
|
101
160
|
disabled={disabled}
|
|
102
161
|
data-testid={testID}
|
package/src/Button/types.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { StyleProp, ViewStyle } from 'react-native';
|
|
|
3
3
|
import type { IconName } from '../Icon/icon-types';
|
|
4
4
|
import { Intent, Size } from '@idealyst/theme';
|
|
5
5
|
import { BaseProps } from '../utils/viewStyleProps';
|
|
6
|
+
import { InteractiveAccessibilityProps } from '../utils/accessibility';
|
|
6
7
|
|
|
7
8
|
// Component-specific type aliases for future extensibility
|
|
8
9
|
export type ButtonType = 'contained' | 'outlined' | 'text';
|
|
@@ -17,7 +18,7 @@ export type ButtonSizeVariant = Size;
|
|
|
17
18
|
*/
|
|
18
19
|
export type ButtonGradient = 'darken' | 'lighten';
|
|
19
20
|
|
|
20
|
-
export interface ButtonProps extends BaseProps {
|
|
21
|
+
export interface ButtonProps extends BaseProps, InteractiveAccessibilityProps {
|
|
21
22
|
/**
|
|
22
23
|
* The text or content to display inside the button
|
|
23
24
|
*/
|
package/src/Card/Card.native.tsx
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import React, { forwardRef, ComponentRef } from 'react';
|
|
1
|
+
import React, { forwardRef, ComponentRef, useMemo } from 'react';
|
|
2
2
|
import { View, Pressable } from 'react-native';
|
|
3
3
|
import { CardProps } from './types';
|
|
4
4
|
import { cardStyles } from './Card.styles';
|
|
5
|
+
import { getNativeInteractiveAccessibilityProps } from '../utils/accessibility';
|
|
5
6
|
|
|
6
7
|
const Card = forwardRef<ComponentRef<typeof View> | ComponentRef<typeof Pressable>, CardProps>(({
|
|
7
8
|
children,
|
|
@@ -21,9 +22,26 @@ const Card = forwardRef<ComponentRef<typeof View> | ComponentRef<typeof Pressabl
|
|
|
21
22
|
marginHorizontal,
|
|
22
23
|
style,
|
|
23
24
|
testID,
|
|
24
|
-
accessibilityLabel,
|
|
25
25
|
id,
|
|
26
|
+
// Accessibility props
|
|
27
|
+
accessibilityLabel,
|
|
28
|
+
accessibilityHint,
|
|
29
|
+
accessibilityDisabled,
|
|
30
|
+
accessibilityHidden,
|
|
31
|
+
accessibilityRole,
|
|
32
|
+
accessibilityPressed,
|
|
26
33
|
}, ref) => {
|
|
34
|
+
// Generate native accessibility props
|
|
35
|
+
const nativeA11yProps = useMemo(() => {
|
|
36
|
+
return getNativeInteractiveAccessibilityProps({
|
|
37
|
+
accessibilityLabel,
|
|
38
|
+
accessibilityHint,
|
|
39
|
+
accessibilityDisabled: accessibilityDisabled ?? disabled,
|
|
40
|
+
accessibilityHidden,
|
|
41
|
+
accessibilityRole: accessibilityRole ?? (clickable ? 'button' : 'none'),
|
|
42
|
+
accessibilityPressed,
|
|
43
|
+
});
|
|
44
|
+
}, [accessibilityLabel, accessibilityHint, accessibilityDisabled, disabled, accessibilityHidden, accessibilityRole, clickable, accessibilityPressed]);
|
|
27
45
|
// Apply variants
|
|
28
46
|
cardStyles.useVariants({
|
|
29
47
|
clickable,
|
|
@@ -48,9 +66,7 @@ const Card = forwardRef<ComponentRef<typeof View> | ComponentRef<typeof Pressabl
|
|
|
48
66
|
nativeID: id,
|
|
49
67
|
style: [cardStyles.card, style],
|
|
50
68
|
testID,
|
|
51
|
-
|
|
52
|
-
// Only use button role for clickable cards in React Native
|
|
53
|
-
...(clickable && { accessibilityRole: 'button' as const }),
|
|
69
|
+
...nativeA11yProps,
|
|
54
70
|
...(clickable && {
|
|
55
71
|
onPress: disabled ? undefined : onPress,
|
|
56
72
|
disabled,
|
package/src/Card/Card.web.tsx
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import React, { forwardRef } from 'react';
|
|
1
|
+
import React, { forwardRef, useMemo } from 'react';
|
|
2
2
|
import { getWebProps } from 'react-native-unistyles/web';
|
|
3
3
|
import { CardProps } from './types';
|
|
4
4
|
import { cardStyles } from './Card.styles';
|
|
5
5
|
import useMergeRefs from '../hooks/useMergeRefs';
|
|
6
|
+
import { getWebInteractiveAriaProps } from '../utils/accessibility';
|
|
6
7
|
|
|
7
8
|
const Card = forwardRef<HTMLDivElement | HTMLButtonElement, CardProps>(({
|
|
8
9
|
children,
|
|
@@ -22,9 +23,26 @@ const Card = forwardRef<HTMLDivElement | HTMLButtonElement, CardProps>(({
|
|
|
22
23
|
marginHorizontal,
|
|
23
24
|
style,
|
|
24
25
|
testID,
|
|
25
|
-
accessibilityLabel,
|
|
26
26
|
id,
|
|
27
|
+
// Accessibility props
|
|
28
|
+
accessibilityLabel,
|
|
29
|
+
accessibilityHint,
|
|
30
|
+
accessibilityDisabled,
|
|
31
|
+
accessibilityHidden,
|
|
32
|
+
accessibilityRole,
|
|
33
|
+
accessibilityPressed,
|
|
27
34
|
}, ref) => {
|
|
35
|
+
// Generate ARIA props
|
|
36
|
+
const ariaProps = useMemo(() => {
|
|
37
|
+
return getWebInteractiveAriaProps({
|
|
38
|
+
accessibilityLabel,
|
|
39
|
+
accessibilityHint,
|
|
40
|
+
accessibilityDisabled: accessibilityDisabled ?? disabled,
|
|
41
|
+
accessibilityHidden,
|
|
42
|
+
accessibilityRole: accessibilityRole ?? (clickable ? 'button' : 'region'),
|
|
43
|
+
accessibilityPressed,
|
|
44
|
+
});
|
|
45
|
+
}, [accessibilityLabel, accessibilityHint, accessibilityDisabled, disabled, accessibilityHidden, accessibilityRole, clickable, accessibilityPressed]);
|
|
28
46
|
const handleClick = () => {
|
|
29
47
|
if (!disabled && clickable && onPress) {
|
|
30
48
|
onPress();
|
|
@@ -58,13 +76,12 @@ const Card = forwardRef<HTMLDivElement | HTMLButtonElement, CardProps>(({
|
|
|
58
76
|
return (
|
|
59
77
|
<Component
|
|
60
78
|
{...webProps}
|
|
79
|
+
{...ariaProps}
|
|
61
80
|
ref={mergedRef as any}
|
|
62
81
|
id={id}
|
|
63
82
|
onClick={clickable ? handleClick : undefined}
|
|
64
83
|
disabled={clickable && disabled}
|
|
65
84
|
data-testid={testID}
|
|
66
|
-
aria-label={accessibilityLabel}
|
|
67
|
-
role={clickable ? 'button' : undefined}
|
|
68
85
|
>
|
|
69
86
|
{children}
|
|
70
87
|
</Component>
|
package/src/Card/types.ts
CHANGED
|
@@ -2,13 +2,14 @@ import { Intent, 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 { InteractiveAccessibilityProps } from '../utils/accessibility';
|
|
5
6
|
|
|
6
7
|
// Component-specific type aliases for future extensibility
|
|
7
8
|
export type CardIntentVariant = Intent;
|
|
8
9
|
export type CardType = 'default' | 'outlined' | 'elevated' | 'filled';
|
|
9
10
|
export type CardRadiusVariant = 'none' | Size;
|
|
10
11
|
|
|
11
|
-
export interface CardProps extends ContainerStyleProps {
|
|
12
|
+
export interface CardProps extends ContainerStyleProps, InteractiveAccessibilityProps {
|
|
12
13
|
/**
|
|
13
14
|
* The content to display inside the card
|
|
14
15
|
*/
|
|
@@ -53,9 +54,4 @@ export interface CardProps extends ContainerStyleProps {
|
|
|
53
54
|
* Test ID for testing
|
|
54
55
|
*/
|
|
55
56
|
testID?: string;
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Accessibility label
|
|
59
|
-
*/
|
|
60
|
-
accessibilityLabel?: string;
|
|
61
57
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import React, { useState, useEffect, forwardRef } from 'react';
|
|
1
|
+
import React, { useState, useEffect, forwardRef, useMemo } from 'react';
|
|
2
2
|
import { View, Text, Pressable } from 'react-native';
|
|
3
3
|
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
|
4
4
|
import { CheckboxProps } from './types';
|
|
5
5
|
import { checkboxStyles } from './Checkbox.styles';
|
|
6
|
+
import { getNativeSelectionAccessibilityProps } from '../utils/accessibility';
|
|
6
7
|
|
|
7
8
|
const Checkbox = forwardRef<View, CheckboxProps>(({
|
|
8
9
|
checked = false,
|
|
@@ -20,11 +21,19 @@ const Checkbox = forwardRef<View, CheckboxProps>(({
|
|
|
20
21
|
marginHorizontal,
|
|
21
22
|
style,
|
|
22
23
|
testID,
|
|
23
|
-
accessibilityLabel,
|
|
24
24
|
required = false,
|
|
25
25
|
error,
|
|
26
26
|
helperText,
|
|
27
27
|
id,
|
|
28
|
+
// Accessibility props
|
|
29
|
+
accessibilityLabel,
|
|
30
|
+
accessibilityHint,
|
|
31
|
+
accessibilityDisabled,
|
|
32
|
+
accessibilityHidden,
|
|
33
|
+
accessibilityRole,
|
|
34
|
+
accessibilityLabelledBy,
|
|
35
|
+
accessibilityDescribedBy,
|
|
36
|
+
accessibilityChecked,
|
|
28
37
|
}, ref) => {
|
|
29
38
|
const [internalChecked, setInternalChecked] = useState(checked);
|
|
30
39
|
|
|
@@ -40,6 +49,40 @@ const Checkbox = forwardRef<View, CheckboxProps>(({
|
|
|
40
49
|
onCheckedChange?.(newChecked);
|
|
41
50
|
};
|
|
42
51
|
|
|
52
|
+
// Generate native accessibility props
|
|
53
|
+
const nativeA11yProps = useMemo(() => {
|
|
54
|
+
const labelContent = children || label;
|
|
55
|
+
const computedLabel = accessibilityLabel ?? (typeof labelContent === 'string' ? labelContent : undefined);
|
|
56
|
+
const computedChecked = accessibilityChecked ?? (indeterminate ? 'mixed' : internalChecked);
|
|
57
|
+
|
|
58
|
+
return getNativeSelectionAccessibilityProps({
|
|
59
|
+
accessibilityLabel: computedLabel,
|
|
60
|
+
accessibilityHint: accessibilityHint ?? (error || helperText),
|
|
61
|
+
accessibilityDisabled: accessibilityDisabled ?? disabled,
|
|
62
|
+
accessibilityHidden,
|
|
63
|
+
accessibilityRole: accessibilityRole ?? 'checkbox',
|
|
64
|
+
accessibilityLabelledBy,
|
|
65
|
+
accessibilityDescribedBy,
|
|
66
|
+
accessibilityChecked: computedChecked,
|
|
67
|
+
});
|
|
68
|
+
}, [
|
|
69
|
+
accessibilityLabel,
|
|
70
|
+
children,
|
|
71
|
+
label,
|
|
72
|
+
accessibilityHint,
|
|
73
|
+
error,
|
|
74
|
+
helperText,
|
|
75
|
+
accessibilityDisabled,
|
|
76
|
+
disabled,
|
|
77
|
+
accessibilityHidden,
|
|
78
|
+
accessibilityRole,
|
|
79
|
+
accessibilityLabelledBy,
|
|
80
|
+
accessibilityDescribedBy,
|
|
81
|
+
accessibilityChecked,
|
|
82
|
+
indeterminate,
|
|
83
|
+
internalChecked,
|
|
84
|
+
]);
|
|
85
|
+
|
|
43
86
|
// Apply variants
|
|
44
87
|
checkboxStyles.useVariants({
|
|
45
88
|
size,
|
|
@@ -62,9 +105,7 @@ const Checkbox = forwardRef<View, CheckboxProps>(({
|
|
|
62
105
|
onPress={handlePress}
|
|
63
106
|
disabled={disabled}
|
|
64
107
|
testID={testID}
|
|
65
|
-
|
|
66
|
-
accessibilityRole="checkbox"
|
|
67
|
-
accessibilityState={{ checked: internalChecked }}
|
|
108
|
+
{...nativeA11yProps}
|
|
68
109
|
style={checkboxStyles.container}
|
|
69
110
|
>
|
|
70
111
|
<View style={checkboxStyles.checkbox({ intent })}>
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import React, { useState, useEffect, forwardRef } from 'react';
|
|
1
|
+
import React, { useState, useEffect, forwardRef, useMemo } from 'react';
|
|
2
2
|
import { getWebProps } from 'react-native-unistyles/web';
|
|
3
3
|
import { CheckboxProps } from './types';
|
|
4
4
|
import { checkboxStyles } from './Checkbox.styles';
|
|
5
5
|
import { IconSvg } from '../Icon/IconSvg/IconSvg.web';
|
|
6
6
|
import { resolveIconPath } from '../Icon/icon-resolver';
|
|
7
7
|
import useMergeRefs from '../hooks/useMergeRefs';
|
|
8
|
+
import { getWebSelectionAriaProps, generateAccessibilityId, combineIds } from '../utils/accessibility';
|
|
8
9
|
|
|
9
10
|
const Checkbox = forwardRef<HTMLDivElement, CheckboxProps>(({
|
|
10
11
|
checked = false,
|
|
@@ -22,11 +23,24 @@ const Checkbox = forwardRef<HTMLDivElement, CheckboxProps>(({
|
|
|
22
23
|
marginHorizontal,
|
|
23
24
|
style,
|
|
24
25
|
testID,
|
|
25
|
-
accessibilityLabel,
|
|
26
26
|
required = false,
|
|
27
27
|
error,
|
|
28
28
|
helperText,
|
|
29
29
|
id,
|
|
30
|
+
// Accessibility props
|
|
31
|
+
accessibilityLabel,
|
|
32
|
+
accessibilityHint,
|
|
33
|
+
accessibilityDisabled,
|
|
34
|
+
accessibilityHidden,
|
|
35
|
+
accessibilityRole,
|
|
36
|
+
accessibilityLabelledBy,
|
|
37
|
+
accessibilityDescribedBy,
|
|
38
|
+
accessibilityControls,
|
|
39
|
+
accessibilityExpanded,
|
|
40
|
+
accessibilityPressed,
|
|
41
|
+
accessibilityOwns,
|
|
42
|
+
accessibilityHasPopup,
|
|
43
|
+
accessibilityChecked,
|
|
30
44
|
}, ref) => {
|
|
31
45
|
const [internalChecked, setInternalChecked] = useState(checked);
|
|
32
46
|
|
|
@@ -42,6 +56,61 @@ const Checkbox = forwardRef<HTMLDivElement, CheckboxProps>(({
|
|
|
42
56
|
onCheckedChange?.(newChecked);
|
|
43
57
|
};
|
|
44
58
|
|
|
59
|
+
// Generate unique IDs for accessibility
|
|
60
|
+
const checkboxId = useMemo(() => id || generateAccessibilityId('checkbox'), [id]);
|
|
61
|
+
const errorId = useMemo(() => `${checkboxId}-error`, [checkboxId]);
|
|
62
|
+
const helperId = useMemo(() => `${checkboxId}-helper`, [checkboxId]);
|
|
63
|
+
|
|
64
|
+
// Generate ARIA props for the input element
|
|
65
|
+
const ariaProps = useMemo(() => {
|
|
66
|
+
const labelContent = children || label;
|
|
67
|
+
const computedLabel = accessibilityLabel ?? (typeof labelContent === 'string' ? labelContent : undefined);
|
|
68
|
+
const computedChecked = accessibilityChecked ?? (indeterminate ? 'mixed' : internalChecked);
|
|
69
|
+
const describedByIds = combineIds(
|
|
70
|
+
accessibilityDescribedBy,
|
|
71
|
+
error ? errorId : helperText ? helperId : undefined
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
return getWebSelectionAriaProps({
|
|
75
|
+
accessibilityLabel: computedLabel,
|
|
76
|
+
accessibilityHint,
|
|
77
|
+
accessibilityDisabled: accessibilityDisabled ?? disabled,
|
|
78
|
+
accessibilityHidden,
|
|
79
|
+
accessibilityRole: accessibilityRole ?? 'checkbox',
|
|
80
|
+
accessibilityLabelledBy,
|
|
81
|
+
accessibilityDescribedBy: describedByIds,
|
|
82
|
+
accessibilityControls,
|
|
83
|
+
accessibilityExpanded,
|
|
84
|
+
accessibilityPressed,
|
|
85
|
+
accessibilityOwns,
|
|
86
|
+
accessibilityHasPopup,
|
|
87
|
+
accessibilityChecked: computedChecked,
|
|
88
|
+
});
|
|
89
|
+
}, [
|
|
90
|
+
accessibilityLabel,
|
|
91
|
+
children,
|
|
92
|
+
label,
|
|
93
|
+
accessibilityHint,
|
|
94
|
+
accessibilityDisabled,
|
|
95
|
+
disabled,
|
|
96
|
+
accessibilityHidden,
|
|
97
|
+
accessibilityRole,
|
|
98
|
+
accessibilityLabelledBy,
|
|
99
|
+
accessibilityDescribedBy,
|
|
100
|
+
error,
|
|
101
|
+
errorId,
|
|
102
|
+
helperText,
|
|
103
|
+
helperId,
|
|
104
|
+
accessibilityControls,
|
|
105
|
+
accessibilityExpanded,
|
|
106
|
+
accessibilityPressed,
|
|
107
|
+
accessibilityOwns,
|
|
108
|
+
accessibilityHasPopup,
|
|
109
|
+
accessibilityChecked,
|
|
110
|
+
indeterminate,
|
|
111
|
+
internalChecked,
|
|
112
|
+
]);
|
|
113
|
+
|
|
45
114
|
// Apply variants
|
|
46
115
|
checkboxStyles.useVariants({
|
|
47
116
|
size,
|
|
@@ -82,12 +151,15 @@ const Checkbox = forwardRef<HTMLDivElement, CheckboxProps>(({
|
|
|
82
151
|
<div style={{ position: 'relative' }}>
|
|
83
152
|
<input
|
|
84
153
|
type="checkbox"
|
|
154
|
+
id={checkboxId}
|
|
85
155
|
checked={internalChecked}
|
|
86
156
|
onChange={handleChange}
|
|
87
157
|
disabled={disabled}
|
|
88
158
|
required={required}
|
|
89
159
|
data-testid={testID}
|
|
90
|
-
|
|
160
|
+
{...ariaProps}
|
|
161
|
+
aria-required={required}
|
|
162
|
+
aria-invalid={Boolean(error)}
|
|
91
163
|
ref={(ref) => {
|
|
92
164
|
if (ref) {
|
|
93
165
|
ref.indeterminate = indeterminate;
|
|
@@ -119,7 +191,11 @@ const Checkbox = forwardRef<HTMLDivElement, CheckboxProps>(({
|
|
|
119
191
|
)}
|
|
120
192
|
</label>
|
|
121
193
|
{displayHelperText && (
|
|
122
|
-
<div
|
|
194
|
+
<div
|
|
195
|
+
{...helperTextProps}
|
|
196
|
+
id={error ? errorId : helperId}
|
|
197
|
+
role={error ? 'alert' : undefined}
|
|
198
|
+
>
|
|
123
199
|
{error || helperText}
|
|
124
200
|
</div>
|
|
125
201
|
)}
|
package/src/Checkbox/types.ts
CHANGED
|
@@ -2,13 +2,14 @@ import { Intent, Size } from '@idealyst/theme';
|
|
|
2
2
|
import type { ReactNode } from 'react';
|
|
3
3
|
import type { StyleProp, ViewStyle } from 'react-native';
|
|
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 CheckboxIntentVariant = Intent;
|
|
8
9
|
export type CheckboxSizeVariant = Size;
|
|
9
10
|
export type CheckboxVariant = 'default' | 'outlined';
|
|
10
11
|
|
|
11
|
-
export interface CheckboxProps extends FormInputStyleProps {
|
|
12
|
+
export interface CheckboxProps extends FormInputStyleProps, SelectionAccessibilityProps {
|
|
12
13
|
/**
|
|
13
14
|
* Whether the checkbox is checked
|
|
14
15
|
*/
|
|
@@ -64,11 +65,6 @@ export interface CheckboxProps extends FormInputStyleProps {
|
|
|
64
65
|
*/
|
|
65
66
|
testID?: string;
|
|
66
67
|
|
|
67
|
-
/**
|
|
68
|
-
* Accessibility label
|
|
69
|
-
*/
|
|
70
|
-
accessibilityLabel?: string;
|
|
71
|
-
|
|
72
68
|
/**
|
|
73
69
|
* Whether the checkbox is required
|
|
74
70
|
*/
|
package/src/Chip/Chip.native.tsx
CHANGED
|
@@ -20,6 +20,9 @@ const Chip = forwardRef<ComponentRef<typeof Pressable>, ChipProps>(({
|
|
|
20
20
|
style,
|
|
21
21
|
testID,
|
|
22
22
|
id,
|
|
23
|
+
// Accessibility props
|
|
24
|
+
accessibilityLabel,
|
|
25
|
+
accessibilityChecked,
|
|
23
26
|
}, ref) => {
|
|
24
27
|
const handlePress = () => {
|
|
25
28
|
if (disabled) return;
|
|
@@ -103,10 +106,12 @@ const Chip = forwardRef<ComponentRef<typeof Pressable>, ChipProps>(({
|
|
|
103
106
|
nativeID={id}
|
|
104
107
|
onPress={handlePress}
|
|
105
108
|
disabled={disabled}
|
|
109
|
+
accessibilityLabel={accessibilityLabel ?? label}
|
|
106
110
|
accessibilityRole="button"
|
|
107
111
|
accessibilityState={{
|
|
108
112
|
disabled,
|
|
109
113
|
selected: selectable ? selected : undefined,
|
|
114
|
+
checked: accessibilityChecked ?? (selectable ? selected : undefined),
|
|
110
115
|
}}
|
|
111
116
|
>
|
|
112
117
|
<View style={[containerStyle, style]} testID={testID}>
|
package/src/Chip/Chip.web.tsx
CHANGED
|
@@ -22,6 +22,9 @@ const Chip = forwardRef<HTMLDivElement, ChipProps>(({
|
|
|
22
22
|
style,
|
|
23
23
|
testID,
|
|
24
24
|
id,
|
|
25
|
+
// Accessibility props
|
|
26
|
+
accessibilityLabel,
|
|
27
|
+
accessibilityChecked,
|
|
25
28
|
}, ref) => {
|
|
26
29
|
// Compute actual selected state
|
|
27
30
|
const isSelected = selectable ? selected : false;
|
|
@@ -102,8 +105,9 @@ const Chip = forwardRef<HTMLDivElement, ChipProps>(({
|
|
|
102
105
|
onClick={handleClick}
|
|
103
106
|
data-testid={testID}
|
|
104
107
|
role={isClickable ? 'button' : undefined}
|
|
108
|
+
aria-label={accessibilityLabel ?? label}
|
|
105
109
|
aria-disabled={disabled}
|
|
106
|
-
aria-pressed={selectable ? selected : undefined}
|
|
110
|
+
aria-pressed={accessibilityChecked ?? (selectable ? selected : undefined)}
|
|
107
111
|
>
|
|
108
112
|
{icon && (
|
|
109
113
|
<span
|
package/src/Chip/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 { BaseProps } from '../utils/viewStyleProps';
|
|
5
|
+
import { SelectionAccessibilityProps } from '../utils/accessibility';
|
|
5
6
|
|
|
6
7
|
export type ChipSize = Size;
|
|
7
8
|
export type ChipType = 'filled' | 'outlined' | 'soft';
|
|
8
9
|
export type ChipIntent = Intent;
|
|
9
10
|
|
|
10
|
-
export interface ChipProps extends BaseProps {
|
|
11
|
+
export interface ChipProps extends BaseProps, SelectionAccessibilityProps {
|
|
11
12
|
/** The text content of the chip */
|
|
12
13
|
label: string;
|
|
13
14
|
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import React, { useEffect, forwardRef } from 'react';
|
|
1
|
+
import React, { useEffect, forwardRef, useMemo } from 'react';
|
|
2
2
|
import { Modal, View, Text, TouchableOpacity, TouchableWithoutFeedback, BackHandler } from 'react-native';
|
|
3
3
|
import Animated, { useSharedValue, useAnimatedStyle, withTiming, Easing } from 'react-native-reanimated';
|
|
4
4
|
import { DialogProps } from './types';
|
|
5
5
|
import { dialogStyles } from './Dialog.styles';
|
|
6
|
+
import { getNativeInteractiveAccessibilityProps } from '../utils/accessibility';
|
|
6
7
|
|
|
7
8
|
const Dialog = forwardRef<View, DialogProps>(({
|
|
8
9
|
open,
|
|
@@ -17,7 +18,24 @@ const Dialog = forwardRef<View, DialogProps>(({
|
|
|
17
18
|
style,
|
|
18
19
|
testID,
|
|
19
20
|
id,
|
|
21
|
+
// Accessibility props
|
|
22
|
+
accessibilityLabel,
|
|
23
|
+
accessibilityHint,
|
|
24
|
+
accessibilityDisabled,
|
|
25
|
+
accessibilityHidden,
|
|
26
|
+
accessibilityRole,
|
|
20
27
|
}, ref) => {
|
|
28
|
+
// Generate native accessibility props
|
|
29
|
+
const nativeA11yProps = useMemo(() => {
|
|
30
|
+
return getNativeInteractiveAccessibilityProps({
|
|
31
|
+
accessibilityLabel: accessibilityLabel ?? title,
|
|
32
|
+
accessibilityHint,
|
|
33
|
+
accessibilityDisabled,
|
|
34
|
+
accessibilityHidden,
|
|
35
|
+
accessibilityRole: accessibilityRole ?? 'none',
|
|
36
|
+
});
|
|
37
|
+
}, [accessibilityLabel, title, accessibilityHint, accessibilityDisabled, accessibilityHidden, accessibilityRole]);
|
|
38
|
+
|
|
21
39
|
const backdropOpacity = useSharedValue(0);
|
|
22
40
|
const containerScale = useSharedValue(0.9);
|
|
23
41
|
const containerOpacity = useSharedValue(0);
|
|
@@ -105,12 +123,11 @@ const Dialog = forwardRef<View, DialogProps>(({
|
|
|
105
123
|
onRequestClose={() => onOpenChange(false)}
|
|
106
124
|
statusBarTranslucent
|
|
107
125
|
testID={testID}
|
|
108
|
-
nativeID={id}
|
|
109
126
|
>
|
|
110
127
|
<TouchableWithoutFeedback onPress={handleBackdropPress}>
|
|
111
128
|
<Animated.View style={[dialogStyles.backdrop, backdropAnimatedStyle]}>
|
|
112
129
|
<TouchableWithoutFeedback onPress={(e) => e.stopPropagation()}>
|
|
113
|
-
<Animated.View ref={ref as any} style={[dialogStyles.container, style, containerAnimatedStyle]}>
|
|
130
|
+
<Animated.View ref={ref as any} style={[dialogStyles.container, style, containerAnimatedStyle]} nativeID={id} {...nativeA11yProps}>
|
|
114
131
|
{(title || showCloseButton) && (
|
|
115
132
|
<View style={dialogStyles.header}>
|
|
116
133
|
{title && (
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import React, { useEffect, useRef, useState, forwardRef } from 'react';
|
|
1
|
+
import React, { useEffect, useRef, useState, forwardRef, useMemo } from 'react';
|
|
2
2
|
import { createPortal } from 'react-dom';
|
|
3
3
|
import { getWebProps } from 'react-native-unistyles/web';
|
|
4
4
|
import { DialogProps } from './types';
|
|
5
5
|
import { dialogStyles } from './Dialog.styles';
|
|
6
6
|
import Icon from '../Icon';
|
|
7
7
|
import useMergeRefs from '../hooks/useMergeRefs';
|
|
8
|
+
import { getWebInteractiveAriaProps, generateAccessibilityId } from '../utils/accessibility';
|
|
8
9
|
|
|
9
10
|
const Dialog = forwardRef<HTMLDivElement, DialogProps>(({
|
|
10
11
|
open,
|
|
@@ -19,12 +20,35 @@ const Dialog = forwardRef<HTMLDivElement, DialogProps>(({
|
|
|
19
20
|
style,
|
|
20
21
|
testID,
|
|
21
22
|
id,
|
|
23
|
+
// Accessibility props
|
|
24
|
+
accessibilityLabel,
|
|
25
|
+
accessibilityHint,
|
|
26
|
+
accessibilityDisabled,
|
|
27
|
+
accessibilityHidden,
|
|
28
|
+
accessibilityRole,
|
|
29
|
+
accessibilityDescribedBy,
|
|
22
30
|
}, ref) => {
|
|
23
31
|
const dialogRef = useRef<HTMLDivElement>(null);
|
|
24
32
|
const previousActiveElementRef = useRef<HTMLElement | null>(null);
|
|
25
33
|
const [isVisible, setIsVisible] = useState(false);
|
|
26
34
|
const [shouldRender, setShouldRender] = useState(false);
|
|
27
35
|
|
|
36
|
+
// Generate unique IDs for accessibility
|
|
37
|
+
const dialogId = useMemo(() => id || generateAccessibilityId('dialog'), [id]);
|
|
38
|
+
const titleId = useMemo(() => `${dialogId}-title`, [dialogId]);
|
|
39
|
+
|
|
40
|
+
// Generate ARIA props
|
|
41
|
+
const ariaProps = useMemo(() => {
|
|
42
|
+
return getWebInteractiveAriaProps({
|
|
43
|
+
accessibilityLabel,
|
|
44
|
+
accessibilityHint,
|
|
45
|
+
accessibilityDisabled,
|
|
46
|
+
accessibilityHidden,
|
|
47
|
+
accessibilityRole: accessibilityRole ?? 'dialog',
|
|
48
|
+
accessibilityDescribedBy,
|
|
49
|
+
});
|
|
50
|
+
}, [accessibilityLabel, accessibilityHint, accessibilityDisabled, accessibilityHidden, accessibilityRole, accessibilityDescribedBy]);
|
|
51
|
+
|
|
28
52
|
// Handle mounting/unmounting with animation
|
|
29
53
|
useEffect(() => {
|
|
30
54
|
if (open && !shouldRender) {
|
|
@@ -130,23 +154,24 @@ const Dialog = forwardRef<HTMLDivElement, DialogProps>(({
|
|
|
130
154
|
<div
|
|
131
155
|
{...backdropProps}
|
|
132
156
|
ref={mergedBackdropRef}
|
|
133
|
-
id={id}
|
|
134
157
|
onClick={handleBackdropClick}
|
|
135
158
|
data-testid={testID}
|
|
136
159
|
>
|
|
137
160
|
<div
|
|
138
161
|
{...containerProps}
|
|
162
|
+
{...ariaProps}
|
|
139
163
|
ref={dialogRef}
|
|
164
|
+
id={dialogId}
|
|
140
165
|
role="dialog"
|
|
141
166
|
aria-modal="true"
|
|
142
|
-
aria-labelledby={title ?
|
|
167
|
+
aria-labelledby={title ? titleId : undefined}
|
|
143
168
|
tabIndex={-1}
|
|
144
169
|
onClick={(e) => e.stopPropagation()}
|
|
145
170
|
>
|
|
146
171
|
{(title || showCloseButton) && (
|
|
147
172
|
<div {...headerProps}>
|
|
148
173
|
{title && (
|
|
149
|
-
<h2 {...titleProps} id=
|
|
174
|
+
<h2 {...titleProps} id={titleId}>
|
|
150
175
|
{title}
|
|
151
176
|
</h2>
|
|
152
177
|
)}
|
package/src/Dialog/types.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
2
|
import type { StyleProp, ViewStyle } from 'react-native';
|
|
3
3
|
import { BaseProps } from '../utils/viewStyleProps';
|
|
4
|
+
import { InteractiveAccessibilityProps } from '../utils/accessibility';
|
|
4
5
|
|
|
5
6
|
// Component-specific type aliases for future extensibility
|
|
6
7
|
export type DialogSizeVariant = 'sm' | 'md' | 'lg' | 'fullscreen';
|
|
7
8
|
export type DialogType = 'standard' | 'alert' | 'confirmation';
|
|
8
9
|
export type DialogAnimationType = 'slide' | 'fade' | 'none';
|
|
9
10
|
|
|
10
|
-
export interface DialogProps extends BaseProps {
|
|
11
|
+
export interface DialogProps extends BaseProps, InteractiveAccessibilityProps {
|
|
11
12
|
/**
|
|
12
13
|
* Whether the dialog is open/visible
|
|
13
14
|
*/
|