@idealyst/components 1.1.4 → 1.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/package.json +8 -3
  2. package/src/Accordion/Accordion.native.tsx +23 -2
  3. package/src/Accordion/Accordion.web.tsx +73 -2
  4. package/src/Accordion/types.ts +2 -1
  5. package/src/ActivityIndicator/ActivityIndicator.native.tsx +15 -1
  6. package/src/ActivityIndicator/ActivityIndicator.web.tsx +19 -2
  7. package/src/ActivityIndicator/types.ts +2 -1
  8. package/src/Avatar/Avatar.native.tsx +19 -2
  9. package/src/Avatar/Avatar.web.tsx +19 -2
  10. package/src/Avatar/types.ts +2 -1
  11. package/src/Breadcrumb/types.ts +3 -2
  12. package/src/Button/Button.native.tsx +48 -1
  13. package/src/Button/Button.styles.tsx +3 -5
  14. package/src/Button/Button.web.tsx +61 -2
  15. package/src/Button/types.ts +2 -1
  16. package/src/Card/Card.native.tsx +21 -5
  17. package/src/Card/Card.web.tsx +21 -4
  18. package/src/Card/types.ts +2 -6
  19. package/src/Checkbox/Checkbox.native.tsx +46 -5
  20. package/src/Checkbox/Checkbox.web.tsx +80 -4
  21. package/src/Checkbox/types.ts +2 -6
  22. package/src/Chip/Chip.native.tsx +5 -0
  23. package/src/Chip/Chip.web.tsx +5 -1
  24. package/src/Chip/types.ts +2 -1
  25. package/src/Dialog/Dialog.native.tsx +20 -3
  26. package/src/Dialog/Dialog.web.tsx +29 -4
  27. package/src/Dialog/types.ts +2 -1
  28. package/src/Image/Image.native.tsx +1 -1
  29. package/src/Image/Image.web.tsx +2 -0
  30. package/src/Input/Input.native.tsx +37 -1
  31. package/src/Input/Input.web.tsx +75 -8
  32. package/src/Input/types.ts +2 -1
  33. package/src/List/List.native.tsx +18 -2
  34. package/src/List/ListItem.native.tsx +44 -8
  35. package/src/List/ListItem.web.tsx +16 -0
  36. package/src/List/types.ts +6 -3
  37. package/src/Menu/Menu.native.tsx +21 -2
  38. package/src/Menu/Menu.web.tsx +110 -3
  39. package/src/Menu/MenuItem.web.tsx +12 -3
  40. package/src/Menu/types.ts +2 -1
  41. package/src/Popover/Popover.native.tsx +17 -1
  42. package/src/Popover/Popover.web.tsx +31 -2
  43. package/src/Popover/types.ts +2 -1
  44. package/src/RadioButton/RadioButton.native.tsx +41 -3
  45. package/src/RadioButton/RadioButton.web.tsx +45 -6
  46. package/src/RadioButton/RadioGroup.native.tsx +20 -2
  47. package/src/RadioButton/RadioGroup.web.tsx +24 -3
  48. package/src/RadioButton/types.ts +3 -2
  49. package/src/Select/types.ts +2 -6
  50. package/src/Skeleton/Skeleton.native.tsx +15 -1
  51. package/src/Skeleton/Skeleton.web.tsx +20 -1
  52. package/src/Skeleton/types.ts +2 -1
  53. package/src/Slider/Slider.native.tsx +42 -2
  54. package/src/Slider/Slider.web.tsx +81 -7
  55. package/src/Slider/types.ts +2 -1
  56. package/src/Switch/Switch.native.tsx +41 -3
  57. package/src/Switch/Switch.web.tsx +45 -5
  58. package/src/Switch/types.ts +2 -1
  59. package/src/TabBar/TabBar.native.tsx +23 -2
  60. package/src/TabBar/TabBar.web.tsx +71 -2
  61. package/src/TabBar/types.ts +2 -1
  62. package/src/Table/Table.native.tsx +17 -1
  63. package/src/Table/Table.web.tsx +20 -3
  64. package/src/Table/types.ts +3 -2
  65. package/src/TextArea/TextArea.native.tsx +50 -1
  66. package/src/TextArea/TextArea.web.tsx +82 -6
  67. package/src/TextArea/types.ts +2 -1
  68. package/src/Tooltip/Tooltip.native.tsx +19 -2
  69. package/src/Tooltip/Tooltip.web.tsx +54 -2
  70. package/src/Tooltip/types.ts +2 -1
  71. package/src/Video/Video.native.tsx +18 -3
  72. package/src/Video/Video.web.tsx +17 -1
  73. package/src/Video/types.ts +2 -1
  74. package/src/examples/InputExamples.tsx +53 -0
  75. package/src/examples/ListExamples.tsx +34 -0
  76. package/src/internal/index.ts +2 -0
  77. package/src/utils/accessibility/ariaHelpers.ts +393 -0
  78. package/src/utils/accessibility/index.ts +210 -0
  79. package/src/utils/accessibility/keyboardPatterns.ts +263 -0
  80. package/src/utils/accessibility/types.ts +223 -0
  81. package/src/utils/accessibility/useAnnounce.ts +210 -0
  82. package/src/utils/accessibility/useFocusTrap.ts +265 -0
  83. package/src/utils/accessibility/useKeyboardNavigation.ts +292 -0
  84. package/src/utils/index.ts +3 -0
@@ -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={id}
158
+ id={buttonId}
100
159
  onClick={handleClick}
101
160
  disabled={disabled}
102
161
  data-testid={testID}
@@ -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
  */
@@ -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
- accessibilityLabel,
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,
@@ -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
- accessibilityLabel={accessibilityLabel}
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
- aria-label={accessibilityLabel}
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 {...helperTextProps}>
194
+ <div
195
+ {...helperTextProps}
196
+ id={error ? errorId : helperId}
197
+ role={error ? 'alert' : undefined}
198
+ >
123
199
  {error || helperText}
124
200
  </div>
125
201
  )}
@@ -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
  */
@@ -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}>
@@ -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 ? 'dialog-title' : undefined}
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="dialog-title">
174
+ <h2 {...titleProps} id={titleId}>
150
175
  {title}
151
176
  </h2>
152
177
  )}
@@ -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
  */