@utilitywarehouse/hearth-react-native 0.18.0 → 0.19.0

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 (127) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-lint.log +21 -18
  3. package/CHANGELOG.md +102 -0
  4. package/build/components/Banner/Banner.d.ts +1 -1
  5. package/build/components/Banner/Banner.js +24 -4
  6. package/build/components/Banner/Banner.props.d.ts +1 -6
  7. package/build/components/Box/Box.props.d.ts +5 -2
  8. package/build/components/Button/Button.d.ts +2 -0
  9. package/build/components/Button/ButtonGroupRoot.d.ts +5 -1
  10. package/build/components/Button/ButtonGroupRoot.js +3 -3
  11. package/build/components/Card/Card.context.d.ts +1 -1
  12. package/build/components/Card/Card.props.d.ts +2 -0
  13. package/build/components/Card/CardContent.js +3 -3
  14. package/build/components/Card/CardRoot.d.ts +1 -1
  15. package/build/components/Card/CardRoot.js +14 -4
  16. package/build/components/Checkbox/CheckboxGroupTextContent.js +1 -1
  17. package/build/components/Checkbox/CheckboxTextContent.js +1 -1
  18. package/build/components/Container/Container.d.ts +1 -1
  19. package/build/components/Container/Container.js +3 -3
  20. package/build/components/Container/Container.props.d.ts +5 -0
  21. package/build/components/Divider/Divider.d.ts +1 -1
  22. package/build/components/Divider/Divider.js +19 -19
  23. package/build/components/Divider/Divider.props.d.ts +6 -0
  24. package/build/components/ExpandableCard/ExpandableCardExpandedContent.js +1 -1
  25. package/build/components/Flex/Flex.d.ts +1 -1
  26. package/build/components/Flex/Flex.js +3 -3
  27. package/build/components/Flex/Flex.props.d.ts +5 -0
  28. package/build/components/Grid/Grid.d.ts +1 -1
  29. package/build/components/Grid/Grid.js +4 -4
  30. package/build/components/Grid/Grid.props.d.ts +5 -0
  31. package/build/components/Modal/Modal.js +14 -4
  32. package/build/components/Radio/RadioGroupTextContent.js +1 -1
  33. package/build/components/Radio/RadioTextContent.js +1 -1
  34. package/build/components/Toast/Toast.context.d.ts +2 -4
  35. package/build/components/Toast/Toast.context.js +14 -2
  36. package/build/components/Toast/Toast.props.d.ts +4 -0
  37. package/build/components/Toast/ToastItem.js +2 -1
  38. package/build/components/VerificationInput/VerificationInput.d.ts +2 -5
  39. package/build/components/VerificationInput/VerificationInput.js +20 -7
  40. package/build/components/VerificationInput/VerificationInput.props.d.ts +10 -0
  41. package/build/components/VerificationInput/index.d.ts +1 -1
  42. package/build/components/VerificationInput/useVerificationInput.d.ts +1 -0
  43. package/build/components/VerificationInput/useVerificationInput.js +24 -9
  44. package/build/core/themes.d.ts +4 -4
  45. package/build/core/themes.js +1 -1
  46. package/build/types/values.d.ts +1 -1
  47. package/docs/components/AllComponents.web.tsx +9 -9
  48. package/docs/layout-components.docs.mdx +3 -3
  49. package/package.json +4 -4
  50. package/src/components/Alert/Alert.stories.tsx +1 -1
  51. package/src/components/Avatar/Avatar.stories.tsx +4 -5
  52. package/src/components/Badge/Badge.stories.tsx +3 -3
  53. package/src/components/Banner/Banner.docs.mdx +1 -1
  54. package/src/components/Banner/Banner.props.ts +13 -20
  55. package/src/components/Banner/Banner.stories.tsx +83 -15
  56. package/src/components/Banner/Banner.tsx +27 -3
  57. package/src/components/Box/Box.props.ts +11 -4
  58. package/src/components/Button/Button.docs.mdx +2 -2
  59. package/src/components/Button/Button.stories.tsx +4 -4
  60. package/src/components/Button/ButtonGroupRoot.tsx +8 -3
  61. package/src/components/Card/Card.context.ts +1 -1
  62. package/src/components/Card/Card.docs.mdx +1 -1
  63. package/src/components/Card/Card.props.ts +2 -0
  64. package/src/components/Card/Card.stories.tsx +9 -9
  65. package/src/components/Card/CardAction/CardAction.stories.tsx +2 -2
  66. package/src/components/Card/CardContent.tsx +3 -3
  67. package/src/components/Card/CardRoot.tsx +15 -5
  68. package/src/components/Checkbox/CheckboxGroupTextContent.tsx +2 -2
  69. package/src/components/Checkbox/CheckboxTextContent.tsx +1 -1
  70. package/src/components/Container/Container.docs.mdx +2 -2
  71. package/src/components/Container/Container.props.ts +5 -0
  72. package/src/components/Container/Container.stories.tsx +2 -2
  73. package/src/components/Container/Container.tsx +3 -3
  74. package/src/components/CurrencyInput/CurrencyInput.docs.mdx +1 -1
  75. package/src/components/CurrencyInput/CurrencyInput.stories.tsx +2 -2
  76. package/src/components/DateInput/DateInput.stories.tsx +3 -3
  77. package/src/components/DatePickerInput/DatePickerInput.stories.tsx +1 -1
  78. package/src/components/DescriptionList/DescriptionList.stories.tsx +1 -1
  79. package/src/components/Divider/Divider.docs.mdx +6 -6
  80. package/src/components/Divider/Divider.props.ts +6 -0
  81. package/src/components/Divider/Divider.tsx +19 -18
  82. package/src/components/ExpandableCard/ExpandableCardExpandedContent.tsx +1 -1
  83. package/src/components/Flex/Flex.docs.mdx +3 -3
  84. package/src/components/Flex/Flex.props.ts +5 -0
  85. package/src/components/Flex/Flex.stories.tsx +2 -2
  86. package/src/components/Flex/Flex.tsx +4 -3
  87. package/src/components/FormField/FormField.docs.mdx +1 -1
  88. package/src/components/FormField/FormField.stories.tsx +1 -1
  89. package/src/components/Grid/Grid.docs.mdx +5 -5
  90. package/src/components/Grid/Grid.props.ts +6 -0
  91. package/src/components/Grid/Grid.stories.tsx +8 -8
  92. package/src/components/Grid/Grid.tsx +4 -3
  93. package/src/components/HighlightBanner/HighlightBanner.docs.mdx +1 -1
  94. package/src/components/HighlightBanner/HighlightBanner.stories.tsx +2 -2
  95. package/src/components/Icon/Icon.docs.mdx +3 -3
  96. package/src/components/IconButton/IconButton.stories.tsx +5 -5
  97. package/src/components/IconContainer/IconContainer.docs.mdx +1 -1
  98. package/src/components/IconContainer/IconContainer.stories.tsx +3 -3
  99. package/src/components/IndicatorIconButton/IndicatorIconButton.stories.tsx +17 -9
  100. package/src/components/InlineLink/InlineLink.stories.tsx +2 -2
  101. package/src/components/Input/Input.stories.tsx +4 -4
  102. package/src/components/List/List.stories.tsx +1 -1
  103. package/src/components/Modal/Modal.tsx +18 -4
  104. package/src/components/PillGroup/PillGroup.stories.tsx +7 -7
  105. package/src/components/ProgressStepper/ProgressStepper.stories.tsx +7 -8
  106. package/src/components/Radio/Radio.stories.tsx +1 -1
  107. package/src/components/Radio/RadioGroup.stories.tsx +1 -1
  108. package/src/components/Radio/RadioGroupTextContent.tsx +2 -2
  109. package/src/components/Radio/RadioTextContent.tsx +1 -1
  110. package/src/components/SectionHeader/SectionHeader.stories.tsx +1 -1
  111. package/src/components/Switch/Switch.docs.mdx +8 -8
  112. package/src/components/Switch/Switch.stories.tsx +2 -2
  113. package/src/components/Tabs/Tabs.stories.tsx +1 -1
  114. package/src/components/Textarea/Textarea.docs.mdx +3 -3
  115. package/src/components/Toast/Toast.context.tsx +24 -3
  116. package/src/components/Toast/Toast.props.ts +5 -0
  117. package/src/components/Toast/Toast.stories.tsx +29 -0
  118. package/src/components/Toast/ToastItem.tsx +4 -1
  119. package/src/components/UnstyledIconButton/UnstyledIconButton.stories.tsx +5 -5
  120. package/src/components/VerificationInput/VerificationInput.docs.mdx +31 -8
  121. package/src/components/VerificationInput/VerificationInput.props.ts +11 -0
  122. package/src/components/VerificationInput/VerificationInput.stories.tsx +79 -3
  123. package/src/components/VerificationInput/VerificationInput.tsx +94 -62
  124. package/src/components/VerificationInput/index.ts +4 -2
  125. package/src/components/VerificationInput/useVerificationInput.ts +26 -10
  126. package/src/core/themes.ts +1 -1
  127. package/src/types/values.ts +1 -1
