@utilitywarehouse/hearth-react-native 0.31.1 → 0.32.1
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 +62 -0
- 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/Rating/Rating.d.ts +6 -0
- package/build/components/Rating/Rating.js +76 -0
- package/build/components/Rating/Rating.props.d.ts +18 -0
- package/build/components/Rating/Rating.props.js +1 -0
- package/build/components/Rating/RatingStarEmpty.d.ts +6 -0
- package/build/components/Rating/RatingStarEmpty.js +9 -0
- package/build/components/Rating/RatingStarFilled.d.ts +6 -0
- package/build/components/Rating/RatingStarFilled.js +9 -0
- package/build/components/Rating/index.d.ts +2 -0
- package/build/components/Rating/index.js +1 -0
- package/build/components/Roundel/Roundel.d.ts +6 -0
- package/build/components/Roundel/Roundel.js +40 -0
- package/build/components/Roundel/Roundel.props.d.ts +6 -0
- package/build/components/Roundel/Roundel.props.js +1 -0
- package/build/components/Roundel/index.d.ts +2 -0
- package/build/components/Roundel/index.js +1 -0
- package/build/components/StepperInput/StepperButton.d.ts +22 -0
- package/build/components/StepperInput/StepperButton.js +55 -0
- package/build/components/StepperInput/StepperInput.d.ts +6 -0
- package/build/components/StepperInput/StepperInput.js +179 -0
- package/build/components/StepperInput/StepperInput.props.d.ts +31 -0
- package/build/components/StepperInput/StepperInput.props.js +1 -0
- package/build/components/StepperInput/index.d.ts +2 -0
- package/build/components/StepperInput/index.js +1 -0
- package/build/components/Textarea/Textarea.d.ts +1 -1
- package/build/components/Textarea/Textarea.js +21 -32
- package/build/components/Textarea/Textarea.props.d.ts +11 -0
- package/build/components/VerificationInput/VerificationInput.js +12 -22
- package/build/components/index.d.ts +3 -0
- package/build/components/index.js +3 -0
- 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/adding-shadows.mdx +2 -2
- package/docs/changelog.mdx +16 -0
- package/docs/components/AllComponents.web.tsx +30 -1
- package/docs/dark-mode-best-practice.mdx +328 -0
- package/package.json +6 -4
- 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/Modal/Modal.docs.mdx +58 -4
- package/src/components/NavModal/NavModal.docs.mdx +2 -2
- package/src/components/Rating/Rating.docs.mdx +178 -0
- package/src/components/Rating/Rating.figma.tsx +20 -0
- package/src/components/Rating/Rating.props.ts +22 -0
- package/src/components/Rating/Rating.stories.tsx +95 -0
- package/src/components/Rating/Rating.tsx +140 -0
- package/src/components/Rating/RatingStarEmpty.tsx +22 -0
- package/src/components/Rating/RatingStarFilled.tsx +27 -0
- package/src/components/Rating/index.ts +2 -0
- package/src/components/Roundel/Roundel.docs.mdx +48 -0
- package/src/components/Roundel/Roundel.figma.tsx +17 -0
- package/src/components/Roundel/Roundel.props.ts +8 -0
- package/src/components/Roundel/Roundel.stories.tsx +49 -0
- package/src/components/Roundel/Roundel.tsx +51 -0
- package/src/components/Roundel/index.ts +2 -0
- package/src/components/StepperInput/StepperButton.tsx +83 -0
- package/src/components/StepperInput/StepperInput.docs.mdx +121 -0
- package/src/components/StepperInput/StepperInput.figma.tsx +45 -0
- package/src/components/StepperInput/StepperInput.props.ts +39 -0
- package/src/components/StepperInput/StepperInput.stories.tsx +270 -0
- package/src/components/StepperInput/StepperInput.tsx +322 -0
- package/src/components/StepperInput/index.ts +2 -0
- package/src/components/Textarea/Textarea.docs.mdx +2 -0
- package/src/components/Textarea/Textarea.props.ts +11 -0
- package/src/components/Textarea/Textarea.stories.tsx +14 -0
- package/src/components/Textarea/Textarea.tsx +22 -34
- package/src/components/VerificationInput/VerificationInput.tsx +13 -25
- package/src/components/index.ts +3 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useFormFieldAccessibility.test.tsx +74 -0
- package/src/hooks/useFormFieldAccessibility.ts +67 -0
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import type { TextInputProps, ViewProps } from 'react-native';
|
|
2
2
|
|
|
3
3
|
export interface TextareaBaseProps {
|
|
4
|
+
/**
|
|
5
|
+
* Sets the initial height of a resizable textarea in pixels.
|
|
6
|
+
* Has no effect unless `resizable` is enabled.
|
|
7
|
+
*
|
|
8
|
+
* @type number
|
|
9
|
+
* @example
|
|
10
|
+
* ```tsx
|
|
11
|
+
* <Textarea resizable defaultHeight={140} />
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
defaultHeight?: number;
|
|
4
15
|
/**
|
|
5
16
|
* If true, the textarea can be resized vertically using a drag handle.
|
|
6
17
|
*
|
|
@@ -66,6 +66,10 @@ const meta = {
|
|
|
66
66
|
description: 'Enables a drag handle to resize the Textarea vertically',
|
|
67
67
|
defaultValue: false,
|
|
68
68
|
},
|
|
69
|
+
defaultHeight: {
|
|
70
|
+
control: { type: 'number', min: 64, step: 8 },
|
|
71
|
+
description: 'Sets the initial height of the Textarea in pixels',
|
|
72
|
+
},
|
|
69
73
|
},
|
|
70
74
|
args: {
|
|
71
75
|
placeholder: 'Textarea placeholder',
|
|
@@ -90,3 +94,13 @@ export const Resizable: Story = {
|
|
|
90
94
|
resizable: true,
|
|
91
95
|
},
|
|
92
96
|
};
|
|
97
|
+
|
|
98
|
+
export const DefaultHeight: Story = {
|
|
99
|
+
args: {
|
|
100
|
+
label: 'Notes',
|
|
101
|
+
helperText: 'Starts taller by default',
|
|
102
|
+
placeholder: 'Add more detail here...',
|
|
103
|
+
resizable: true,
|
|
104
|
+
defaultHeight: 140,
|
|
105
|
+
},
|
|
106
|
+
};
|
|
@@ -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';
|
|
@@ -33,6 +33,7 @@ const Textarea = ({
|
|
|
33
33
|
validationStatus = 'initial',
|
|
34
34
|
children,
|
|
35
35
|
resizable = false,
|
|
36
|
+
defaultHeight,
|
|
36
37
|
disabled,
|
|
37
38
|
focused,
|
|
38
39
|
readonly,
|
|
@@ -56,9 +57,20 @@ const Textarea = ({
|
|
|
56
57
|
const textareaDisabled = disabled ?? formFieldContext?.disabled;
|
|
57
58
|
const textareaReadonly = readonly ?? formFieldContext?.readonly;
|
|
58
59
|
const textareaValidationStatus = formFieldContext?.validationStatus ?? validationStatus;
|
|
59
|
-
const
|
|
60
|
-
const
|
|
60
|
+
const textareaDefaultHeight = defaultHeight ?? DEFAULT_TEXTAREA_HEIGHT;
|
|
61
|
+
const textareaHeight = useSharedValue(textareaDefaultHeight);
|
|
62
|
+
const resizeStartHeight = useSharedValue(textareaDefaultHeight);
|
|
61
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
|
+
});
|
|
62
74
|
|
|
63
75
|
useEffect(() => {
|
|
64
76
|
if (formFieldContext?.setShouldHandleAccessibility) {
|
|
@@ -66,36 +78,12 @@ const Textarea = ({
|
|
|
66
78
|
}
|
|
67
79
|
}, [formFieldContext]);
|
|
68
80
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
if (textareaRequired) {
|
|
75
|
-
accessibilityLabel = accessibilityLabel + ', required';
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return accessibilityLabel || props.accessibilityLabel;
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
const getAccessibilityHint = () => {
|
|
82
|
-
let accessibilityHint = '';
|
|
83
|
-
if (textareaHelperText) {
|
|
84
|
-
accessibilityHint = accessibilityHint + textareaHelperText;
|
|
85
|
-
}
|
|
86
|
-
if (textareaValidationStatus !== 'initial') {
|
|
87
|
-
if (accessibilityHint.length > 0) {
|
|
88
|
-
accessibilityHint = accessibilityHint + ', ';
|
|
89
|
-
}
|
|
90
|
-
if (textareaValidationStatus === 'invalid' && textareaInvalidText) {
|
|
91
|
-
accessibilityHint = accessibilityHint + textareaInvalidText;
|
|
92
|
-
}
|
|
93
|
-
if (textareaValidationStatus === 'valid' && textareaValidText) {
|
|
94
|
-
accessibilityHint = accessibilityHint + textareaValidText;
|
|
95
|
-
}
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
if (!hasMeasuredHeight.current) {
|
|
83
|
+
textareaHeight.value = textareaDefaultHeight;
|
|
84
|
+
resizeStartHeight.value = textareaDefaultHeight;
|
|
96
85
|
}
|
|
97
|
-
|
|
98
|
-
};
|
|
86
|
+
}, [resizeStartHeight, textareaDefaultHeight, textareaHeight]);
|
|
99
87
|
|
|
100
88
|
const handleTextareaLayout = (event: LayoutChangeEvent) => {
|
|
101
89
|
if (!hasMeasuredHeight.current) {
|
|
@@ -162,8 +150,8 @@ const Textarea = ({
|
|
|
162
150
|
isDisabled={textareaDisabled}
|
|
163
151
|
isFocused={focused}
|
|
164
152
|
required={textareaRequired}
|
|
165
|
-
aria-label={
|
|
166
|
-
accessibilityHint={
|
|
153
|
+
aria-label={accessibilityLabel}
|
|
154
|
+
accessibilityHint={accessibilityHint}
|
|
167
155
|
>
|
|
168
156
|
{children ? (
|
|
169
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/components/index.ts
CHANGED
|
@@ -48,11 +48,14 @@ export * from './ProgressBar';
|
|
|
48
48
|
export * from './ProgressStepper';
|
|
49
49
|
export * from './Radio';
|
|
50
50
|
export * from './RadioCard';
|
|
51
|
+
export * from './Rating';
|
|
52
|
+
export * from './Roundel';
|
|
51
53
|
export * from './SectionHeader';
|
|
52
54
|
export * from './SegmentedControl';
|
|
53
55
|
export * from './Select';
|
|
54
56
|
export * from './Skeleton';
|
|
55
57
|
export * from './Spinner';
|
|
58
|
+
export * from './StepperInput';
|
|
56
59
|
export * from './Switch';
|
|
57
60
|
export * from './Table';
|
|
58
61
|
export * from './Tabs';
|
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;
|