@utilitywarehouse/hearth-react-native 0.32.0 → 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.
Files changed (30) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-lint.log +12 -15
  3. package/CHANGELOG.md +8 -1
  4. package/build/components/Card/Card.props.d.ts +1 -1
  5. package/build/components/Card/CardRoot.js +19 -0
  6. package/build/components/Input/Input.js +13 -31
  7. package/build/components/StepperInput/StepperInput.js +12 -29
  8. package/build/components/Textarea/Textarea.js +12 -30
  9. package/build/components/VerificationInput/VerificationInput.js +12 -22
  10. package/build/hooks/index.d.ts +1 -0
  11. package/build/hooks/index.js +1 -0
  12. package/build/hooks/useFormFieldAccessibility.d.ts +17 -0
  13. package/build/hooks/useFormFieldAccessibility.js +32 -0
  14. package/build/hooks/useFormFieldAccessibility.test.d.ts +1 -0
  15. package/build/hooks/useFormFieldAccessibility.test.js +56 -0
  16. package/package.json +4 -2
  17. package/src/components/Banner/Banner.stories.tsx +14 -0
  18. package/src/components/Card/Card.docs.mdx +16 -17
  19. package/src/components/Card/Card.props.ts +1 -0
  20. package/src/components/Card/Card.stories.tsx +35 -21
  21. package/src/components/Card/CardRoot.tsx +19 -0
  22. package/src/components/Icon/Icon.docs.mdx +1 -1
  23. package/src/components/Input/Input.tsx +14 -35
  24. package/src/components/List/List.docs.mdx +4 -2
  25. package/src/components/StepperInput/StepperInput.tsx +13 -40
  26. package/src/components/Textarea/Textarea.tsx +13 -34
  27. package/src/components/VerificationInput/VerificationInput.tsx +13 -25
  28. package/src/hooks/index.ts +1 -0
  29. package/src/hooks/useFormFieldAccessibility.test.tsx +74 -0
  30. package/src/hooks/useFormFieldAccessibility.ts +67 -0
@@ -1,4 +1,4 @@
1
1
 
2
- > @utilitywarehouse/hearth-react-native@0.32.0 build /home/runner/work/hearth/hearth/packages/react-native
2
+ > @utilitywarehouse/hearth-react-native@0.32.1 build /home/runner/work/hearth/hearth/packages/react-native
3
3
  > tsc
4
4
 
@@ -1,5 +1,5 @@
1
1
 
2
- > @utilitywarehouse/hearth-react-native@0.32.0 lint /home/runner/work/hearth/hearth/packages/react-native
2
+ > @utilitywarehouse/hearth-react-native@0.32.1 lint /home/runner/work/hearth/hearth/packages/react-native
3
3
  > TIMING=1 eslint .
4
4
 
5
5
 
@@ -27,9 +27,6 @@
27
27
  /home/runner/work/hearth/hearth/packages/react-native/src/components/DatePicker/DatePickerYears.tsx
28
28
  52:6 warning React Hook useCallback has a missing dependency: 'containerHeight'. Either include it or remove the dependency array. Outer scope values like 'styles' aren't valid dependencies because mutating them doesn't re-render the component react-hooks/exhaustive-deps
29
29
 
30
- /home/runner/work/hearth/hearth/packages/react-native/src/components/Input/Input.tsx
31
- 78:8 warning React Hook useEffect has a missing dependency: 'formFieldContext'. Either include it or remove the dependency array react-hooks/exhaustive-deps
32
-
33
30
  /home/runner/work/hearth/hearth/packages/react-native/src/components/PillGroup/PillGroup.tsx
34
31
  17:9 warning The 'normalizedValue' conditional could make the dependencies of useMemo Hook (at line 33) change on every render. Move it inside the useMemo callback. Alternatively, wrap the initialization of 'normalizedValue' in its own useMemo() Hook react-hooks/exhaustive-deps
35
32
 
@@ -46,17 +43,17 @@
46
43
  14:14 warning Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components react-refresh/only-export-components
47
44
  106:14 warning Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components react-refresh/only-export-components
48
45
 
49
- 21 problems (0 errors, 21 warnings)
46
+ 20 problems (0 errors, 20 warnings)
50
47
 
51
48
  Rule | Time (ms) | Relative
52
49
  :-----------------------------------------|----------:|--------:
