@utilitywarehouse/hearth-react-native 0.17.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.
- package/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-lint.log +21 -18
- package/CHANGELOG.md +144 -0
- package/build/components/Banner/Banner.d.ts +1 -1
- package/build/components/Banner/Banner.js +24 -4
- package/build/components/Banner/Banner.props.d.ts +1 -6
- package/build/components/BodyText/BodyText.js +2 -2
- package/build/components/Box/Box.props.d.ts +5 -2
- package/build/components/Button/Button.d.ts +2 -0
- package/build/components/Button/ButtonGroupRoot.d.ts +5 -1
- package/build/components/Button/ButtonGroupRoot.js +3 -3
- package/build/components/Card/Card.context.d.ts +1 -1
- package/build/components/Card/Card.props.d.ts +2 -0
- package/build/components/Card/CardContent.js +3 -3
- package/build/components/Card/CardRoot.d.ts +1 -1
- package/build/components/Card/CardRoot.js +14 -4
- package/build/components/Checkbox/CheckboxGroupTextContent.js +1 -1
- package/build/components/Checkbox/CheckboxTextContent.js +1 -1
- package/build/components/Container/Container.d.ts +1 -1
- package/build/components/Container/Container.js +3 -3
- package/build/components/Container/Container.props.d.ts +5 -0
- package/build/components/Divider/Divider.d.ts +1 -1
- package/build/components/Divider/Divider.js +19 -19
- package/build/components/Divider/Divider.props.d.ts +6 -0
- package/build/components/ExpandableCard/ExpandableCardExpandedContent.js +1 -1
- package/build/components/Flex/Flex.d.ts +1 -1
- package/build/components/Flex/Flex.js +3 -3
- package/build/components/Flex/Flex.props.d.ts +5 -0
- package/build/components/Grid/Grid.d.ts +1 -1
- package/build/components/Grid/Grid.js +4 -4
- package/build/components/Grid/Grid.props.d.ts +5 -0
- package/build/components/IconButton/IconButton.props.d.ts +19 -0
- package/build/components/IconButton/IconButtonRoot.d.ts +1 -1
- package/build/components/IconButton/IconButtonRoot.js +43 -2
- package/build/components/List/ListItem/ListItemHelperText.d.ts +1 -1
- package/build/components/List/ListItem/ListItemHelperText.js +2 -2
- package/build/components/Modal/Modal.js +14 -4
- package/build/components/Radio/RadioGroupTextContent.js +1 -1
- package/build/components/Radio/RadioTextContent.js +1 -1
- package/build/components/Toast/Toast.context.d.ts +2 -4
- package/build/components/Toast/Toast.context.js +14 -2
- package/build/components/Toast/Toast.props.d.ts +4 -0
- package/build/components/Toast/ToastItem.js +2 -1
- package/build/components/VerificationInput/VerificationInput.d.ts +2 -5
- package/build/components/VerificationInput/VerificationInput.js +20 -7
- package/build/components/VerificationInput/VerificationInput.props.d.ts +10 -0
- package/build/components/VerificationInput/index.d.ts +1 -1
- package/build/components/VerificationInput/useVerificationInput.d.ts +1 -0
- package/build/components/VerificationInput/useVerificationInput.js +24 -9
- package/build/core/themes.d.ts +4 -4
- package/build/core/themes.js +1 -1
- package/build/types/values.d.ts +1 -1
- package/docs/components/AllComponents.web.tsx +9 -9
- package/docs/layout-components.docs.mdx +3 -3
- package/package.json +4 -4
- package/src/components/Alert/Alert.stories.tsx +1 -1
- package/src/components/Avatar/Avatar.stories.tsx +4 -5
- package/src/components/Badge/Badge.stories.tsx +3 -3
- package/src/components/Banner/Banner.docs.mdx +1 -1
- package/src/components/Banner/Banner.props.ts +13 -20
- package/src/components/Banner/Banner.stories.tsx +83 -15
- package/src/components/Banner/Banner.tsx +27 -3
- package/src/components/BodyText/BodyText.tsx +2 -2
- package/src/components/Box/Box.props.ts +11 -4
- package/src/components/Button/Button.docs.mdx +2 -2
- package/src/components/Button/Button.stories.tsx +4 -4
- package/src/components/Button/ButtonGroupRoot.tsx +8 -3
- package/src/components/Card/Card.context.ts +1 -1
- package/src/components/Card/Card.docs.mdx +1 -1
- package/src/components/Card/Card.props.ts +2 -0
- package/src/components/Card/Card.stories.tsx +9 -9
- package/src/components/Card/CardAction/CardAction.stories.tsx +2 -2
- package/src/components/Card/CardContent.tsx +3 -3
- package/src/components/Card/CardRoot.tsx +15 -5
- package/src/components/Checkbox/CheckboxGroupTextContent.tsx +2 -2
- package/src/components/Checkbox/CheckboxTextContent.tsx +1 -1
- package/src/components/Container/Container.docs.mdx +2 -2
- package/src/components/Container/Container.props.ts +5 -0
- package/src/components/Container/Container.stories.tsx +2 -2
- package/src/components/Container/Container.tsx +3 -3
- package/src/components/CurrencyInput/CurrencyInput.docs.mdx +1 -1
- package/src/components/CurrencyInput/CurrencyInput.stories.tsx +2 -2
- package/src/components/DateInput/DateInput.stories.tsx +3 -3
- package/src/components/DatePickerInput/DatePickerInput.stories.tsx +1 -1
- package/src/components/DescriptionList/DescriptionList.stories.tsx +1 -1
- package/src/components/Divider/Divider.docs.mdx +6 -6
- package/src/components/Divider/Divider.props.ts +6 -0
- package/src/components/Divider/Divider.tsx +19 -18
- package/src/components/ExpandableCard/ExpandableCardExpandedContent.tsx +1 -1
- package/src/components/Flex/Flex.docs.mdx +3 -3
- package/src/components/Flex/Flex.props.ts +5 -0
- package/src/components/Flex/Flex.stories.tsx +2 -2
- package/src/components/Flex/Flex.tsx +4 -3
- package/src/components/FormField/FormField.docs.mdx +1 -1
- package/src/components/FormField/FormField.stories.tsx +1 -1
- package/src/components/Grid/Grid.docs.mdx +5 -5
- package/src/components/Grid/Grid.props.ts +6 -0
- package/src/components/Grid/Grid.stories.tsx +8 -8
- package/src/components/Grid/Grid.tsx +4 -3
- package/src/components/HighlightBanner/HighlightBanner.docs.mdx +1 -1
- package/src/components/HighlightBanner/HighlightBanner.stories.tsx +2 -2
- package/src/components/Icon/Icon.docs.mdx +3 -3
- package/src/components/IconButton/IconButton.docs.mdx +91 -9
- package/src/components/IconButton/IconButton.props.ts +19 -0
- package/src/components/IconButton/IconButton.stories.tsx +60 -4
- package/src/components/IconButton/IconButtonRoot.tsx +54 -1
- package/src/components/IconContainer/IconContainer.docs.mdx +1 -1
- package/src/components/IconContainer/IconContainer.stories.tsx +3 -3
- package/src/components/IndicatorIconButton/IndicatorIconButton.stories.tsx +17 -9
- package/src/components/InlineLink/InlineLink.stories.tsx +2 -2
- package/src/components/Input/Input.stories.tsx +4 -4
- package/src/components/List/List.stories.tsx +1 -1
- package/src/components/List/ListItem/ListItemHelperText.tsx +2 -2
- package/src/components/Modal/Modal.tsx +18 -4
- package/src/components/PillGroup/PillGroup.stories.tsx +7 -7
- package/src/components/ProgressStepper/ProgressStepper.stories.tsx +7 -8
- package/src/components/Radio/Radio.stories.tsx +1 -1
- package/src/components/Radio/RadioGroup.stories.tsx +1 -1
- package/src/components/Radio/RadioGroupTextContent.tsx +2 -2
- package/src/components/Radio/RadioTextContent.tsx +1 -1
- package/src/components/SectionHeader/SectionHeader.stories.tsx +1 -1
- package/src/components/Switch/Switch.docs.mdx +8 -8
- package/src/components/Switch/Switch.stories.tsx +2 -2
- package/src/components/Tabs/Tabs.stories.tsx +1 -1
- package/src/components/Textarea/Textarea.docs.mdx +3 -3
- package/src/components/Toast/Toast.context.tsx +24 -3
- package/src/components/Toast/Toast.props.ts +5 -0
- package/src/components/Toast/Toast.stories.tsx +29 -0
- package/src/components/Toast/ToastItem.tsx +4 -1
- package/src/components/UnstyledIconButton/UnstyledIconButton.stories.tsx +5 -5
- package/src/components/VerificationInput/VerificationInput.docs.mdx +31 -8
- package/src/components/VerificationInput/VerificationInput.props.ts +11 -0
- package/src/components/VerificationInput/VerificationInput.stories.tsx +79 -3
- package/src/components/VerificationInput/VerificationInput.tsx +94 -62
- package/src/components/VerificationInput/index.ts +4 -2
- package/src/components/VerificationInput/useVerificationInput.ts +26 -10
- package/src/core/themes.ts +1 -1
- 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 {
|
|
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<
|
|
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
|
|
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"
|
|
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
|
-
- [
|
|
21
|
-
|
|
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
|
-
##
|
|
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
|
|
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"
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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 {
|
|
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
|
-
|
|
38
|
+
const currentValue = latestValueRef.current;
|
|
24
39
|
const chars = Array(length).fill('');
|
|
25
|
-
for (let i = 0; i <
|
|
26
|
-
chars[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
|
-
|
|
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 <
|
|
56
|
-
chars[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
|
-
|
|
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,
|
package/src/core/themes.ts
CHANGED
package/src/types/values.ts
CHANGED
|
@@ -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']['
|
|
84
|
+
export type SpacingValues = keyof (typeof themes)['light']['globalStyle']['variants']['spacing'];
|