@utilitywarehouse/hearth-react-native 0.18.0 → 0.19.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 (147) hide show
  1. package/.storybook/preview.tsx +1 -0
  2. package/.turbo/turbo-build.log +1 -1
  3. package/.turbo/turbo-lint.log +24 -21
  4. package/CHANGELOG.md +128 -0
  5. package/build/components/Banner/Banner.d.ts +1 -1
  6. package/build/components/Banner/Banner.js +24 -4
  7. package/build/components/Banner/Banner.props.d.ts +1 -6
  8. package/build/components/Box/Box.props.d.ts +5 -2
  9. package/build/components/Button/Button.d.ts +2 -0
  10. package/build/components/Button/ButtonGroupRoot.d.ts +5 -1
  11. package/build/components/Button/ButtonGroupRoot.js +3 -3
  12. package/build/components/Card/Card.context.d.ts +1 -1
  13. package/build/components/Card/Card.props.d.ts +2 -0
  14. package/build/components/Card/CardContent.js +3 -3
  15. package/build/components/Card/CardRoot.d.ts +1 -1
  16. package/build/components/Card/CardRoot.js +14 -4
  17. package/build/components/Checkbox/CheckboxGroupTextContent.js +1 -1
  18. package/build/components/Checkbox/CheckboxTextContent.js +1 -1
  19. package/build/components/Container/Container.d.ts +1 -1
  20. package/build/components/Container/Container.js +3 -3
  21. package/build/components/Container/Container.props.d.ts +5 -0
  22. package/build/components/Divider/Divider.d.ts +1 -1
  23. package/build/components/Divider/Divider.js +19 -19
  24. package/build/components/Divider/Divider.props.d.ts +6 -0
  25. package/build/components/ExpandableCard/ExpandableCardExpandedContent.js +1 -1
  26. package/build/components/Flex/Flex.d.ts +1 -1
  27. package/build/components/Flex/Flex.js +3 -3
  28. package/build/components/Flex/Flex.props.d.ts +5 -0
  29. package/build/components/Grid/Grid.d.ts +1 -1
  30. package/build/components/Grid/Grid.js +4 -4
  31. package/build/components/Grid/Grid.props.d.ts +5 -0
  32. package/build/components/Menu/Menu.d.ts +1 -1
  33. package/build/components/Menu/Menu.js +2 -2
  34. package/build/components/Menu/Menu.props.d.ts +2 -6
  35. package/build/components/Modal/Modal.d.ts +1 -1
  36. package/build/components/Modal/Modal.js +16 -6
  37. package/build/components/Modal/Modal.props.d.ts +1 -0
  38. package/build/components/Modal/Modal.web.d.ts +1 -1
  39. package/build/components/Modal/Modal.web.js +2 -2
  40. package/build/components/Radio/RadioGroupTextContent.js +1 -1
  41. package/build/components/Radio/RadioTextContent.js +1 -1
  42. package/build/components/Toast/Toast.context.d.ts +2 -4
  43. package/build/components/Toast/Toast.context.js +14 -2
  44. package/build/components/Toast/Toast.props.d.ts +4 -0
  45. package/build/components/Toast/ToastItem.js +5 -2
  46. package/build/components/VerificationInput/VerificationInput.d.ts +2 -5
  47. package/build/components/VerificationInput/VerificationInput.js +20 -7
  48. package/build/components/VerificationInput/VerificationInput.props.d.ts +10 -0
  49. package/build/components/VerificationInput/index.d.ts +1 -1
  50. package/build/components/VerificationInput/useVerificationInput.d.ts +1 -0
  51. package/build/components/VerificationInput/useVerificationInput.js +24 -9
  52. package/build/core/themes.d.ts +4 -4
  53. package/build/core/themes.js +1 -1
  54. package/build/types/values.d.ts +1 -1
  55. package/docs/changelog.mdx +687 -0
  56. package/docs/components/AllComponents.web.tsx +9 -9
  57. package/docs/layout-components.docs.mdx +3 -3
  58. package/package.json +7 -7
  59. package/scripts/copyChangelog.js +50 -0
  60. package/src/components/Alert/Alert.stories.tsx +1 -1
  61. package/src/components/Avatar/Avatar.stories.tsx +4 -5
  62. package/src/components/Badge/Badge.stories.tsx +3 -3
  63. package/src/components/Banner/Banner.docs.mdx +1 -1
  64. package/src/components/Banner/Banner.props.ts +13 -20
  65. package/src/components/Banner/Banner.stories.tsx +83 -15
  66. package/src/components/Banner/Banner.tsx +27 -3
  67. package/src/components/Box/Box.props.ts +11 -4
  68. package/src/components/Button/Button.docs.mdx +2 -2
  69. package/src/components/Button/Button.stories.tsx +4 -4
  70. package/src/components/Button/ButtonGroupRoot.tsx +8 -3
  71. package/src/components/Card/Card.context.ts +1 -1
  72. package/src/components/Card/Card.docs.mdx +1 -1
  73. package/src/components/Card/Card.props.ts +2 -0
  74. package/src/components/Card/Card.stories.tsx +9 -9
  75. package/src/components/Card/CardAction/CardAction.stories.tsx +2 -2
  76. package/src/components/Card/CardContent.tsx +3 -3
  77. package/src/components/Card/CardRoot.tsx +15 -5
  78. package/src/components/Checkbox/CheckboxGroupTextContent.tsx +2 -2
  79. package/src/components/Checkbox/CheckboxTextContent.tsx +1 -1
  80. package/src/components/Container/Container.docs.mdx +2 -2
  81. package/src/components/Container/Container.props.ts +5 -0
  82. package/src/components/Container/Container.stories.tsx +2 -2
  83. package/src/components/Container/Container.tsx +3 -3
  84. package/src/components/CurrencyInput/CurrencyInput.docs.mdx +1 -1
  85. package/src/components/CurrencyInput/CurrencyInput.stories.tsx +2 -2
  86. package/src/components/DateInput/DateInput.stories.tsx +3 -3
  87. package/src/components/DatePickerInput/DatePickerInput.stories.tsx +1 -1
  88. package/src/components/DescriptionList/DescriptionList.stories.tsx +1 -1
  89. package/src/components/Divider/Divider.docs.mdx +6 -6
  90. package/src/components/Divider/Divider.props.ts +6 -0
  91. package/src/components/Divider/Divider.tsx +19 -18
  92. package/src/components/ExpandableCard/ExpandableCardExpandedContent.tsx +1 -1
  93. package/src/components/Flex/Flex.docs.mdx +3 -3
  94. package/src/components/Flex/Flex.props.ts +5 -0
  95. package/src/components/Flex/Flex.stories.tsx +2 -2
  96. package/src/components/Flex/Flex.tsx +4 -3
  97. package/src/components/FormField/FormField.docs.mdx +1 -1
  98. package/src/components/FormField/FormField.stories.tsx +1 -1
  99. package/src/components/Grid/Grid.docs.mdx +5 -5
  100. package/src/components/Grid/Grid.props.ts +6 -0
  101. package/src/components/Grid/Grid.stories.tsx +8 -8
  102. package/src/components/Grid/Grid.tsx +4 -3
  103. package/src/components/HighlightBanner/HighlightBanner.docs.mdx +1 -1
  104. package/src/components/HighlightBanner/HighlightBanner.stories.tsx +2 -2
  105. package/src/components/Icon/Icon.docs.mdx +3 -3
  106. package/src/components/IconButton/IconButton.stories.tsx +5 -5
  107. package/src/components/IconContainer/IconContainer.docs.mdx +1 -1
  108. package/src/components/IconContainer/IconContainer.stories.tsx +3 -3
  109. package/src/components/IndicatorIconButton/IndicatorIconButton.stories.tsx +17 -9
  110. package/src/components/InlineLink/InlineLink.stories.tsx +2 -2
  111. package/src/components/Input/Input.stories.tsx +4 -4
  112. package/src/components/List/List.stories.tsx +1 -1
  113. package/src/components/Menu/Menu.docs.mdx +8 -5
  114. package/src/components/Menu/Menu.figma.tsx +27 -27
  115. package/src/components/Menu/Menu.props.ts +2 -6
  116. package/src/components/Menu/Menu.tsx +3 -6
  117. package/src/components/Menu/MenuItem.figma.tsx +26 -18
  118. package/src/components/Modal/Modal.docs.mdx +22 -21
  119. package/src/components/Modal/Modal.figma.tsx +58 -47
  120. package/src/components/Modal/Modal.props.ts +1 -0
  121. package/src/components/Modal/Modal.stories.tsx +4 -0
  122. package/src/components/Modal/Modal.tsx +20 -5
  123. package/src/components/Modal/Modal.web.tsx +2 -1
  124. package/src/components/PillGroup/PillGroup.stories.tsx +7 -7
  125. package/src/components/ProgressStepper/ProgressStepper.stories.tsx +7 -8
  126. package/src/components/Radio/Radio.stories.tsx +1 -1
  127. package/src/components/Radio/RadioGroup.stories.tsx +1 -1
  128. package/src/components/Radio/RadioGroupTextContent.tsx +2 -2
  129. package/src/components/Radio/RadioTextContent.tsx +1 -1
  130. package/src/components/SectionHeader/SectionHeader.stories.tsx +1 -1
  131. package/src/components/Switch/Switch.docs.mdx +8 -8
  132. package/src/components/Switch/Switch.stories.tsx +2 -2
  133. package/src/components/Tabs/Tabs.stories.tsx +1 -1
  134. package/src/components/Textarea/Textarea.docs.mdx +3 -3
  135. package/src/components/Toast/Toast.context.tsx +24 -3
  136. package/src/components/Toast/Toast.props.ts +5 -0
  137. package/src/components/Toast/Toast.stories.tsx +29 -0
  138. package/src/components/Toast/ToastItem.tsx +7 -2
  139. package/src/components/UnstyledIconButton/UnstyledIconButton.stories.tsx +5 -5
  140. package/src/components/VerificationInput/VerificationInput.docs.mdx +31 -8
  141. package/src/components/VerificationInput/VerificationInput.props.ts +11 -0
  142. package/src/components/VerificationInput/VerificationInput.stories.tsx +79 -3
  143. package/src/components/VerificationInput/VerificationInput.tsx +94 -62
  144. package/src/components/VerificationInput/index.ts +4 -2
  145. package/src/components/VerificationInput/useVerificationInput.ts +26 -10
  146. package/src/core/themes.ts +1 -1
  147. package/src/types/values.ts +1 -1
