@utilitywarehouse/hearth-react-native 0.27.0 → 0.27.2-test
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/.storybook/vitest.setup.ts +35 -3
- package/.turbo/turbo-build.log +5 -4
- package/CHANGELOG.md +42 -0
- package/build/components/Carousel/Carousel.js +6 -1
- package/build/components/List/List.js +2 -2
- package/build/components/Modal/Modal.js +1 -1
- package/build/components/SegmentedControl/SegmentedControl.js +4 -1
- package/build/components/SegmentedControl/SegmentedControlOption.js +24 -2
- package/build/components/SegmentedControl/SegmentedControlOption.props.d.ts +3 -1
- package/build/components/TimePicker/TimePickerWheel.js +9 -1
- package/build/components/Toast/Toast.context.js +1 -1
- package/build/components/VerificationInput/VerificationInput.js +10 -21
- package/build/components/VerificationInput/VerificationInput.utils.d.ts +8 -0
- package/build/components/VerificationInput/VerificationInput.utils.js +17 -0
- package/build/components/VerificationInput/VerificationInput.utils.test.d.ts +1 -0
- package/build/components/VerificationInput/VerificationInput.utils.test.js +36 -0
- package/docs/changelog.mdx +113 -0
- package/package.json +6 -5
- package/src/components/Carousel/Carousel.tsx +6 -2
- package/src/components/IconContainer/IconContainer.stories.tsx +35 -30
- package/src/components/List/List.tsx +5 -4
- package/src/components/Modal/Modal.tsx +1 -1
- package/src/components/SegmentedControl/SegmentedControl.docs.mdx +42 -15
- package/src/components/SegmentedControl/SegmentedControl.figma.tsx +8 -9
- package/src/components/SegmentedControl/SegmentedControl.stories.tsx +21 -0
- package/src/components/SegmentedControl/SegmentedControl.tsx +4 -1
- package/src/components/SegmentedControl/SegmentedControlOption.props.ts +3 -1
- package/src/components/SegmentedControl/SegmentedControlOption.tsx +58 -34
- package/src/components/Select/SelectOption.figma.tsx +2 -2
- package/src/components/TimePicker/TimePickerWheel.tsx +11 -4
- package/src/components/Toast/Toast.context.tsx +1 -1
- package/src/components/VerificationInput/VerificationInput.stories.tsx +33 -0
- package/src/components/VerificationInput/VerificationInput.tsx +16 -29
- package/src/components/VerificationInput/VerificationInput.utils.test.ts +48 -0
- package/src/components/VerificationInput/VerificationInput.utils.ts +32 -0
- package/tsconfig.eslint.json +2 -1
- package/vitest.config.js +11 -13
- package/vitest.unit.config.ts +9 -0
- package/.turbo/turbo-lint.log +0 -72
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { Meta, StoryObj } from '@storybook/react-
|
|
1
|
+
import { Meta, StoryObj } from '@storybook/react-native';
|
|
2
2
|
import * as Icons from '@utilitywarehouse/hearth-react-native-icons';
|
|
3
|
+
import { ComponentType } from 'react';
|
|
3
4
|
import { IconContainer } from '.';
|
|
4
5
|
import { VariantTitle } from '../../../docs/components';
|
|
5
6
|
import { Box } from '../Box';
|
|
@@ -53,7 +54,9 @@ export default meta;
|
|
|
53
54
|
|
|
54
55
|
type Story = StoryObj<typeof meta>;
|
|
55
56
|
|
|
56
|
-
export const Playground: Story = {
|
|
57
|
+
export const Playground: Story = {
|
|
58
|
+
render: (args: typeof meta.args) => <IconContainer {...args} />,
|
|
59
|
+
};
|
|
57
60
|
|
|
58
61
|
export const Subtle: Story = {
|
|
59
62
|
parameters: {
|
|
@@ -86,7 +89,7 @@ export const KitchenSink: Story = {
|
|
|
86
89
|
parameters: {
|
|
87
90
|
controls: { exclude: ['radiusNone', 'variant', 'color', 'size'] },
|
|
88
91
|
},
|
|
89
|
-
render: ({ icon }) => {
|
|
92
|
+
render: ({ icon }: { icon: ComponentType }) => {
|
|
90
93
|
const sizes: Array<'sm' | 'md' | 'lg'> = ['sm', 'md', 'lg'];
|
|
91
94
|
const colors: Array<
|
|
92
95
|
'pig' | 'energy' | 'broadband' | 'mobile' | 'insurance' | 'cashback' | 'highlight'
|
|
@@ -95,33 +98,35 @@ export const KitchenSink: Story = {
|
|
|
95
98
|
<Flex direction="column" spacing="lg">
|
|
96
99
|
{sizes.map(size => (
|
|
97
100
|
<Box key={size} gap="300">
|
|
98
|
-
<VariantTitle title={`Size: ${size.toUpperCase()} / Subtle`}>
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
<
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
color
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
101
|
+
<VariantTitle title={`Size: ${size.toUpperCase()} / Subtle`}>
|
|
102
|
+
<Flex direction="row" wrap="wrap" spacing="md">
|
|
103
|
+
{colors.map(color => (
|
|
104
|
+
<IconContainer
|
|
105
|
+
key={`${size}-subtle-${color}`}
|
|
106
|
+
icon={icon}
|
|
107
|
+
size={size}
|
|
108
|
+
variant="subtle"
|
|
109
|
+
color={color}
|
|
110
|
+
/>
|
|
111
|
+
))}
|
|
112
|
+
</Flex>
|
|
113
|
+
</VariantTitle>
|
|
114
|
+
<VariantTitle title={`Size: ${size.toUpperCase()} / Emphasis`}>
|
|
115
|
+
<Flex direction="row" wrap="wrap" spacing="md">
|
|
116
|
+
{colors.map(
|
|
117
|
+
color =>
|
|
118
|
+
color !== 'highlight' && (
|
|
119
|
+
<IconContainer
|
|
120
|
+
key={`${size}-emphasis-${color}`}
|
|
121
|
+
icon={icon}
|
|
122
|
+
size={size}
|
|
123
|
+
variant="emphasis"
|
|
124
|
+
color={color}
|
|
125
|
+
/>
|
|
126
|
+
)
|
|
127
|
+
)}
|
|
128
|
+
</Flex>
|
|
129
|
+
</VariantTitle>
|
|
125
130
|
</Box>
|
|
126
131
|
))}
|
|
127
132
|
</Flex>
|
|
@@ -14,7 +14,8 @@ const List = ({
|
|
|
14
14
|
invalidText,
|
|
15
15
|
...props
|
|
16
16
|
}: ListProps) => {
|
|
17
|
-
const { loading, disabled, container = 'none' } = props;
|
|
17
|
+
const { loading, disabled, container = 'none', testID, style, ...rest } = props;
|
|
18
|
+
|
|
18
19
|
const orderRef = useRef<string[]>([]);
|
|
19
20
|
const [firstItemId, setFirstItemId] = useState<string | undefined>(undefined);
|
|
20
21
|
const containerToCard: {
|
|
@@ -51,7 +52,7 @@ const List = ({
|
|
|
51
52
|
styles.useVariants({ disabled });
|
|
52
53
|
return (
|
|
53
54
|
<ListContext.Provider value={value}>
|
|
54
|
-
<View {...
|
|
55
|
+
<View {...rest} style={[styles.container, style]}>
|
|
55
56
|
{heading ? (
|
|
56
57
|
<SectionHeader
|
|
57
58
|
heading={heading}
|
|
@@ -61,10 +62,10 @@ const List = ({
|
|
|
61
62
|
/>
|
|
62
63
|
) : null}
|
|
63
64
|
{container === 'none' ? (
|
|
64
|
-
<View>{children}</View>
|
|
65
|
+
<View testID={testID}>{children}</View>
|
|
65
66
|
) : (
|
|
66
67
|
React.Children.count(children) > 0 && (
|
|
67
|
-
<Card {...containerToCard} noPadding style={styles.card}>
|
|
68
|
+
<Card {...containerToCard} noPadding style={styles.card} testID={testID}>
|
|
68
69
|
<>{children}</>
|
|
69
70
|
</Card>
|
|
70
71
|
)
|
|
@@ -292,7 +292,7 @@ const Modal = ({
|
|
|
292
292
|
</View>
|
|
293
293
|
) : null}
|
|
294
294
|
{inNavModal && (
|
|
295
|
-
<InNavModalContainer style={{
|
|
295
|
+
<InNavModalContainer style={{ flex: stickyFooter ? 1 : 0 }}>
|
|
296
296
|
{children}
|
|
297
297
|
{!stickyFooter ? <View style={styles.inNavModalFooterContainer}>{footer}</View> : null}
|
|
298
298
|
</InNavModalContainer>
|
|
@@ -16,8 +16,10 @@ Each option is presented as an equal-priority segment in a single horizontal gro
|
|
|
16
16
|
|
|
17
17
|
- [Playground](#playground)
|
|
18
18
|
- [Usage](#usage)
|
|
19
|
-
- [Sizes](#sizes)
|
|
20
19
|
- [Props](#props)
|
|
20
|
+
- [Examples](#examples)
|
|
21
|
+
- [Sizes](#sizes)
|
|
22
|
+
- [Icons](#icons)
|
|
21
23
|
- [Accessibility](#accessibility)
|
|
22
24
|
|
|
23
25
|
## Playground
|
|
@@ -50,15 +52,6 @@ const Example = () => {
|
|
|
50
52
|
};
|
|
51
53
|
```
|
|
52
54
|
|
|
53
|
-
## Sizes
|
|
54
|
-
|
|
55
|
-
Figma defines two size variants for Segmented Control:
|
|
56
|
-
|
|
57
|
-
- `sm` maps to `SM-32`
|
|
58
|
-
- `md` maps to `MD-48`
|
|
59
|
-
|
|
60
|
-
<Canvas of={Stories.Sizes} />
|
|
61
|
-
|
|
62
55
|
## Props
|
|
63
56
|
|
|
64
57
|
### SegmentedControl
|
|
@@ -74,11 +67,45 @@ Figma defines two size variants for Segmented Control:
|
|
|
74
67
|
|
|
75
68
|
### SegmentedControlOption
|
|
76
69
|
|
|
77
|
-
| Property | Type
|
|
78
|
-
| ---------- |
|
|
79
|
-
| `value` | `string`
|
|
80
|
-
| `children` | `ReactNode`
|
|
81
|
-
| `
|
|
70
|
+
| Property | Type | Description | Default |
|
|
71
|
+
| ---------- | --------------- | ------------------------------ | -------- |
|
|
72
|
+
| `value` | `string` | Unique value for this segment. | required |
|
|
73
|
+
| `children` | `ReactNode` | Option label/content. | required |
|
|
74
|
+
| `icon` | `ComponentType` | Optional leading icon. | - |
|
|
75
|
+
| `disabled` | `boolean` | Disables only this option. | `false` |
|
|
76
|
+
|
|
77
|
+
## Examples
|
|
78
|
+
|
|
79
|
+
### Sizes
|
|
80
|
+
|
|
81
|
+
Figma defines two size variants for Segmented Control:
|
|
82
|
+
|
|
83
|
+
- `sm` maps to `SM-32`
|
|
84
|
+
- `md` maps to `MD-48`
|
|
85
|
+
|
|
86
|
+
<Canvas of={Stories.Sizes} />
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
<SegmentedControl size="sm">...</SegmentedControl>
|
|
90
|
+
<SegmentedControl size="md">...</SegmentedControl>
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Icons
|
|
94
|
+
|
|
95
|
+
SegmentedControlOption supports an optional leading icon.
|
|
96
|
+
|
|
97
|
+
<Canvas of={Stories.WithIcons} />
|
|
98
|
+
|
|
99
|
+
```tsx
|
|
100
|
+
import { MobileSmallIcon } from '@utilitywarehouse/hearth-react-native-icons';
|
|
101
|
+
|
|
102
|
+
<SegmentedControl>
|
|
103
|
+
<SegmentedControlOption value="mobile" icon={MobileSmallIcon}>
|
|
104
|
+
Mobile
|
|
105
|
+
</SegmentedControlOption>
|
|
106
|
+
...
|
|
107
|
+
</SegmentedControl>;
|
|
108
|
+
```
|
|
82
109
|
|
|
83
110
|
## Accessibility
|
|
84
111
|
|
|
@@ -3,20 +3,17 @@ import { SegmentedControl, SegmentedControlOption } from '../';
|
|
|
3
3
|
|
|
4
4
|
figma.connect(
|
|
5
5
|
SegmentedControl,
|
|
6
|
-
'https://www.figma.com/design/6NKZXZhFSExXrcbBgc6zTR/Hearth-Components---Tokens?node-id=6185-1021&
|
|
6
|
+
'https://www.figma.com/design/6NKZXZhFSExXrcbBgc6zTR/Hearth-Components---Tokens?node-id=6185-1021&m=dev',
|
|
7
7
|
{
|
|
8
8
|
props: {
|
|
9
9
|
size: figma.enum('Size', {
|
|
10
10
|
'SM-32': 'sm',
|
|
11
11
|
'MD-48': 'md',
|
|
12
12
|
}),
|
|
13
|
-
disabled: figma.enum('State', {
|
|
14
|
-
Disabled: true,
|
|
15
|
-
}),
|
|
16
13
|
options: figma.children('Option'),
|
|
17
14
|
},
|
|
18
15
|
example: props => (
|
|
19
|
-
<SegmentedControl defaultValue="option-1" size={props.size}
|
|
16
|
+
<SegmentedControl defaultValue="option-1" size={props.size}>
|
|
20
17
|
{props.options}
|
|
21
18
|
</SegmentedControl>
|
|
22
19
|
),
|
|
@@ -25,15 +22,17 @@ figma.connect(
|
|
|
25
22
|
|
|
26
23
|
figma.connect(
|
|
27
24
|
SegmentedControlOption,
|
|
28
|
-
'https://www.figma.com/design/6NKZXZhFSExXrcbBgc6zTR/Hearth-Components---Tokens?node-id=4340-1252&
|
|
25
|
+
'https://www.figma.com/design/6NKZXZhFSExXrcbBgc6zTR/Hearth-Components---Tokens?node-id=4340-1252&m=dev',
|
|
29
26
|
{
|
|
30
27
|
props: {
|
|
31
28
|
label: figma.string('Label'),
|
|
32
|
-
|
|
29
|
+
icon: figma.boolean('Icon?', {
|
|
30
|
+
true: figma.instance('Icon-20'),
|
|
31
|
+
}),
|
|
33
32
|
},
|
|
34
33
|
example: props => (
|
|
35
|
-
<SegmentedControlOption value={
|
|
36
|
-
{props.label
|
|
34
|
+
<SegmentedControlOption value={'option'} icon={props.icon}>
|
|
35
|
+
{props.label}
|
|
37
36
|
</SegmentedControlOption>
|
|
38
37
|
),
|
|
39
38
|
}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BroadbandSmallIcon,
|
|
3
|
+
ElectricitySmallIcon,
|
|
4
|
+
MobileSmallIcon,
|
|
5
|
+
} from '@utilitywarehouse/hearth-react-native-icons';
|
|
1
6
|
import { useState } from 'react';
|
|
2
7
|
import { BodyText, Flex, SegmentedControl, SegmentedControlOption } from '../';
|
|
3
8
|
|
|
@@ -75,3 +80,19 @@ export const Disabled = {
|
|
|
75
80
|
</SegmentedControl>
|
|
76
81
|
),
|
|
77
82
|
};
|
|
83
|
+
|
|
84
|
+
export const WithIcons = {
|
|
85
|
+
render: () => (
|
|
86
|
+
<SegmentedControl defaultValue="energy" size="md">
|
|
87
|
+
<SegmentedControlOption value="energy" icon={ElectricitySmallIcon}>
|
|
88
|
+
Energy
|
|
89
|
+
</SegmentedControlOption>
|
|
90
|
+
<SegmentedControlOption value="broadband" icon={BroadbandSmallIcon}>
|
|
91
|
+
Broadband
|
|
92
|
+
</SegmentedControlOption>
|
|
93
|
+
<SegmentedControlOption value="mobile" icon={MobileSmallIcon}>
|
|
94
|
+
Mobile
|
|
95
|
+
</SegmentedControlOption>
|
|
96
|
+
</SegmentedControl>
|
|
97
|
+
),
|
|
98
|
+
};
|
|
@@ -206,7 +206,7 @@ const SegmentedControl = ({
|
|
|
206
206
|
{...remainingProps}
|
|
207
207
|
>
|
|
208
208
|
{hasIndicator ? (
|
|
209
|
-
<Indicator
|
|
209
|
+
<Indicator style={[styles.indicator, styles.pointerEventsNone, indicatorStyle]} />
|
|
210
210
|
) : null}
|
|
211
211
|
{children}
|
|
212
212
|
</View>
|
|
@@ -252,6 +252,9 @@ const styles = StyleSheet.create(theme => ({
|
|
|
252
252
|
borderRadius: theme.components.segmentedControl.borderRadius,
|
|
253
253
|
backgroundColor: theme.color.interactive.brand.surface.strong.default,
|
|
254
254
|
},
|
|
255
|
+
pointerEventsNone: {
|
|
256
|
+
pointerEvents: 'none',
|
|
257
|
+
},
|
|
255
258
|
}));
|
|
256
259
|
|
|
257
260
|
export default SegmentedControl;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ReactNode } from 'react';
|
|
1
|
+
import type { ComponentType, ReactNode } from 'react';
|
|
2
2
|
import type { PressableProps, ViewProps } from 'react-native';
|
|
3
3
|
|
|
4
4
|
export interface SegmentedControlOptionProps extends Omit<PressableProps, 'children'> {
|
|
@@ -6,6 +6,8 @@ export interface SegmentedControlOptionProps extends Omit<PressableProps, 'child
|
|
|
6
6
|
value: string;
|
|
7
7
|
/** Option label/content. */
|
|
8
8
|
children: ReactNode;
|
|
9
|
+
/** Optional leading icon. */
|
|
10
|
+
icon?: ComponentType<any>;
|
|
9
11
|
/** Disables only this option. */
|
|
10
12
|
disabled?: boolean;
|
|
11
13
|
style?: ViewProps['style'];
|
|
@@ -10,6 +10,7 @@ import Animated, {
|
|
|
10
10
|
} from 'react-native-reanimated';
|
|
11
11
|
import { StyleSheet } from 'react-native-unistyles';
|
|
12
12
|
import { BodyText } from '../BodyText';
|
|
13
|
+
import { Icon } from '../Icon';
|
|
13
14
|
import { useSegmentedControlContext } from './SegmentedControl.context';
|
|
14
15
|
import type SegmentedControlOptionProps from './SegmentedControlOption.props';
|
|
15
16
|
|
|
@@ -18,6 +19,7 @@ const AnimatedView = Animated.createAnimatedComponent(View);
|
|
|
18
19
|
const SegmentedControlOptionRoot = ({
|
|
19
20
|
value,
|
|
20
21
|
children,
|
|
22
|
+
icon,
|
|
21
23
|
accessibilityLabel,
|
|
22
24
|
disabled = false,
|
|
23
25
|
style,
|
|
@@ -79,47 +81,48 @@ const SegmentedControlOptionRoot = ({
|
|
|
79
81
|
: null)}
|
|
80
82
|
>
|
|
81
83
|
<View
|
|
82
|
-
style={styles.
|
|
84
|
+
style={styles.contentWrap}
|
|
83
85
|
accessible={false}
|
|
84
86
|
accessibilityElementsHidden
|
|
85
87
|
importantForAccessibility="no-hide-descendants"
|
|
86
88
|
{...(Platform.OS === 'web' ? ({ 'aria-hidden': true } as any) : null)}
|
|
87
89
|
>
|
|
88
|
-
<
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
<AnimatedView
|
|
100
|
-
pointerEvents="none"
|
|
101
|
-
style={[styles.textLayer, regularLabelStyle]}
|
|
102
|
-
accessible={false}
|
|
103
|
-
accessibilityElementsHidden
|
|
104
|
-
importantForAccessibility="no-hide-descendants"
|
|
105
|
-
{...(Platform.OS === 'web' ? ({ 'aria-hidden': true } as any) : null)}
|
|
106
|
-
>
|
|
107
|
-
<BodyText size="md" weight="regular" style={styles.textRegular}>
|
|
90
|
+
{icon ? <Icon as={icon} size="sm" style={styles.icon} /> : null}
|
|
91
|
+
<View style={styles.labelWrap}>
|
|
92
|
+
<BodyText
|
|
93
|
+
size="md"
|
|
94
|
+
weight="semibold"
|
|
95
|
+
style={styles.labelSizer}
|
|
96
|
+
accessible={false}
|
|
97
|
+
accessibilityElementsHidden
|
|
98
|
+
importantForAccessibility="no-hide-descendants"
|
|
99
|
+
{...(Platform.OS === 'web' ? ({ 'aria-hidden': true } as any) : null)}
|
|
100
|
+
>
|
|
108
101
|
{children}
|
|
109
102
|
</BodyText>
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
103
|
+
<AnimatedView
|
|
104
|
+
style={[styles.textLayer, styles.pointerEventsNone, regularLabelStyle]}
|
|
105
|
+
accessible={false}
|
|
106
|
+
accessibilityElementsHidden
|
|
107
|
+
importantForAccessibility="no-hide-descendants"
|
|
108
|
+
{...(Platform.OS === 'web' ? ({ 'aria-hidden': true } as any) : null)}
|
|
109
|
+
>
|
|
110
|
+
<BodyText size="md" weight="regular" style={styles.textRegular}>
|
|
111
|
+
{children}
|
|
112
|
+
</BodyText>
|
|
113
|
+
</AnimatedView>
|
|
114
|
+
<AnimatedView
|
|
115
|
+
style={[styles.textLayer, styles.pointerEventsNone, selectedLabelStyle]}
|
|
116
|
+
accessible={false}
|
|
117
|
+
accessibilityElementsHidden
|
|
118
|
+
importantForAccessibility="no-hide-descendants"
|
|
119
|
+
{...(Platform.OS === 'web' ? ({ 'aria-hidden': true } as any) : null)}
|
|
120
|
+
>
|
|
121
|
+
<BodyText size="md" weight="semibold" style={styles.textSelected}>
|
|
122
|
+
{children}
|
|
123
|
+
</BodyText>
|
|
124
|
+
</AnimatedView>
|
|
125
|
+
</View>
|
|
123
126
|
</View>
|
|
124
127
|
</Pressable>
|
|
125
128
|
);
|
|
@@ -180,6 +183,12 @@ const styles = StyleSheet.create(theme => ({
|
|
|
180
183
|
},
|
|
181
184
|
},
|
|
182
185
|
},
|
|
186
|
+
contentWrap: {
|
|
187
|
+
flexDirection: 'row',
|
|
188
|
+
alignItems: 'center',
|
|
189
|
+
justifyContent: 'center',
|
|
190
|
+
gap: theme.components.segmentedControl.gap,
|
|
191
|
+
},
|
|
183
192
|
labelWrap: {
|
|
184
193
|
position: 'relative',
|
|
185
194
|
alignItems: 'center',
|
|
@@ -195,6 +204,21 @@ const styles = StyleSheet.create(theme => ({
|
|
|
195
204
|
alignItems: 'center',
|
|
196
205
|
justifyContent: 'center',
|
|
197
206
|
},
|
|
207
|
+
pointerEventsNone: {
|
|
208
|
+
pointerEvents: 'none',
|
|
209
|
+
},
|
|
210
|
+
icon: {
|
|
211
|
+
variants: {
|
|
212
|
+
selected: {
|
|
213
|
+
true: {
|
|
214
|
+
color: theme.color.icon.inverted,
|
|
215
|
+
},
|
|
216
|
+
false: {
|
|
217
|
+
color: theme.color.icon.primary,
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
},
|
|
198
222
|
textRegular: {
|
|
199
223
|
color: theme.color.text.primary,
|
|
200
224
|
},
|
|
@@ -3,10 +3,10 @@ import { SelectOption } from '../';
|
|
|
3
3
|
|
|
4
4
|
figma.connect(
|
|
5
5
|
SelectOption,
|
|
6
|
-
'https://www.figma.com/design/6NKZXZhFSExXrcbBgc6zTR?node-id=
|
|
6
|
+
'https://www.figma.com/design/6NKZXZhFSExXrcbBgc6zTR/Hearth-Components---Tokens?node-id=3394-3663&m=dev',
|
|
7
7
|
{
|
|
8
8
|
props: {
|
|
9
|
-
label: figma.string('
|
|
9
|
+
label: figma.string('Text'),
|
|
10
10
|
disabled: figma.enum('State', {
|
|
11
11
|
Disabled: true,
|
|
12
12
|
}),
|
|
@@ -46,8 +46,8 @@ const TimePickerWheel = ({ value, setValue = () => {}, items }: TimePickerWheelP
|
|
|
46
46
|
|
|
47
47
|
const renderOverlay = useCallback(
|
|
48
48
|
() => (
|
|
49
|
-
<View style={[styles.overlayContainer]}
|
|
50
|
-
<View
|
|
49
|
+
<View style={[styles.overlayContainer, styles.pointerEventsNone]}>
|
|
50
|
+
<View style={[styles.fadeOverlay, styles.pointerEventsNone, { height: fadeHeight }]}>
|
|
51
51
|
<Svg width="100%" height="100%" preserveAspectRatio="none">
|
|
52
52
|
<Defs>
|
|
53
53
|
<LinearGradient id={`${gradientId}-top`} x1="0" y1="0" x2="0" y2="1">
|
|
@@ -59,8 +59,12 @@ const TimePickerWheel = ({ value, setValue = () => {}, items }: TimePickerWheelP
|
|
|
59
59
|
</Svg>
|
|
60
60
|
</View>
|
|
61
61
|
<View
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
style={[
|
|
63
|
+
styles.fadeOverlay,
|
|
64
|
+
styles.fadeOverlayBottom,
|
|
65
|
+
styles.pointerEventsNone,
|
|
66
|
+
{ height: fadeHeight },
|
|
67
|
+
]}
|
|
64
68
|
>
|
|
65
69
|
<Svg width="100%" height="100%" preserveAspectRatio="none">
|
|
66
70
|
<Defs>
|
|
@@ -149,6 +153,9 @@ const styles = StyleSheet.create(theme => ({
|
|
|
149
153
|
top: undefined,
|
|
150
154
|
bottom: 0,
|
|
151
155
|
},
|
|
156
|
+
pointerEventsNone: {
|
|
157
|
+
pointerEvents: 'none',
|
|
158
|
+
},
|
|
152
159
|
}));
|
|
153
160
|
|
|
154
161
|
export default TimePickerWheel;
|
|
@@ -85,7 +85,7 @@ export const ToastProvider: React.FC<ToastProviderProps> = ({
|
|
|
85
85
|
return (
|
|
86
86
|
<ToastContext.Provider value={{ addToast, removeToast }}>
|
|
87
87
|
{children}
|
|
88
|
-
<View
|
|
88
|
+
<View style={styles.container as any}>
|
|
89
89
|
<View style={styles.stack as any}>
|
|
90
90
|
{toasts.map(t => (
|
|
91
91
|
<ToastItem
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
2
|
import { InfoMediumIcon } from '@utilitywarehouse/hearth-react-native-icons';
|
|
3
3
|
import { useRef, useState } from 'react';
|
|
4
|
+
import { expect, userEvent, waitFor, within } from 'storybook/test';
|
|
4
5
|
import { VerificationInput, type VerificationInputHandle } from '.';
|
|
5
6
|
import { VariantTitle } from '../../../docs/components';
|
|
6
7
|
import { BodyText } from '../BodyText';
|
|
@@ -214,3 +215,35 @@ export const RefMethods: Story = {
|
|
|
214
215
|
);
|
|
215
216
|
},
|
|
216
217
|
};
|
|
218
|
+
|
|
219
|
+
export const FocusProgressionAfterEmptySlotSelection: Story = {
|
|
220
|
+
parameters: {
|
|
221
|
+
controls: { include: [] },
|
|
222
|
+
},
|
|
223
|
+
render: () => {
|
|
224
|
+
const [value, setValue] = useState('12');
|
|
225
|
+
|
|
226
|
+
return (
|
|
227
|
+
<Flex direction="column" spacing="sm" style={{ width: 400 }}>
|
|
228
|
+
<VerificationInput label="Verification Code" value={value} onChangeText={setValue} />
|
|
229
|
+
</Flex>
|
|
230
|
+
);
|
|
231
|
+
},
|
|
232
|
+
play: async ({ canvasElement }) => {
|
|
233
|
+
const canvas = within(canvasElement);
|
|
234
|
+
const input = canvas.getByLabelText('Verification Code') as HTMLInputElement;
|
|
235
|
+
|
|
236
|
+
input.focus();
|
|
237
|
+
|
|
238
|
+
input.setSelectionRange(4, 4);
|
|
239
|
+
input.dispatchEvent(new Event('select', { bubbles: true }));
|
|
240
|
+
|
|
241
|
+
await userEvent.keyboard('3');
|
|
242
|
+
|
|
243
|
+
await waitFor(() => {
|
|
244
|
+
expect(input.value).toBe('123');
|
|
245
|
+
expect(input.selectionStart).toBe(3);
|
|
246
|
+
expect(input.selectionEnd).toBe(3);
|
|
247
|
+
});
|
|
248
|
+
},
|
|
249
|
+
};
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
|
2
|
-
import {
|
|
1
|
+
import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
|
2
|
+
import { TextInput, View } from 'react-native';
|
|
3
3
|
import { StyleSheet } from 'react-native-unistyles';
|
|
4
4
|
import { FormField } from '../FormField';
|
|
5
5
|
import type { VerificationInputHandle, VerificationInputProps } from './VerificationInput.props';
|
|
6
|
+
import { getNextIndexFromValueChange } from './VerificationInput.utils';
|
|
6
7
|
import { VerificationInputSlot } from './VerificationInputSlot';
|
|
7
8
|
|
|
8
9
|
const VerificationInput = forwardRef<VerificationInputHandle, VerificationInputProps>(
|
|
@@ -49,12 +50,15 @@ const VerificationInput = forwardRef<VerificationInputHandle, VerificationInputP
|
|
|
49
50
|
}
|
|
50
51
|
}, [length, value]);
|
|
51
52
|
|
|
52
|
-
const updateValue = (
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
53
|
+
const updateValue = useCallback(
|
|
54
|
+
(nextValue: string) => {
|
|
55
|
+
const trimmedValue = nextValue.slice(0, length);
|
|
56
|
+
latestValueRef.current = trimmedValue;
|
|
57
|
+
setDisplayValue(trimmedValue);
|
|
58
|
+
onChangeText?.(trimmedValue);
|
|
59
|
+
},
|
|
60
|
+
[length, onChangeText]
|
|
61
|
+
);
|
|
58
62
|
|
|
59
63
|
const setSelectionIndex = (index: number) => {
|
|
60
64
|
const clampedIndex = Math.max(0, Math.min(index, length));
|
|
@@ -76,16 +80,6 @@ const VerificationInput = forwardRef<VerificationInputHandle, VerificationInputP
|
|
|
76
80
|
setFocusedIndex(Math.min(clampedIndex, length - 1));
|
|
77
81
|
};
|
|
78
82
|
|
|
79
|
-
const findDiffIndex = (prevValue: string, nextValue: string) => {
|
|
80
|
-
const minLength = Math.min(prevValue.length, nextValue.length);
|
|
81
|
-
for (let i = 0; i < minLength; i += 1) {
|
|
82
|
-
if (prevValue[i] !== nextValue[i]) {
|
|
83
|
-
return i;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
return minLength;
|
|
87
|
-
};
|
|
88
|
-
|
|
89
83
|
const handleChangeText = (text: string) => {
|
|
90
84
|
const prevValue = latestValueRef.current;
|
|
91
85
|
const nextValue = text.slice(0, length);
|
|
@@ -94,14 +88,7 @@ const VerificationInput = forwardRef<VerificationInputHandle, VerificationInputP
|
|
|
94
88
|
const diff = nextLength - prevLength;
|
|
95
89
|
const isBulkInsert = text.length > 1 && diff > 1;
|
|
96
90
|
const shouldBlur = nextLength >= length;
|
|
97
|
-
|
|
98
|
-
0,
|
|
99
|
-
Math.min(latestSelectionRef.current.start + (diff >= 0 ? 1 : diff), length)
|
|
100
|
-
);
|
|
101
|
-
if (Platform.OS === 'android') {
|
|
102
|
-
const editedIndex = findDiffIndex(prevValue, nextValue);
|
|
103
|
-
nextIndex = diff >= 0 ? Math.min(editedIndex + 1, length) : Math.max(editedIndex, 0);
|
|
104
|
-
}
|
|
91
|
+
const nextIndex = getNextIndexFromValueChange({ prevValue, nextValue, length });
|
|
105
92
|
updateValue(nextValue);
|
|
106
93
|
if (isBulkInsert) {
|
|
107
94
|
setCaretIndex(Math.min(nextLength, length));
|
|
@@ -171,7 +158,7 @@ const VerificationInput = forwardRef<VerificationInputHandle, VerificationInputP
|
|
|
171
158
|
}
|
|
172
159
|
},
|
|
173
160
|
}),
|
|
174
|
-
[length,
|
|
161
|
+
[length, updateValue]
|
|
175
162
|
);
|
|
176
163
|
|
|
177
164
|
const slots = Array.from({ length }, (_, index) => index);
|
|
@@ -252,7 +239,6 @@ const VerificationInput = forwardRef<VerificationInputHandle, VerificationInputP
|
|
|
252
239
|
maxLength={length}
|
|
253
240
|
caretHidden
|
|
254
241
|
style={styles.hiddenInput}
|
|
255
|
-
pointerEvents="none"
|
|
256
242
|
/>
|
|
257
243
|
{slots.map(index => {
|
|
258
244
|
const char = displayValue[index] || '';
|
|
@@ -299,11 +285,12 @@ const styles = StyleSheet.create(theme => ({
|
|
|
299
285
|
position: 'absolute',
|
|
300
286
|
width: '100%',
|
|
301
287
|
height: '100%',
|
|
288
|
+
pointerEvents: 'none',
|
|
302
289
|
left: 0,
|
|
303
290
|
top: 0,
|
|
304
291
|
color: 'transparent',
|
|
305
292
|
fontSize: 1,
|
|
306
|
-
opacity: 0.
|
|
293
|
+
opacity: 0.01,
|
|
307
294
|
},
|
|
308
295
|
}));
|
|
309
296
|
|