@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@idealyst/components",
|
|
3
|
-
"version": "1.1.
|
|
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.
|
|
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.
|
|
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={
|
|
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>
|
package/src/Accordion/types.ts
CHANGED
|
@@ -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}
|
package/src/Avatar/types.ts
CHANGED
|
@@ -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
|
*/
|
package/src/Breadcrumb/types.ts
CHANGED
|
@@ -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
|
|
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:
|
|
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 },
|