@utilitywarehouse/hearth-react-native 0.32.0 → 0.32.2
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/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-lint.log +15 -18
- package/CHANGELOG.md +30 -1
- package/build/components/Badge/Badge.js +2 -2
- package/build/components/Card/Card.props.d.ts +1 -1
- package/build/components/Card/CardRoot.js +19 -0
- package/build/components/Input/Input.js +13 -31
- package/build/components/StepperInput/StepperInput.js +12 -29
- package/build/components/Tabs/Tab.js +2 -2
- package/build/components/Textarea/Textarea.js +12 -30
- package/build/components/VerificationInput/VerificationInput.js +12 -22
- package/build/hooks/index.d.ts +1 -0
- package/build/hooks/index.js +1 -0
- package/build/hooks/useFormFieldAccessibility.d.ts +17 -0
- package/build/hooks/useFormFieldAccessibility.js +32 -0
- package/build/hooks/useFormFieldAccessibility.test.d.ts +1 -0
- package/build/hooks/useFormFieldAccessibility.test.js +56 -0
- package/docs/changelog.mdx +84 -0
- package/package.json +11 -9
- package/src/components/Badge/Badge.tsx +2 -2
- package/src/components/Banner/Banner.stories.tsx +14 -0
- package/src/components/Card/Card.docs.mdx +16 -17
- package/src/components/Card/Card.props.ts +1 -0
- package/src/components/Card/Card.stories.tsx +35 -21
- package/src/components/Card/CardRoot.tsx +19 -0
- package/src/components/Icon/Icon.docs.mdx +1 -1
- package/src/components/Input/Input.tsx +14 -35
- package/src/components/List/List.docs.mdx +4 -2
- package/src/components/StepperInput/StepperInput.tsx +13 -40
- package/src/components/Tabs/Tab.tsx +2 -2
- package/src/components/Textarea/Textarea.tsx +13 -34
- package/src/components/VerificationInput/VerificationInput.tsx +13 -25
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useFormFieldAccessibility.test.tsx +74 -0
- package/src/hooks/useFormFieldAccessibility.ts +67 -0
|
@@ -3,6 +3,7 @@ import { useEffect, useImperativeHandle, useRef, useState } from 'react';
|
|
|
3
3
|
import type { TextInput, TextInputFocusEvent } from 'react-native';
|
|
4
4
|
import { View } from 'react-native';
|
|
5
5
|
import { StyleSheet } from 'react-native-unistyles';
|
|
6
|
+
import { useFormFieldAccessibility } from '../../hooks';
|
|
6
7
|
import { FormField } from '../FormField';
|
|
7
8
|
import { InputComponent, InputField } from '../Input/Input';
|
|
8
9
|
import StepperButton from './StepperButton';
|
|
@@ -143,6 +144,16 @@ const StepperInput = ({
|
|
|
143
144
|
const allowDecimal = decimalPrecision > 0;
|
|
144
145
|
const keyboardType = allowNegative || allowDecimal ? 'numeric' : 'number-pad';
|
|
145
146
|
const inputMode = allowDecimal ? 'decimal' : 'numeric';
|
|
147
|
+
const { accessibilityHint, accessibilityLabel } = useFormFieldAccessibility({
|
|
148
|
+
label,
|
|
149
|
+
helperText,
|
|
150
|
+
validText,
|
|
151
|
+
invalidText,
|
|
152
|
+
required,
|
|
153
|
+
validationStatus,
|
|
154
|
+
fallbackLabel: props.accessibilityLabel,
|
|
155
|
+
fallbackHint: props.accessibilityHint,
|
|
156
|
+
});
|
|
146
157
|
|
|
147
158
|
useImperativeHandle(ref, () => inputRef.current as TextInput, []);
|
|
148
159
|
|
|
@@ -216,44 +227,6 @@ const StepperInput = ({
|
|
|
216
227
|
onBlur?.(event);
|
|
217
228
|
};
|
|
218
229
|
|
|
219
|
-
const getAccessibilityLabel = () => {
|
|
220
|
-
let accessibilityLabel = '';
|
|
221
|
-
|
|
222
|
-
if (label) {
|
|
223
|
-
accessibilityLabel = accessibilityLabel + label;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
if (required) {
|
|
227
|
-
accessibilityLabel = accessibilityLabel + ', required';
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
return accessibilityLabel || props.accessibilityLabel;
|
|
231
|
-
};
|
|
232
|
-
|
|
233
|
-
const getAccessibilityHint = () => {
|
|
234
|
-
let accessibilityHint = '';
|
|
235
|
-
|
|
236
|
-
if (helperText) {
|
|
237
|
-
accessibilityHint = accessibilityHint + helperText;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
if (validationStatus !== 'initial') {
|
|
241
|
-
if (accessibilityHint.length > 0) {
|
|
242
|
-
accessibilityHint = accessibilityHint + ', ';
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
if (validationStatus === 'invalid' && invalidText) {
|
|
246
|
-
accessibilityHint = accessibilityHint + invalidText;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
if (validationStatus === 'valid' && validText) {
|
|
250
|
-
accessibilityHint = accessibilityHint + validText;
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
return accessibilityHint || props.accessibilityHint;
|
|
255
|
-
};
|
|
256
|
-
|
|
257
230
|
return (
|
|
258
231
|
<FormField
|
|
259
232
|
label={label}
|
|
@@ -297,8 +270,8 @@ const StepperInput = ({
|
|
|
297
270
|
onFocus={handleFocus}
|
|
298
271
|
onBlur={handleBlur}
|
|
299
272
|
onChangeText={handleChangeText}
|
|
300
|
-
accessibilityLabel={
|
|
301
|
-
accessibilityHint={
|
|
273
|
+
accessibilityLabel={accessibilityLabel}
|
|
274
|
+
accessibilityHint={accessibilityHint}
|
|
302
275
|
accessibilityState={{
|
|
303
276
|
...(props.accessibilityState ?? {}),
|
|
304
277
|
disabled: disabled || readonly,
|
|
@@ -87,8 +87,8 @@ const styles = StyleSheet.create(theme => ({
|
|
|
87
87
|
},
|
|
88
88
|
variants: {
|
|
89
89
|
size: {
|
|
90
|
-
md: {
|
|
91
|
-
lg: {
|
|
90
|
+
md: { minHeight: theme.components.tabs.md.height },
|
|
91
|
+
lg: { minHeight: theme.components.tabs.lg.height },
|
|
92
92
|
},
|
|
93
93
|
pressed: {
|
|
94
94
|
true: {
|
|
@@ -13,7 +13,7 @@ import { Gesture, GestureDetector } from 'react-native-gesture-handler';
|
|
|
13
13
|
import { useAnimatedStyle, useSharedValue } from 'react-native-reanimated';
|
|
14
14
|
import { Path, Svg } from 'react-native-svg';
|
|
15
15
|
import { StyleSheet } from 'react-native-unistyles';
|
|
16
|
-
import { useTheme } from '../../hooks';
|
|
16
|
+
import { useFormFieldAccessibility, useTheme } from '../../hooks';
|
|
17
17
|
import { FormField, useFormFieldContext } from '../FormField';
|
|
18
18
|
import TextareaFieldComponent from './TextareaField';
|
|
19
19
|
import TextareaRoot from './TextareaRoot';
|
|
@@ -61,6 +61,16 @@ const Textarea = ({
|
|
|
61
61
|
const textareaHeight = useSharedValue(textareaDefaultHeight);
|
|
62
62
|
const resizeStartHeight = useSharedValue(textareaDefaultHeight);
|
|
63
63
|
const theme = useTheme();
|
|
64
|
+
const { accessibilityHint, accessibilityLabel } = useFormFieldAccessibility({
|
|
65
|
+
label: textareaLabel,
|
|
66
|
+
helperText: textareaHelperText,
|
|
67
|
+
validText: textareaValidText,
|
|
68
|
+
invalidText: textareaInvalidText,
|
|
69
|
+
required: textareaRequired,
|
|
70
|
+
validationStatus: textareaValidationStatus,
|
|
71
|
+
fallbackLabel: props.accessibilityLabel,
|
|
72
|
+
fallbackHint: props.accessibilityHint,
|
|
73
|
+
});
|
|
64
74
|
|
|
65
75
|
useEffect(() => {
|
|
66
76
|
if (formFieldContext?.setShouldHandleAccessibility) {
|
|
@@ -75,37 +85,6 @@ const Textarea = ({
|
|
|
75
85
|
}
|
|
76
86
|
}, [resizeStartHeight, textareaDefaultHeight, textareaHeight]);
|
|
77
87
|
|
|
78
|
-
const getAccessibilityLabel = () => {
|
|
79
|
-
let accessibilityLabel = '';
|
|
80
|
-
if (textareaLabel) {
|
|
81
|
-
accessibilityLabel = accessibilityLabel + textareaLabel;
|
|
82
|
-
}
|
|
83
|
-
if (textareaRequired) {
|
|
84
|
-
accessibilityLabel = accessibilityLabel + ', required';
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return accessibilityLabel || props.accessibilityLabel;
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
const getAccessibilityHint = () => {
|
|
91
|
-
let accessibilityHint = '';
|
|
92
|
-
if (textareaHelperText) {
|
|
93
|
-
accessibilityHint = accessibilityHint + textareaHelperText;
|
|
94
|
-
}
|
|
95
|
-
if (textareaValidationStatus !== 'initial') {
|
|
96
|
-
if (accessibilityHint.length > 0) {
|
|
97
|
-
accessibilityHint = accessibilityHint + ', ';
|
|
98
|
-
}
|
|
99
|
-
if (textareaValidationStatus === 'invalid' && textareaInvalidText) {
|
|
100
|
-
accessibilityHint = accessibilityHint + textareaInvalidText;
|
|
101
|
-
}
|
|
102
|
-
if (textareaValidationStatus === 'valid' && textareaValidText) {
|
|
103
|
-
accessibilityHint = accessibilityHint + textareaValidText;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
return accessibilityHint || props.accessibilityHint;
|
|
107
|
-
};
|
|
108
|
-
|
|
109
88
|
const handleTextareaLayout = (event: LayoutChangeEvent) => {
|
|
110
89
|
if (!hasMeasuredHeight.current) {
|
|
111
90
|
textareaHeight.value = event.nativeEvent.layout.height;
|
|
@@ -171,8 +150,8 @@ const Textarea = ({
|
|
|
171
150
|
isDisabled={textareaDisabled}
|
|
172
151
|
isFocused={focused}
|
|
173
152
|
required={textareaRequired}
|
|
174
|
-
aria-label={
|
|
175
|
-
accessibilityHint={
|
|
153
|
+
aria-label={accessibilityLabel}
|
|
154
|
+
accessibilityHint={accessibilityHint}
|
|
176
155
|
>
|
|
177
156
|
{children ? (
|
|
178
157
|
<>{children}</>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
|
2
2
|
import { TextInput, View } from 'react-native';
|
|
3
3
|
import { StyleSheet } from 'react-native-unistyles';
|
|
4
|
+
import { useFormFieldAccessibility } from '../../hooks';
|
|
4
5
|
import { FormField } from '../FormField';
|
|
5
6
|
import type { VerificationInputHandle, VerificationInputProps } from './VerificationInput.props';
|
|
6
7
|
import { getNextIndexFromValueChange } from './VerificationInput.utils';
|
|
@@ -163,29 +164,16 @@ const VerificationInput = forwardRef<VerificationInputHandle, VerificationInputP
|
|
|
163
164
|
);
|
|
164
165
|
|
|
165
166
|
const slots = Array.from({ length }, (_, index) => index);
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
if (validationStatus !== 'initial') {
|
|
177
|
-
if (accessibilityHint.length > 0) {
|
|
178
|
-
accessibilityHint = accessibilityHint + ', ';
|
|
179
|
-
}
|
|
180
|
-
if (validationStatus === 'invalid' && invalidText) {
|
|
181
|
-
accessibilityHint = accessibilityHint + invalidText;
|
|
182
|
-
}
|
|
183
|
-
if (validationStatus === 'valid' && validText) {
|
|
184
|
-
accessibilityHint = accessibilityHint + validText;
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
return accessibilityHint || props.accessibilityHint;
|
|
188
|
-
};
|
|
167
|
+
const { accessibilityHint, accessibilityLabel } = useFormFieldAccessibility({
|
|
168
|
+
label,
|
|
169
|
+
helperText,
|
|
170
|
+
validText,
|
|
171
|
+
invalidText,
|
|
172
|
+
validationStatus,
|
|
173
|
+
fallbackLabel: props.accessibilityLabel,
|
|
174
|
+
fallbackHint: props.accessibilityHint,
|
|
175
|
+
includeRequiredInLabel: false,
|
|
176
|
+
});
|
|
189
177
|
|
|
190
178
|
return (
|
|
191
179
|
<FormField
|
|
@@ -208,8 +196,8 @@ const VerificationInput = forwardRef<VerificationInputHandle, VerificationInputP
|
|
|
208
196
|
value={displayValue}
|
|
209
197
|
autoFocus={autoFocus}
|
|
210
198
|
editable={!disabled && !readonly}
|
|
211
|
-
accessibilityLabel={
|
|
212
|
-
accessibilityHint={
|
|
199
|
+
accessibilityLabel={accessibilityLabel}
|
|
200
|
+
accessibilityHint={accessibilityHint}
|
|
213
201
|
accessibilityState={{ disabled: disabled || readonly }}
|
|
214
202
|
importantForAccessibility="yes"
|
|
215
203
|
onChangeText={handleChangeText}
|
package/src/hooks/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { default as useBreakpointValue } from './useBreakpointValue';
|
|
2
2
|
export { default as useColorMode } from './useColorMode';
|
|
3
|
+
export { default as useFormFieldAccessibility } from './useFormFieldAccessibility';
|
|
3
4
|
export { default as useMedia } from './useMedia';
|
|
4
5
|
export { usePrevious } from './usePrevious';
|
|
5
6
|
export { useStyleProps } from './useStyleProps';
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { renderToStaticMarkup } from 'react-dom/server';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import useFormFieldAccessibility from './useFormFieldAccessibility';
|
|
4
|
+
|
|
5
|
+
type HookArgs = Parameters<typeof useFormFieldAccessibility>[0];
|
|
6
|
+
|
|
7
|
+
const renderHook = (args: HookArgs) => {
|
|
8
|
+
let result: ReturnType<typeof useFormFieldAccessibility> | undefined;
|
|
9
|
+
|
|
10
|
+
const TestComponent = () => {
|
|
11
|
+
result = useFormFieldAccessibility(args);
|
|
12
|
+
return null;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
renderToStaticMarkup(<TestComponent />);
|
|
16
|
+
|
|
17
|
+
if (!result) {
|
|
18
|
+
throw new Error('Hook did not return a result');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return result;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
describe('useFormFieldAccessibility', () => {
|
|
25
|
+
it('builds a label from the field label and required state', () => {
|
|
26
|
+
expect(
|
|
27
|
+
renderHook({
|
|
28
|
+
label: 'Email',
|
|
29
|
+
required: true,
|
|
30
|
+
})
|
|
31
|
+
).toEqual({
|
|
32
|
+
accessibilityHint: undefined,
|
|
33
|
+
accessibilityLabel: 'Email, required',
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('builds a hint from helper text and validation feedback', () => {
|
|
38
|
+
expect(
|
|
39
|
+
renderHook({
|
|
40
|
+
helperText: 'Enter the code we sent you',
|
|
41
|
+
validationStatus: 'invalid',
|
|
42
|
+
invalidText: 'Code is invalid',
|
|
43
|
+
})
|
|
44
|
+
).toEqual({
|
|
45
|
+
accessibilityHint: 'Enter the code we sent you, Code is invalid',
|
|
46
|
+
accessibilityLabel: undefined,
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('falls back to provided accessibility props when no composed value exists', () => {
|
|
51
|
+
expect(
|
|
52
|
+
renderHook({
|
|
53
|
+
fallbackLabel: 'Custom label',
|
|
54
|
+
fallbackHint: 'Custom hint',
|
|
55
|
+
})
|
|
56
|
+
).toEqual({
|
|
57
|
+
accessibilityHint: 'Custom hint',
|
|
58
|
+
accessibilityLabel: 'Custom label',
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('supports omitting the required suffix from the label', () => {
|
|
63
|
+
expect(
|
|
64
|
+
renderHook({
|
|
65
|
+
label: 'Verification code',
|
|
66
|
+
required: true,
|
|
67
|
+
includeRequiredInLabel: false,
|
|
68
|
+
})
|
|
69
|
+
).toEqual({
|
|
70
|
+
accessibilityHint: undefined,
|
|
71
|
+
accessibilityLabel: 'Verification code',
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
|
|
3
|
+
type ValidationStatus = 'initial' | 'valid' | 'invalid';
|
|
4
|
+
|
|
5
|
+
type UseFormFieldAccessibilityArgs = {
|
|
6
|
+
label?: string;
|
|
7
|
+
helperText?: string;
|
|
8
|
+
validText?: string;
|
|
9
|
+
invalidText?: string;
|
|
10
|
+
required?: boolean;
|
|
11
|
+
validationStatus?: ValidationStatus;
|
|
12
|
+
fallbackLabel?: string;
|
|
13
|
+
fallbackHint?: string;
|
|
14
|
+
includeRequiredInLabel?: boolean;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const useFormFieldAccessibility = ({
|
|
18
|
+
label,
|
|
19
|
+
helperText,
|
|
20
|
+
validText,
|
|
21
|
+
invalidText,
|
|
22
|
+
required = false,
|
|
23
|
+
validationStatus = 'initial',
|
|
24
|
+
fallbackLabel,
|
|
25
|
+
fallbackHint,
|
|
26
|
+
includeRequiredInLabel = true,
|
|
27
|
+
}: UseFormFieldAccessibilityArgs) => {
|
|
28
|
+
const accessibilityLabel = useMemo(() => {
|
|
29
|
+
const nextAccessibilityLabelParts: string[] = [];
|
|
30
|
+
|
|
31
|
+
if (label) {
|
|
32
|
+
nextAccessibilityLabelParts.push(label);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (includeRequiredInLabel && required) {
|
|
36
|
+
nextAccessibilityLabelParts.push('required');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const nextAccessibilityLabel = nextAccessibilityLabelParts.join(', ');
|
|
40
|
+
return nextAccessibilityLabel || fallbackLabel;
|
|
41
|
+
}, [fallbackLabel, includeRequiredInLabel, label, required]);
|
|
42
|
+
|
|
43
|
+
const accessibilityHint = useMemo(() => {
|
|
44
|
+
const accessibilityHints: string[] = [];
|
|
45
|
+
|
|
46
|
+
if (helperText) {
|
|
47
|
+
accessibilityHints.push(helperText);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (validationStatus === 'invalid' && invalidText) {
|
|
51
|
+
accessibilityHints.push(invalidText);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (validationStatus === 'valid' && validText) {
|
|
55
|
+
accessibilityHints.push(validText);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return accessibilityHints.join(', ') || fallbackHint;
|
|
59
|
+
}, [fallbackHint, helperText, invalidText, validText, validationStatus]);
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
accessibilityHint,
|
|
63
|
+
accessibilityLabel,
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export default useFormFieldAccessibility;
|