@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.
- package/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-lint.log +21 -18
- package/CHANGELOG.md +102 -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/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/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/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.stories.tsx +5 -5
- 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/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
|
@@ -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"
|
|
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
|
|
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 {
|
|
2
|
-
import
|
|
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
|
|
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
|
|
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"
|
|
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'}
|
|
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 =
|
|
119
|
-
setValue(
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|