@@ -4,7 +4,7 @@ import FlexProps from '../Flex/Flex.props';
4
4
 
5
5
  const RadioTextContent = ({ children, style, ...props }: FlexProps) => {
6
6
  return (
7
- <Flex direction="column" space="none" style={[styles.content, style]} {...props}>
7
+ <Flex direction="column" spacing="none" style={[styles.content, style]} {...props}>
8
8
  {children}
9
9
  </Flex>
10
10
  );
@@ -39,7 +39,7 @@ export const KitchenSink: Story = {
39
39
  },
40
40
  render: () => {
41
41
  return (
42
- <Flex space="xl" direction="column" style={{ width: '100%' }}>
42
+ <Flex spacing="xl" direction="column" style={{ width: '100%' }}>
43
43
  <VariantTitle title="Default SectionHeader with helper text and link">
44
44
  <SectionHeader
45
45
  heading="Heading"
@@ -1,13 +1,13 @@
1
- import { Meta, Controls, Story, Canvas } from '@storybook/addon-docs/blocks';
2
- import * as Stories from './Switch.stories';
3
- import { Center, Switch, Flex, Label, FormField, FormFieldLabel, FormFieldLabelText } from '../../';
1
+ import { Canvas, Controls, Meta, Story } from '@storybook/addon-docs/blocks';
2
+ import { Center, Flex, FormField, FormFieldLabel, FormFieldLabelText, Label, Switch } from '../../';
4
3
  import {
5
- ViewFigmaButton,
6
4
  BackToTopButton,
7
- UsageWrap,
8
- SwitchList,
9
5
  SwitchExample,
6
+ SwitchList,
7
+ UsageWrap,
8
+ ViewFigmaButton,
10
9
  } from '../../../docs/components';
10
+ import * as Stories from './Switch.stories';
11
11
 
12
12
  <Meta title="Components / Switch" />
13
13
 
@@ -76,7 +76,7 @@ the [`FormField` docs](/?path=/docs/native-ui-components-form-field--docs) for m
76
76
 
77
77
  <UsageWrap>
78
78
  <FormField>
79
- <Flex space="sm" direction="row" align="center" justify="center">
79
+ <Flex spacing="sm" direction="row" align="center" justify="center">
80
80
  <SwitchExample />
81
81
  <FormFieldLabel>
82
82
  <FormFieldLabelText>Enable notifications</FormFieldLabelText>
@@ -95,7 +95,7 @@ const MyComponent = () => {
95
95
 
96
96
  return (
97
97
  <FormField>
98
- <Flex space="sm" direction="row" align="center">
98
+ <Flex spacing="sm" direction="row" align="center">
99
99
  <Switch value={notifications} onValueChange={handleChange} />
100
100
  <FormFieldLabel>
101
101
  <FormFieldLabelText>Enable notifications</FormFieldLabelText>
@@ -1,6 +1,6 @@
1
+ import { Meta, StoryObj } from '@storybook/react-vite';
1
2
  import React, { useEffect } from 'react';
2
3
  import { Switch } from '.';
3
- import { Meta, StoryObj } from '@storybook/react-vite';
4
4
  import { VariantTitle } from '../../../docs/components';
5
5
  import { Flex } from '../Flex';
6
6
 
@@ -50,7 +50,7 @@ export const Playground: Story = {
50
50
 
51
51
  export const Variants: Story = {
52
52
  render: () => (
53
- <Flex direction="column" space="sm">
53
+ <Flex direction="column" spacing="sm">
54
54
  <VariantTitle title="Off - medium">
55
55
  <Switch value={false} />
56
56
  </VariantTitle>
@@ -245,7 +245,7 @@ export const Controlled: Story = {
245
245
  setValue(tabs[nextIndex]);
246
246
  };
247
247
  return (
248
- <Flex align={Platform.OS === 'web' ? 'flex-start' : 'stretch'} space="lg">
248
+ <Flex align={Platform.OS === 'web' ? 'flex-start' : 'stretch'} spacing="lg">
249
249
  <Tabs value={value}>
250
250
  <TabsList>
251
251
  <Tab value="account">Account</Tab>
@@ -115,8 +115,8 @@ import { Textarea } from '@utilitywarehouse/hearth-react-native';
115
115
 
116
116
  const MyComponent = () => {
117
117
  const [value, setValue] = useState('');
118
- const handleChange = e => {
119
- setValue(e.target.value);
118
+ const handleChange = text => {
119
+ setValue(text);
120
120
  };
121
121
  return (
122
122
  <Textarea
@@ -124,7 +124,7 @@ const MyComponent = () => {
124
124
  helperText="Provide a detailed description"
125
125
  placeholder="Enter your text here..."
126
126
  value={value}
127
- onTextChange={handleChange}
127
+ onChangeText={handleChange}
128
128
  />
129
129
  );
130
130
  };
@@ -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>
@@ -192,6 +194,7 @@ const styles = StyleSheet.create(theme => ({
192
194
  alignItems: 'center',
193
195
  minWidth: 0,
194
196
  },
197
+ text: { flexShrink: 1 },
195
198
  actions: {
196
199
  flexShrink: 0,
197
200
  },
@@ -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
-