@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@idealyst/components",
3
- "version": "1.1.4",
3
+ "version": "1.1.5",
4
4
  "description": "Shared component library for React and React Native",
5
5
  "documentation": "https://github.com/IdealystIO/idealyst-framework/tree/main/packages/components#readme",
6
6
  "readme": "README.md",
@@ -39,6 +39,11 @@
39
39
  "import": "./src/examples/index.ts",
40
40
  "require": "./src/examples/index.ts",
41
41
  "types": "./src/examples/index.ts"
42
+ },
43
+ "./internal": {
44
+ "import": "./src/internal/index.ts",
45
+ "require": "./src/internal/index.ts",
46
+ "types": "./src/internal/index.ts"
42
47
  }
43
48
  },
44
49
  "scripts": {
@@ -46,7 +51,7 @@
46
51
  "publish:npm": "npm publish"
47
52
  },
48
53
  "peerDependencies": {
49
- "@idealyst/theme": "^1.1.4",
54
+ "@idealyst/theme": "^1.1.5",
50
55
  "@mdi/js": ">=7.0.0",
51
56
  "@mdi/react": ">=1.0.0",
52
57
  "@react-native-vector-icons/common": ">=12.0.0",
@@ -96,7 +101,7 @@
96
101
  }
97
102
  },