@@ -1,7 +1,12 @@
1
1
  import React, { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react';
2
2
  import { View } from 'react-native';
3
3
  import { StyleSheet } from 'react-native-unistyles';
4
- import type { ToastContextValue, ToastInstance, ToastOptions } from './Toast.props';
4
+ import type {
5
+ ToastContextValue,
6
+ ToastInstance,
7
+ ToastOptions,
8
+ ToastProviderProps,
9
+ } from './Toast.props';
5
10
  import ToastItem, { type ToastItemHandle } from './ToastItem';
6
11
 
7
12
  const ToastContext = createContext<ToastContextValue | undefined>(undefined);
@@ -12,7 +17,10 @@ export const useToastContext = () => {
12
17
  return ctx;
13
18
  };
14
19
 
15
- export const ToastProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
20
+ export const ToastProvider: React.FC<ToastProviderProps> = ({
21
+ children,
22
+ safeAreaPadding = true,
23
+ }) => {
16
24
  const [toasts, setToasts] = useState<ToastInstance[]>([]);
17
25
  const timers = useRef<Record<string, number>>({});
18
26
  const toastRefs = useRef<Record<string, ToastItemHandle | null>>({});
@@ -62,6 +70,10 @@ export const ToastProvider: React.FC<{ children: React.ReactNode }> = ({ childre
62
70
  [removeToast]
63
71
  );
64
72
 
73
+ styles.useVariants({
74
+ safeAreaPadding,
75
+ });
76
+
65
77
  useEffect(() => {
66
78
  return () => {
67
79
  // cleanup timers on unmount
@@ -99,7 +111,7 @@ export const useToast = () => {
99
111
 
100
112
  export default ToastContext;
101
113
 
102
- const styles = StyleSheet.create(theme => ({
114
+ const styles = StyleSheet.create((theme, rt) => ({
103
115
  container: {
104
116
  position: 'absolute',
105
117
  left: 0,
@@ -108,6 +120,15 @@ const styles = StyleSheet.create(theme => ({
108
120
  alignItems: 'stretch',
109
121
  paddingBottom: theme.space['200'],
110
122
  pointerEvents: 'box-none',
123
+ variants: {
124
+ safeAreaPadding: {
125
+ true: {
126
+ paddingBottom: theme.space['200'] + rt.insets.bottom,
127
+ paddingTop: rt.insets.top,
128
+ },
129
+ false: {},
130
+ },
131
+ },
111
132
  },
112
133
  stack: {
113
134
  width: '100%',
@@ -25,6 +25,11 @@ export interface ToastInstance extends ToastOptions {
25
25
  duration: number;
26
26
  }
27
27
 
28
+ export interface ToastProviderProps {
29
+ children: ReactNode;
30
+ safeAreaPadding?: boolean;
31
+ }
32
+
28
33
  export interface ToastContextValue {
29
34
  addToast: (opts: ToastOptions) => string;
30
35
  removeToast: (id: string) => void;
@@ -73,6 +73,35 @@ export const BasicToast: Story = {
73
73
  ),
74
74
  };
75
75
 
76
+ const LongToastDemo = () => {
77
+ const { addToast } = useToast();
78
+
79
+ return (
80
+ <View style={{ gap: 12, padding: 16 }}>
81
+ <Button
82
+ onPress={() =>
83
+ addToast({
84
+ text: "Couldn't update top-up. Please check your connection and try again.",
85
+ icon: WarningSmallIcon,
86
+ actionText: 'Retry',
87
+ onPress: () => console.log('Retry clicked'),
88
+ })
89
+ }
90
+ >
91
+ Show Long Toast
92
+ </Button>
93
+ </View>
94
+ );
95
+ };
96
+
97
+ export const LongToastMessage = {
98
+ render: () => (
99
+ <ViewWrap>
100
+ <LongToastDemo />
101
+ </ViewWrap>
102
+ ),
103
+ };
104
+
76
105
  const WithIconDemo = () => {
77
106
  const { addToast } = useToast();
78
107
 
@@ -113,7 +113,9 @@ const ToastItem = forwardRef<ToastItemHandle, Props>(({ toast, onClose }, ref) =
113
113
  <Icon as={IconComp} style={styles.icon} />
114
114
  </View>
115
115
  ) : null}
116
- <BodyText inverted>{toast.text}</BodyText>
116
+ <BodyText inverted style={styles.text}>
117
+ {toast.text}
118
+ </BodyText>
117
119
  </View>
118
120
  {toast.actionText ? (
119
121
  <Link onPress={handlePress} showIcon={false} inverted>
@@ -179,7 +181,8 @@ const styles = StyleSheet.create(theme => ({
179
181
  width: 24,
180
182
  height: 24,
181
183
  justifyContent: 'center',
182
- alignItems: 'center',
184
+ alignSelf: 'flex-start',
185
+ alignItems: 'flex-start',
183
186
  flexShrink: 0,
184
187
  },
185
188
  icon: {
@@ -192,8 +195,10 @@ const styles = StyleSheet.create(theme => ({
192
195
  alignItems: 'center',
193
196
  minWidth: 0,
194
197
  },
198
+ text: { flexShrink: 1 },
195
199
  actions: {
196
200
  flexShrink: 0,
201
+ alignSelf: 'flex-start',
197
202
  },
198
203
  }));
199
204
 
@@ -1,10 +1,10 @@
1
- import UnstyledIconButton from './UnstyledIconButton';
2
1
  import { Meta, StoryObj } from '@storybook/react-vite';
3
- import { VariantTitle } from '../../../docs/components';
4
2
  import * as Icons from '@utilitywarehouse/hearth-react-native-icons';
5
- import { Flex } from '../Flex';
6
- import { Platform } from 'react-native';
7
3
  import { CloseMediumIcon } from '@utilitywarehouse/hearth-react-native-icons';
4
+ import { Platform } from 'react-native';
5
+ import { VariantTitle } from '../../../docs/components';
6
+ import { Flex } from '../Flex';
7
+ import UnstyledIconButton from './UnstyledIconButton';
8
8
 
9
9
  const meta = {
10
10
  title: 'Stories / UnstyledIconButton',
@@ -77,7 +77,7 @@ export const States: Story = {
77
77
  const iconComponent =
78
78
  typeof iconProp === 'string' ? Icons[iconProp as keyof typeof Icons] : iconProp;
79
79
  return (
80
- <Flex direction="column" space="lg">
80
+ <Flex direction="column" spacing="lg">
81
81
  <VariantTitle title="Default" invert={inverted}>
82
82
  <UnstyledIconButton size={size} inverted={inverted} icon={iconComponent} />
83
83
  </VariantTitle>
@@ -17,9 +17,8 @@ The verification input component is used to capture OTP (One Time Password) or o
17
17
  - [Playground](#playground)
18
18
  - [Usage](#usage)
19
19
  - [Props](#props)
20
- - [Variants](#variants)
21
- - [States](#states)
22
- - [Secure Text Entry](#secure-text-entry)
20
+ - [Ref Methods](#ref-methods)
21
+ - [States](#states)
23
22
 
24
23
  ## Playground
25
24
 
@@ -45,10 +44,6 @@ const MyComponent = () => {
45
44
 
46
45
  ## Props
47
46
 
48
- ### `VerificationInput`
49
-
50
- The component accepts the following props:
51
-
52
47
  | Prop | Type | Default | Description |
53
48
  | :----------------- | :---------------------------------- | :---------- | :----------------------------------------------------------- |
54
49
  | `value` | `string` | - | The value of the input. |
@@ -63,7 +58,35 @@ The component accepts the following props:
63
58
  | `disabled` | `boolean` | `false` | Whether the input is disabled. |
64
59
  | `readonly` | `boolean` | `false` | Whether the input is read-only. |
65
60
  | `secureTextEntry` | `boolean` | `false` | Whether to obscure the text entry (e.g. for passwords/OTPs). |
61
+ | `autoFocus` | `boolean` | `false` | Whether the input should auto-focus when mounted. |
62
+
63
+ ## Ref Methods
64
+
65
+ <Canvas of={Stories.RefMethods} />
66
+
67
+ ```tsx
68
+ import { useRef } from 'react';
69
+ import {
70
+ VerificationInput,
71
+ type VerificationInputHandle,
72
+ } from '@utilitywarehouse/hearth-react-native';
73
+
74
+ const MyComponent = () => {
75
+ const inputRef = useRef<VerificationInputHandle>(null);
76
+
77
+ return (
78
+ <VerificationInput ref={inputRef} label="Enter Code" onChangeText={code => console.log(code)} />
79
+ );
80
+ };
81
+ ```
82
+
83
+ Available methods:
84
+
85
+ - `focus()`
86
+ - `blur()`
87
+ - `clear()`
88
+ - `focusIndex(index: number)`
66
89
 
67
- ## Variants
90
+ ## States
68
91
 
69
92
  <Canvas of={Stories.Variants} />
@@ -1,6 +1,13 @@
1
1
  import type { ComponentType } from 'react';
2
2
  import { ViewProps } from 'react-native';
3
3
 
4
+ export interface VerificationInputHandle {
5
+ focus: () => void;
6
+ blur: () => void;
7
+ clear: () => void;
8
+ focusIndex: (index: number) => void;
9
+ }
10
+
4
11
  export interface VerificationInputProps extends ViewProps {
5
12
  /**
6
13
  * The value of the input.
@@ -50,6 +57,10 @@ export interface VerificationInputProps extends ViewProps {
50
57
  * Whether to obscure the text entry (e.g. for passwords/OTPs).
51
58
  */
52
59
  secureTextEntry?: boolean;
60
+ /**
61
+ * Whether the input should auto-focus when mounted.
62
+ */
63
+ autoFocus?: boolean;
53
64
  }
54
65
 
55
66
  export default VerificationInputProps;
@@ -1,8 +1,10 @@
1
1
  import { Meta, StoryObj } from '@storybook/react-vite';
2
2
  import { InfoMediumIcon } from '@utilitywarehouse/hearth-react-native-icons';
3
- import React, { useState } from 'react';
4
- import { VerificationInput } from '.';
3
+ import { useRef, useState } from 'react';
4
+ import { VerificationInput, type VerificationInputHandle } from '.';
5
5
  import { VariantTitle } from '../../../docs/components';
6
+ import { BodyText } from '../BodyText';
7
+ import { Button } from '../Button';
6
8
  import { Flex } from '../Flex';
7
9
 
8
10
  const meta = {
@@ -24,10 +26,13 @@ const meta = {
24
26
  disabled: { control: 'boolean' },
25
27
  readonly: { control: 'boolean' },
26
28
  secureTextEntry: { control: 'boolean' },
29
+ autoFocus: { control: 'boolean' },
27
30
  },
28
31
  args: {
29
32
  label: 'Verification Code',
30
33
  validationStatus: 'initial',
34
+ autoFocus: true,
35
+ secureTextEntry: false,
31
36
  },
32
37
  } satisfies Meta<typeof VerificationInput>;
33
38
 
@@ -69,7 +74,7 @@ export const Variants: Story = {
69
74
  };
70
75
 
71
76
  return (
72
- <Flex direction="column" space="lg" style={{ width: 400 }}>
77
+ <Flex direction="column" spacing="lg" style={{ width: 400 }}>
73
78
  <VariantTitle title="Default">
74
79
  <VerificationInput
75
80
  label="Verification Code"
@@ -138,3 +143,74 @@ export const Variants: Story = {
138
143
  );
139
144
  },
140
145
  };
146
+
147
+ export const RefMethods: Story = {
148
+ parameters: {
149
+ controls: { include: [] },
150
+ },
151
+ render: () => {
152
+ const inputRef = useRef<VerificationInputHandle>(null);
153
+ const [value, setValue] = useState('123456');
154
+ const [status, setStatus] = useState('Ref not tested yet');
155
+
156
+ const handleFocus = () => {
157
+ if (inputRef.current) {
158
+ inputRef.current.focus();
159
+ setStatus('OK: focused first slot');
160
+ } else {
161
+ setStatus('Error: ref is null');
162
+ }
163
+ };
164
+
165
+ const handleBlur = () => {
166
+ if (inputRef.current) {
167
+ inputRef.current.blur();
168
+ setStatus('OK: blurred inputs');
169
+ } else {
170
+ setStatus('Error: ref is null');
171
+ }
172
+ };
173
+
174
+ const handleClear = () => {
175
+ if (inputRef.current) {
176
+ inputRef.current.clear();
177
+ setStatus('OK: cleared value');
178
+ } else {
179
+ setStatus('Error: ref is null');
180
+ }
181
+ };
182
+
183
+ const handleFocusIndex = () => {
184
+ if (inputRef.current) {
185
+ inputRef.current.focusIndex(3);
186
+ setStatus('OK: focused slot 4');
187
+ } else {
188
+ setStatus('Error: ref is null');
189
+ }
190
+ };
191
+
192
+ return (
193
+ <Flex direction="column" space="lg" style={{ width: 400 }}>
194
+ <VariantTitle title="Ref Methods">
195
+ <VerificationInput
196
+ ref={inputRef}
197
+ label="Verification Code"
198
+ value={value}
199
+ onChangeText={setValue}
200
+ />
201
+ </VariantTitle>
202
+ <VariantTitle title="Actions">
203
+ <Flex direction="column" space="sm">
204
+ <Flex direction="row" space="sm">
205
+ <Button onPress={handleFocus}>Focus</Button>
206
+ <Button onPress={handleFocusIndex}>Focus Slot 4</Button>
207
+ <Button onPress={handleBlur}>Blur</Button>
208
+ <Button onPress={handleClear}>Clear</Button>
209
+ </Flex>
210
+ <BodyText>{status}</BodyText>
211
+ </Flex>
212
+ </VariantTitle>
213
+ </Flex>
214
+ );
215
+ },
216
+ };
@@ -1,77 +1,109 @@
1
+ import { forwardRef, useImperativeHandle } from 'react';
1
2
  import { View } from 'react-native';
2
3
  import { StyleSheet } from 'react-native-unistyles';
3
4
  import { FormField } from '../FormField';
4
5
  import { useVerificationInput } from './useVerificationInput';
5
- import VerificationInputProps from './VerificationInput.props';
6
+ import type { VerificationInputHandle, VerificationInputProps } from './VerificationInput.props';
6
7
  import { VerificationInputSlot } from './VerificationInputSlot';
7
8
 
8
- const VerificationInput = ({
9
- value = '',
10
- onChangeText,
11
- label,
12
- labelVariant = 'body',
13
- helperText,
14
- helperIcon,
15
- validationStatus = 'initial',
16
- validText,
17
- invalidText,
18
- disabled = false,
19
- readonly = false,
20
- secureTextEntry = false,
21
- style,
22
- ...props
23
- }: VerificationInputProps) => {
24
- const length = 6;
25
- const { inputRefs, focusedIndex, handleFocus, handleBlur, handleChangeText, handleKeyPress } =
26
- useVerificationInput({
9
+ const VerificationInput = forwardRef<VerificationInputHandle, VerificationInputProps>(
10
+ (
11
+ {
12
+ value = '',
13
+ onChangeText,
14
+ label,
15
+ labelVariant = 'body',
16
+ helperText,
17
+ helperIcon,
18
+ validationStatus = 'initial',
19
+ validText,
20
+ invalidText,
21
+ disabled = false,
22
+ readonly = false,
23
+ secureTextEntry = false,
24
+ autoFocus = false,
25
+ style,
26
+ ...props
27
+ },
28
+ ref
29
+ ) => {
30
+ const length = 6;
31
+ const {
32
+ inputRefs,
33
+ displayValue,
34
+ focusedIndex,
35
+ handleFocus,
36
+ handleBlur,
37
+ handleChangeText,
38
+ handleKeyPress,
39
+ } = useVerificationInput({
27
40
  value,
28
41
  onChangeText,
29
42
  });
30
43
 
31
- const slots = Array.from({ length }, (_, index) => index);
44
+ useImperativeHandle(
45
+ ref,
46
+ () => ({
47
+ focus: () => inputRefs.current[0]?.focus(),
48
+ blur: () => {
49
+ inputRefs.current.forEach(input => input?.blur());
50
+ },
51
+ clear: () => onChangeText?.(''),
52
+ focusIndex: (index: number) => {
53
+ if (index >= 0 && index < length) {
54
+ inputRefs.current[index]?.focus();
55
+ }
56
+ },
57
+ }),
58
+ [length, onChangeText]
59
+ );
60
+
61
+ const slots = Array.from({ length }, (_, index) => index);
32
62
 
33
- return (
34
- <FormField
35
- label={label}
36
- labelVariant={labelVariant}
37
- helperText={helperText}
38
- helperIcon={helperIcon}
39
- validationStatus={validationStatus}
40
- validText={validText}
41
- invalidText={invalidText}
42
- disabled={disabled}
43
- readonly={readonly}
44
- style={[styles.root, style]}
45
- {...props}
46
- >
47
- <View style={styles.slotsContainer}>
48
- {slots.map(index => {
49
- const char = value[index] || '';
50
- const isActive = focusedIndex === index;
63
+ return (
64
+ <FormField
65
+ label={label}
66
+ labelVariant={labelVariant}
67
+ helperText={helperText}
68
+ helperIcon={helperIcon}
69
+ validationStatus={validationStatus}
70
+ validText={validText}
71
+ invalidText={invalidText}
72
+ disabled={disabled}
73
+ readonly={readonly}
74
+ style={[styles.root, style]}
75
+ {...props}
76
+ >
77
+ <View style={styles.slotsContainer}>
78
+ {slots.map(index => {
79
+ const char = displayValue[index] || '';
80
+ const isActive = focusedIndex === index;
51
81
 
52
- return (
53
- <VerificationInputSlot
54
- key={index}
55
- ref={ref => {
56
- inputRefs.current[index] = ref;
57
- }}
58
- value={char}
59
- isActive={isActive}
60
- validationStatus={validationStatus}
61
- disabled={disabled}
62
- readonly={readonly}
63
- secureTextEntry={secureTextEntry}
64
- onChangeText={text => handleChangeText(text, index)}
65
- onKeyPress={e => handleKeyPress(e, index)}
66
- onFocus={() => handleFocus(index)}
67
- onBlur={handleBlur}
68
- />
69
- );
70
- })}
71
- </View>
72
- </FormField>
73
- );
74
- };
82
+ return (
83
+ <VerificationInputSlot
84
+ key={index}
85
+ ref={inputRef => {
86
+ inputRefs.current[index] = inputRef;
87
+ }}
88
+ autoFocus={index === 0 && autoFocus}
89
+ value={char}
90
+ isActive={isActive}
91
+ validationStatus={validationStatus}
92
+ disabled={disabled}
93
+ readonly={readonly}
94
+ secureTextEntry={secureTextEntry}
95
+ onChangeText={text => handleChangeText(text, index)}
96
+ onKeyPress={e => handleKeyPress(e, index)}
97
+ onFocus={() => handleFocus(index)}
98
+ onBlur={handleBlur}
99
+ />
100
+ );
101
+ })}
102
+ </View>
103
+ </FormField>
104
+ );
105
+ }
106
+ );
75
107
 
76
108
  const styles = StyleSheet.create(theme => ({
77
109
  root: {
@@ -1,5 +1,7 @@
1
1
  import VerificationInput from './VerificationInput';
2
2
  export { default as VerificationInput } from './VerificationInput';
3
- export type { default as VerificationInputProps } from './VerificationInput.props';
3
+ export type {
4
+ VerificationInputHandle,
5
+ default as VerificationInputProps,
6
+ } from './VerificationInput.props';
4
7
  export default VerificationInput;
5
-
@@ -1,4 +1,4 @@
1
- import { useRef, useState } from 'react';
1
+ import { useEffect, useRef, useState } from 'react';
2
2
  import { NativeSyntheticEvent, TextInput, TextInputKeyPressEventData } from 'react-native';
3
3
 
4
4
  interface UseVerificationInputProps {
@@ -9,8 +9,17 @@ interface UseVerificationInputProps {
9
9
  export const useVerificationInput = ({ value, onChangeText }: UseVerificationInputProps) => {
10
10
  const length = 6;
11
11
  const inputRefs = useRef<(TextInput | null)[]>([]);
12
+ const latestValueRef = useRef(value);
13
+ const [displayValue, setDisplayValue] = useState(value);
12
14
  const [focusedIndex, setFocusedIndex] = useState<number | null>(null);
13
15
 
16
+ useEffect(() => {
17
+ if (value !== latestValueRef.current) {
18
+ latestValueRef.current = value;
19
+ setDisplayValue(value);
20
+ }
21
+ }, [value]);
22
+
14
23
  const handleFocus = (index: number) => {
15
24
  setFocusedIndex(index);
16
25
  };
@@ -19,11 +28,17 @@ export const useVerificationInput = ({ value, onChangeText }: UseVerificationInp
19
28
  setFocusedIndex(null);
20
29
  };
21
30
 
31
+ const updateValue = (nextValue: string) => {
32
+ latestValueRef.current = nextValue;
33
+ setDisplayValue(nextValue);
34
+ onChangeText?.(nextValue);
35
+ };
36
+
22
37
  const handleChangeText = (text: string, index: number) => {
23
- // Break down the text into an array of characters
38
+ const currentValue = latestValueRef.current;
24
39
  const chars = Array(length).fill('');
25
- for (let i = 0; i < value.length && i < length; i++) {
26
- chars[i] = value[i];
40
+ for (let i = 0; i < currentValue.length && i < length; i++) {
41
+ chars[i] = currentValue[i];
27
42
  }
28
43
 
29
44
  if (text.length > 1) {
@@ -41,28 +56,29 @@ export const useVerificationInput = ({ value, onChangeText }: UseVerificationInp
41
56
  inputRefs.current[index + 1]?.focus();
42
57
  }
43
58
  }
44
-
45
- onChangeText?.(chars.join(''));
59
+ updateValue(chars.join(''));
46
60
  };
47
61
 
48
62
  const handleKeyPress = (e: NativeSyntheticEvent<TextInputKeyPressEventData>, index: number) => {
49
63
  if (e.nativeEvent.key === 'Backspace') {
50
- if (!value[index] && index > 0) {
64
+ const currentValue = latestValueRef.current;
65
+ if (!currentValue[index] && index > 0) {
51
66
  e.preventDefault();
52
67
  inputRefs.current[index - 1]?.focus();
53
68
 
54
69
  const chars = Array(length).fill('');
55
- for (let i = 0; i < value.length && i < length; i++) {
56
- chars[i] = value[i];
70
+ for (let i = 0; i < currentValue.length && i < length; i++) {
71
+ chars[i] = currentValue[i];
57
72
  }
58
73
  chars[index - 1] = '';
59
- onChangeText?.(chars.join(''));
74
+ updateValue(chars.join(''));
60
75
  }
61
76
  }
62
77
  };
63
78
 
64
79
  return {
65
80
  inputRefs,
81
+ displayValue,
66
82
  focusedIndex,
67
83
  handleFocus,
68
84
  handleBlur,
@@ -205,7 +205,7 @@ const shared = {
205
205
  },
206
206
  },
207
207
  },
208
- space: {
208
+ spacing: {
209
209
  none: {
210
210
  gap: {
211
211
  base: layout.mobile.spacing.none,
@@ -81,4 +81,4 @@ export type BordeWidthValue =
81
81
 
82
82
  export type OpacityValue = AnimatableNumericValue | undefined;
83
83
 
84
- export type SpacingValues = keyof (typeof themes)['light']['globalStyle']['variants']['space'];
84
+ export type SpacingValues = keyof (typeof themes)['light']['globalStyle']['variants']['spacing'];