@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.
Files changed (35) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-lint.log +15 -18
  3. package/CHANGELOG.md +30 -1
  4. package/build/components/Badge/Badge.js +2 -2
  5. package/build/components/Card/Card.props.d.ts +1 -1
  6. package/build/components/Card/CardRoot.js +19 -0
  7. package/build/components/Input/Input.js +13 -31
  8. package/build/components/StepperInput/StepperInput.js +12 -29
  9. package/build/components/Tabs/Tab.js +2 -2
  10. package/build/components/Textarea/Textarea.js +12 -30
  11. package/build/components/VerificationInput/VerificationInput.js +12 -22
  12. package/build/hooks/index.d.ts +1 -0
  13. package/build/hooks/index.js +1 -0
  14. package/build/hooks/useFormFieldAccessibility.d.ts +17 -0
  15. package/build/hooks/useFormFieldAccessibility.js +32 -0
  16. package/build/hooks/useFormFieldAccessibility.test.d.ts +1 -0
  17. package/build/hooks/useFormFieldAccessibility.test.js +56 -0
  18. package/docs/changelog.mdx +84 -0
  19. package/package.json +11 -9
  20. package/src/components/Badge/Badge.tsx +2 -2
  21. package/src/components/Banner/Banner.stories.tsx +14 -0
  22. package/src/components/Card/Card.docs.mdx +16 -17
  23. package/src/components/Card/Card.props.ts +1 -0
  24. package/src/components/Card/Card.stories.tsx +35 -21
  25. package/src/components/Card/CardRoot.tsx +19 -0
  26. package/src/components/Icon/Icon.docs.mdx +1 -1
  27. package/src/components/Input/Input.tsx +14 -35
  28. package/src/components/List/List.docs.mdx +4 -2
  29. package/src/components/StepperInput/StepperInput.tsx +13 -40
  30. package/src/components/Tabs/Tab.tsx +2 -2
  31. package/src/components/Textarea/Textarea.tsx +13 -34
  32. package/src/components/VerificationInput/VerificationInput.tsx +13 -25
  33. package/src/hooks/index.ts +1 -0
  34. package/src/hooks/useFormFieldAccessibility.test.tsx +74 -0
  35. 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={getAccessibilityLabel()}
301
- accessibilityHint={getAccessibilityHint()}
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: { height: theme.components.tabs.md.height },
91
- lg: { height: theme.components.tabs.lg.height },
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={getAccessibilityLabel()}
175
- accessibilityHint={getAccessibilityHint()}
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
- const getAccessibilityLabel = () => {
168
- return label || props.accessibilityLabel;
169
- };
170
-
171
- const getAccessibilityHint = () => {
172
- let accessibilityHint = '';
173
- if (helperText) {
174
- accessibilityHint = accessibilityHint + helperText;
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={getAccessibilityLabel()}
212
- accessibilityHint={getAccessibilityHint()}
199
+ accessibilityLabel={accessibilityLabel}
200
+ accessibilityHint={accessibilityHint}
213
201
  accessibilityState={{ disabled: disabled || readonly }}
214
202
  importantForAccessibility="yes"
215
203
  onChangeText={handleChangeText}
@@ -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;