98
103
  "devDependencies": {
99
- "@idealyst/theme": "^1.1.4",
104
+ "@idealyst/theme": "^1.1.5",
100
105
  "@mdi/react": "^1.6.1",
101
106
  "@types/react": "^19.1.0",
102
107
  "react": "^19.1.0",
@@ -1,10 +1,11 @@
1
- import React, { useState, forwardRef, useEffect } from 'react';
1
+ import React, { useState, forwardRef, useEffect, useMemo } from 'react';
2
2
  import { View, TouchableOpacity, LayoutChangeEvent } from 'react-native';
3
3
  import Animated, { useSharedValue, useAnimatedStyle, withTiming, Easing } from 'react-native-reanimated';
4
4
  import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
5
5
  import { accordionStyles } from './Accordion.styles';
6
6
  import Text from '../Text';
7
7
  import type { AccordionProps, AccordionItem as AccordionItemType } from './types';
8
+ import { getNativeAccessibilityProps } from '../utils/accessibility';
8
9
 
9
10
  interface AccordionItemProps {
10
11
  item: AccordionItemType;
@@ -79,6 +80,9 @@ const AccordionItem: React.FC<AccordionItemProps> = ({
79
80
  onPress={onToggle}
80
81
  disabled={item.disabled}
81
82
  activeOpacity={0.7}
83
+ accessibilityRole="button"
84
+ accessibilityLabel={item.title}
85
+ accessibilityState={{ expanded: isExpanded, disabled: item.disabled }}
82
86
  >
83
87
  <View style={accordionStyles.title}>
84
88
  <Text style={accordionStyles.header}>
@@ -143,9 +147,26 @@ const Accordion = forwardRef<View, AccordionProps>(({
143
147
  style,
144
148
  testID,
145
149
  id,
150
+ // Accessibility props
151
+ accessibilityLabel,
152
+ accessibilityHint,
153
+ accessibilityDisabled,
154
+ accessibilityHidden,
155
+ accessibilityRole,
146
156
  }, ref) => {
147
157
  const [expandedItems, setExpandedItems] = useState<string[]>(defaultExpanded);
148
158
 
159
+ // Generate native accessibility props
160
+ const nativeA11yProps = useMemo(() => {
161
+ return getNativeAccessibilityProps({
162
+ accessibilityLabel,
163
+ accessibilityHint,
164
+ accessibilityDisabled,
165
+ accessibilityHidden,
166
+ accessibilityRole: accessibilityRole ?? 'none',
167
+ });
168
+ }, [accessibilityLabel, accessibilityHint, accessibilityDisabled, accessibilityHidden, accessibilityRole]);
169
+
149
170
  // Apply variants
150
171
  accordionStyles.useVariants({
151
172
  type,
@@ -176,7 +197,7 @@ const Accordion = forwardRef<View, AccordionProps>(({
176
197
  };
177
198
 
178
199
  return (
179
- <View ref={ref} nativeID={id} style={[accordionStyles.container, style]} testID={testID}>
200
+ <View ref={ref} nativeID={id} style={[accordionStyles.container, style]} testID={testID} {...nativeA11yProps}>
180
201
  {items.map((item, index) => (
181
202
  <AccordionItem
182
203
  key={item.id}
@@ -1,9 +1,10 @@
1
- import React, { useState, useRef, useEffect } from 'react';
1
+ import React, { useState, useRef, useEffect, useMemo, useCallback } from 'react';
2
2
  import { getWebProps } from 'react-native-unistyles/web';
3
3
  import { accordionStyles } from './Accordion.styles';
4
4
  import type { AccordionProps, AccordionItem as AccordionItemType } from './types';
5
5
  import { IconSvg } from '../Icon/IconSvg/IconSvg.web';
6
6
  import { resolveIconPath } from '../Icon/icon-resolver';
7
+ import { getWebAriaProps, generateAccessibilityId, ACCORDION_KEYS } from '../utils/accessibility';
7
8
 
8
9
  interface AccordionItemProps {
9
10
  item: AccordionItemType;
@@ -12,6 +13,9 @@ interface AccordionItemProps {
12
13
  onToggle: () => void;
13
14
  size: AccordionProps['size'];
14
15
  testID?: string;
16
+ headerId: string;
17
+ panelId: string;
18
+ onKeyDown: (e: React.KeyboardEvent, itemId: string) => void;
15
19
  }
16
20
 
17
21
  const AccordionItem: React.FC<AccordionItemProps> = ({
@@ -21,6 +25,9 @@ const AccordionItem: React.FC<AccordionItemProps> = ({
21
25
  type,
22
26
  size,
23
27
  testID,
28
+ headerId,
29
+ panelId,
30
+ onKeyDown,
24
31
  }) => {
25
32
  const contentInnerRef = useRef<HTMLDivElement>(null);
26
33
  const [contentHeight, setContentHeight] = useState(0);
@@ -61,9 +68,12 @@ const AccordionItem: React.FC<AccordionItemProps> = ({
61
68
  >
62
69
  <button
63
70
  {...headerProps}
71
+ id={headerId}
64
72
  onClick={onToggle}
73
+ onKeyDown={(e) => onKeyDown(e, item.id)}
65
74
  disabled={item.disabled}
66
75
  aria-expanded={isExpanded}
76
+ aria-controls={panelId}
67
77
  aria-disabled={item.disabled}
68
78
  >
69
79
  <span {...titleProps}>
@@ -80,6 +90,9 @@ const AccordionItem: React.FC<AccordionItemProps> = ({
80
90
 
81
91
  <div
82
92
  {...contentProps}
93
+ id={panelId}
94
+ role="region"
95
+ aria-labelledby={headerId}
83
96
  aria-hidden={!isExpanded}
84
97
  >
85
98
  <div ref={contentInnerRef}>
@@ -109,8 +122,63 @@ const Accordion: React.FC<AccordionProps> = ({
109
122
  style,
110
123
  testID,
111
124
  id,
125
+ // Accessibility props
126
+ accessibilityLabel,
127
+ accessibilityHint,
128
+ accessibilityDisabled,
129
+ accessibilityHidden,
130
+ accessibilityRole,
112
131
  }) => {
113
132
  const [expandedItems, setExpandedItems] = useState<string[]>(defaultExpanded);
133
+ const headerRefs = useRef<Map<string, HTMLButtonElement>>(new Map());
134
+
135
+ // Generate unique ID for the accordion
136
+ const accordionId = useMemo(() => id || generateAccessibilityId('accordion'), [id]);
137
+
138
+ // Generate header and panel IDs for each item
139
+ const getHeaderId = useCallback((itemId: string) => `${accordionId}-header-${itemId}`, [accordionId]);
140
+ const getPanelId = useCallback((itemId: string) => `${accordionId}-panel-${itemId}`, [accordionId]);
141
+
142
+ // Get enabled items for keyboard navigation
143
+ const enabledItems = useMemo(() => items.filter(item => !item.disabled), [items]);
144
+
145
+ // Keyboard navigation handler
146
+ const handleKeyDown = useCallback((e: React.KeyboardEvent, itemId: string) => {
147
+ const key = e.key;
148
+ const currentIndex = enabledItems.findIndex(item => item.id === itemId);
149
+ let nextIndex = -1;
150
+
151
+ if (ACCORDION_KEYS.next.includes(key)) {
152
+ e.preventDefault();
153
+ nextIndex = currentIndex < enabledItems.length - 1 ? currentIndex + 1 : 0;
154
+ } else if (ACCORDION_KEYS.prev.includes(key)) {
155
+ e.preventDefault();
156
+ nextIndex = currentIndex > 0 ? currentIndex - 1 : enabledItems.length - 1;
157
+ } else if (ACCORDION_KEYS.first.includes(key)) {
158
+ e.preventDefault();
159
+ nextIndex = 0;
160
+ } else if (ACCORDION_KEYS.last.includes(key)) {
161
+ e.preventDefault();
162
+ nextIndex = enabledItems.length - 1;
163
+ }
164
+
165
+ if (nextIndex >= 0) {
166
+ const nextItem = enabledItems[nextIndex];
167
+ const headerButton = headerRefs.current.get(nextItem.id);
168
+ headerButton?.focus();
169
+ }
170
+ }, [enabledItems]);
171
+
172
+ // Generate ARIA props
173
+ const ariaProps = useMemo(() => {
174
+ return getWebAriaProps({
175
+ accessibilityLabel,
176
+ accessibilityHint,
177
+ accessibilityDisabled,
178
+ accessibilityHidden,
179
+ accessibilityRole: accessibilityRole ?? 'group',
180
+ });
181
+ }, [accessibilityLabel, accessibilityHint, accessibilityDisabled, accessibilityHidden, accessibilityRole]);
114
182
 
115
183
  // Apply variants
116
184
  accordionStyles.useVariants({
@@ -144,7 +212,7 @@ const Accordion: React.FC<AccordionProps> = ({
144
212
  };
145
213
 
146
214
  return (
147
- <div {...containerProps} id={id} data-testid={testID}>
215
+ <div {...containerProps} {...ariaProps} id={accordionId} data-testid={testID}>
148
216
  {items.map((item) => (
149
217
  <AccordionItem
150
218
  key={item.id}
@@ -154,6 +222,9 @@ const Accordion: React.FC<AccordionProps> = ({
154
222
  onToggle={() => toggleItem(item.id, item.disabled)}
155
223
  size={size}
156
224
  testID={`${testID}-item-${item.id}`}
225
+ headerId={getHeaderId(item.id)}
226
+ panelId={getPanelId(item.id)}
227
+ onKeyDown={handleKeyDown}
157
228
  />
158
229
  ))}
159
230
  </div>
@@ -1,6 +1,7 @@
1
1
  import { Size } from '@idealyst/theme';
2
2
  import type { StyleProp, ViewStyle } from 'react-native';
3
3
  import { ContainerStyleProps } from '../utils/viewStyleProps';
4
+ import { AccessibilityProps } from '../utils/accessibility';
4
5
 
5
6
  // Component-specific type aliases for future extensibility
6
7
  export type AccordionType = 'standard' | 'separated' | 'bordered';
@@ -13,7 +14,7 @@ export interface AccordionItem {
13
14
  disabled?: boolean;
14
15
  }
15
16
 
16
- export interface AccordionProps extends ContainerStyleProps {
17
+ export interface AccordionProps extends ContainerStyleProps, AccessibilityProps {
17
18
  items: AccordionItem[];
18
19
  allowMultiple?: boolean;
19
20
  defaultExpanded?: string[];
@@ -1,7 +1,8 @@
1
- import React, { forwardRef } from 'react';
1
+ import React, { forwardRef, useMemo } from 'react';
2
2
  import { ActivityIndicator as RNActivityIndicator, View } from 'react-native';
3
3
  import { ActivityIndicatorProps } from './types';
4
4
  import { activityIndicatorStyles } from './ActivityIndicator.styles';
5
+ import { getNativeLiveRegionAccessibilityProps } from '../utils/accessibility';
5
6
 
6
7
  const ActivityIndicator = forwardRef<View, ActivityIndicatorProps>(({
7
8
  animating = true,
@@ -12,7 +13,19 @@ const ActivityIndicator = forwardRef<View, ActivityIndicatorProps>(({
12
13
  testID,
13
14
  hidesWhenStopped = true,
14
15
  id,
16
+ // Accessibility props
17
+ accessibilityLabel,
18
+ accessibilityLiveRegion,
19
+ accessibilityBusy,
15
20
  }, ref) => {
21
+ // Generate native accessibility props
22
+ const nativeA11yProps = useMemo(() => {
23
+ return getNativeLiveRegionAccessibilityProps({
24
+ accessibilityLabel: accessibilityLabel ?? 'Loading',
25
+ accessibilityLiveRegion: accessibilityLiveRegion ?? 'polite',
26
+ accessibilityBusy: accessibilityBusy ?? animating,
27
+ });
28
+ }, [accessibilityLabel, accessibilityLiveRegion, accessibilityBusy, animating]);
16
29
  // Handle numeric size
17
30
  const sizeVariant = typeof size === 'number' ? 'md' : size;
18
31
  const customSize = typeof size === 'number' ? size : undefined;
@@ -44,6 +57,7 @@ const ActivityIndicator = forwardRef<View, ActivityIndicatorProps>(({
44
57
  ref={ref}
45
58
  nativeID={id}
46
59
  testID={testID}
60
+ {...nativeA11yProps}
47
61
  >
48
62
  <RNActivityIndicator
49
63
  animating={animating}
@@ -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 { ActivityIndicatorProps } from './types';
4
4
  import { activityIndicatorStyles } from './ActivityIndicator.styles';
5
5
  import useMergeRefs from '../hooks/useMergeRefs';
6
+ import { getWebLiveRegionAriaProps } from '../utils/accessibility';
6
7
 
7
8
  const ActivityIndicator = forwardRef<HTMLDivElement, ActivityIndicatorProps>(({
8
9
  animating = true,
@@ -13,7 +14,23 @@ const ActivityIndicator = forwardRef<HTMLDivElement, ActivityIndicatorProps>(({
13
14
  testID,
14
15
  hidesWhenStopped = true,
15
16
  id,
17
+ // Accessibility props
18
+ accessibilityLabel,
19
+ accessibilityLiveRegion,
20
+ accessibilityBusy,
21
+ accessibilityAtomic,
22
+ accessibilityRelevant,
16
23
  }, ref) => {
24
+ // Generate ARIA props for loading state
25
+ const ariaProps = useMemo(() => {
26
+ return getWebLiveRegionAriaProps({
27
+ accessibilityLabel: accessibilityLabel ?? 'Loading',
28
+ accessibilityLiveRegion: accessibilityLiveRegion ?? 'polite',
29
+ accessibilityBusy: accessibilityBusy ?? animating,
30
+ accessibilityAtomic,
31
+ accessibilityRelevant,
32
+ });
33
+ }, [accessibilityLabel, accessibilityLiveRegion, accessibilityBusy, animating, accessibilityAtomic, accessibilityRelevant]);
17
34
  // Handle numeric size
18
35
  const sizeVariant = typeof size === 'number' ? 'md' : size;
19
36
  const customSize = typeof size === 'number' ? size : undefined;
@@ -72,7 +89,7 @@ const ActivityIndicator = forwardRef<HTMLDivElement, ActivityIndicatorProps>(({
72
89
  }
73
90
  `}
74
91
  </style>
75
- <div {...containerProps} ref={mergedRef} id={id} data-testid={testID}>
92
+ <div {...containerProps} {...ariaProps} ref={mergedRef} role="status" id={id} data-testid={testID}>
76
93
  <div {...spinnerProps} />
77
94
  </div>
78
95
  </>
@@ -1,12 +1,13 @@
1
1
  import { Intent, Size } from '@idealyst/theme';
2
2
  import type { StyleProp, ViewStyle } from 'react-native';
3
3
  import { BaseProps } from '../utils/viewStyleProps';
4
+ import { LiveRegionAccessibilityProps } from '../utils/accessibility';
4
5
 
5
6
  // Component-specific type aliases for future extensibility
6
7
  export type ActivityIndicatorIntentVariant = Intent;
7
8
  export type ActivityIndicatorSizeVariant = Size;
8
9
 
9
- export interface ActivityIndicatorProps extends BaseProps {
10
+ export interface ActivityIndicatorProps extends BaseProps, LiveRegionAccessibilityProps {
10
11
  /**
11
12
  * Whether the indicator is animating (visible)
12
13
  * @default true
@@ -1,7 +1,8 @@
1
- import React, { useState, forwardRef } from 'react';
1
+ import React, { useState, forwardRef, useMemo } from 'react';
2
2
  import { View, Text, Image } from 'react-native';
3
3
  import { AvatarProps } from './types';
4
4
  import { avatarStyles } from './Avatar.styles';
5
+ import { getNativeAccessibilityProps } from '../utils/accessibility';
5
6
 
6
7
  const Avatar = forwardRef<View, AvatarProps>(({
7
8
  src,
@@ -12,7 +13,23 @@ const Avatar = forwardRef<View, AvatarProps>(({
12
13
  style,
13
14
  testID,
14
15
  id,
16
+ // Accessibility props
17
+ accessibilityLabel,
18
+ accessibilityHint,
19
+ accessibilityDisabled,
20
+ accessibilityHidden,
21
+ accessibilityRole,
15
22
  }, ref) => {
23
+ // Generate native accessibility props
24
+ const nativeA11yProps = useMemo(() => {
25
+ return getNativeAccessibilityProps({
26
+ accessibilityLabel: accessibilityLabel ?? alt,
27
+ accessibilityHint,
28
+ accessibilityDisabled,
29
+ accessibilityHidden,
30
+ accessibilityRole: accessibilityRole ?? 'image',
31
+ });
32
+ }, [accessibilityLabel, alt, accessibilityHint, accessibilityDisabled, accessibilityHidden, accessibilityRole]);
16
33
  const [hasError, setHasError] = useState(false);
17
34
 
18
35
  avatarStyles.useVariants({
@@ -25,7 +42,7 @@ const Avatar = forwardRef<View, AvatarProps>(({
25
42
  };
26
43
 
27
44
  return (
28
- <View ref={ref} nativeID={id} style={[avatarStyles.avatar, style]} testID={testID}>
45
+ <View ref={ref} nativeID={id} style={[avatarStyles.avatar, style]} testID={testID} {...nativeA11yProps}>
29
46
  {src && !hasError ? (
30
47
  <Image
31
48
  source={typeof src === 'string' ? { uri: src } : src}
@@ -1,8 +1,9 @@
1
- import React, { useState, forwardRef } from 'react';
1
+ import React, { useState, forwardRef, useMemo } from 'react';
2
2
  import { getWebProps } from 'react-native-unistyles/web';
3
3
  import { AvatarProps } from './types';
4
4
  import { avatarStyles } from './Avatar.styles';
5
5
  import useMergeRefs from '../hooks/useMergeRefs';
6
+ import { getWebAriaProps } from '../utils/accessibility';
6
7
 
7
8
  const Avatar = forwardRef<HTMLDivElement, AvatarProps>(({
8
9
  src,
@@ -13,7 +14,23 @@ const Avatar = forwardRef<HTMLDivElement, AvatarProps>(({
13
14
  style,
14
15
  testID,
15
16
  id,
17
+ // Accessibility props
18
+ accessibilityLabel,
19
+ accessibilityHint,
20
+ accessibilityDisabled,
21
+ accessibilityHidden,
22
+ accessibilityRole,
16
23
  }, ref) => {
24
+ // Generate ARIA props
25
+ const ariaProps = useMemo(() => {
26
+ return getWebAriaProps({
27
+ accessibilityLabel: accessibilityLabel ?? alt,
28
+ accessibilityHint,
29
+ accessibilityDisabled,
30
+ accessibilityHidden,
31
+ accessibilityRole: accessibilityRole ?? 'img',
32
+ });
33
+ }, [accessibilityLabel, alt, accessibilityHint, accessibilityDisabled, accessibilityHidden, accessibilityRole]);
17
34
  const [hasError, setHasError] = useState(false);
18
35
 
19
36
  avatarStyles.useVariants({
@@ -35,7 +52,7 @@ const Avatar = forwardRef<HTMLDivElement, AvatarProps>(({
35
52
  const mergedRef = useMergeRefs(ref, avatarProps.ref);
36
53
 
37
54
  return (
38
- <div {...avatarProps} ref={mergedRef} id={id} data-testid={testID}>
55
+ <div {...avatarProps} {...ariaProps} ref={mergedRef} id={id} data-testid={testID}>
39
56
  {src && !hasError ? (
40
57
  <img
41
58
  src={src as any}
@@ -1,13 +1,14 @@
1
1
  import { Color } from '@idealyst/theme';
2
2
  import type { StyleProp, ViewStyle, ImageSourcePropType } from 'react-native';
3
3
  import { BaseProps } from '../utils/viewStyleProps';
4
+ import { AccessibilityProps } from '../utils/accessibility';
4
5
 
5
6
  // Component-specific type aliases for future extensibility
6
7
  export type AvatarColorVariant = Color;
7
8
  export type AvatarSizeVariant = 'sm' | 'md' | 'lg' | 'xl';
8
9
  export type AvatarShapeVariant = 'circle' | 'square';
9
10
 
10
- export interface AvatarProps extends BaseProps {
11
+ export interface AvatarProps extends BaseProps, AccessibilityProps {
11
12
  /**
12
13
  * Image source (URL or require())
13
14
  */
@@ -2,12 +2,13 @@ import type { StyleProp, ViewStyle, TextStyle } from 'react-native';
2
2
  import type { IconName } from '../Icon/icon-types';
3
3
  import { Size } from '@idealyst/theme';
4
4
  import { BaseProps } from '../utils/viewStyleProps';
5
+ import { AccessibilityProps, CurrentAccessibilityProps } from '../utils/accessibility';
5
6
 
6
7
  // Component-specific type aliases for future extensibility
7
8
  export type BreadcrumbIntentVariant = 'primary' | 'neutral';
8
9
  export type BreadcrumbSizeVariant = Size;
9
10
 
10
- export interface BreadcrumbItem {
11
+ export interface BreadcrumbItem extends CurrentAccessibilityProps {
11
12
  /** Label text for the breadcrumb item */
12
13
  label: string;
13
14
 
@@ -21,7 +22,7 @@ export interface BreadcrumbItem {
21
22
  disabled?: boolean;
22
23
  }
23
24
 
24
- export interface BreadcrumbProps extends BaseProps {
25
+ export interface BreadcrumbProps extends BaseProps, AccessibilityProps {
25
26
  /** Array of breadcrumb items */
26
27
  items: BreadcrumbItem[];
27
28
 
@@ -1,9 +1,10 @@
1
- import React, { ComponentRef, forwardRef, isValidElement } from 'react';
1
+ import React, { ComponentRef, forwardRef, isValidElement, useMemo } from 'react';
2
2
  import { StyleSheet as RNStyleSheet, Text, TouchableOpacity, View } from 'react-native';
3
3
  import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
4
4
  import Svg, { Defs, LinearGradient, Stop, Rect } from 'react-native-svg';
5
5
  import { buttonStyles } from './Button.styles';
6
6
  import { ButtonProps } from './types';
7
+ import { getNativeInteractiveAccessibilityProps } from '../utils/accessibility';
7
8
 
8
9
  const Button = forwardRef<ComponentRef<typeof TouchableOpacity>, ButtonProps>((props, ref) => {
9
10
  const {
@@ -20,6 +21,17 @@ const Button = forwardRef<ComponentRef<typeof TouchableOpacity>, ButtonProps>((p
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
+ accessibilityControls,
33
+ accessibilityExpanded,
34
+ accessibilityPressed,
23
35
  } = props;
24
36
 
25
37
  // Apply variants
@@ -83,6 +95,40 @@ const Button = forwardRef<ComponentRef<typeof TouchableOpacity>, ButtonProps>((p
83
95
  // Determine if we need to wrap content in icon container
84
96
  const hasIcons = leftIcon || rightIcon;
85
97
 
98
+ // Generate native accessibility props - especially important for icon-only buttons
99
+ const nativeA11yProps = useMemo(() => {
100
+ const isIconOnly = !buttonContent && (leftIcon || rightIcon);
101
+ const computedLabel = accessibilityLabel ?? (isIconOnly && typeof leftIcon === 'string' ? leftIcon : undefined);
102
+
103
+ return getNativeInteractiveAccessibilityProps({
104
+ accessibilityLabel: computedLabel,
105
+ accessibilityHint,
106
+ accessibilityDisabled: accessibilityDisabled ?? disabled,
107
+ accessibilityHidden,
108
+ accessibilityRole: accessibilityRole ?? 'button',
109
+ accessibilityLabelledBy,
110
+ accessibilityDescribedBy,
111
+ accessibilityControls,
112
+ accessibilityExpanded,
113
+ accessibilityPressed,
114
+ });
115
+ }, [
116
+ accessibilityLabel,
117
+ buttonContent,
118
+ leftIcon,
119
+ rightIcon,
120
+ accessibilityHint,
121
+ accessibilityDisabled,
122
+ disabled,
123
+ accessibilityHidden,
124
+ accessibilityRole,
125
+ accessibilityLabelledBy,
126
+ accessibilityDescribedBy,
127
+ accessibilityControls,
128
+ accessibilityExpanded,
129
+ accessibilityPressed,
130
+ ]);
131
+
86
132
  // Render gradient background layer
87
133
  const renderGradientLayer = () => {
88
134
  if (!showGradient) return null;
@@ -121,6 +167,7 @@ const Button = forwardRef<ComponentRef<typeof TouchableOpacity>, ButtonProps>((p
121
167
  showGradient && { overflow: 'hidden' },
122
168
  style,
123
169
  ],
170
+ ...nativeA11yProps,
124
171
  };
125
172
 
126
173
  return (
@@ -29,13 +29,12 @@ function createIntentVariants(theme: Theme) {
29
29
  /**
30
30
  * Create type variants (structure only, colors handled by compound variants)
31
31
  */
32
- function createTypeVariants(theme: Theme) {
32
+ function createButtonTypeVariants(theme: Theme) {
33
33
  return {
34
34
  contained: {
35
35
  borderWidth: 0,
36
36
  },
37
37
  outlined: {
38
- boxSizing: 'border-box',
39
38
  borderWidth: 1,
40
39
  borderStyle: 'solid' ,
41
40
  backgroundColor: theme.colors.surface.primary,
@@ -208,6 +207,7 @@ const createButtonTextStyles = (theme: Theme) => {
208
207
  export const buttonStyles = StyleSheet.create((theme: Theme) => {
209
208
  return {
210
209
  button: {
210
+ boxSizing: 'border-box',
211
211
  alignItems: 'center',
212
212
  justifyContent: 'center',
213
213
  borderRadius: 8,
@@ -223,7 +223,7 @@ export const buttonStyles = StyleSheet.create((theme: Theme) => {
223
223
  paddingHorizontal: size.paddingHorizontal,
224
224
  minHeight: size.minHeight,
225
225
  })),
226
- type: createTypeVariants(theme),
226
+ type: createButtonTypeVariants(theme),
227
227
  disabled: {
228
228
  true: { opacity: 0.6 },
229
229
  false: { opacity: 1, _web: {
@@ -250,7 +250,6 @@ export const buttonStyles = StyleSheet.create((theme: Theme) => {
250
250
  height: size.iconSize,
251
251
  })),
252
252
  intent: createIntentVariants(theme),
253
- type: createTypeVariants(theme),
254
253
  } as const,
255
254
  compoundVariants: createIconCompoundVariants(theme),
256
255
  } as const,
@@ -269,7 +268,6 @@ export const buttonStyles = StyleSheet.create((theme: Theme) => {
269
268
  fontSize: size.fontSize,
270
269
  })),
271
270
  intent: createIntentVariants(theme),
272
- type: createTypeVariants(theme),
273
271
  disabled: {
274
272
  true: { opacity: 0.6 },
275
273
  false: { opacity: 1 },