@idealyst/components 1.1.3 → 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,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={`${testID}-error`}>
216
+ <span {...helperTextProps} id={errorId} role="alert">
141
217
  {error}
142
218
  </span>
143
219
  )}
144
220
  {!error && helperText && (
145
- <span {...helperTextProps} id={`${testID}-helper`}>
221
+ <span {...helperTextProps} id={helperId}>
146
222
  {helperText}
147
223
  </span>
148
224
  )}
@@ -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
- id={id}
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}
@@ -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}
@@ -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}
@@ -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">
@@ -0,0 +1,2 @@
1
+ // Internal components - not part of public API, but exported for use by other @idealyst packages
2
+ export { PositionedPortal } from './PositionedPortal';