53
- @typescript-eslint/no-unused-vars | 1597.986 | 54.8%
54
- react-hooks/exhaustive-deps | 222.159 | 7.6%
55
- no-global-assign | 113.864 | 3.9%
56
- react-hooks/rules-of-hooks | 86.855 | 3.0%
57
- no-misleading-character-class | 82.947 | 2.8%
58
- no-unexpected-multiline | 65.031 | 2.2%
59
- @typescript-eslint/ban-ts-comment | 59.965 | 2.1%
60
- no-regex-spaces | 42.086 | 1.4%
61
- no-useless-escape | 37.102 | 1.3%
62
- @typescript-eslint/triple-slash-reference | 36.097 | 1.2%
50
+ @typescript-eslint/no-unused-vars | 1452.998 | 57.8%
51
+ react-hooks/exhaustive-deps | 126.724 | 5.0%
52
+ no-global-assign | 93.240 | 3.7%
53
+ react-hooks/rules-of-hooks | 91.457 | 3.6%
54
+ @typescript-eslint/ban-ts-comment | 64.809 | 2.6%
55
+ no-unexpected-multiline | 56.861 | 2.3%
56
+ @typescript-eslint/triple-slash-reference | 35.707 | 1.4%
57
+ no-misleading-character-class | 35.239 | 1.4%
58
+ no-regex-spaces | 34.132 | 1.4%
59
+ no-useless-escape | 32.069 | 1.3%
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @utilitywarehouse/hearth-react-native
2
2
 
3
+ ## 0.32.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#1144](https://github.com/utilitywarehouse/hearth/pull/1144) [`85459f2`](https://github.com/utilitywarehouse/hearth/commit/85459f2f4d7dcd8a99685a11dcda070530cee8dc) Thanks [@jordmccord](https://github.com/jordmccord)! - 🐛 [FIX]: Add the missing `highlight` color scheme support across the `Banner` and `Card` components.
8
+
3
9
  ## 0.32.0
4
10
 
5
11
  ### Minor Changes
@@ -9,6 +15,7 @@
9
15
  `Roundel` is a compact status indicator with `success`, `pending`, and `error` variants, intended for inline state cues.
10
16
 
11
17
  **Components affected**:
18
+
12
19
  - `Roundel`
13
20
 
14
21
  **Developer changes**:
@@ -18,7 +25,7 @@ Import and use `Roundel` from `@utilitywarehouse/hearth-react-native`:
18
25
  ```tsx
19
26
  import { Roundel } from '@utilitywarehouse/hearth-react-native';
20
27
 
21
- <Roundel variant="success" />
28
+ <Roundel variant="success" />;
22
29
  ```
23
30
 
