@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,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={id} data-testid={testID}>
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
- role="slider"
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 */}
@@ -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
- accessibilityRole="switch"
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={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}>
@@ -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={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
@@ -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 */}
@@ -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
  }}
@@ -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;