@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.
Files changed (87) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-lint.log +15 -18
  3. package/CHANGELOG.md +62 -0
  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/Rating/Rating.d.ts +6 -0
  8. package/build/components/Rating/Rating.js +76 -0
  9. package/build/components/Rating/Rating.props.d.ts +18 -0
  10. package/build/components/Rating/Rating.props.js +1 -0
  11. package/build/components/Rating/RatingStarEmpty.d.ts +6 -0
  12. package/build/components/Rating/RatingStarEmpty.js +9 -0
  13. package/build/components/Rating/RatingStarFilled.d.ts +6 -0
  14. package/build/components/Rating/RatingStarFilled.js +9 -0
  15. package/build/components/Rating/index.d.ts +2 -0
  16. package/build/components/Rating/index.js +1 -0
  17. package/build/components/Roundel/Roundel.d.ts +6 -0
  18. package/build/components/Roundel/Roundel.js +40 -0
  19. package/build/components/Roundel/Roundel.props.d.ts +6 -0
  20. package/build/components/Roundel/Roundel.props.js +1 -0
  21. package/build/components/Roundel/index.d.ts +2 -0
  22. package/build/components/Roundel/index.js +1 -0
  23. package/build/components/StepperInput/StepperButton.d.ts +22 -0
  24. package/build/components/StepperInput/StepperButton.js +55 -0
  25. package/build/components/StepperInput/StepperInput.d.ts +6 -0
  26. package/build/components/StepperInput/StepperInput.js +179 -0
  27. package/build/components/StepperInput/StepperInput.props.d.ts +31 -0
  28. package/build/components/StepperInput/StepperInput.props.js +1 -0
  29. package/build/components/StepperInput/index.d.ts +2 -0
  30. package/build/components/StepperInput/index.js +1 -0
  31. package/build/components/Textarea/Textarea.d.ts +1 -1
  32. package/build/components/Textarea/Textarea.js +21 -32
  33. package/build/components/Textarea/Textarea.props.d.ts +11 -0
  34. package/build/components/VerificationInput/VerificationInput.js +12 -22
  35. package/build/components/index.d.ts +3 -0
  36. package/build/components/index.js +3 -0
  37. package/build/hooks/index.d.ts +1 -0
  38. package/build/hooks/index.js +1 -0
  39. package/build/hooks/useFormFieldAccessibility.d.ts +17 -0
  40. package/build/hooks/useFormFieldAccessibility.js +32 -0
  41. package/build/hooks/useFormFieldAccessibility.test.d.ts +1 -0
  42. package/build/hooks/useFormFieldAccessibility.test.js +56 -0
  43. package/docs/adding-shadows.mdx +2 -2
  44. package/docs/changelog.mdx +16 -0
  45. package/docs/components/AllComponents.web.tsx +30 -1
  46. package/docs/dark-mode-best-practice.mdx +328 -0
  47. package/package.json +6 -4
  48. package/src/components/Banner/Banner.stories.tsx +14 -0
  49. package/src/components/Card/Card.docs.mdx +16 -17
  50. package/src/components/Card/Card.props.ts +1 -0
  51. package/src/components/Card/Card.stories.tsx +35 -21
  52. package/src/components/Card/CardRoot.tsx +19 -0
  53. package/src/components/Icon/Icon.docs.mdx +1 -1
  54. package/src/components/Input/Input.tsx +14 -35
  55. package/src/components/List/List.docs.mdx +4 -2
  56. package/src/components/Modal/Modal.docs.mdx +58 -4
  57. package/src/components/NavModal/NavModal.docs.mdx +2 -2
  58. package/src/components/Rating/Rating.docs.mdx +178 -0
  59. package/src/components/Rating/Rating.figma.tsx +20 -0
  60. package/src/components/Rating/Rating.props.ts +22 -0
  61. package/src/components/Rating/Rating.stories.tsx +95 -0
  62. package/src/components/Rating/Rating.tsx +140 -0
  63. package/src/components/Rating/RatingStarEmpty.tsx +22 -0
  64. package/src/components/Rating/RatingStarFilled.tsx +27 -0
  65. package/src/components/Rating/index.ts +2 -0
  66. package/src/components/Roundel/Roundel.docs.mdx +48 -0
  67. package/src/components/Roundel/Roundel.figma.tsx +17 -0
  68. package/src/components/Roundel/Roundel.props.ts +8 -0
  69. package/src/components/Roundel/Roundel.stories.tsx +49 -0
  70. package/src/components/Roundel/Roundel.tsx +51 -0
  71. package/src/components/Roundel/index.ts +2 -0
  72. package/src/components/StepperInput/StepperButton.tsx +83 -0
  73. package/src/components/StepperInput/StepperInput.docs.mdx +121 -0
  74. package/src/components/StepperInput/StepperInput.figma.tsx +45 -0
  75. package/src/components/StepperInput/StepperInput.props.ts +39 -0
  76. package/src/components/StepperInput/StepperInput.stories.tsx +270 -0
  77. package/src/components/StepperInput/StepperInput.tsx +322 -0
  78. package/src/components/StepperInput/index.ts +2 -0
  79. package/src/components/Textarea/Textarea.docs.mdx +2 -0
  80. package/src/components/Textarea/Textarea.props.ts +11 -0
  81. package/src/components/Textarea/Textarea.stories.tsx +14 -0
  82. package/src/components/Textarea/Textarea.tsx +22 -34
  83. package/src/components/VerificationInput/VerificationInput.tsx +13 -25
  84. package/src/components/index.ts +3 -0
  85. package/src/hooks/index.ts +1 -0
  86. package/src/hooks/useFormFieldAccessibility.test.tsx +74 -0
  87. package/src/hooks/useFormFieldAccessibility.ts +67 -0
