@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
@@ -61,23 +61,22 @@ const MyComponent = () => (
61
61
 
62
62
  ## Props
63
63
 
64
- | Property | Type | Description | Default |
65
- | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ | ----------------- |
66
- | variant | `'subtle' \| 'emphasis' ` | The variant of the card. | `'subtle'` |
67
- | colorScheme | `'neutralStrong' \| 'neutralSubtle' \| 'purple' \| 'energy'` <br /> `'broadband' \| 'mobile' \| 'insurance' \| 'cashback' \|` <br /> `'pig'` | The color scheme of the card. | `'neutralStrong'` |
68
- | shadowColor | `'functional' \| 'brand' \| 'energy' \| 'broadband' \| 'mobile' `<br /> `'insurance' \| 'cashback' \| 'pig'` | The shadow color of the card. | `-` |
69
- | noPadding | `boolean` | Whether or not the card has padding. | `false` |
70
- | selected | `boolean` | Whether the card is selected. | `false` |
71
- | onPress | `() => void` | Callback function to be called. | `-` |
72
- | disabled | `boolean` | Whether the card is disabled. | `false` |
73
- | spacing | `'none' \| '2xs' \| 'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| '2xl'` | The space between the content. | `none` |
74
- | alignItems | `'flex-start' \| 'flex-end' \| `<br />`'center' \| 'stretch' \| 'baseline'` | The align items of the flex container. | `flex-start` |
75
- | justifyContent | `'flex-start' \| 'flex-end' \| 'center' \| 'space-between' \| `<br />` 'space-around' \| 'space-evenly'` | The justify content of the flex container. | `flex-start` |
76
- | flexWrap | `'wrap' \| 'nowrap' \| 'wrap-reverse'` | The wrap of the flex container. | `nowrap` |
77
- | flexDirection | `'row' \| 'column' \| 'row-reverse' \| 'column-reverse'` | The direction of the flex container. | `column` |
78
- | gap | `number` | The gap between the content. | `0` |
79
- | rowGap | `number` | The row gap between the content. | `0` |
80
- | columnGap | `number` | The column gap between the content. | `0` |
64
+ | Property | Type | Description | Default |
65
+ | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ | ----------------- |
66
+ | variant | `'subtle' \| 'emphasis' ` | The variant of the card. | `'subtle'` |
67
+ | colorScheme | `'neutralStrong' \| 'neutralSubtle' \| 'brand' \| 'energy'` <br /> `'broadband' \| 'mobile' \| 'insurance' \| 'cashback' \|` <br /> `'highlight' \| 'pig'` | The color scheme of the card. | `'neutralStrong'` |
68
+ | shadowColor | `'functional' \| 'brand' \| 'energy' \| 'broadband' \| 'mobile' `<br /> `'insurance' \| 'cashback' \| 'pig'` | The shadow color of the card. | `-` |
69
+ | noPadding | `boolean` | Whether or not the card has padding. | `false` |
70
+ | onPress | `() => void` | Callback function to be called. | `-` |
71
+ | disabled | `boolean` | Whether the card is disabled. | `false` |
72
+ | spacing | `'none' \| '2xs' \| 'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| '2xl'` | The space between the content. | `none` |
73
+ | alignItems | `'flex-start' \| 'flex-end' \| `<br />`'center' \| 'stretch' \| 'baseline'` | The align items of the flex container. | `flex-start` |
74
+ | justifyContent | `'flex-start' \| 'flex-end' \| 'center' \| 'space-between' \| `<br />` 'space-around' \| 'space-evenly'` | The justify content of the flex container. | `flex-start` |
75
+ | flexWrap | `'wrap' \| 'nowrap' \| 'wrap-reverse'` | The wrap of the flex container. | `nowrap` |
76
+ | flexDirection | `'row' \| 'column' \| 'row-reverse' \| 'column-reverse'` | The direction of the flex container. | `column` |
77
+ | gap | `number` | The gap between the content. | `0` |
78
+ | rowGap | `number` | The row gap between the content. | `0` |
79
+ | columnGap | `number` | The column gap between the content. | `0` |
81
80
 
82
81
  ### `CardPressHandler` Props
83
82
 
@@ -10,6 +10,7 @@ interface CardProps
10
10
  | 'brand'
11
11
  | 'energy'
12
12
  | 'broadband'
13
+ | 'highlight'
13
14
  | 'mobile'
14
15
  | 'insurance'
15
16
  | 'cashback'
@@ -1,5 +1,6 @@
1
- import { Meta, StoryObj } from '@storybook/react-vite';
1
+ import { Meta, StoryObj } from '@storybook/react-native';
2
2
  import { BellMediumIcon } from '@utilitywarehouse/hearth-react-native-icons';
3
+ import { ComponentProps } from 'react';
3
4
  import { Card, CardAction, CardActions, CardPressHandler } from '.';
4
5
  import { VariantTitle } from '../../../docs/components';
5
6
  import { BodyText } from '../BodyText';
@@ -7,6 +8,8 @@ import { Button } from '../Button';
7
8
  import { Flex } from '../Flex';
8
9
  import { Heading } from '../Heading';
9
10
 
11
+ type CardStoryProps = ComponentProps<typeof Card>;
12
+
10
13
  const meta = {
11
14
  title: 'Stories / Card',
12
15
  component: Card,
@@ -31,6 +34,7 @@ const meta = {
31
34
  'brand',
32
35
  'energy',
33
36
  'broadband',
37
+ 'highlight',
34
38
  'mobile',
35
39
  'insurance',
36
40
  'cashback',
@@ -65,7 +69,7 @@ export default meta;
65
69
  type Story = StoryObj<typeof meta>;
66
70
 
67
71
  export const Playground: Story = {
68
- render: ({ children, ...props }) => {
72
+ render: ({ children, ...props }: CardStoryProps) => {
69
73
  return (
70
74
  <Card {...props}>
71
75
  <BodyText>{children as string}</BodyText>
@@ -75,7 +79,7 @@ export const Playground: Story = {
75
79
  };
76
80
 
77
81
  export const WithActions: Story = {
78
- render: ({ children, ...props }) => {
82
+ render: ({ children, ...props }: CardStoryProps) => {
79
83
  return (
80
84
  <Flex gap="400">
81
85
  <Card
@@ -139,7 +143,7 @@ export const WithOnlyAction: Story = {
139
143
  args: {
140
144
  variant: 'emphasis',
141
145
  },
142
- render: ({ ...props }) => {
146
+ render: ({ ...props }: CardStoryProps) => {
143
147
  return (
144
148
  <Card {...props} flexDirection="column" alignItems="stretch" spacing="md">
145
149
  <CardActions>
@@ -159,7 +163,7 @@ export const Variants: Story = {
159
163
  parameters: {
160
164
  controls: { exclude: ['variant', 'colorScheme'] },
161
165
  },
162
- render: ({ children, ...props }) => {
166
+ render: ({ children, ...props }: CardStoryProps) => {
163
167
  return (
164
168
  <Flex spacing="lg">
165
169
  <VariantTitle title="Subtle - White">
@@ -182,76 +186,86 @@ export const Variants: Story = {
182
186
  <BodyText>{children as string}</BodyText>
183
187
  </Card>
184
188
  </VariantTitle>
185
- <VariantTitle title="Purple - Subtle">
189
+ <VariantTitle title="Brand - Subtle">
186
190
  <Card {...props} colorScheme="brand" variant="subtle">
187
191
  <BodyText>{children as string}</BodyText>
188
192
  </Card>
189
193
  </VariantTitle>
190
- <VariantTitle title="Purple - Emphasis">
194
+ <VariantTitle title="Brand - Emphasis">
191
195
  <Card {...props} colorScheme="brand" variant="emphasis">
192
196
  <BodyText inverted>{children as string}</BodyText>
193
197
  </Card>
194
198
  </VariantTitle>
195
- <VariantTitle title="Energy Blue - Subtle">
199
+ <VariantTitle title="Energy - Subtle">
196
200
  <Card {...props} colorScheme="energy" variant="subtle">
197
201
  <BodyText>{children as string}</BodyText>
198
202
  </Card>
199
203
  </VariantTitle>
200
- <VariantTitle title="Energy Blue - Emphasis">
204
+ <VariantTitle title="Energy - Emphasis">
201
205
  <Card {...props} colorScheme="energy" variant="emphasis">
202
206
  <BodyText>{children as string}</BodyText>
203
207
  </Card>
204
208
  </VariantTitle>
205
- <VariantTitle title="Broadband Green - Subtle">
209
+ <VariantTitle title="Broadband - Subtle">
206
210
  <Card {...props} colorScheme="broadband" variant="subtle">
207
211
  <BodyText>{children as string}</BodyText>
208
212
  </Card>
209
213
  </VariantTitle>
210
- <VariantTitle title="Broadband Green - Emphasis">
214
+ <VariantTitle title="Broadband - Emphasis">
211
215
  <Card {...props} colorScheme="broadband" variant="emphasis">
212
216
  <BodyText>{children as string}</BodyText>
213
217
  </Card>
214
218
  </VariantTitle>
215
- <VariantTitle title="Mobile Rose - Subtle">
219
+ <VariantTitle title="Mobile - Subtle">
216
220
  <Card {...props} colorScheme="mobile" variant="subtle">
217
221
  <BodyText>{children as string}</BodyText>
218
222
  </Card>
219
223
  </VariantTitle>
220
- <VariantTitle title="Mobile Rose - Emphasis">
224
+ <VariantTitle title="Mobile - Emphasis">
221
225
  <Card {...props} colorScheme="mobile" variant="emphasis">
222
226
  <BodyText>{children as string}</BodyText>
223
227
  </Card>
224
228
  </VariantTitle>
225
- <VariantTitle title="Insurance Orange - Subtle">
229
+ <VariantTitle title="Insurance - Subtle">
226
230
  <Card {...props} colorScheme="insurance" variant="subtle">
227
231
  <BodyText>{children as string}</BodyText>
228
232
  </Card>
229
233
  </VariantTitle>
230
- <VariantTitle title="Insurance Orange - Emphasis">
234
+ <VariantTitle title="Insurance - Emphasis">
231
235
  <Card {...props} colorScheme="insurance" variant="emphasis">
232
236
  <BodyText>{children as string}</BodyText>
233
237
  </Card>
234
238
  </VariantTitle>
235
- <VariantTitle title="Cashback Lilac - Subtle">
239
+ <VariantTitle title="Cashback - Subtle">
236
240
  <Card {...props} colorScheme="cashback" variant="subtle">
237
241
  <BodyText>{children as string}</BodyText>
238
242
  </Card>
239
243
  </VariantTitle>
240
- <VariantTitle title="Cashback Lilac - Emphasis">
244
+ <VariantTitle title="Cashback - Emphasis">
241
245
  <Card {...props} colorScheme="cashback" variant="emphasis">
242
246
  <BodyText>{children as string}</BodyText>
243
247
  </Card>
244
248
  </VariantTitle>
245
- <VariantTitle title="Piggy Pink - Subtle">
249
+ <VariantTitle title="Piggy - Subtle">
246
250
  <Card {...props} colorScheme="pig" variant="subtle">
247
251
  <BodyText>{children as string}</BodyText>
248
252
  </Card>
249
253
  </VariantTitle>
250
- <VariantTitle title="Piggy Pink - Emphasis">
254
+ <VariantTitle title="Piggy - Emphasis">
251
255
  <Card {...props} colorScheme="pig" variant="emphasis">
252
256
  <BodyText>{children as string}</BodyText>
253
257
  </Card>
254
258
  </VariantTitle>
259
+ <VariantTitle title="Highlight - Subtle">
260
+ <Card {...props} colorScheme="highlight" variant="subtle">
261
+ <BodyText>{children as string}</BodyText>
262
+ </Card>
263
+ </VariantTitle>
264
+ <VariantTitle title="Highlight - Emphasis">
265
+ <Card {...props} colorScheme="highlight" variant="emphasis">
266
+ <BodyText>{children as string}</BodyText>
267
+ </Card>
268
+ </VariantTitle>
255
269
  </Flex>
256
270
  );
257
271
  },
@@ -264,7 +278,7 @@ export const WithShadow: Story = {
264
278
  parameters: {
265
279
  controls: { exclude: ['variant'] },
266
280
  },
267
- render: ({ children, ...props }) => {
281
+ render: ({ children, ...props }: CardStoryProps) => {
268
282
  return (
269
283
  <Flex spacing="lg">
270
284
  <VariantTitle title="Subtle - White - Shadow">
@@ -286,7 +300,7 @@ export const Interactive: Story = {
286
300
  parameters: {
287
301
  controls: { exclude: ['variant', 'colorScheme'] },
288
302
  },
289
- render: ({ children, ...props }) => {
303
+ render: ({ children, ...props }: CardStoryProps) => {
290
304
  return (
291
305
  <Flex spacing="lg">
292
306
  <VariantTitle title="Pressable - Subtle - White">
@@ -164,6 +164,9 @@ const styles = StyleSheet.create(theme => ({
164
164
  pig: {
165
165
  borderWidth: theme.components.card.brand.borderWidth,
166
166
  },
167
+ highlight: {
168
+ borderWidth: theme.components.card.brand.borderWidth,
169
+ },
167
170
  },
168
171
  shadowColor: {
169
172
  functional: {
@@ -331,6 +334,22 @@ const styles = StyleSheet.create(theme => ({
331
334
  borderColor: theme.color.border.strong,
332
335
  },
333
336
  },
337
+ {
338
+ variant: 'subtle',
339
+ colorScheme: 'highlight',
340
+ styles: {
341
+ backgroundColor: theme.color.surface.highlight.subtle,
342
+ borderColor: theme.color.border.strong,
343
+ },
344
+ },
345
+ {
346
+ variant: 'emphasis',
347
+ colorScheme: 'highlight',
348
+ styles: {
349
+ backgroundColor: theme.color.surface.highlight.default,
350
+ borderColor: theme.color.border.strong,
351
+ },
352
+ },
334
353
  ],
335
354
  },
336
355
  }));
@@ -34,7 +34,7 @@ Icons are often used to enhance the usability and accessibility of digital produ
34
34
  You can either use the React Native components directy from our `@utilitywarehouse/hearth-react-native-icons` package or use the `Icon`
35
35
  component from our `@utilitywarehouse/hearth-react-native` package to render the icons with utility props such as `color`.
36
36
 
37
- We reccomend that you use the `Icon` component to ensure that the icons are styled correctly and consistently across your application.
37
+ We recommend that you use the `Icon` component to ensure that the icons are styled correctly and consistently across your application.
38
38
  The `Icon` component also provides additional functionality such as handling different color modes and sizes as well as automatically applying
39
39
  light and dark mode colors from the theme.
40
40
 
@@ -9,7 +9,7 @@ import {
9
9
  EyeSmallIcon,
10
10
  SearchMediumIcon,
11
11
  } from '@utilitywarehouse/hearth-react-native-icons';
12
- import { useTheme } from '../../hooks';
12
+ import { useFormFieldAccessibility, useTheme } from '../../hooks';
13
13
  import { BodyText } from '../BodyText';
14
14
  import { FormField, useFormFieldContext } from '../FormField';
15
15
  import { Spinner } from '../Spinner';
@@ -75,7 +75,7 @@ const Input = forwardRef<TextInput, InputProps>(
75
75
  if (formFieldContext?.setShouldHandleAccessibility) {
76
76
  formFieldContext.setShouldHandleAccessibility(true);
77
77
  }
78
- }, []);
78
+ }, [formFieldContext]);
79
79
 
80
80
  const [fieldType, setFieldType] = useState<'password' | 'text'>(
81
81
  type === 'password' ? 'password' : 'text'
@@ -88,6 +88,16 @@ const Input = forwardRef<TextInput, InputProps>(
88
88
 
89
89
  const shouldShowPasswordToggle = type === 'password' && showPasswordToggle;
90
90
  const shouldShowClear = clearable && !!(props as InputWithoutChildrenProps)?.value;
91
+ const { accessibilityHint, accessibilityLabel } = useFormFieldAccessibility({
92
+ label: inputLabel,
93
+ helperText: inputHelperText,
94
+ validText: inputValidText,
95
+ invalidText: inputInvalidText,
96
+ required: inputRequired,
97
+ validationStatus: inputValidationStatus,
98
+ fallbackLabel: props.accessibilityLabel,
99
+ fallbackHint: props.accessibilityHint,
100
+ });
91
101
 
92
102
  const toggleFieldType = () => {
93
103
  setFieldType(fieldType === 'password' ? 'text' : 'password');
@@ -107,37 +117,6 @@ const Input = forwardRef<TextInput, InputProps>(
107
117
  return undefined;
108
118
  })();
109
119
 
110
- const getAccessibilityLabel = () => {
111
- let accessibilityLabel = '';
112
- if (inputLabel) {
113
- accessibilityLabel = accessibilityLabel + inputLabel;
114
- }
115
- if (inputRequired) {
116
- accessibilityLabel = accessibilityLabel + ', required';
117
- }
118
-
119
- return accessibilityLabel || props.accessibilityLabel;
120
- };
121
-
122
- const getAccessibilityHint = () => {
123
- let accessibilityHint = '';
124
- if (inputHelperText) {
125
- accessibilityHint = accessibilityHint + inputHelperText;
126
- }
127
- if (inputValidationStatus !== 'initial') {
128
- if (accessibilityHint.length > 0) {
129
- accessibilityHint = accessibilityHint + ', ';
130
- }
131
- if (inputValidationStatus === 'invalid' && inputInvalidText) {
132
- accessibilityHint = accessibilityHint + inputInvalidText;
133
- }
134
- if (inputValidationStatus === 'valid' && inputValidText) {
135
- accessibilityHint = accessibilityHint + inputValidText;
136
- }
137
- }
138
- return accessibilityHint || props.accessibilityHint;
139
- };
140
-
141
120
  return (
142
121
  <FormField
143
122
  label={label}
@@ -188,8 +167,8 @@ const Input = forwardRef<TextInput, InputProps>(
188
167
  inputMode={getInputMode}
189
168
  inBottomSheet={inBottomSheet}
190
169
  {...props}
191
- aria-label={getAccessibilityLabel()}
192
- accessibilityHint={getAccessibilityHint()}
170
+ aria-label={accessibilityLabel}
171
+ accessibilityHint={accessibilityHint}
193
172
  />
194
173
  {shouldShowClear && (
195
174
  <InputSlot>
@@ -32,6 +32,8 @@ import {
32
32
  import { BackToTopButton, BadgeList, UsageWrap, ViewFigmaButton } from '../../../docs/components';
33
33
  import * as Stories from './List.stories';
34
34
 
35
+ import StorybookLink from '../../../../../shared/storybook/StorybookLink';
36
+
35
37
  <Meta title="Components / List" />
36
38
 
37
39
  <ViewFigmaButton url="https://www.figma.com/design/6NKZXZhFSExXrcbBgc6zTR/Hearth-Components-%26-Tokens?node-id=1-163" />
@@ -146,11 +148,11 @@ are ignored, so conditional list items will not affect which item loses the top
146
148
 
147
149
  #### - `ListItemHeading`
148
150
 
149
- Has all props of the [`BodyText` component](?path=/docs/typography-body-text--docs).
151
+ Has all props of the <StorybookLink to="typography-body-text">`BodyText` component</StorybookLink>.
150
152
 
151
153
  #### - `ListItemHelperText`
152
154
 
153
- Has all props of the [`BodyText` component](?path=/docs/typography-body-text--docs).
155
+ Has all props of the <StorybookLink to="typography-body-text">`BodyText` component</StorybookLink>.
154
156
 
155
157
  #### - `ListItemTrailingContent`
156
158
 
@@ -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,
@@ -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';