24
31
  - [#1132](https://github.com/utilitywarehouse/hearth/pull/1132) [`8824186`](https://github.com/utilitywarehouse/hearth/commit/882418633ee8c3a11e204329d07363dc411996dc) Thanks [@jordmccord](https://github.com/jordmccord)! - 🌟 [FEATURE]: Add the `Rating` component
@@ -2,7 +2,7 @@ import { PressableProps } from 'react-native';
2
2
  import { DisplayProps, FlexLayoutProps, GapProps, MarginProps, SpacingValues } from '../../types';
3
3
  interface CardProps extends PressableProps, MarginProps, GapProps, FlexLayoutProps, Omit<DisplayProps, 'direction'> {
4
4
  variant?: 'emphasis' | 'subtle';
5
- colorScheme?: 'neutralStrong' | 'neutralSubtle' | 'brand' | 'energy' | 'broadband' | 'mobile' | 'insurance' | 'cashback' | 'pig';
5
+ colorScheme?: 'neutralStrong' | 'neutralSubtle' | 'brand' | 'energy' | 'broadband' | 'highlight' | 'mobile' | 'insurance' | 'cashback' | 'pig';
6
6
  shadowColor?: 'functional' | 'brand' | 'energy' | 'broadband' | 'mobile' | 'insurance' | 'cashback' | 'pig';
7
7
  noPadding?: boolean;
8
8
  disabled?: boolean;
@@ -111,6 +111,9 @@ const styles = StyleSheet.create(theme => ({
111
111
  pig: {
112
112
  borderWidth: theme.components.card.brand.borderWidth,
113
113
  },
114
+ highlight: {
115
+ borderWidth: theme.components.card.brand.borderWidth,
116
+ },
114
117
  },
115
118
  shadowColor: {
116
119
  functional: {
@@ -278,6 +281,22 @@ const styles = StyleSheet.create(theme => ({
278
281
  borderColor: theme.color.border.strong,
279
282
  },
280
283
  },
284
+ {
285
+ variant: 'subtle',
286
+ colorScheme: 'highlight',
287
+ styles: {
288
+ backgroundColor: theme.color.surface.highlight.subtle,
289
+ borderColor: theme.color.border.strong,
290
+ },
291
+ },
292
+ {
293
+ variant: 'emphasis',
294
+ colorScheme: 'highlight',
295
+ styles: {
296
+ backgroundColor: theme.color.surface.highlight.default,
297
+ borderColor: theme.color.border.strong,
298
+ },
299
+ },
281
300
  ],
282
301
  },
283
302
  }));
@@ -2,7 +2,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
2
2
  import { createInput } from '@gluestack-ui/input';
3
3
  import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
4
4
  import { CloseSmallIcon, EyeOffSmallIcon, EyeSmallIcon, SearchMediumIcon, } from '@utilitywarehouse/hearth-react-native-icons';
5
- import { useTheme } from '../../hooks';
5
+ import { useFormFieldAccessibility, useTheme } from '../../hooks';
6
6
  import { BodyText } from '../BodyText';
7
7
  import { FormField, useFormFieldContext } from '../FormField';
8
8
  import { Spinner } from '../Spinner';
@@ -34,7 +34,7 @@ const Input = forwardRef(({ validationStatus = 'initial', children, disabled, fo
34
34
  if (formFieldContext?.setShouldHandleAccessibility) {
35
35
  formFieldContext.setShouldHandleAccessibility(true);
36
36
  }
37
- }, []);
37
+ }, [formFieldContext]);
38
38
  const [fieldType, setFieldType] = useState(type === 'password' ? 'password' : 'text');
39
39
  const { color } = useTheme();
40
40
  const inputRef = useRef(null);
@@ -42,6 +42,16 @@ const Input = forwardRef(({ validationStatus = 'initial', children, disabled, fo
42
42
  useImperativeHandle(ref, () => inputRef.current, []);
43
43
  const shouldShowPasswordToggle = type === 'password' && showPasswordToggle;
44
44
  const shouldShowClear = clearable && !!props?.value;
45
+ const { accessibilityHint, accessibilityLabel } = useFormFieldAccessibility({
46
+ label: inputLabel,
47
+ helperText: inputHelperText,
48
+ validText: inputValidText,
49
+ invalidText: inputInvalidText,
50
+ required: inputRequired,
51
+ validationStatus: inputValidationStatus,
52
+ fallbackLabel: props.accessibilityLabel,
53
+ fallbackHint: props.accessibilityHint,
54
+ });
45
55
  const toggleFieldType = () => {
46
56
  setFieldType(fieldType === 'password' ? 'text' : 'password');
47
57
  };
@@ -57,39 +67,11 @@ const Input = forwardRef(({ validationStatus = 'initial', children, disabled, fo
57
67
  }
58
68
  return undefined;
59
69
  })();
60
- const getAccessibilityLabel = () => {
61
- let accessibilityLabel = '';
62
- if (inputLabel) {
63
- accessibilityLabel = accessibilityLabel + inputLabel;
64
- }
65
- if (inputRequired) {
66
- accessibilityLabel = accessibilityLabel + ', required';
67
- }
68
- return accessibilityLabel || props.accessibilityLabel;
69
- };
70
- const getAccessibilityHint = () => {
71
- let accessibilityHint = '';
72
- if (inputHelperText) {
73
- accessibilityHint = accessibilityHint + inputHelperText;
74
- }
75
- if (inputValidationStatus !== 'initial') {
76
- if (accessibilityHint.length > 0) {
77
- accessibilityHint = accessibilityHint + ', ';
78
- }
79
- if (inputValidationStatus === 'invalid' && inputInvalidText) {
80
- accessibilityHint = accessibilityHint + inputInvalidText;
81
- }
82
- if (inputValidationStatus === 'valid' && inputValidText) {
83
- accessibilityHint = accessibilityHint + inputValidText;
84
- }
85
- }
86
- return accessibilityHint || props.accessibilityHint;
87
- };
88
70
  return (_jsx(FormField, { label: label, labelVariant: labelVariant, helperText: helperText, helperIcon: helperIcon, validText: validText, invalidText: invalidText, required: required, validationStatus: validationStatus, disabled: disabled, readonly: readonly, accessibilityHandledByChildren: true, children: _jsx(InputComponent, { ...(children ? props : {}), validationStatus: inputValidationStatus, isInvalid: inputValidationStatus === 'invalid', isReadOnly: inputReadonly, isDisabled: inputDisabled, isFocused: focused, type: type, isRequired: inputRequired, style: style, children: children ? (_jsx(_Fragment, { children: children })) : (_jsxs(_Fragment, { children: [!!leadingIconComponent && (_jsx(InputSlot, { children: _jsx(InputIcon, { as: leadingIconComponent }) })), !!prefix && (_jsx(InputSlot, { children: typeof prefix === 'string' || typeof prefix === 'number' ? (_jsx(BodyText, { children: prefix })) : (prefix) })), _jsx(InputField
89
71
  // @ts-expect-error - ref forwarding issue
90
72
  , {
91
73
  // @ts-expect-error - ref forwarding issue
92
- ref: inputRef, type: fieldType, inputMode: getInputMode, inBottomSheet: inBottomSheet, ...props, "aria-label": getAccessibilityLabel(), accessibilityHint: getAccessibilityHint() }), shouldShowClear && (_jsx(InputSlot, { children: _jsx(UnstyledIconButton, { onPress: onClear, icon: CloseSmallIcon }) })), loading && (_jsx(InputSlot, { children: _jsx(Spinner, { size: "xs", color: color.icon.primary }) })), shouldShowPasswordToggle && (_jsx(InputSlot, { children: _jsx(UnstyledIconButton, { onPress: toggleFieldType, icon: fieldType === 'password' ? EyeSmallIcon : EyeOffSmallIcon }) })), !!suffix && (_jsx(InputSlot, { children: typeof suffix === 'string' || typeof suffix === 'number' ? (_jsx(BodyText, { children: suffix })) : (suffix) })), !!trailingIcon && (_jsx(InputSlot, { children: _jsx(InputIcon, { as: trailingIcon }) }))] })) }) }));
74
+ ref: inputRef, type: fieldType, inputMode: getInputMode, inBottomSheet: inBottomSheet, ...props, "aria-label": accessibilityLabel, accessibilityHint: accessibilityHint }), shouldShowClear && (_jsx(InputSlot, { children: _jsx(UnstyledIconButton, { onPress: onClear, icon: CloseSmallIcon }) })), loading && (_jsx(InputSlot, { children: _jsx(Spinner, { size: "xs", color: color.icon.primary }) })), shouldShowPasswordToggle && (_jsx(InputSlot, { children: _jsx(UnstyledIconButton, { onPress: toggleFieldType, icon: fieldType === 'password' ? EyeSmallIcon : EyeOffSmallIcon }) })), !!suffix && (_jsx(InputSlot, { children: typeof suffix === 'string' || typeof suffix === 'number' ? (_jsx(BodyText, { children: suffix })) : (suffix) })), !!trailingIcon && (_jsx(InputSlot, { children: _jsx(InputIcon, { as: trailingIcon }) }))] })) }) }));
93
75
  });