@@ -0,0 +1,45 @@
1
+ import figma from '@figma/code-connect';
2
+ import { StepperInput } from '../';
3
+
4
+ const props = {
5
+ value: figma.string('Value'),
6
+ label: figma.string('Label'),
7
+ labelVariant: figma.enum('Label variant', {
8
+ Body: 'body',
9
+ Heading: 'heading',
10
+ }),
11
+ helperText: figma.boolean('Helper text?', {
12
+ true: figma.string('Helper text'),
13
+ false: undefined,
14
+ }),
15
+ validationStatus: figma.enum('State', {
16
+ Default: 'initial',
17
+ Invalid: 'invalid',
18
+ }),
19
+ invalidText: figma.enum('State', {
20
+ Invalid: figma.string('Validation'),
21
+ }),
22
+ };
23
+
24
+ const handleChangeText = () => {
25
+ // Placeholder for Code Connect examples.
26
+ };
27
+
28
+ figma.connect(
29
+ StepperInput,
30
+ 'https://www.figma.com/design/6NKZXZhFSExXrcbBgc6zTR/Hearth-Components---Tokens?node-id=10612%3A1860&m=dev',
31
+ {
32
+ props,
33
+ example: props => (
34
+ <StepperInput
35
+ value={props.value}
36
+ label={props.label}
37
+ labelVariant={props.labelVariant}
38
+ helperText={props.helperText}
39
+ validationStatus={props.validationStatus}
40
+ invalidText={props.invalidText}
41
+ onChangeText={handleChangeText}
42
+ />
43
+ ),
44
+ }
45
+ );
@@ -0,0 +1,39 @@
1
+ import type { ComponentType, Ref } from 'react';
2
+ import type { TextInput, TextInputProps, ViewProps } from 'react-native';
3
+
4
+ export interface StepperBaseProps {
5
+ ref?: Ref<TextInput>;
6
+ disabled?: boolean;
7
+ validationStatus?: 'initial' | 'valid' | 'invalid';
8
+ readonly?: boolean;
9
+ focused?: boolean;
10
+ placeholder?: string;
11
+ inBottomSheet?: boolean;
12
+ required?: boolean;
13
+ label?: string;
14
+ labelVariant?: 'heading' | 'body';
15
+ helperText?: string;
16
+ helperIcon?: ComponentType;
17
+ validText?: string;
18
+ invalidText?: string;
19
+ value?: number | string;
20
+ defaultValue?: number;
21
+ min?: number;
22
+ max?: number;
23
+ step?: number;
24
+ onChangeValue?: (value: number) => void;
25
+ focusInputOnStepPress?: boolean;
26
+ decrementAccessibilityLabel?: string;
27
+ incrementAccessibilityLabel?: string;
28
+ }
29
+
30
+ export type StepperInputProps = StepperBaseProps &
31
+ Omit<
32
+ TextInputProps,
33
+ 'children' | 'value' | 'defaultValue' | 'onChangeText' | 'editable' | 'keyboardType'
34
+ > &
35
+ ViewProps & {
36
+ onChangeText?: (text: string) => void;
37
+ };
38
+
39
+ export default StepperInputProps;
@@ -0,0 +1,270 @@
1
+ import { Meta, StoryObj } from '@storybook/react-native';
2
+ import type { ComponentProps } from 'react';
3
+ import { useEffect, useState } from 'react';
4
+ import { StepperInput } from '.';
5
+ import { VariantTitle } from '../../../docs/components';
6
+ import { Flex } from '../Flex';
7
+
8
+ type StepperInputStoryProps = ComponentProps<typeof StepperInput>;
9
+
10
+ const meta = {
11
+ title: 'Stories / StepperInput',
12
+ component: StepperInput,
13
+ parameters: {
14
+ layout: 'centered',
15
+ },
16
+ argTypes: {
17
+ value: {
18
+ control: 'text',
19
+ },
20
+ label: {
21
+ control: 'text',
22
+ },
23
+ helperText: {
24
+ control: 'text',
25
+ },
26
+ labelVariant: {
27
+ control: 'radio',
28
+ options: ['body', 'heading'],
29
+ },
30
+ validationStatus: {
31
+ control: 'select',
32
+ options: ['initial', 'valid', 'invalid'],
33
+ },
34
+ validText: {
35
+ control: 'text',
36
+ },
37
+ invalidText: {
38
+ control: 'text',
39
+ },
40
+ disabled: {
41
+ control: 'boolean',
42
+ },
43
+ readonly: {
44
+ control: 'boolean',
45
+ },
46
+ focused: {
47
+ control: 'boolean',
48
+ },
49
+ min: {
50
+ control: 'number',
51
+ },
52
+ max: {
53
+ control: 'number',
54
+ },
55
+ step: {
56
+ control: 'number',
57
+ },
58
+ focusInputOnStepPress: {
59
+ control: 'boolean',
60
+ },
61
+ },
62
+ args: {
63
+ label: 'Label',
64
+ helperText: 'Helper text',
65
+ value: '10',
66
+ validationStatus: 'initial',
67
+ disabled: false,
68
+ readonly: false,
69
+ focused: false,
70
+ step: 1,
71
+ min: 0,
72
+ focusInputOnStepPress: false,
73
+ },
74
+ } satisfies Meta<typeof StepperInput>;
75
+
76
+ export default meta;
77
+ type Story = StoryObj<typeof meta>;
78
+
79
+ export const Playground: Story = {
80
+ render: ({
81
+ value: initialValue,
82
+ onChangeText: handleChangeText,
83
+ onChangeValue: handleChangeValue,
84
+ ...args
85
+ }: StepperInputStoryProps) => {
86
+ const [value, setValue] = useState(initialValue ?? '10');
87
+
88
+ useEffect(() => {
89
+ setValue(initialValue ?? '');
90
+ }, [initialValue]);
91
+
92
+ return (
93
+ <StepperInput
94
+ {...args}
95
+ value={value}
96
+ onChangeText={text => {
97
+ setValue(text);
98
+ handleChangeText?.(text);
99
+ }}
100
+ onChangeValue={nextValue => {
101
+ setValue(`${nextValue}`);
102
+ handleChangeValue?.(nextValue);
103
+ }}
104
+ />
105
+ );
106
+ },
107
+ };
108
+
109
+ export const States: Story = {
110
+ parameters: {
111
+ controls: { include: [] },
112
+ },
113
+ render: () => {
114
+ const [values, setValues] = useState({
115
+ default: '10',
116
+ focused: '10',
117
+ invalid: '10',
118
+ invalidFocused: '10',
119
+ heading: '10',
120
+ headingInvalid: '10',
121
+ disabled: '10',
122
+ });
123
+
124
+ const updateValue = (key: keyof typeof values) => (text: string) => {
125
+ setValues(currentValues => ({ ...currentValues, [key]: text }));
126
+ };
127
+
128
+ return (
129
+ <Flex direction="column" spacing="lg" style={{ width: 420 }}>
130
+ <VariantTitle title="Default">
131
+ <StepperInput
132
+ label="Label"
133
+ helperText="Helper text"
134
+ value={values.default}
135
+ onChangeText={updateValue('default')}
136
+ />
137
+ </VariantTitle>
138
+ <VariantTitle title="Focused">
139
+ <StepperInput
140
+ label="Label"
141
+ helperText="Helper text"
142
+ focused
143
+ value={values.focused}
144
+ onChangeText={updateValue('focused')}
145
+ />
146
+ </VariantTitle>
147
+ <VariantTitle title="Invalid">
148
+ <StepperInput
149
+ label="Label"
150
+ helperText="Helper text"
151
+ validationStatus="invalid"
152
+ invalidText="Validation text"
153
+ value={values.invalid}
154
+ onChangeText={updateValue('invalid')}
155
+ />
156
+ </VariantTitle>
157
+ <VariantTitle title="Invalid Focused">
158
+ <StepperInput
159
+ label="Label"
160
+ helperText="Helper text"
161
+ focused
162
+ validationStatus="invalid"
163
+ invalidText="Validation text"
164
+ value={values.invalidFocused}
165
+ onChangeText={updateValue('invalidFocused')}
166
+ />
167
+ </VariantTitle>
168
+ <VariantTitle title="Heading Label">
169
+ <StepperInput
170
+ label="Label"
171
+ helperText="Helper text"
172
+ labelVariant="heading"
173
+ value={values.heading}
174
+ onChangeText={updateValue('heading')}
175
+ />
176
+ </VariantTitle>
177
+ <VariantTitle title="Heading Invalid">
178
+ <StepperInput
179
+ label="Label"
180
+ helperText="Helper text"
181
+ labelVariant="heading"
182
+ validationStatus="invalid"
183
+ invalidText="Validation text"
184
+ value={values.headingInvalid}
185
+ onChangeText={updateValue('headingInvalid')}
186
+ />
187
+ </VariantTitle>
188
+ <VariantTitle title="Disabled">
189
+ <StepperInput
190
+ label="Label"
191
+ helperText="Helper text"
192
+ disabled
193
+ value={values.disabled}
194
+ onChangeText={updateValue('disabled')}
195
+ />
196
+ </VariantTitle>
197
+ </Flex>
198
+ );
199
+ },
200
+ };
201
+
202
+ export const Bounds: Story = {
203
+ parameters: {
204
+ controls: { include: ['min', 'max', 'step', 'focusInputOnStepPress'] },
205
+ },
206
+ args: {
207
+ min: 0,
208
+ max: 12,
209
+ step: 2,
210
+ },
211
+ render: (args: StepperInputStoryProps) => {
212
+ const [value, setValue] = useState('10');
213
+
214
+ return <StepperInput {...args} value={value} onChangeText={setValue} />;
215
+ },
216
+ };
217
+
218
+ export const FocusOnStepPress: Story = {
219
+ parameters: {
220
+ controls: { include: ['focusInputOnStepPress'] },
221
+ },
222
+ args: {
223
+ focusInputOnStepPress: true,
224
+ label: 'Guests',
225
+ helperText: 'Button presses will keep focus in the input',
226
+ min: 1,
227
+ max: 10,
228
+ },
229
+ render: (args: StepperInputStoryProps) => {
230
+ const [value, setValue] = useState('2');
231
+
232
+ return <StepperInput {...args} value={value} onChangeText={setValue} />;
233
+ },
234
+ };
235
+
236
+ export const LargeStep: Story = {
237
+ parameters: {
238
+ controls: { include: ['step'] },
239
+ },
240
+ args: {
241
+ step: 10,
242
+ label: 'Large Step',
243
+ helperText: 'Step value of 10',
244
+ min: 0,
245
+ max: 100,
246
+ },
247
+ render: (args: StepperInputStoryProps) => {
248
+ const [value, setValue] = useState('20');
249
+
250
+ return <StepperInput {...args} value={value} onChangeText={setValue} />;
251
+ },
252
+ };
253
+
254
+ export const DecimalStep: Story = {
255
+ parameters: {
256
+ controls: { include: ['min', 'max', 'step'] },
257
+ },
258
+ args: {
259
+ step: 0.5,
260
+ label: 'Weight',
261
+ helperText: 'Supports fractional values',
262
+ min: -2,
263
+ max: 2,
264
+ },
265
+ render: (args: StepperInputStoryProps) => {
266
+ const [value, setValue] = useState('0.5');
267
+
268
+ return <StepperInput {...args} value={value} onChangeText={setValue} />;
269
+ },
270
+ };
@@ -0,0 +1,322 @@
1
+ import { AddSmallIcon, MinusSmallIcon } from '@utilitywarehouse/hearth-react-native-icons';
2
+ import { useEffect, useImperativeHandle, useRef, useState } from 'react';
3
+ import type { TextInput, TextInputFocusEvent } from 'react-native';
4
+ import { View } from 'react-native';
5
+ import { StyleSheet } from 'react-native-unistyles';
6
+ import { useFormFieldAccessibility } from '../../hooks';
7
+ import { FormField } from '../FormField';
8
+ import { InputComponent, InputField } from '../Input/Input';
9
+ import StepperButton from './StepperButton';
10
+ import StepperInputProps from './StepperInput.props';
11
+
12
+ const normalizeValue = (value?: string | number) => {
13
+ if (value === undefined || value === null || value === '') {
14
+ return '';
15
+ }
16
+
17
+ return `${value}`;
18
+ };
19
+
20
+ const getDecimalPlaces = (value?: number | string) => {
21
+ if (value === undefined || value === null || value === '') {
22
+ return 0;
23
+ }
24
+
25
+ const normalizedValue = `${value}`;
26
+ const decimalPart = normalizedValue.split('.')[1];
27
+
28
+ return decimalPart ? decimalPart.length : 0;
29
+ };
30
+
31
+ const formatNumber = (value: number, precision: number) => {
32
+ if (precision <= 0) {
33
+ return `${Math.trunc(value)}`;
34
+ }
35
+
36
+ return value
37
+ .toFixed(precision)
38
+ .replace(/\.0+$/, '')
39
+ .replace(/(\.\d*?)0+$/, '$1');
40
+ };
41
+
42
+ const sanitizeValue = (value: string, allowNegative: boolean, allowDecimal: boolean) => {
43
+ const strippedValue = value.replace(
44
+ allowDecimal ? /[^\d,.-]/g : allowNegative ? /[^\d-]/g : /\D/g,
45
+ ''
46
+ );
47
+ const normalizedValue = allowDecimal ? strippedValue.replace(/,/g, '.') : strippedValue;
48
+
49
+ if (!allowNegative) {
50
+ const unsignedValue = normalizedValue.replace(/-/g, '');
51
+
52
+ if (!allowDecimal) {
53
+ return unsignedValue;
54
+ }
55
+
56
+ const [integerPart = '', ...decimalParts] = unsignedValue.split('.');
57
+ const decimalPart = decimalParts.join('');
58
+
59
+ return decimalParts.length > 0 ? `${integerPart}.${decimalPart}` : integerPart;
60
+ }
61
+
62
+ const hasLeadingMinus = normalizedValue.startsWith('-');
63
+ const unsignedValue = normalizedValue.replace(/-/g, '');
64
+
65
+ if (!allowDecimal) {
66
+ return `${hasLeadingMinus ? '-' : ''}${unsignedValue}`;
67
+ }
68
+
69
+ const [integerPart = '', ...decimalParts] = unsignedValue.split('.');
70
+ const decimalPart = decimalParts.join('');
71
+ const composedValue = decimalParts.length > 0 ? `${integerPart}.${decimalPart}` : integerPart;
72
+
73
+ return `${hasLeadingMinus ? '-' : ''}${composedValue}`;
74
+ };
75
+
76
+ const parseValue = (value: string) => {
77
+ if (!value || value === '-' || value === '.' || value === '-.') {
78
+ return null;
79
+ }
80
+
81
+ const parsedValue = Number(value);
82
+ return Number.isNaN(parsedValue) ? null : parsedValue;
83
+ };
84
+
85
+ const clampValue = (value: number, min?: number, max?: number) => {
86
+ let nextValue = value;
87
+
88
+ if (typeof min === 'number') {
89
+ nextValue = Math.max(min, nextValue);
90
+ }
91
+
92
+ if (typeof max === 'number') {
93
+ nextValue = Math.min(max, nextValue);
94
+ }
95
+
96
+ return nextValue;
97
+ };
98
+
99
+ const StepperInput = ({
100
+ value,
101
+ defaultValue,
102
+ onChangeText,
103
+ onChangeValue,
104
+ min,
105
+ max,
106
+ step = 1,
107
+ focusInputOnStepPress = false,
108
+ validationStatus = 'initial',
109
+ disabled = false,
110
+ readonly = false,
111
+ focused = false,
112
+ inBottomSheet = false,
113
+ required = true,
114
+ label,
115
+ labelVariant = 'body',
116
+ helperText,
117
+ helperIcon,
118
+ validText,
119
+ invalidText,
120
+ style,
121
+ decrementAccessibilityLabel = 'Decrease value',
122
+ incrementAccessibilityLabel = 'Increase value',
123
+ onFocus,
124
+ onBlur,
125
+ ref,
126
+ ...props
127
+ }: StepperInputProps) => {
128
+ const inputRef = useRef<TextInput>(null);
129
+ const isControlled = value !== undefined;
130
+ const [internalValue, setInternalValue] = useState(() => normalizeValue(defaultValue));
131
+ const [isInputFocused, setIsInputFocused] = useState(false);
132
+
133
+ const displayValue = isControlled ? normalizeValue(value) : internalValue;
134
+ const parsedValue = parseValue(displayValue);
135
+ const resolvedFocused = focused || isInputFocused;
136
+ const allowNegative = typeof min !== 'number' || min < 0 || (typeof max === 'number' && max < 0);
137
+ const decimalPrecision = Math.max(
138
+ getDecimalPlaces(value),
139
+ getDecimalPlaces(defaultValue),
140
+ getDecimalPlaces(min),
141
+ getDecimalPlaces(max),
142
+ getDecimalPlaces(step)
143
+ );
144
+ const allowDecimal = decimalPrecision > 0;
145
+ const keyboardType = allowNegative || allowDecimal ? 'numeric' : 'number-pad';
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
+ });
157
+
158
+ useImperativeHandle(ref, () => inputRef.current as TextInput, []);
159
+
160
+ useEffect(() => {
161
+ if (!isControlled && defaultValue !== undefined) {
162
+ setInternalValue(normalizeValue(defaultValue));
163
+ }
164
+ }, [defaultValue, isControlled]);
165
+
166
+ const updateValue = (nextValue: string) => {
167
+ if (!isControlled) {
168
+ setInternalValue(nextValue);
169
+ }
170
+
171
+ onChangeText?.(nextValue);
172
+
173
+ const nextParsedValue = parseValue(nextValue);
174
+ if (nextParsedValue !== null) {
175
+ onChangeValue?.(clampValue(nextParsedValue, min, max));
176
+ }
177
+ };
178
+
179
+ const handleChangeText = (nextText: string) => {
180
+ const sanitizedValue = sanitizeValue(nextText, allowNegative, allowDecimal);
181
+
182
+ if (
183
+ sanitizedValue === '' ||
184
+ sanitizedValue === '-' ||
185
+ sanitizedValue === '.' ||
186
+ sanitizedValue === '-.' ||
187
+ (allowDecimal && sanitizedValue.endsWith('.'))
188
+ ) {
189
+ updateValue(sanitizedValue);
190
+ return;
191
+ }
192
+
193
+ const nextParsedValue = parseValue(sanitizedValue);
194
+
195
+ if (nextParsedValue === null) {
196
+ updateValue(sanitizedValue);
197
+ return;
198
+ }
199
+
200
+ const clampedText = formatNumber(clampValue(nextParsedValue, min, max), decimalPrecision);
201
+ updateValue(clampedText);
202
+ };
203
+
204
+ const handleStepPress = (direction: 1 | -1) => {
205
+ const baseValue = parsedValue ?? (typeof min === 'number' ? min : 0);
206
+ const nextValue = clampValue(baseValue + direction * step, min, max);
207
+ const normalizedValue = formatNumber(nextValue, decimalPrecision);
208
+
209
+ updateValue(normalizedValue);
210
+ if (focusInputOnStepPress) {
211
+ inputRef.current?.focus();
212
+ }
213
+ };
214
+
215
+ const decrementDisabled =
216
+ disabled || readonly || (typeof min === 'number' && parsedValue !== null && parsedValue <= min);
217
+ const incrementDisabled =
218
+ disabled || readonly || (typeof max === 'number' && parsedValue !== null && parsedValue >= max);
219
+
220
+ const handleFocus = (event: TextInputFocusEvent) => {
221
+ setIsInputFocused(true);
222
+ onFocus?.(event);
223
+ };
224
+
225
+ const handleBlur = (event: TextInputFocusEvent) => {
226
+ setIsInputFocused(false);
227
+ onBlur?.(event);
228
+ };
229
+
230
+ return (
231
+ <FormField
232
+ label={label}
233
+ labelVariant={labelVariant}
234
+ helperText={helperText}
235
+ helperIcon={helperIcon}
236
+ validText={validText}
237
+ invalidText={invalidText}
238
+ required={required}
239
+ validationStatus={validationStatus}
240
+ disabled={disabled}
241
+ readonly={readonly}
242
+ accessibilityHandledByChildren
243
+ style={[styles.root, style]}
244
+ >
245
+ <View style={styles.controls}>
246
+ <StepperButton
247
+ icon={MinusSmallIcon}
248
+ disabled={decrementDisabled}
249
+ accessibilityLabel={decrementAccessibilityLabel}
250
+ onPress={() => handleStepPress(-1)}
251
+ />
252
+ <InputComponent
253
+ validationStatus={validationStatus}
254
+ isInvalid={validationStatus === 'invalid'}
255
+ isReadOnly={readonly}
256
+ isDisabled={disabled}
257
+ isFocused={resolvedFocused}
258
+ isRequired={required}
259
+ style={styles.inputRoot}
260
+ >
261
+ <InputField
262
+ // @ts-expect-error - ref forwarding issue mirrors the base Input component
263
+ ref={inputRef}
264
+ inputMode={inputMode}
265
+ keyboardType={keyboardType}
266
+ inBottomSheet={inBottomSheet}
267
+ editable={!disabled && !readonly}
268
+ textAlign="center"
269
+ value={displayValue}
270
+ onFocus={handleFocus}
271
+ onBlur={handleBlur}
272
+ onChangeText={handleChangeText}
273
+ accessibilityLabel={accessibilityLabel}
274
+ accessibilityHint={accessibilityHint}
275
+ accessibilityState={{
276
+ ...(props.accessibilityState ?? {}),
277
+ disabled: disabled || readonly,
278
+ }}
279
+ aria-disabled={disabled || readonly}
280
+ aria-readonly={readonly}
281
+ aria-required={required}
282
+ aria-invalid={validationStatus === 'invalid'}
283
+ {...props}
284
+ style={styles.inputField}
285
+ />
286
+ </InputComponent>
287
+ <StepperButton
288
+ icon={AddSmallIcon}
289
+ disabled={incrementDisabled}
290
+ accessibilityLabel={incrementAccessibilityLabel}
291
+ onPress={() => handleStepPress(1)}
292
+ />
293
+ </View>
294
+ </FormField>
295
+ );
296
+ };
297
+
298
+ StepperInput.displayName = 'StepperInput';
299
+
300
+ const styles = StyleSheet.create(theme => ({
301
+ root: {
302
+ width: '100%',
303
+ maxWidth: theme.components.input.maxWidth,
304
+ },
305
+ controls: {
306
+ flexDirection: 'row',
307
+ alignItems: 'center',
308
+ gap: theme.components.input.stepper.gap,
309
+ },
310
+ inputRoot: {
311
+ width: 80,
312
+ minWidth: 80,
313
+ paddingHorizontal: 0,
314
+ justifyContent: 'center',
315
+ },
316
+ inputField: {
317
+ textAlign: 'center',
318
+ paddingHorizontal: 0,
319
+ },
320
+ }));
321
+
322
+ export default StepperInput;
@@ -0,0 +1,2 @@
1
+ export { default as StepperInput } from './StepperInput';
2
+ export type { StepperInputProps } from './StepperInput.props';
@@ -72,6 +72,7 @@ all of the React Native [`View` props](https://reactnative.dev/docs/view).
72
72
  | validText | `string` | `-` | Text to display when validation status is 'valid'. **(Only to be used if the input has no children)** |
73
73
  | invalidText | `string` | `-` | Text to display when validation status is 'invalid'. |
74
74
  | required | `boolean` | `true` | Whether the input is required. **(Only to be used if the input has no children)** |
75
+ | defaultHeight | `number` | `96` | The initial height of the textarea in pixels when `resizable` is `true`. |
75
76
  | resizable | `boolean` | `false` | Adds a bottom-right drag handle so the textarea can be resized vertically. |
76
77
  | value | `string` | `-` | The value of the input. **(Only to be used if the input has no children)** |
77
78
  | onChange | `function` | `-` | Callback function that is triggered when the input value changes. **(Only to be used if the input has no children)** **(Only to be used if the input has no children)** |
@@ -157,6 +158,7 @@ const MyComponent = () => {
157
158
  helperText="Drag the corner handle to resize"
158
159
  placeholder="Enter your text here..."
159
160
  resizable
161
+ defaultHeight={140}
160
162
  />
161
163
  );
162
164
  };