@idealyst/components 1.1.4 → 1.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +8 -3
- package/src/Accordion/Accordion.native.tsx +23 -2
- package/src/Accordion/Accordion.web.tsx +73 -2
- package/src/Accordion/types.ts +2 -1
- package/src/ActivityIndicator/ActivityIndicator.native.tsx +15 -1
- package/src/ActivityIndicator/ActivityIndicator.web.tsx +19 -2
- package/src/ActivityIndicator/types.ts +2 -1
- package/src/Avatar/Avatar.native.tsx +19 -2
- package/src/Avatar/Avatar.web.tsx +19 -2
- package/src/Avatar/types.ts +2 -1
- package/src/Breadcrumb/types.ts +3 -2
- package/src/Button/Button.native.tsx +48 -1
- package/src/Button/Button.styles.tsx +3 -5
- package/src/Button/Button.web.tsx +61 -2
- package/src/Button/types.ts +2 -1
- package/src/Card/Card.native.tsx +21 -5
- package/src/Card/Card.web.tsx +21 -4
- package/src/Card/types.ts +2 -6
- package/src/Checkbox/Checkbox.native.tsx +46 -5
- package/src/Checkbox/Checkbox.web.tsx +80 -4
- package/src/Checkbox/types.ts +2 -6
- package/src/Chip/Chip.native.tsx +5 -0
- package/src/Chip/Chip.web.tsx +5 -1
- package/src/Chip/types.ts +2 -1
- package/src/Dialog/Dialog.native.tsx +20 -3
- package/src/Dialog/Dialog.web.tsx +29 -4
- package/src/Dialog/types.ts +2 -1
- package/src/Image/Image.native.tsx +1 -1
- package/src/Image/Image.web.tsx +2 -0
- package/src/Input/Input.native.tsx +37 -1
- package/src/Input/Input.web.tsx +75 -8
- package/src/Input/types.ts +2 -1
- package/src/List/List.native.tsx +18 -2
- package/src/List/ListItem.native.tsx +44 -8
- package/src/List/ListItem.web.tsx +16 -0
- package/src/List/types.ts +6 -3
- package/src/Menu/Menu.native.tsx +21 -2
- package/src/Menu/Menu.web.tsx +110 -3
- package/src/Menu/MenuItem.web.tsx +12 -3
- package/src/Menu/types.ts +2 -1
- package/src/Popover/Popover.native.tsx +17 -1
- package/src/Popover/Popover.web.tsx +31 -2
- package/src/Popover/types.ts +2 -1
- package/src/RadioButton/RadioButton.native.tsx +41 -3
- package/src/RadioButton/RadioButton.web.tsx +45 -6
- package/src/RadioButton/RadioGroup.native.tsx +20 -2
- package/src/RadioButton/RadioGroup.web.tsx +24 -3
- package/src/RadioButton/types.ts +3 -2
- package/src/Select/types.ts +2 -6
- package/src/Skeleton/Skeleton.native.tsx +15 -1
- package/src/Skeleton/Skeleton.web.tsx +20 -1
- package/src/Skeleton/types.ts +2 -1
- package/src/Slider/Slider.native.tsx +42 -2
- package/src/Slider/Slider.web.tsx +81 -7
- package/src/Slider/types.ts +2 -1
- package/src/Switch/Switch.native.tsx +41 -3
- package/src/Switch/Switch.web.tsx +45 -5
- package/src/Switch/types.ts +2 -1
- package/src/TabBar/TabBar.native.tsx +23 -2
- package/src/TabBar/TabBar.web.tsx +71 -2
- package/src/TabBar/types.ts +2 -1
- package/src/Table/Table.native.tsx +17 -1
- package/src/Table/Table.web.tsx +20 -3
- package/src/Table/types.ts +3 -2
- package/src/TextArea/TextArea.native.tsx +50 -1
- package/src/TextArea/TextArea.web.tsx +82 -6
- package/src/TextArea/types.ts +2 -1
- package/src/Tooltip/Tooltip.native.tsx +19 -2
- package/src/Tooltip/Tooltip.web.tsx +54 -2
- package/src/Tooltip/types.ts +2 -1
- package/src/Video/Video.native.tsx +18 -3
- package/src/Video/Video.web.tsx +17 -1
- package/src/Video/types.ts +2 -1
- package/src/examples/InputExamples.tsx +53 -0
- package/src/examples/ListExamples.tsx +34 -0
- package/src/internal/index.ts +2 -0
- package/src/utils/accessibility/ariaHelpers.ts +393 -0
- package/src/utils/accessibility/index.ts +210 -0
- package/src/utils/accessibility/keyboardPatterns.ts +263 -0
- package/src/utils/accessibility/types.ts +223 -0
- package/src/utils/accessibility/useAnnounce.ts +210 -0
- package/src/utils/accessibility/useFocusTrap.ts +265 -0
- package/src/utils/accessibility/useKeyboardNavigation.ts +292 -0
- package/src/utils/index.ts +3 -0
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import React, { useState, forwardRef } from 'react';
|
|
1
|
+
import React, { useState, forwardRef, useMemo } from 'react';
|
|
2
2
|
import { View, TextInput, NativeSyntheticEvent, TextInputContentSizeChangeEventData } from 'react-native';
|
|
3
3
|
import { textAreaStyles } from './TextArea.styles';
|
|
4
4
|
import Text from '../Text';
|
|
5
5
|
import type { TextAreaProps } from './types';
|
|
6
|
+
import { getNativeFormAccessibilityProps } from '../utils/accessibility';
|
|
6
7
|
|
|
7
8
|
const TextArea = forwardRef<TextInput, TextAreaProps>(({
|
|
8
9
|
value: controlledValue,
|
|
@@ -29,6 +30,17 @@ const TextArea = forwardRef<TextInput, TextAreaProps>(({
|
|
|
29
30
|
textareaStyle,
|
|
30
31
|
testID,
|
|
31
32
|
id,
|
|
33
|
+
// Accessibility props
|
|
34
|
+
accessibilityLabel,
|
|
35
|
+
accessibilityHint,
|
|
36
|
+
accessibilityDisabled,
|
|
37
|
+
accessibilityHidden,
|
|
38
|
+
accessibilityRole,
|
|
39
|
+
accessibilityLabelledBy,
|
|
40
|
+
accessibilityDescribedBy,
|
|
41
|
+
accessibilityRequired,
|
|
42
|
+
accessibilityInvalid,
|
|
43
|
+
accessibilityErrorMessage,
|
|
32
44
|
}, ref) => {
|
|
33
45
|
const [internalValue, setInternalValue] = useState(defaultValue);
|
|
34
46
|
const [contentHeight, setContentHeight] = useState<number | undefined>(undefined);
|
|
@@ -36,6 +48,42 @@ const TextArea = forwardRef<TextInput, TextAreaProps>(({
|
|
|
36
48
|
const value = controlledValue !== undefined ? controlledValue : internalValue;
|
|
37
49
|
const hasError = Boolean(error);
|
|
38
50
|
|
|
51
|
+
// Generate native accessibility props
|
|
52
|
+
const nativeA11yProps = useMemo(() => {
|
|
53
|
+
const computedLabel = accessibilityLabel ?? label ?? placeholder;
|
|
54
|
+
const isInvalid = accessibilityInvalid ?? hasError;
|
|
55
|
+
|
|
56
|
+
return getNativeFormAccessibilityProps({
|
|
57
|
+
accessibilityLabel: computedLabel,
|
|
58
|
+
accessibilityHint: accessibilityHint ?? (error || helperText),
|
|
59
|
+
accessibilityDisabled: accessibilityDisabled ?? disabled,
|
|
60
|
+
accessibilityHidden,
|
|
61
|
+
accessibilityRole: accessibilityRole ?? 'textbox',
|
|
62
|
+
accessibilityLabelledBy,
|
|
63
|
+
accessibilityDescribedBy,
|
|
64
|
+
accessibilityRequired,
|
|
65
|
+
accessibilityInvalid: isInvalid,
|
|
66
|
+
accessibilityErrorMessage: accessibilityErrorMessage ?? error,
|
|
67
|
+
});
|
|
68
|
+
}, [
|
|
69
|
+
accessibilityLabel,
|
|
70
|
+
label,
|
|
71
|
+
placeholder,
|
|
72
|
+
accessibilityHint,
|
|
73
|
+
error,
|
|
74
|
+
helperText,
|
|
75
|
+
accessibilityDisabled,
|
|
76
|
+
disabled,
|
|
77
|
+
accessibilityHidden,
|
|
78
|
+
accessibilityRole,
|
|
79
|
+
accessibilityLabelledBy,
|
|
80
|
+
accessibilityDescribedBy,
|
|
81
|
+
accessibilityRequired,
|
|
82
|
+
accessibilityInvalid,
|
|
83
|
+
hasError,
|
|
84
|
+
accessibilityErrorMessage,
|
|
85
|
+
]);
|
|
86
|
+
|
|
39
87
|
// Apply variants
|
|
40
88
|
textAreaStyles.useVariants({
|
|
41
89
|
size,
|
|
@@ -90,6 +138,7 @@ const TextArea = forwardRef<TextInput, TextAreaProps>(({
|
|
|
90
138
|
<View style={textAreaStyles.textareaContainer}>
|
|
91
139
|
<TextInput
|
|
92
140
|
ref={ref}
|
|
141
|
+
{...nativeA11yProps}
|
|
93
142
|
style={[
|
|
94
143
|
textAreaStyles.textarea({ intent, disabled, hasError }),
|
|
95
144
|
{
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import React, { useState, useRef, useEffect, forwardRef } from 'react';
|
|
1
|
+
import React, { useState, useRef, useEffect, forwardRef, useMemo } from 'react';
|
|
2
2
|
import { getWebProps } from 'react-native-unistyles/web';
|
|
3
3
|
import { textAreaStyles } from './TextArea.styles';
|
|
4
4
|
import type { TextAreaProps } from './types';
|
|
5
5
|
import useMergeRefs from '../hooks/useMergeRefs';
|
|
6
|
+
import { getWebFormAriaProps, combineIds, generateAccessibilityId } from '../utils/accessibility';
|
|
6
7
|
|
|
7
8
|
const TextArea = forwardRef<HTMLDivElement, TextAreaProps>(({
|
|
8
9
|
value: controlledValue,
|
|
@@ -30,6 +31,23 @@ const TextArea = forwardRef<HTMLDivElement, TextAreaProps>(({
|
|
|
30
31
|
textareaStyle,
|
|
31
32
|
testID,
|
|
32
33
|
id,
|
|
34
|
+
// Accessibility props
|
|
35
|
+
accessibilityLabel,
|
|
36
|
+
accessibilityHint,
|
|
37
|
+
accessibilityDisabled,
|
|
38
|
+
accessibilityHidden,
|
|
39
|
+
accessibilityRole,
|
|
40
|
+
accessibilityLabelledBy,
|
|
41
|
+
accessibilityDescribedBy,
|
|
42
|
+
accessibilityControls,
|
|
43
|
+
accessibilityExpanded,
|
|
44
|
+
accessibilityPressed,
|
|
45
|
+
accessibilityOwns,
|
|
46
|
+
accessibilityHasPopup,
|
|
47
|
+
accessibilityRequired,
|
|
48
|
+
accessibilityInvalid,
|
|
49
|
+
accessibilityErrorMessage,
|
|
50
|
+
accessibilityAutoComplete,
|
|
33
51
|
}, ref) => {
|
|
34
52
|
const [internalValue, setInternalValue] = useState(defaultValue);
|
|
35
53
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
@@ -37,6 +55,64 @@ const TextArea = forwardRef<HTMLDivElement, TextAreaProps>(({
|
|
|
37
55
|
const value = controlledValue !== undefined ? controlledValue : internalValue;
|
|
38
56
|
const hasError = Boolean(error);
|
|
39
57
|
|
|
58
|
+
// Generate unique IDs for accessibility
|
|
59
|
+
const textareaId = useMemo(() => id || generateAccessibilityId('textarea'), [id]);
|
|
60
|
+
const errorId = useMemo(() => `${textareaId}-error`, [textareaId]);
|
|
61
|
+
const helperId = useMemo(() => `${textareaId}-helper`, [textareaId]);
|
|
62
|
+
const labelId = useMemo(() => label ? `${textareaId}-label` : undefined, [textareaId, label]);
|
|
63
|
+
|
|
64
|
+
// Generate ARIA props for the textarea element
|
|
65
|
+
const ariaProps = useMemo(() => {
|
|
66
|
+
const isInvalid = accessibilityInvalid ?? hasError;
|
|
67
|
+
const describedByIds = combineIds(
|
|
68
|
+
accessibilityDescribedBy,
|
|
69
|
+
error ? errorId : helperText ? helperId : undefined
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
return getWebFormAriaProps({
|
|
73
|
+
accessibilityLabel,
|
|
74
|
+
accessibilityHint,
|
|
75
|
+
accessibilityDisabled: accessibilityDisabled ?? disabled,
|
|
76
|
+
accessibilityHidden,
|
|
77
|
+
accessibilityRole: accessibilityRole ?? 'textbox',
|
|
78
|
+
accessibilityLabelledBy: accessibilityLabelledBy ?? labelId,
|
|
79
|
+
accessibilityDescribedBy: describedByIds,
|
|
80
|
+
accessibilityControls,
|
|
81
|
+
accessibilityExpanded,
|
|
82
|
+
accessibilityPressed,
|
|
83
|
+
accessibilityOwns,
|
|
84
|
+
accessibilityHasPopup,
|
|
85
|
+
accessibilityRequired,
|
|
86
|
+
accessibilityInvalid: isInvalid,
|
|
87
|
+
accessibilityErrorMessage: accessibilityErrorMessage ?? (error ? errorId : undefined),
|
|
88
|
+
accessibilityAutoComplete,
|
|
89
|
+
});
|
|
90
|
+
}, [
|
|
91
|
+
accessibilityLabel,
|
|
92
|
+
accessibilityHint,
|
|
93
|
+
accessibilityDisabled,
|
|
94
|
+
disabled,
|
|
95
|
+
accessibilityHidden,
|
|
96
|
+
accessibilityRole,
|
|
97
|
+
accessibilityLabelledBy,
|
|
98
|
+
labelId,
|
|
99
|
+
accessibilityDescribedBy,
|
|
100
|
+
error,
|
|
101
|
+
errorId,
|
|
102
|
+
helperText,
|
|
103
|
+
helperId,
|
|
104
|
+
accessibilityControls,
|
|
105
|
+
accessibilityExpanded,
|
|
106
|
+
accessibilityPressed,
|
|
107
|
+
accessibilityOwns,
|
|
108
|
+
accessibilityHasPopup,
|
|
109
|
+
accessibilityRequired,
|
|
110
|
+
accessibilityInvalid,
|
|
111
|
+
hasError,
|
|
112
|
+
accessibilityErrorMessage,
|
|
113
|
+
accessibilityAutoComplete,
|
|
114
|
+
]);
|
|
115
|
+
|
|
40
116
|
// Apply variants
|
|
41
117
|
textAreaStyles.useVariants({
|
|
42
118
|
size,
|
|
@@ -115,12 +191,14 @@ const TextArea = forwardRef<HTMLDivElement, TextAreaProps>(({
|
|
|
115
191
|
return (
|
|
116
192
|
<div {...containerProps} ref={mergedRef} id={id} data-testid={testID}>
|
|
117
193
|
{label && (
|
|
118
|
-
<label {...labelProps}>{label}</label>
|
|
194
|
+
<label {...labelProps} id={labelId} htmlFor={textareaId}>{label}</label>
|
|
119
195
|
)}
|
|
120
196
|
|
|
121
197
|
<div {...textareaContainerProps}>
|
|
122
198
|
<textarea
|
|
123
199
|
{...computedTextareaProps}
|
|
200
|
+
{...ariaProps}
|
|
201
|
+
id={textareaId}
|
|
124
202
|
ref={mergedTextareaRef}
|
|
125
203
|
value={value}
|
|
126
204
|
onChange={handleChange}
|
|
@@ -128,8 +206,6 @@ const TextArea = forwardRef<HTMLDivElement, TextAreaProps>(({
|
|
|
128
206
|
disabled={disabled}
|
|
129
207
|
rows={autoGrow ? undefined : rows}
|
|
130
208
|
maxLength={maxLength}
|
|
131
|
-
aria-invalid={hasError}
|
|
132
|
-
aria-describedby={error ? `${testID}-error` : helperText ? `${testID}-helper` : undefined}
|
|
133
209
|
/>
|
|
134
210
|
</div>
|
|
135
211
|
|
|
@@ -137,12 +213,12 @@ const TextArea = forwardRef<HTMLDivElement, TextAreaProps>(({
|
|
|
137
213
|
<div {...footerProps}>
|
|
138
214
|
<div style={{ flex: 1 }}>
|
|
139
215
|
{error && (
|
|
140
|
-
<span {...helperTextProps} id={
|
|
216
|
+
<span {...helperTextProps} id={errorId} role="alert">
|
|
141
217
|
{error}
|
|
142
218
|
</span>
|
|
143
219
|
)}
|
|
144
220
|
{!error && helperText && (
|
|
145
|
-
<span {...helperTextProps} id={
|
|
221
|
+
<span {...helperTextProps} id={helperId}>
|
|
146
222
|
{helperText}
|
|
147
223
|
</span>
|
|
148
224
|
)}
|
package/src/TextArea/types.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { Intent, Size } from '@idealyst/theme';
|
|
2
2
|
import type { StyleProp, ViewStyle, TextStyle } from 'react-native';
|
|
3
3
|
import { FormInputStyleProps } from '../utils/viewStyleProps';
|
|
4
|
+
import { FormAccessibilityProps } from '../utils/accessibility';
|
|
4
5
|
|
|
5
6
|
// Component-specific type aliases for future extensibility
|
|
6
7
|
export type TextAreaIntentVariant = Intent;
|
|
7
8
|
export type TextAreaSizeVariant = Size;
|
|
8
9
|
export type TextAreaResizeVariant = 'none' | 'vertical' | 'horizontal' | 'both';
|
|
9
10
|
|
|
10
|
-
export interface TextAreaProps extends FormInputStyleProps {
|
|
11
|
+
export interface TextAreaProps extends FormInputStyleProps, FormAccessibilityProps {
|
|
11
12
|
value?: string;
|
|
12
13
|
defaultValue?: string;
|
|
13
14
|
onChange?: (value: string) => void;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import React, { useState, useRef, useEffect, isValidElement, cloneElement, forwardRef } from 'react';
|
|
1
|
+
import React, { useState, useRef, useEffect, isValidElement, cloneElement, forwardRef, useMemo } from 'react';
|
|
2
2
|
import { View, Modal, Text, Pressable } from 'react-native';
|
|
3
3
|
import { tooltipStyles } from './Tooltip.styles';
|
|
4
4
|
import type { TooltipProps } from './types';
|
|
5
|
+
import { getNativeAccessibilityProps } from '../utils/accessibility';
|
|
5
6
|
|
|
6
7
|
const Tooltip = forwardRef<View, TooltipProps>(({
|
|
7
8
|
content,
|
|
@@ -13,7 +14,23 @@ const Tooltip = forwardRef<View, TooltipProps>(({
|
|
|
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 native accessibility props
|
|
25
|
+
const nativeA11yProps = useMemo(() => {
|
|
26
|
+
return getNativeAccessibilityProps({
|
|
27
|
+
accessibilityLabel,
|
|
28
|
+
accessibilityHint: accessibilityHint ?? (typeof content === 'string' ? content : undefined),
|
|
29
|
+
accessibilityDisabled,
|
|
30
|
+
accessibilityHidden,
|
|
31
|
+
accessibilityRole: accessibilityRole ?? 'none',
|
|
32
|
+
});
|
|
33
|
+
}, [accessibilityLabel, accessibilityHint, content, accessibilityDisabled, accessibilityHidden, accessibilityRole]);
|
|
17
34
|
const [visible, setVisible] = useState(false);
|
|
18
35
|
const [tooltipPosition, setTooltipPosition] = useState({ top: 0, left: 0, width: 0 });
|
|
19
36
|
const triggerRef = useRef<any>(null);
|
|
@@ -127,7 +144,7 @@ const Tooltip = forwardRef<View, TooltipProps>(({
|
|
|
127
144
|
|
|
128
145
|
return (
|
|
129
146
|
<>
|
|
130
|
-
<View ref={ref} nativeID={id} collapsable={false} style={style}>
|
|
147
|
+
<View ref={ref} nativeID={id} collapsable={false} style={style} {...nativeA11yProps}>
|
|
131
148
|
{trigger}
|
|
132
149
|
</View>
|
|
133
150
|
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import React, { useState, useRef, useEffect } from 'react';
|
|
1
|
+
import React, { useState, useRef, useEffect, useMemo } from 'react';
|
|
2
2
|
import { getWebProps } from 'react-native-unistyles/web';
|
|
3
3
|
import { tooltipStyles } from './Tooltip.styles';
|
|
4
4
|
import type { TooltipProps } from './types';
|
|
5
5
|
import { PositionedPortal } from '../internal/PositionedPortal';
|
|
6
|
+
import { getWebAriaProps, generateAccessibilityId } from '../utils/accessibility';
|
|
6
7
|
|
|
7
8
|
const Tooltip: React.FC<TooltipProps> = ({
|
|
8
9
|
content,
|
|
@@ -14,11 +15,32 @@ const Tooltip: React.FC<TooltipProps> = ({
|
|
|
14
15
|
style,
|
|
15
16
|
testID,
|
|
16
17
|
id,
|
|
18
|
+
// Accessibility props
|
|
19
|
+
accessibilityLabel,
|
|
20
|
+
accessibilityHint,
|
|
21
|
+
accessibilityDisabled,
|
|
22
|
+
accessibilityHidden,
|
|
23
|
+
accessibilityRole,
|
|
17
24
|
}) => {
|
|
18
25
|
const [visible, setVisible] = useState(false);
|
|
19
26
|
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
20
27
|
const anchorRef = useRef<HTMLDivElement>(null);
|
|
21
28
|
|
|
29
|
+
// Generate unique ID for tooltip
|
|
30
|
+
const tooltipId = useMemo(() => id ? `${id}-tooltip` : generateAccessibilityId('tooltip'), [id]);
|
|
31
|
+
const triggerId = useMemo(() => id || generateAccessibilityId('tooltip-trigger'), [id]);
|
|
32
|
+
|
|
33
|
+
// Generate ARIA props for trigger
|
|
34
|
+
const ariaProps = useMemo(() => {
|
|
35
|
+
return getWebAriaProps({
|
|
36
|
+
accessibilityLabel,
|
|
37
|
+
accessibilityHint,
|
|
38
|
+
accessibilityDisabled,
|
|
39
|
+
accessibilityHidden,
|
|
40
|
+
accessibilityRole,
|
|
41
|
+
});
|
|
42
|
+
}, [accessibilityLabel, accessibilityHint, accessibilityDisabled, accessibilityHidden, accessibilityRole]);
|
|
43
|
+
|
|
22
44
|
// Apply variants - PositionedPortal handles positioning and visibility
|
|
23
45
|
tooltipStyles.useVariants({
|
|
24
46
|
size,
|
|
@@ -46,6 +68,28 @@ const Tooltip: React.FC<TooltipProps> = ({
|
|
|
46
68
|
setVisible(false);
|
|
47
69
|
};
|
|
48
70
|
|
|
71
|
+
// Keyboard accessibility - show on focus, hide on blur
|
|
72
|
+
const handleFocus = () => {
|
|
73
|
+
if (timeoutRef.current) {
|
|
74
|
+
clearTimeout(timeoutRef.current);
|
|
75
|
+
}
|
|
76
|
+
setVisible(true);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const handleBlur = () => {
|
|
80
|
+
if (timeoutRef.current) {
|
|
81
|
+
clearTimeout(timeoutRef.current);
|
|
82
|
+
}
|
|
83
|
+
setVisible(false);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// Handle Escape key to dismiss tooltip
|
|
87
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
88
|
+
if (e.key === 'Escape' && visible) {
|
|
89
|
+
setVisible(false);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
49
93
|
useEffect(() => {
|
|
50
94
|
return () => {
|
|
51
95
|
if (timeoutRef.current) {
|
|
@@ -59,9 +103,15 @@ const Tooltip: React.FC<TooltipProps> = ({
|
|
|
59
103
|
<div
|
|
60
104
|
ref={anchorRef}
|
|
61
105
|
{...containerProps}
|
|
106
|
+
{...ariaProps}
|
|
107
|
+
id={triggerId}
|
|
62
108
|
onMouseEnter={handleMouseEnter}
|
|
63
109
|
onMouseLeave={handleMouseLeave}
|
|
64
|
-
|
|
110
|
+
onFocus={handleFocus}
|
|
111
|
+
onBlur={handleBlur}
|
|
112
|
+
onKeyDown={handleKeyDown}
|
|
113
|
+
tabIndex={0}
|
|
114
|
+
aria-describedby={visible ? tooltipId : undefined}
|
|
65
115
|
data-testid={testID}
|
|
66
116
|
>
|
|
67
117
|
{children}
|
|
@@ -76,7 +126,9 @@ const Tooltip: React.FC<TooltipProps> = ({
|
|
|
76
126
|
>
|
|
77
127
|
<div
|
|
78
128
|
{...tooltipContentProps}
|
|
129
|
+
id={tooltipId}
|
|
79
130
|
role="tooltip"
|
|
131
|
+
aria-hidden={!visible}
|
|
80
132
|
data-testid={`${testID}-tooltip`}
|
|
81
133
|
>
|
|
82
134
|
{content}
|
package/src/Tooltip/types.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
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 { AccessibilityProps } from '../utils/accessibility';
|
|
4
5
|
|
|
5
6
|
// Component-specific type aliases for future extensibility
|
|
6
7
|
export type TooltipIntentVariant = Intent;
|
|
7
8
|
export type TooltipSizeVariant = Size;
|
|
8
9
|
export type TooltipPlacement = 'top' | 'bottom' | 'left' | 'right';
|
|
9
10
|
|
|
10
|
-
export interface TooltipProps extends BaseProps {
|
|
11
|
+
export interface TooltipProps extends BaseProps, AccessibilityProps {
|
|
11
12
|
content: string | React.ReactNode;
|
|
12
13
|
children: React.ReactNode;
|
|
13
14
|
placement?: TooltipPlacement;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
2
|
import { View, StyleSheet } from 'react-native';
|
|
3
3
|
import { videoStyles } from './Video.styles';
|
|
4
4
|
import type { VideoProps, VideoSource } from './types';
|
|
5
|
+
import { getNativeAccessibilityProps } from '../utils/accessibility';
|
|
5
6
|
|
|
6
7
|
// Import react-native-video - it's a peer dependency
|
|
7
8
|
let RNVideo: any;
|
|
@@ -33,11 +34,25 @@ const Video = React.forwardRef<View, VideoProps>(({
|
|
|
33
34
|
style,
|
|
34
35
|
testID,
|
|
35
36
|
id,
|
|
37
|
+
// Accessibility props
|
|
38
|
+
accessibilityLabel,
|
|
39
|
+
accessibilityHint,
|
|
40
|
+
accessibilityRole,
|
|
41
|
+
accessibilityHidden,
|
|
36
42
|
}, ref) => {
|
|
43
|
+
// Generate native accessibility props
|
|
44
|
+
const nativeA11yProps = useMemo(() => {
|
|
45
|
+
return getNativeAccessibilityProps({
|
|
46
|
+
accessibilityLabel: accessibilityLabel ?? 'Video player',
|
|
47
|
+
accessibilityHint,
|
|
48
|
+
accessibilityRole: accessibilityRole ?? 'none',
|
|
49
|
+
accessibilityHidden,
|
|
50
|
+
});
|
|
51
|
+
}, [accessibilityLabel, accessibilityHint, accessibilityRole, accessibilityHidden]);
|
|
37
52
|
|
|
38
53
|
if (!RNVideo) {
|
|
39
54
|
return (
|
|
40
|
-
<View ref={ref} nativeID={id} style={[videoStyles.container, { aspectRatio, borderRadius }, style]} testID={testID}>
|
|
55
|
+
<View ref={ref} nativeID={id} style={[videoStyles.container, { aspectRatio, borderRadius }, style]} testID={testID} {...nativeA11yProps}>
|
|
41
56
|
<View style={videoStyles.fallback}>
|
|
42
57
|
{/* Fallback when react-native-video is not installed */}
|
|
43
58
|
</View>
|
|
@@ -80,7 +95,7 @@ const Video = React.forwardRef<View, VideoProps>(({
|
|
|
80
95
|
};
|
|
81
96
|
|
|
82
97
|
return (
|
|
83
|
-
<View ref={ref} nativeID={id} style={containerStyle} testID={testID}>
|
|
98
|
+
<View ref={ref} nativeID={id} style={containerStyle} testID={testID} {...nativeA11yProps}>
|
|
84
99
|
<RNVideo
|
|
85
100
|
source={videoSource}
|
|
86
101
|
poster={poster}
|
package/src/Video/Video.web.tsx
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import React, { useRef } from 'react';
|
|
1
|
+
import React, { useRef, useMemo } from 'react';
|
|
2
2
|
import { getWebProps } from 'react-native-unistyles/web';
|
|
3
3
|
import { videoStyles } from './Video.styles';
|
|
4
4
|
import type { VideoProps, VideoSource } from './types';
|
|
5
5
|
import useMergeRefs from '../hooks/useMergeRefs';
|
|
6
|
+
import { getWebAriaProps } from '../utils/accessibility';
|
|
6
7
|
|
|
7
8
|
const Video: React.FC<VideoProps> = ({
|
|
8
9
|
source,
|
|
@@ -26,7 +27,21 @@ const Video: React.FC<VideoProps> = ({
|
|
|
26
27
|
style,
|
|
27
28
|
testID,
|
|
28
29
|
id,
|
|
30
|
+
// Accessibility props
|
|
31
|
+
accessibilityLabel,
|
|
32
|
+
accessibilityHint,
|
|
33
|
+
accessibilityRole,
|
|
34
|
+
accessibilityHidden,
|
|
29
35
|
}) => {
|
|
36
|
+
// Generate ARIA props
|
|
37
|
+
const ariaProps = useMemo(() => {
|
|
38
|
+
return getWebAriaProps({
|
|
39
|
+
accessibilityLabel: accessibilityLabel ?? 'Video player',
|
|
40
|
+
accessibilityHint,
|
|
41
|
+
accessibilityRole,
|
|
42
|
+
accessibilityHidden,
|
|
43
|
+
});
|
|
44
|
+
}, [accessibilityLabel, accessibilityHint, accessibilityRole, accessibilityHidden]);
|
|
30
45
|
const videoRef = useRef<HTMLVideoElement>(null);
|
|
31
46
|
|
|
32
47
|
const containerProps = getWebProps([videoStyles.container, style as any]);
|
|
@@ -85,6 +100,7 @@ const Video: React.FC<VideoProps> = ({
|
|
|
85
100
|
return (
|
|
86
101
|
<div
|
|
87
102
|
{...containerProps}
|
|
103
|
+
{...ariaProps}
|
|
88
104
|
style={containerStyle}
|
|
89
105
|
id={id}
|
|
90
106
|
data-testid={testID}
|
package/src/Video/types.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import type { StyleProp, ViewStyle } from 'react-native';
|
|
2
2
|
import { BaseProps } from '../utils/viewStyleProps';
|
|
3
|
+
import { AccessibilityProps } from '../utils/accessibility';
|
|
3
4
|
|
|
4
5
|
export interface VideoSource {
|
|
5
6
|
uri: string;
|
|
6
7
|
type?: string;
|
|
7
8
|
}
|
|
8
9
|
|
|
9
|
-
export interface VideoProps extends BaseProps {
|
|
10
|
+
export interface VideoProps extends BaseProps, AccessibilityProps {
|
|
10
11
|
source: VideoSource | string;
|
|
11
12
|
poster?: string;
|
|
12
13
|
width?: number | string;
|
|
@@ -186,6 +186,59 @@ export const InputExamples = () => {
|
|
|
186
186
|
/>
|
|
187
187
|
</View>
|
|
188
188
|
</View>
|
|
189
|
+
|
|
190
|
+
{/* Accessibility Examples */}
|
|
191
|
+
<View gap="md">
|
|
192
|
+
<Text typography="subtitle1">Accessibility</Text>
|
|
193
|
+
<View gap="sm" style={{ gap: 10 }}>
|
|
194
|
+
{/* Basic accessible input with label */}
|
|
195
|
+
<Input
|
|
196
|
+
leftIcon="email"
|
|
197
|
+
value={emailValue}
|
|
198
|
+
onChangeText={setEmailValue}
|
|
199
|
+
placeholder="Email address"
|
|
200
|
+
inputType="email"
|
|
201
|
+
accessibilityLabel="Email address"
|
|
202
|
+
accessibilityRequired
|
|
203
|
+
/>
|
|
204
|
+
<Text id="email-helper" typography="caption" color="muted">
|
|
205
|
+
Enter your work email address
|
|
206
|
+
</Text>
|
|
207
|
+
|
|
208
|
+
{/* Input with error state and accessible error association */}
|
|
209
|
+
<Input
|
|
210
|
+
leftIcon="lock"
|
|
211
|
+
value={passwordValue}
|
|
212
|
+
onChangeText={setPasswordValue}
|
|
213
|
+
placeholder="Password"
|
|
214
|
+
inputType="password"
|
|
215
|
+
accessibilityLabel="Password"
|
|
216
|
+
accessibilityDescribedBy="password-helper"
|
|
217
|
+
accessibilityInvalid={passwordValue.length > 0 && passwordValue.length < 8}
|
|
218
|
+
accessibilityRequired
|
|
219
|
+
hasError={passwordValue.length > 0 && passwordValue.length < 8}
|
|
220
|
+
/>
|
|
221
|
+
<Text
|
|
222
|
+
id="password-helper"
|
|
223
|
+
typography="caption"
|
|
224
|
+
color={passwordValue.length > 0 && passwordValue.length < 8 ? 'error' : 'muted'}
|
|
225
|
+
>
|
|
226
|
+
{passwordValue.length > 0 && passwordValue.length < 8
|
|
227
|
+
? 'Password must be at least 8 characters'
|
|
228
|
+
: 'Enter a secure password'}
|
|
229
|
+
</Text>
|
|
230
|
+
|
|
231
|
+
{/* Disabled input with accessibility indication */}
|
|
232
|
+
<Input
|
|
233
|
+
leftIcon="account"
|
|
234
|
+
value="readonly@example.com"
|
|
235
|
+
placeholder="Readonly input"
|
|
236
|
+
disabled
|
|
237
|
+
accessibilityLabel="Email (read-only)"
|
|
238
|
+
accessibilityDisabled
|
|
239
|
+
/>
|
|
240
|
+
</View>
|
|
241
|
+
</View>
|
|
189
242
|
</View>
|
|
190
243
|
</Screen>
|
|
191
244
|
);
|
|
@@ -134,6 +134,40 @@ export const ListExamples: React.FC = () => {
|
|
|
134
134
|
</List>
|
|
135
135
|
</View>
|
|
136
136
|
|
|
137
|
+
<View gap="md">
|
|
138
|
+
<Text typography="h5">Custom Icon Colors</Text>
|
|
139
|
+
<List type="bordered">
|
|
140
|
+
<ListItem
|
|
141
|
+
label="Primary Color"
|
|
142
|
+
leading="home"
|
|
143
|
+
trailing="chevron-right"
|
|
144
|
+
iconColor="primary"
|
|
145
|
+
onPress={() => {}}
|
|
146
|
+
/>
|
|
147
|
+
<ListItem
|
|
148
|
+
label="Success Color"
|
|
149
|
+
leading="check-circle"
|
|
150
|
+
trailing="chevron-right"
|
|
151
|
+
iconColor="success"
|
|
152
|
+
onPress={() => {}}
|
|
153
|
+
/>
|
|
154
|
+
<ListItem
|
|
155
|
+
label="Error Color"
|
|
156
|
+
leading="alert-circle"
|
|
157
|
+
trailing="chevron-right"
|
|
158
|
+
iconColor="error"
|
|
159
|
+
onPress={() => {}}
|
|
160
|
+
/>
|
|
161
|
+
<ListItem
|
|
162
|
+
label="Warning Color"
|
|
163
|
+
leading="alert"
|
|
164
|
+
trailing="chevron-right"
|
|
165
|
+
iconColor="warning"
|
|
166
|
+
onPress={() => {}}
|
|
167
|
+
/>
|
|
168
|
+
</List>
|
|
169
|
+
</View>
|
|
170
|
+
|
|
137
171
|
<View gap="md">
|
|
138
172
|
<Text typography="h5">Navigation Sidebar</Text>
|
|
139
173
|
<List type="bordered">
|