94
76
  Input.displayName = 'Input';
95
77
  export default Input;
@@ -3,6 +3,7 @@ import { AddSmallIcon, MinusSmallIcon } from '@utilitywarehouse/hearth-react-nat
3
3
  import { useEffect, useImperativeHandle, useRef, useState } from 'react';
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';
@@ -81,6 +82,16 @@ const StepperInput = ({ value, defaultValue, onChangeText, onChangeValue, min, m
81
82
  const allowDecimal = decimalPrecision > 0;
82
83
  const keyboardType = allowNegative || allowDecimal ? 'numeric' : 'number-pad';
83
84
  const inputMode = allowDecimal ? 'decimal' : 'numeric';
85
+ const { accessibilityHint, accessibilityLabel } = useFormFieldAccessibility({
86
+ label,
87
+ helperText,
88
+ validText,
89
+ invalidText,
90
+ required,
91
+ validationStatus,
92
+ fallbackLabel: props.accessibilityLabel,
93
+ fallbackHint: props.accessibilityHint,
94
+ });
84
95
  useImperativeHandle(ref, () => inputRef.current, []);
85
96
  useEffect(() => {
86
97
  if (!isControlled && defaultValue !== undefined) {
@@ -134,39 +145,11 @@ const StepperInput = ({ value, defaultValue, onChangeText, onChangeValue, min, m
134
145
  setIsInputFocused(false);
135
146
  onBlur?.(event);
136
147
  };
137
- const getAccessibilityLabel = () => {
138
- let accessibilityLabel = '';
139
- if (label) {
140
- accessibilityLabel = accessibilityLabel + label;
141
- }
142
- if (required) {
143
- accessibilityLabel = accessibilityLabel + ', required';
144
- }
145
- return accessibilityLabel || props.accessibilityLabel;
146
- };
147
- const getAccessibilityHint = () => {
148
- let accessibilityHint = '';
149
- if (helperText) {
150
- accessibilityHint = accessibilityHint + helperText;
151
- }
152
- if (validationStatus !== 'initial') {
153
- if (accessibilityHint.length > 0) {
154
- accessibilityHint = accessibilityHint + ', ';
155
- }
156
- if (validationStatus === 'invalid' && invalidText) {
157
- accessibilityHint = accessibilityHint + invalidText;
158
- }
159
- if (validationStatus === 'valid' && validText) {
160
- accessibilityHint = accessibilityHint + validText;
161
- }
162
- }
163
- return accessibilityHint || props.accessibilityHint;
164
- };
165
148
  return (_jsx(FormField, { label: label, labelVariant: labelVariant, helperText: helperText, helperIcon: helperIcon, validText: validText, invalidText: invalidText, required: required, validationStatus: validationStatus, disabled: disabled, readonly: readonly, accessibilityHandledByChildren: true, style: [styles.root, style], children: _jsxs(View, { style: styles.controls, children: [_jsx(StepperButton, { icon: MinusSmallIcon, disabled: decrementDisabled, accessibilityLabel: decrementAccessibilityLabel, onPress: () => handleStepPress(-1) }), _jsx(InputComponent, { validationStatus: validationStatus, isInvalid: validationStatus === 'invalid', isReadOnly: readonly, isDisabled: disabled, isFocused: resolvedFocused, isRequired: required, style: styles.inputRoot, children: _jsx(InputField
166
149
  // @ts-expect-error - ref forwarding issue mirrors the base Input component
167
150
  , {
168
151
  // @ts-expect-error - ref forwarding issue mirrors the base Input component
169
- ref: inputRef, inputMode: inputMode, keyboardType: keyboardType, inBottomSheet: inBottomSheet, editable: !disabled && !readonly, textAlign: "center", value: displayValue, onFocus: handleFocus, onBlur: handleBlur, onChangeText: handleChangeText, accessibilityLabel: getAccessibilityLabel(), accessibilityHint: getAccessibilityHint(), accessibilityState: {
152
+ ref: inputRef, inputMode: inputMode, keyboardType: keyboardType, inBottomSheet: inBottomSheet, editable: !disabled && !readonly, textAlign: "center", value: displayValue, onFocus: handleFocus, onBlur: handleBlur, onChangeText: handleChangeText, accessibilityLabel: accessibilityLabel, accessibilityHint: accessibilityHint, accessibilityState: {
170
153
  ...(props.accessibilityState ?? {}),
171
154
  disabled: disabled || readonly,
172
155
  }, "aria-disabled": disabled || readonly, "aria-readonly": readonly, "aria-required": required, "aria-invalid": validationStatus === 'invalid', ...props, style: styles.inputField }) }), _jsx(StepperButton, { icon: AddSmallIcon, disabled: incrementDisabled, accessibilityLabel: incrementAccessibilityLabel, onPress: () => handleStepPress(1) })] }) }));
@@ -6,7 +6,7 @@ import { Gesture, GestureDetector } from 'react-native-gesture-handler';
6
6
  import { useAnimatedStyle, useSharedValue } from 'react-native-reanimated';
7
7
  import { Path, Svg } from 'react-native-svg';
8
8
  import { StyleSheet } from 'react-native-unistyles';
9
- import { useTheme } from '../../hooks';
9
+ import { useFormFieldAccessibility, useTheme } from '../../hooks';
10
10
  import { FormField, useFormFieldContext } from '../FormField';
11
11
  import TextareaFieldComponent from './TextareaField';
12
12
  import TextareaRoot from './TextareaRoot';
@@ -33,6 +33,16 @@ const Textarea = ({ validationStatus = 'initial', children, resizable = false, d
33
33
  const textareaHeight = useSharedValue(textareaDefaultHeight);
34
34
  const resizeStartHeight = useSharedValue(textareaDefaultHeight);
35
35
  const theme = useTheme();
36
+ const { accessibilityHint, accessibilityLabel } = useFormFieldAccessibility({
37
+ label: textareaLabel,
38
+ helperText: textareaHelperText,
39
+ validText: textareaValidText,
40
+ invalidText: textareaInvalidText,
41
+ required: textareaRequired,
42
+ validationStatus: textareaValidationStatus,
43
+ fallbackLabel: props.accessibilityLabel,
44
+ fallbackHint: props.accessibilityHint,
45
+ });
36
46
  useEffect(() => {
37
47
  if (formFieldContext?.setShouldHandleAccessibility) {
38
48
  formFieldContext.setShouldHandleAccessibility(true);
@@ -44,34 +54,6 @@ const Textarea = ({ validationStatus = 'initial', children, resizable = false, d
44
54
  resizeStartHeight.value = textareaDefaultHeight;
45
55
  }
46
56
  }, [resizeStartHeight, textareaDefaultHeight, textareaHeight]);
47
- const getAccessibilityLabel = () => {
48
- let accessibilityLabel = '';
49
- if (textareaLabel) {
50
- accessibilityLabel = accessibilityLabel + textareaLabel;
51
- }
52
- if (textareaRequired) {
53
- accessibilityLabel = accessibilityLabel + ', required';
54
- }
55
- return accessibilityLabel || props.accessibilityLabel;
56
- };
57
- const getAccessibilityHint = () => {
58
- let accessibilityHint = '';
59
- if (textareaHelperText) {
60
- accessibilityHint = accessibilityHint + textareaHelperText;
61
- }
62
- if (textareaValidationStatus !== 'initial') {
63
- if (accessibilityHint.length > 0) {
64
- accessibilityHint = accessibilityHint + ', ';
65
- }
66
- if (textareaValidationStatus === 'invalid' && textareaInvalidText) {
67
- accessibilityHint = accessibilityHint + textareaInvalidText;
68
- }
69
- if (textareaValidationStatus === 'valid' && textareaValidText) {
70
- accessibilityHint = accessibilityHint + textareaValidText;
71
- }
72
- }
73
- return accessibilityHint || props.accessibilityHint;
74
- };
75
57
  const handleTextareaLayout = (event) => {
76
58
  if (!hasMeasuredHeight.current) {
77
59
  textareaHeight.value = event.nativeEvent.layout.height;
@@ -97,7 +79,7 @@ const Textarea = ({ validationStatus = 'initial', children, resizable = false, d
97
79
  const rootStyle = (children ? props.style : undefined);
98
80
  const inputStyle = (!children ? props.style : undefined);
99
81
  const textareaStyle = (resizable ? [rootStyle, animatedHeightStyle] : rootStyle);
100
- return (_jsx(FormField, { label: label, labelVariant: labelVariant, helperText: helperText, helperIcon: helperIcon, validText: validText, invalidText: invalidText, required: required, validationStatus: validationStatus, disabled: disabled, readonly: readonly, accessibilityHandledByChildren: true, children: _jsxs(TextareaComponent, { ...(children ? props : {}), onLayout: handleTextareaLayout, style: textareaStyle, validationStatus: textareaValidationStatus, isInvalid: textareaValidationStatus === 'invalid', isReadOnly: textareaReadonly, isDisabled: textareaDisabled, isFocused: focused, required: textareaRequired, "aria-label": getAccessibilityLabel(), accessibilityHint: getAccessibilityHint(), children: [children ? (_jsx(_Fragment, { children: children })) : (_jsx(_Fragment, { children: _jsx(TextareaField, { ...props, onLayout: onLayout, style: [styles.textarea, inputStyle] }) })), resizable && !textareaDisabled ? (_jsx(GestureDetector, { gesture: resizeGesture, children: _jsx(View, { style: styles.resizeHandle, children: _jsx(Svg, { width: RESIZE_HANDLE_ICON_SIZE, height: RESIZE_HANDLE_ICON_SIZE, viewBox: "0 0 9 9", fill: "none", children: _jsx(Path, { d: "M0.353516 8.35355L8.35352 0.353546M4.35352 8.35355L8.35352 4.35355", stroke: theme.color.icon.primary }) }) }) })) : null] }) }));
82
+ return (_jsx(FormField, { label: label, labelVariant: labelVariant, helperText: helperText, helperIcon: helperIcon, validText: validText, invalidText: invalidText, required: required, validationStatus: validationStatus, disabled: disabled, readonly: readonly, accessibilityHandledByChildren: true, children: _jsxs(TextareaComponent, { ...(children ? props : {}), onLayout: handleTextareaLayout, style: textareaStyle, validationStatus: textareaValidationStatus, isInvalid: textareaValidationStatus === 'invalid', isReadOnly: textareaReadonly, isDisabled: textareaDisabled, isFocused: focused, required: textareaRequired, "aria-label": accessibilityLabel, accessibilityHint: accessibilityHint, children: [children ? (_jsx(_Fragment, { children: children })) : (_jsx(_Fragment, { children: _jsx(TextareaField, { ...props, onLayout: onLayout, style: [styles.textarea, inputStyle] }) })), resizable && !textareaDisabled ? (_jsx(GestureDetector, { gesture: resizeGesture, children: _jsx(View, { style: styles.resizeHandle, children: _jsx(Svg, { width: RESIZE_HANDLE_ICON_SIZE, height: RESIZE_HANDLE_ICON_SIZE, viewBox: "0 0 9 9", fill: "none", children: _jsx(Path, { d: "M0.353516 8.35355L8.35352 0.353546M4.35352 8.35355L8.35352 4.35355", stroke: theme.color.icon.primary }) }) }) })) : null] }) }));
101
83
  };
102
84
  const styles = StyleSheet.create({
103
85
  textarea: {
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
3
3
  import { TextInput, View } from 'react-native';
4
4
  import { StyleSheet } from 'react-native-unistyles';
5
+ import { useFormFieldAccessibility } from '../../hooks';
5
6
  import { FormField } from '../FormField';
6
7
  import { getNextIndexFromValueChange } from './VerificationInput.utils';
7
8
  import { VerificationInputSlot } from './VerificationInputSlot';
@@ -125,28 +126,17 @@ const VerificationInput = forwardRef(({ value = '', onChangeText, label, labelVa
125
126
  },
126
127
  }), [length, updateValue]);
127
128
  const slots = Array.from({ length }, (_, index) => index);
128
- const getAccessibilityLabel = () => {
129
- return label || props.accessibilityLabel;
130
- };
131
- const getAccessibilityHint = () => {
132
- let accessibilityHint = '';
133
- if (helperText) {
134
- accessibilityHint = accessibilityHint + helperText;
135
- }
136
- if (validationStatus !== 'initial') {
137
- if (accessibilityHint.length > 0) {
138
- accessibilityHint = accessibilityHint + ', ';
139
- }
140
- if (validationStatus === 'invalid' && invalidText) {
141
- accessibilityHint = accessibilityHint + invalidText;
142
- }
143
- if (validationStatus === 'valid' && validText) {
144
- accessibilityHint = accessibilityHint + validText;
145
- }
146
- }
147
- return accessibilityHint || props.accessibilityHint;
148
- };
149
- return (_jsx(FormField, { label: label, labelVariant: labelVariant, helperText: helperText, helperIcon: helperIcon, validationStatus: validationStatus, validText: validText, invalidText: invalidText, disabled: disabled, readonly: readonly, accessibilityHandledByChildren: true, style: [styles.root, style], ...props, children: _jsxs(View, { style: styles.slotsContainer, children: [_jsx(TextInput, { ref: inputRef, value: displayValue, autoFocus: autoFocus, editable: !disabled && !readonly, accessibilityLabel: getAccessibilityLabel(), accessibilityHint: getAccessibilityHint(), accessibilityState: { disabled: disabled || readonly }, importantForAccessibility: "yes", onChangeText: handleChangeText, onSelectionChange: event => {
129
+ const { accessibilityHint, accessibilityLabel } = useFormFieldAccessibility({
130
+ label,
131
+ helperText,
132
+ validText,
133
+ invalidText,
134
+ validationStatus,
135
+ fallbackLabel: props.accessibilityLabel,
136
+ fallbackHint: props.accessibilityHint,
137
+ includeRequiredInLabel: false,
138
+ });
139
+ return (_jsx(FormField, { label: label, labelVariant: labelVariant, helperText: helperText, helperIcon: helperIcon, validationStatus: validationStatus, validText: validText, invalidText: invalidText, disabled: disabled, readonly: readonly, accessibilityHandledByChildren: true, style: [styles.root, style], ...props, children: _jsxs(View, { style: styles.slotsContainer, children: [_jsx(TextInput, { ref: inputRef, value: displayValue, autoFocus: autoFocus, editable: !disabled && !readonly, accessibilityLabel: accessibilityLabel, accessibilityHint: accessibilityHint, accessibilityState: { disabled: disabled || readonly }, importantForAccessibility: "yes", onChangeText: handleChangeText, onSelectionChange: event => {
150
140
  const nextSelection = event.nativeEvent.selection;
151
141
  if (ignoreNextSelectionRef.current &&
152
142
  (nextSelection.start !== latestSelectionRef.current.start ||
@@ -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';
@@ -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,17 @@
1
+ type ValidationStatus = 'initial' | 'valid' | 'invalid';
2
+ type UseFormFieldAccessibilityArgs = {
3
+ label?: string;
4
+ helperText?: string;
5
+ validText?: string;
6
+ invalidText?: string;
7
+ required?: boolean;
8
+ validationStatus?: ValidationStatus;
9
+ fallbackLabel?: string;
10
+ fallbackHint?: string;
11
+ includeRequiredInLabel?: boolean;
12
+ };
13
+ declare const useFormFieldAccessibility: ({ label, helperText, validText, invalidText, required, validationStatus, fallbackLabel, fallbackHint, includeRequiredInLabel, }: UseFormFieldAccessibilityArgs) => {
14
+ accessibilityHint: string | undefined;
15
+ accessibilityLabel: string | undefined;
16
+ };
17
+ export default useFormFieldAccessibility;
@@ -0,0 +1,32 @@
1
+ import { useMemo } from 'react';
2
+ const useFormFieldAccessibility = ({ label, helperText, validText, invalidText, required = false, validationStatus = 'initial', fallbackLabel, fallbackHint, includeRequiredInLabel = true, }) => {
3
+ const accessibilityLabel = useMemo(() => {
4
+ const nextAccessibilityLabelParts = [];
5
+ if (label) {
6
+ nextAccessibilityLabelParts.push(label);
7
+ }
8
+ if (includeRequiredInLabel && required) {
9
+ nextAccessibilityLabelParts.push('required');
10
+ }
11
+ const nextAccessibilityLabel = nextAccessibilityLabelParts.join(', ');
12
+ return nextAccessibilityLabel || fallbackLabel;
13
+ }, [fallbackLabel, includeRequiredInLabel, label, required]);
14
+ const accessibilityHint = useMemo(() => {
15
+ const accessibilityHints = [];
16
+ if (helperText) {
17
+ accessibilityHints.push(helperText);
18
+ }
19
+ if (validationStatus === 'invalid' && invalidText) {
20
+ accessibilityHints.push(invalidText);
21
+ }
22
+ if (validationStatus === 'valid' && validText) {
23
+ accessibilityHints.push(validText);
24
+ }
25
+ return accessibilityHints.join(', ') || fallbackHint;
26
+ }, [fallbackHint, helperText, invalidText, validText, validationStatus]);
27
+ return {
28
+ accessibilityHint,
29
+ accessibilityLabel,
30
+ };
31
+ };
32
+ export default useFormFieldAccessibility;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,56 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { renderToStaticMarkup } from 'react-dom/server';
3
+ import { describe, expect, it } from 'vitest';
4
+ import useFormFieldAccessibility from './useFormFieldAccessibility';
5
+ const renderHook = (args) => {
6
+ let result;
7
+ const TestComponent = () => {
8
+ result = useFormFieldAccessibility(args);
9
+ return null;
10
+ };
11
+ renderToStaticMarkup(_jsx(TestComponent, {}));
12
+ if (!result) {
13
+ throw new Error('Hook did not return a result');
14
+ }
15
+ return result;
16
+ };
17
+ describe('useFormFieldAccessibility', () => {
18
+ it('builds a label from the field label and required state', () => {
19
+ expect(renderHook({
20
+ label: 'Email',
21
+ required: true,
22
+ })).toEqual({
23
+ accessibilityHint: undefined,
24
+ accessibilityLabel: 'Email, required',
25
+ });
26
+ });
27
+ it('builds a hint from helper text and validation feedback', () => {
28
+ expect(renderHook({
29
+ helperText: 'Enter the code we sent you',
30
+ validationStatus: 'invalid',
31
+ invalidText: 'Code is invalid',
32
+ })).toEqual({
33
+ accessibilityHint: 'Enter the code we sent you, Code is invalid',
34
+ accessibilityLabel: undefined,
35
+ });
36
+ });
37
+ it('falls back to provided accessibility props when no composed value exists', () => {
38
+ expect(renderHook({
39
+ fallbackLabel: 'Custom label',
40
+ fallbackHint: 'Custom hint',
41
+ })).toEqual({
42
+ accessibilityHint: 'Custom hint',
43
+ accessibilityLabel: 'Custom label',
44
+ });
45
+ });
46
+ it('supports omitting the required suffix from the label', () => {
47
+ expect(renderHook({
48
+ label: 'Verification code',
49
+ required: true,
50
+ includeRequiredInLabel: false,
51
+ })).toEqual({
52
+ accessibilityHint: undefined,
53
+ accessibilityLabel: 'Verification code',
54
+ });
55
+ });
56
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@utilitywarehouse/hearth-react-native",
3
- "version": "0.32.0",
3
+ "version": "0.32.1",
4
4
  "description": "Utility Warehouse React Native UI library",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -33,6 +33,7 @@
33
33
  "@storybook/addon-vitest": "^10.2.1",
34
34
  "@storybook/react-native-web-vite": "^10.2.1",
35
35
  "@types/prismjs": "^1.26.5",
36
+ "@types/react-dom": "^19.1.6",
36
37
  "@types/react": "^19.1.10",
37
38
  "@vitest/browser": "^3.2.4",
38
39
  "@vitest/coverage-v8": "^3.2.4",
@@ -41,6 +42,7 @@
41
42
  "playwright": "^1.55.1",
42
43
  "prismjs": "^1.30.0",
43
44
  "react": "^19.1.0",
45
+ "react-dom": "^19.1.0",
44
46
  "react-native": "0.80.0",
45
47
  "react-native-edge-to-edge": "1.6.1",
46
48
  "react-native-gesture-handler": "2.28.0",
@@ -86,7 +88,7 @@
86
88
  "watch": "tsc --watch",
87
89
  "figma:create": "figma connect create",
88
90
  "figma:publish": "figma connect publish",
89
- "test": "echo \"Error: no test specified\" && exit 1",
91
+ "test": "vitest run --config vitest.unit.config.ts",
90
92
  "test:storybook": "vitest run --project storybook",
91
93
  "dev": "npm run copyChangelog && storybook dev -p 6006",
92
94
  "dev:docs": "storybook dev -p 6002 --no-open --docs",
@@ -55,6 +55,7 @@ const meta = {
55
55
  'insurance',
56
56
  'cashback',
57
57
  'pig',
58
+ 'highlight',
58
59
  ],
59
60
  },
60
61
  iconContainerVariant: {
@@ -548,6 +549,19 @@ export const ColorSchemes: Story = {
548
549
  </Button>
549
550
  }
550
551
  />
552
+ <Banner
553
+ icon={HomeMediumIcon}
554
+ iconContainerColor="highlight"
555
+ iconContainerVariant="emphasis"
556
+ colorScheme="highlight"
557
+ heading="Highlight Yellow"
558
+ description="Banner with highlight color scheme."
559
+ button={
560
+ <Button size="sm" onPress={() => console.log('Action pressed')}>
561
+ Action
562
+ </Button>
563
+ }
564
+ />
551
565
  </Flex>
552
566
  </View>
553
567
  );