@utilitywarehouse/hearth-react-native 0.12.0 → 0.13.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 +1 -1
- package/CHANGELOG.md +6 -0
- package/build/components/Accordion/AccordionTrigger.js +1 -1
- package/build/components/Checkbox/CheckboxIndicator.js +3 -3
- package/build/components/DatePicker/DatePickerDay.js +3 -3
- package/build/components/PillGroup/Pill.js +2 -2
- package/build/components/Radio/RadioIndicator.js +3 -3
- package/build/components/RadioCard/RadioCardIndicator.js +3 -3
- package/build/components/RadioCard/RadioCardRoot.js +3 -3
- package/build/components/Tabs/Tab.js +5 -5
- package/build/components/ToggleButton/ToggleButtonRoot.js +2 -2
- package/build/components/ToggleButtonCard/ToggleButtonCardRoot.js +3 -3
- package/build/components/UnstyledIconButton/UnstyledIconButtonRoot.js +1 -1
- package/build/components/VerificationInput/VerificationInput.d.ts +6 -0
- package/build/components/VerificationInput/VerificationInput.js +35 -0
- package/build/components/VerificationInput/VerificationInput.props.d.ts +49 -0
- package/build/components/VerificationInput/VerificationInput.props.js +1 -0
- package/build/components/VerificationInput/VerificationInputSlot.d.ts +9 -0
- package/build/components/VerificationInput/VerificationInputSlot.js +72 -0
- package/build/components/VerificationInput/index.d.ts +4 -0
- package/build/components/VerificationInput/index.js +3 -0
- package/build/components/VerificationInput/useVerificationInput.d.ts +14 -0
- package/build/components/VerificationInput/useVerificationInput.js +58 -0
- package/build/components/index.d.ts +1 -0
- package/build/components/index.js +1 -0
- package/docs/components/AllComponents.web.tsx +9 -0
- package/package.json +11 -12
- package/src/components/Accordion/Accordion.figma.tsx +23 -0
- package/src/components/Accordion/AccordionItemRoot.figma.tsx +47 -0
- package/src/components/Accordion/AccordionTrigger.tsx +1 -1
- package/src/components/Alert/Alert.figma.tsx +47 -0
- package/src/components/Avatar/Avatar.figma.tsx +33 -0
- package/src/components/Badge/Badge.figma.tsx +48 -24
- package/src/components/Banner/Banner.figma.tsx +15 -0
- package/src/components/Banner/BannerIllustration.figma.tsx +30 -0
- package/src/components/BottomSheet/BottomSheetModal.figma.tsx +20 -0
- package/src/components/Button/Button.figma.tsx +60 -229
- package/src/components/Card/Card.figma.tsx +43 -71
- package/src/components/Card/CardAction/CardAction.figma.tsx +44 -0
- package/src/components/Card/CardAction/CardAction.stories.tsx +1 -1
- package/src/components/Carousel/Carousel.figma.tsx +19 -0
- package/src/components/Checkbox/Checkbox.figma.tsx +26 -41
- package/src/components/Checkbox/CheckboxGroup.figma.tsx +20 -0
- package/src/components/Checkbox/CheckboxImage.figma.tsx +27 -0
- package/src/components/Checkbox/CheckboxIndicator.tsx +3 -3
- package/src/components/Checkbox/CheckboxTileRoot.figma.tsx +32 -0
- package/src/components/CurrencyInput/CurrencyInput.figma.tsx +56 -0
- package/src/components/DateInput/DateInput.figma.tsx +75 -0
- package/src/components/DatePicker/DatePickerCalendar.figma.tsx +34 -0
- package/src/components/DatePicker/DatePickerDay.tsx +3 -3
- package/src/components/DatePickerInput/DatePickerInput.figma.tsx +62 -0
- package/src/components/DescriptionList/DescriptionList.figma.tsx +23 -0
- package/src/components/Divider/Divider.figma.tsx +23 -18
- package/src/components/ExpandableCard/ExpandableCard.figma.tsx +54 -0
- package/src/components/ExpandableCard/ExpandableCardGroup.figma.tsx +23 -0
- package/src/components/FormField/FormField.figma.tsx +23 -0
- package/src/components/Helper/HelperText.figma.tsx +23 -0
- package/src/components/IconButton/IconButton.figma.tsx +55 -161
- package/src/components/IconContainer/IconContainer.figma.tsx +50 -0
- package/src/components/InlineLink/InlineLink.figma.tsx +33 -0
- package/src/components/Input/Input.figma.tsx +52 -110
- package/src/components/Label/Label.figma.tsx +24 -0
- package/src/components/Link/Link.figma.tsx +42 -0
- package/src/components/List/List.figma.tsx +29 -108
- package/src/components/List/ListAction/ListAction.figma.tsx +29 -0
- package/src/components/List/ListItem/ListItem.figma.tsx +40 -220
- package/src/components/List/ListItem/ListItemLeadingContent.figma.tsx +29 -0
- package/src/components/List/ListItem/ListItemTrailingContent.figma.tsx +27 -0
- package/src/components/Menu/Menu.figma.tsx +30 -0
- package/src/components/Menu/MenuItem.figma.tsx +31 -0
- package/src/components/Modal/Modal.figma.tsx +56 -0
- package/src/components/PillGroup/Pill.figma.tsx +25 -0
- package/src/components/PillGroup/Pill.tsx +3 -3
- package/src/components/PillGroup/PillGroup.figma.tsx +21 -0
- package/src/components/ProgressStepper/ProgressStep.figma.tsx +30 -0
- package/src/components/ProgressStepper/ProgressStepper.figma.tsx +20 -0
- package/src/components/Radio/Radio.figma.tsx +22 -42
- package/src/components/Radio/RadioGroup.figma.tsx +54 -0
- package/src/components/Radio/RadioImage.figma.tsx +27 -0
- package/src/components/Radio/RadioIndicator.tsx +3 -3
- package/src/components/Radio/RadioTileRoot.figma.tsx +31 -0
- package/src/components/RadioCard/RadioCardIndicator.tsx +3 -3
- package/src/components/RadioCard/RadioCardRoot.tsx +3 -3
- package/src/components/SectionHeader/SectionHeader.figma.tsx +30 -16
- package/src/components/Select/Select.figma.tsx +55 -0
- package/src/components/Select/SelectOption.figma.tsx +36 -0
- package/src/components/Spinner/Spinner.figma.tsx +20 -12
- package/src/components/Switch/Switch.figma.tsx +31 -23
- package/src/components/Tabs/Tab.tsx +5 -5
- package/src/components/Tabs/Tabs.figma.tsx +29 -0
- package/src/components/ThemedImage/ThemedImage.stories.tsx +1 -1
- package/src/components/Toast/ToastItem.figma.tsx +22 -0
- package/src/components/ToggleButton/ToggleButtonRoot.tsx +2 -2
- package/src/components/ToggleButtonCard/ToggleButtonCardRoot.tsx +3 -3
- package/src/components/UnstyledIconButton/UnstyledIconButton.figma.tsx +49 -0
- package/src/components/UnstyledIconButton/UnstyledIconButtonRoot.tsx +1 -1
- package/src/components/VerificationInput/VerificationInput.docs.mdx +68 -0
- package/src/components/VerificationInput/VerificationInput.props.ts +52 -0
- package/src/components/VerificationInput/VerificationInput.stories.tsx +140 -0
- package/src/components/VerificationInput/VerificationInput.tsx +89 -0
- package/src/components/VerificationInput/VerificationInputSlot.tsx +94 -0
- package/src/components/VerificationInput/index.ts +5 -0
- package/src/components/VerificationInput/useVerificationInput.ts +72 -0
- package/src/components/index.ts +1 -0
- package/src/components/Checkbox/CheckboxIndicator.figma.tsx +0 -19
- package/src/components/Radio/RadioIndicator.figma.tsx +0 -21
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import figma from '@figma/code-connect';
|
|
2
|
+
import UnstyledIconButton from './UnstyledIconButton';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* -- This file was auto-generated by Code Connect --
|
|
6
|
+
* `props` includes a mapping from your code props to Figma properties.
|
|
7
|
+
* You should check this is correct, and update the `example` function
|
|
8
|
+
* to return the code example you'd like to see in Figma
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
figma.connect(
|
|
12
|
+
UnstyledIconButton,
|
|
13
|
+
'https://www.figma.com/design/6NKZXZhFSExXrcbBgc6zTR?node-id=2926%3A2430',
|
|
14
|
+
{
|
|
15
|
+
props: {
|
|
16
|
+
// These props were automatically mapped based on your linked code:
|
|
17
|
+
disabled: figma.enum('State', {
|
|
18
|
+
Disabled: true,
|
|
19
|
+
}),
|
|
20
|
+
loading: figma.enum('State', {
|
|
21
|
+
Loading: true,
|
|
22
|
+
}),
|
|
23
|
+
size: figma.enum('Size', {
|
|
24
|
+
'SM-20': 'sm',
|
|
25
|
+
'MD-24': 'md',
|
|
26
|
+
}),
|
|
27
|
+
inverted: figma.boolean('Inverted?'),
|
|
28
|
+
'aria-disabled': figma.enum('State', {
|
|
29
|
+
Disabled: true,
|
|
30
|
+
}),
|
|
31
|
+
focusable: figma.enum('State', {
|
|
32
|
+
Focus: true,
|
|
33
|
+
}),
|
|
34
|
+
// No matching props could be found for these Figma properties:
|
|
35
|
+
// "icon24": figma.instance('Icon-24'),
|
|
36
|
+
// "icon20": figma.instance('Icon-20')
|
|
37
|
+
},
|
|
38
|
+
example: props => (
|
|
39
|
+
<UnstyledIconButton
|
|
40
|
+
disabled={props.disabled}
|
|
41
|
+
icon={null}
|
|
42
|
+
loading={props.loading}
|
|
43
|
+
size={props.size}
|
|
44
|
+
inverted={props.inverted}
|
|
45
|
+
focusable={props.focusable}
|
|
46
|
+
/>
|
|
47
|
+
),
|
|
48
|
+
}
|
|
49
|
+
);
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Canvas, Controls, Meta } from '@storybook/addon-docs/blocks';
|
|
2
|
+
import { InfoMediumIcon } from '@utilitywarehouse/hearth-react-native-icons';
|
|
3
|
+
import { VerificationInput } from '../../';
|
|
4
|
+
import { BackToTopButton, UsageWrap, ViewFigmaButton } from '../../../docs/components';
|
|
5
|
+
import * as Stories from './VerificationInput.stories';
|
|
6
|
+
|
|
7
|
+
<Meta title="Forms / Verification Input" />
|
|
8
|
+
|
|
9
|
+
<BackToTopButton />
|
|
10
|
+
|
|
11
|
+
<ViewFigmaButton url="https://www.figma.com/design/6NKZXZhFSExXrcbBgc6zTR/Hearth-Components---Tokens?node-id=4049-3615&t=m0WHtPBmyE9YVP8Q-0" />
|
|
12
|
+
|
|
13
|
+
# Verification Input
|
|
14
|
+
|
|
15
|
+
The verification input component is used to capture OTP (One Time Password) or other verification codes.
|
|
16
|
+
|
|
17
|
+
- [Playground](#playground)
|
|
18
|
+
- [Usage](#usage)
|
|
19
|
+
- [Props](#props)
|
|
20
|
+
- [Variants](#variants)
|
|
21
|
+
- [States](#states)
|
|
22
|
+
- [Secure Text Entry](#secure-text-entry)
|
|
23
|
+
|
|
24
|
+
## Playground
|
|
25
|
+
|
|
26
|
+
<Canvas of={Stories.Playground} />
|
|
27
|
+
|
|
28
|
+
<Controls of={Stories.Playground} />
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
<UsageWrap>
|
|
33
|
+
<VerificationInput label="Enter Code" onChangeText={code => console.log(code)} />
|
|
34
|
+
</UsageWrap>
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
import { VerificationInput } from '@utilitywarehouse/hearth-react-native';
|
|
38
|
+
|
|
39
|
+
const MyComponent = () => {
|
|
40
|
+
const [code, setCode] = useState('');
|
|
41
|
+
|
|
42
|
+
return <VerificationInput label="Enter Code" value={code} onChangeText={setCode} />;
|
|
43
|
+
};
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Props
|
|
47
|
+
|
|
48
|
+
### `VerificationInput`
|
|
49
|
+
|
|
50
|
+
The component accepts the following props:
|
|
51
|
+
|
|
52
|
+
| Prop | Type | Default | Description |
|
|
53
|
+
| :----------------- | :---------------------------------- | :---------- | :----------------------------------------------------------- |
|
|
54
|
+
| `value` | `string` | - | The value of the input. |
|
|
55
|
+
| `onChangeText` | `(text: string) => void` | - | Callback when the value changes. |
|
|
56
|
+
| `label` | `string` | - | The label for the input. |
|
|
57
|
+
| `helperText` | `string` | - | Helper text to display below the input. |
|
|
58
|
+
| `helperIcon` | `ComponentType` | - | Icon to display alongside the helper text. |
|
|
59
|
+
| `validationStatus` | `'initial' \| 'valid' \| 'invalid'` | `'initial'` | The validation status of the input. |
|
|
60
|
+
| `validText` | `string` | - | Text to display when validation status is 'valid'. |
|
|
61
|
+
| `invalidText` | `string` | - | Text to display when validation status is 'invalid'. |
|
|
62
|
+
| `disabled` | `boolean` | `false` | Whether the input is disabled. |
|
|
63
|
+
| `readonly` | `boolean` | `false` | Whether the input is read-only. |
|
|
64
|
+
| `secureTextEntry` | `boolean` | `false` | Whether to obscure the text entry (e.g. for passwords/OTPs). |
|
|
65
|
+
|
|
66
|
+
## Variants
|
|
67
|
+
|
|
68
|
+
<Canvas of={Stories.Variants} />
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { ComponentType } from 'react';
|
|
2
|
+
import { ViewProps } from 'react-native';
|
|
3
|
+
|
|
4
|
+
export interface VerificationInputProps extends ViewProps {
|
|
5
|
+
/**
|
|
6
|
+
* The value of the input.
|
|
7
|
+
*/
|
|
8
|
+
value?: string;
|
|
9
|
+
/**
|
|
10
|
+
* Callback when the value changes.
|
|
11
|
+
*/
|
|
12
|
+
onChangeText?: (text: string) => void;
|
|
13
|
+
/**
|
|
14
|
+
* The label for the input.
|
|
15
|
+
*/
|
|
16
|
+
label?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Helper text to display below the input.
|
|
19
|
+
*/
|
|
20
|
+
helperText?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Icon to display alongside the helper text.
|
|
23
|
+
*/
|
|
24
|
+
helperIcon?: ComponentType;
|
|
25
|
+
/**
|
|
26
|
+
* The validation status of the input.
|
|
27
|
+
*/
|
|
28
|
+
validationStatus?: 'initial' | 'valid' | 'invalid';
|
|
29
|
+
/**
|
|
30
|
+
* Text to display when validation status is 'valid'.
|
|
31
|
+
*/
|
|
32
|
+
validText?: string;
|
|
33
|
+
/**
|
|
34
|
+
* Text to display when validation status is 'invalid'.
|
|
35
|
+
*/
|
|
36
|
+
invalidText?: string;
|
|
37
|
+
/**
|
|
38
|
+
* Whether the input is disabled.
|
|
39
|
+
*/
|
|
40
|
+
disabled?: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Whether the input is read-only.
|
|
43
|
+
*/
|
|
44
|
+
readonly?: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Whether to obscure the text entry (e.g. for passwords/OTPs).
|
|
47
|
+
*/
|
|
48
|
+
secureTextEntry?: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export default VerificationInputProps;
|
|
52
|
+
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
import { InfoMediumIcon } from '@utilitywarehouse/hearth-react-native-icons';
|
|
3
|
+
import React, { useState } from 'react';
|
|
4
|
+
import { VerificationInput } from '.';
|
|
5
|
+
import { VariantTitle } from '../../../docs/components';
|
|
6
|
+
import { Flex } from '../Flex';
|
|
7
|
+
|
|
8
|
+
const meta = {
|
|
9
|
+
title: 'Stories / VerificationInput',
|
|
10
|
+
component: VerificationInput,
|
|
11
|
+
parameters: {
|
|
12
|
+
layout: 'centered',
|
|
13
|
+
},
|
|
14
|
+
argTypes: {
|
|
15
|
+
value: { control: 'text' },
|
|
16
|
+
label: { control: 'text' },
|
|
17
|
+
helperText: { control: 'text' },
|
|
18
|
+
validationStatus: {
|
|
19
|
+
control: 'select',
|
|
20
|
+
options: ['initial', 'valid', 'invalid'],
|
|
21
|
+
},
|
|
22
|
+
validText: { control: 'text' },
|
|
23
|
+
invalidText: { control: 'text' },
|
|
24
|
+
disabled: { control: 'boolean' },
|
|
25
|
+
readonly: { control: 'boolean' },
|
|
26
|
+
secureTextEntry: { control: 'boolean' },
|
|
27
|
+
},
|
|
28
|
+
args: {
|
|
29
|
+
label: 'Verification Code',
|
|
30
|
+
validationStatus: 'initial',
|
|
31
|
+
},
|
|
32
|
+
} satisfies Meta<typeof VerificationInput>;
|
|
33
|
+
|
|
34
|
+
export default meta;
|
|
35
|
+
type Story = StoryObj<typeof meta>;
|
|
36
|
+
|
|
37
|
+
export const Playground: Story = {
|
|
38
|
+
render: args => {
|
|
39
|
+
const [value, setValue] = useState(args.value || '');
|
|
40
|
+
return (
|
|
41
|
+
<VerificationInput
|
|
42
|
+
{...args}
|
|
43
|
+
value={value}
|
|
44
|
+
onChangeText={text => {
|
|
45
|
+
setValue(text);
|
|
46
|
+
args.onChangeText?.(text);
|
|
47
|
+
}}
|
|
48
|
+
/>
|
|
49
|
+
);
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const Variants: Story = {
|
|
54
|
+
parameters: {
|
|
55
|
+
controls: { include: [] },
|
|
56
|
+
},
|
|
57
|
+
render: () => {
|
|
58
|
+
const [values, setValues] = useState({
|
|
59
|
+
default: '',
|
|
60
|
+
filled: '123456',
|
|
61
|
+
invalid: '123',
|
|
62
|
+
valid: '123456',
|
|
63
|
+
disabled: '',
|
|
64
|
+
secure: '123',
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const updateValue = (key: keyof typeof values) => (text: string) => {
|
|
68
|
+
setValues(prev => ({ ...prev, [key]: text }));
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<Flex direction="column" space="lg" style={{ width: 400 }}>
|
|
73
|
+
<VariantTitle title="Default">
|
|
74
|
+
<VerificationInput
|
|
75
|
+
label="Verification Code"
|
|
76
|
+
helperText="Enter the code sent to your phone"
|
|
77
|
+
value={values.default}
|
|
78
|
+
onChangeText={updateValue('default')}
|
|
79
|
+
/>
|
|
80
|
+
</VariantTitle>
|
|
81
|
+
|
|
82
|
+
<VariantTitle title="Filled">
|
|
83
|
+
<VerificationInput
|
|
84
|
+
label="Filled Input"
|
|
85
|
+
value={values.filled}
|
|
86
|
+
onChangeText={updateValue('filled')}
|
|
87
|
+
/>
|
|
88
|
+
</VariantTitle>
|
|
89
|
+
|
|
90
|
+
<VariantTitle title="Invalid">
|
|
91
|
+
<VerificationInput
|
|
92
|
+
label="Invalid Input"
|
|
93
|
+
validationStatus="invalid"
|
|
94
|
+
invalidText="The code you entered is incorrect"
|
|
95
|
+
value={values.invalid}
|
|
96
|
+
onChangeText={updateValue('invalid')}
|
|
97
|
+
/>
|
|
98
|
+
</VariantTitle>
|
|
99
|
+
|
|
100
|
+
<VariantTitle title="Valid">
|
|
101
|
+
<VerificationInput
|
|
102
|
+
label="Valid Input"
|
|
103
|
+
validationStatus="valid"
|
|
104
|
+
validText="Code verified!"
|
|
105
|
+
value={values.valid}
|
|
106
|
+
onChangeText={updateValue('valid')}
|
|
107
|
+
/>
|
|
108
|
+
</VariantTitle>
|
|
109
|
+
|
|
110
|
+
<VariantTitle title="Disabled">
|
|
111
|
+
<VerificationInput
|
|
112
|
+
label="Disabled Input"
|
|
113
|
+
disabled
|
|
114
|
+
value={values.disabled}
|
|
115
|
+
onChangeText={updateValue('disabled')}
|
|
116
|
+
/>
|
|
117
|
+
</VariantTitle>
|
|
118
|
+
|
|
119
|
+
<VariantTitle title="Secure Text Entry">
|
|
120
|
+
<VerificationInput
|
|
121
|
+
label="Secure Input"
|
|
122
|
+
secureTextEntry
|
|
123
|
+
value={values.secure}
|
|
124
|
+
onChangeText={updateValue('secure')}
|
|
125
|
+
/>
|
|
126
|
+
</VariantTitle>
|
|
127
|
+
|
|
128
|
+
<VariantTitle title="With Helper Icon">
|
|
129
|
+
<VerificationInput
|
|
130
|
+
label="Helper Icon"
|
|
131
|
+
helperText="Some information"
|
|
132
|
+
helperIcon={InfoMediumIcon}
|
|
133
|
+
value={values.default}
|
|
134
|
+
onChangeText={updateValue('default')}
|
|
135
|
+
/>
|
|
136
|
+
</VariantTitle>
|
|
137
|
+
</Flex>
|
|
138
|
+
);
|
|
139
|
+
},
|
|
140
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { View } from 'react-native';
|
|
2
|
+
import { StyleSheet } from 'react-native-unistyles';
|
|
3
|
+
import { FormField } from '../FormField';
|
|
4
|
+
import { useVerificationInput } from './useVerificationInput';
|
|
5
|
+
import VerificationInputProps from './VerificationInput.props';
|
|
6
|
+
import { VerificationInputSlot } from './VerificationInputSlot';
|
|
7
|
+
|
|
8
|
+
const VerificationInput = ({
|
|
9
|
+
value = '',
|
|
10
|
+
onChangeText,
|
|
11
|
+
label,
|
|
12
|
+
helperText,
|
|
13
|
+
helperIcon,
|
|
14
|
+
validationStatus = 'initial',
|
|
15
|
+
validText,
|
|
16
|
+
invalidText,
|
|
17
|
+
disabled = false,
|
|
18
|
+
readonly = false,
|
|
19
|
+
secureTextEntry = false,
|
|
20
|
+
style,
|
|
21
|
+
...props
|
|
22
|
+
}: VerificationInputProps) => {
|
|
23
|
+
const length = 6;
|
|
24
|
+
const { inputRefs, focusedIndex, handleFocus, handleBlur, handleChangeText, handleKeyPress } =
|
|
25
|
+
useVerificationInput({
|
|
26
|
+
value,
|
|
27
|
+
onChangeText,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const slots = Array.from({ length }, (_, index) => index);
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<FormField
|
|
34
|
+
label={label}
|
|
35
|
+
helperText={helperText}
|
|
36
|
+
helperIcon={helperIcon}
|
|
37
|
+
validationStatus={validationStatus}
|
|
38
|
+
validText={validText}
|
|
39
|
+
invalidText={invalidText}
|
|
40
|
+
disabled={disabled}
|
|
41
|
+
readonly={readonly}
|
|
42
|
+
style={[styles.root, style]}
|
|
43
|
+
{...props}
|
|
44
|
+
>
|
|
45
|
+
<View style={styles.slotsContainer}>
|
|
46
|
+
{slots.map(index => {
|
|
47
|
+
const char = value[index] || '';
|
|
48
|
+
const isActive = focusedIndex === index;
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<VerificationInputSlot
|
|
52
|
+
key={index}
|
|
53
|
+
ref={ref => {
|
|
54
|
+
inputRefs.current[index] = ref;
|
|
55
|
+
}}
|
|
56
|
+
value={char}
|
|
57
|
+
isActive={isActive}
|
|
58
|
+
validationStatus={validationStatus}
|
|
59
|
+
disabled={disabled}
|
|
60
|
+
readonly={readonly}
|
|
61
|
+
secureTextEntry={secureTextEntry}
|
|
62
|
+
onChangeText={text => handleChangeText(text, index)}
|
|
63
|
+
onKeyPress={e => handleKeyPress(e, index)}
|
|
64
|
+
onFocus={() => handleFocus(index)}
|
|
65
|
+
onBlur={handleBlur}
|
|
66
|
+
/>
|
|
67
|
+
);
|
|
68
|
+
})}
|
|
69
|
+
</View>
|
|
70
|
+
</FormField>
|
|
71
|
+
);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const styles = StyleSheet.create(theme => ({
|
|
75
|
+
root: {
|
|
76
|
+
gap: theme.components.input.verification.gap,
|
|
77
|
+
width: '100%',
|
|
78
|
+
maxWidth: theme.components.input.maxWidth,
|
|
79
|
+
},
|
|
80
|
+
slotsContainer: {
|
|
81
|
+
flexDirection: 'row',
|
|
82
|
+
gap: theme.components.input.verification.gap,
|
|
83
|
+
width: '100%',
|
|
84
|
+
},
|
|
85
|
+
}));
|
|
86
|
+
|
|
87
|
+
VerificationInput.displayName = 'VerificationInput';
|
|
88
|
+
|
|
89
|
+
export default VerificationInput;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { forwardRef } from 'react';
|
|
2
|
+
import { TextInput, TextInputProps } from 'react-native';
|
|
3
|
+
import { StyleSheet } from 'react-native-unistyles';
|
|
4
|
+
import InputField from '../Input/InputField';
|
|
5
|
+
|
|
6
|
+
interface VerificationInputSlotProps extends TextInputProps {
|
|
7
|
+
isActive: boolean;
|
|
8
|
+
validationStatus: 'initial' | 'valid' | 'invalid';
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
readonly?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const VerificationInputSlot = forwardRef<TextInput, VerificationInputSlotProps>(
|
|
14
|
+
({ isActive, validationStatus, disabled, readonly, style, ...props }, ref) => {
|
|
15
|
+
styles.useVariants({
|
|
16
|
+
disabled,
|
|
17
|
+
readonly,
|
|
18
|
+
validationStatus,
|
|
19
|
+
active: isActive,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<InputField
|
|
24
|
+
ref={ref}
|
|
25
|
+
{...props}
|
|
26
|
+
editable={!disabled && !readonly}
|
|
27
|
+
selectTextOnFocus
|
|
28
|
+
keyboardType="number-pad"
|
|
29
|
+
style={[styles.slot, style]}
|
|
30
|
+
/>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
VerificationInputSlot.displayName = 'VerificationInputSlot';
|
|
36
|
+
|
|
37
|
+
const styles = StyleSheet.create(theme => ({
|
|
38
|
+
slot: {
|
|
39
|
+
flex: 0,
|
|
40
|
+
width: theme.components.input.height,
|
|
41
|
+
height: theme.components.input.height,
|
|
42
|
+
borderWidth: theme.components.input.borderWidth,
|
|
43
|
+
borderColor: theme.color.border.strong,
|
|
44
|
+
borderRadius: theme.components.input.borderRadius,
|
|
45
|
+
backgroundColor: theme.color.surface.neutral.strong,
|
|
46
|
+
textAlign: 'center',
|
|
47
|
+
padding: 0,
|
|
48
|
+
variants: {
|
|
49
|
+
disabled: {
|
|
50
|
+
true: {
|
|
51
|
+
opacity: theme.opacity.disabled,
|
|
52
|
+
color: theme.color.text.secondary,
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
readonly: {
|
|
56
|
+
true: {
|
|
57
|
+
borderColor: theme.color.border.subtle,
|
|
58
|
+
backgroundColor: theme.color.surface.neutral.subtle,
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
validationStatus: {
|
|
62
|
+
initial: {},
|
|
63
|
+
valid: {
|
|
64
|
+
borderColor: theme.color.feedback.positive.border,
|
|
65
|
+
},
|
|
66
|
+
invalid: {
|
|
67
|
+
borderColor: theme.color.feedback.danger.border,
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
active: {
|
|
71
|
+
true: {
|
|
72
|
+
borderColor: theme.color.border.strong,
|
|
73
|
+
borderWidth: theme.components.input.borderWidthFocused,
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
compoundVariants: [
|
|
78
|
+
{
|
|
79
|
+
validationStatus: 'invalid',
|
|
80
|
+
active: true,
|
|
81
|
+
styles: {
|
|
82
|
+
borderColor: theme.color.feedback.danger.border,
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
validationStatus: 'valid',
|
|
87
|
+
active: true,
|
|
88
|
+
styles: {
|
|
89
|
+
borderColor: theme.color.border.strong,
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
},
|
|
94
|
+
}));
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { useRef, useState } from 'react';
|
|
2
|
+
import { NativeSyntheticEvent, TextInput, TextInputKeyPressEventData } from 'react-native';
|
|
3
|
+
|
|
4
|
+
interface UseVerificationInputProps {
|
|
5
|
+
value: string;
|
|
6
|
+
onChangeText?: (text: string) => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const useVerificationInput = ({ value, onChangeText }: UseVerificationInputProps) => {
|
|
10
|
+
const length = 6;
|
|
11
|
+
const inputRefs = useRef<(TextInput | null)[]>([]);
|
|
12
|
+
const [focusedIndex, setFocusedIndex] = useState<number | null>(null);
|
|
13
|
+
|
|
14
|
+
const handleFocus = (index: number) => {
|
|
15
|
+
setFocusedIndex(index);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const handleBlur = () => {
|
|
19
|
+
setFocusedIndex(null);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const handleChangeText = (text: string, index: number) => {
|
|
23
|
+
// Break down the text into an array of characters
|
|
24
|
+
const chars = Array(length).fill('');
|
|
25
|
+
for (let i = 0; i < value.length && i < length; i++) {
|
|
26
|
+
chars[i] = value[i];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (text.length > 1) {
|
|
30
|
+
// Handle paste
|
|
31
|
+
const pastedChars = text.slice(0, length - index).split('');
|
|
32
|
+
for (let i = 0; i < pastedChars.length; i++) {
|
|
33
|
+
chars[index + i] = pastedChars[i];
|
|
34
|
+
}
|
|
35
|
+
const nextIndex = Math.min(index + pastedChars.length, length - 1);
|
|
36
|
+
inputRefs.current[nextIndex]?.focus();
|
|
37
|
+
} else {
|
|
38
|
+
// Handle single char input
|
|
39
|
+
chars[index] = text;
|
|
40
|
+
if (text.length === 1 && index < length - 1) {
|
|
41
|
+
inputRefs.current[index + 1]?.focus();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
onChangeText?.(chars.join(''));
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const handleKeyPress = (e: NativeSyntheticEvent<TextInputKeyPressEventData>, index: number) => {
|
|
49
|
+
if (e.nativeEvent.key === 'Backspace') {
|
|
50
|
+
if (!value[index] && index > 0) {
|
|
51
|
+
e.preventDefault();
|
|
52
|
+
inputRefs.current[index - 1]?.focus();
|
|
53
|
+
|
|
54
|
+
const chars = Array(length).fill('');
|
|
55
|
+
for (let i = 0; i < value.length && i < length; i++) {
|
|
56
|
+
chars[i] = value[i];
|
|
57
|
+
}
|
|
58
|
+
chars[index - 1] = '';
|
|
59
|
+
onChangeText?.(chars.join(''));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
inputRefs,
|
|
66
|
+
focusedIndex,
|
|
67
|
+
handleFocus,
|
|
68
|
+
handleBlur,
|
|
69
|
+
handleChangeText,
|
|
70
|
+
handleKeyPress,
|
|
71
|
+
};
|
|
72
|
+
};
|
package/src/components/index.ts
CHANGED
|
@@ -54,6 +54,7 @@ export * from './Textarea';
|
|
|
54
54
|
export * from './ThemedImage';
|
|
55
55
|
export * from './Toast';
|
|
56
56
|
export * from './ToggleButtonCard';
|
|
57
|
+
export * from './VerificationInput';
|
|
57
58
|
|
|
58
59
|
export { FlatList, Image, KeyboardAvoidingView, ScrollView, SectionList, View } from 'react-native';
|
|
59
60
|
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { Checkbox } from './';
|
|
2
|
-
import figma from '@figma/code-connect';
|
|
3
|
-
|
|
4
|
-
const value = 'some-value';
|
|
5
|
-
const setValue = (isChecked: boolean) => console.log(isChecked);
|
|
6
|
-
|
|
7
|
-
figma.connect(
|
|
8
|
-
Checkbox,
|
|
9
|
-
'https://www.figma.com/design/3RY3OvLA88yZksRjOfjQJm/UW-App-UI?node-id=4454-3759&m=dev',
|
|
10
|
-
{
|
|
11
|
-
props: {
|
|
12
|
-
checked: figma.boolean('checked'),
|
|
13
|
-
disabled: figma.boolean('disabled'),
|
|
14
|
-
},
|
|
15
|
-
example: ({ disabled, checked }) => (
|
|
16
|
-
<Checkbox value={value} onChange={setValue} disabled={disabled} checked={checked} />
|
|
17
|
-
),
|
|
18
|
-
}
|
|
19
|
-
);
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { Radio, RadioGroup } from './';
|
|
2
|
-
import figma from '@figma/code-connect';
|
|
3
|
-
|
|
4
|
-
const value = 'some-value';
|
|
5
|
-
const setValue = (value: string) => console.log(value);
|
|
6
|
-
|
|
7
|
-
figma.connect(
|
|
8
|
-
Radio,
|
|
9
|
-
'https://www.figma.com/design/3RY3OvLA88yZksRjOfjQJm/UW-App-UI?node-id=4461-7535&m=dev',
|
|
10
|
-
{
|
|
11
|
-
props: {
|
|
12
|
-
checked: figma.boolean('checked', { true: 'some-value', false: undefined }),
|
|
13
|
-
disabled: figma.boolean('disabled'),
|
|
14
|
-
},
|
|
15
|
-
example: ({ disabled, checked }) => (
|
|
16
|
-
<RadioGroup value={checked} onChange={setValue}>
|
|
17
|
-
<Radio value={value} disabled={disabled} />
|
|
18
|
-
</RadioGroup>
|
|
19
|
-
),
|
|
20
|
-
}
|
|
21
|
-
);
|