@utilitywarehouse/hearth-react-native 0.21.0 → 0.22.1
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 +14 -14
- package/CHANGELOG.md +38 -0
- package/build/components/Card/Card.props.d.ts +4 -8
- package/build/components/Card/CardRoot.js +0 -1
- package/build/components/Checkbox/Checkbox.d.ts +1 -1
- package/build/components/Checkbox/Checkbox.js +2 -2
- package/build/components/Checkbox/Checkbox.props.d.ts +2 -0
- package/build/components/Modal/Modal.js +1 -1
- package/build/components/Radio/Radio.d.ts +1 -1
- package/build/components/Radio/Radio.js +2 -2
- package/build/components/Radio/Radio.props.d.ts +2 -0
- package/build/components/VerificationInput/VerificationInput.js +182 -20
- package/build/components/VerificationInput/VerificationInputSlot.d.ts +7 -3
- package/build/components/VerificationInput/VerificationInputSlot.js +45 -7
- package/docs/changelog.mdx +249 -0
- package/docs/components/NextPrevPage.tsx +2 -2
- package/package.json +6 -6
- package/src/components/Card/Card.props.ts +5 -8
- package/src/components/Card/CardRoot.tsx +0 -1
- package/src/components/Checkbox/Checkbox.docs.mdx +1 -0
- package/src/components/Checkbox/Checkbox.props.ts +2 -0
- package/src/components/Checkbox/Checkbox.stories.tsx +26 -0
- package/src/components/Checkbox/Checkbox.tsx +2 -0
- package/src/components/Modal/Modal.tsx +1 -1
- package/src/components/Radio/Radio.docs.mdx +1 -0
- package/src/components/Radio/Radio.props.ts +2 -0
- package/src/components/Radio/Radio.stories.tsx +22 -0
- package/src/components/Radio/Radio.tsx +2 -0
- package/src/components/Radio/RadioTile.figma.tsx +4 -0
- package/src/components/VerificationInput/VerificationInput.tsx +218 -29
- package/src/components/VerificationInput/VerificationInputSlot.tsx +90 -14
- package/build/components/VerificationInput/useVerificationInput.d.ts +0 -15
- package/build/components/VerificationInput/useVerificationInput.js +0 -73
- package/src/components/VerificationInput/useVerificationInput.ts +0 -88
package/.turbo/turbo-build.log
CHANGED
package/.turbo/turbo-lint.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @utilitywarehouse/hearth-react-native@0.
|
|
2
|
+
> @utilitywarehouse/hearth-react-native@0.22.1 lint /home/runner/work/hearth/hearth/packages/react-native
|
|
3
3
|
> TIMING=1 eslint .
|
|
4
4
|
|
|
5
5
|
|
|
@@ -54,19 +54,19 @@
|
|
|
54
54
|
106:14 warning Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components react-refresh/only-export-components
|
|
55
55
|
|
|
56
56
|
/home/runner/work/hearth/hearth/packages/react-native/src/components/VerificationInput/VerificationInput.tsx
|
|
57
|
-
|
|
57
|
+
174:7 warning React Hook useImperativeHandle has a missing dependency: 'updateValue'. Either include it or remove the dependency array react-hooks/exhaustive-deps
|
|
58
58
|
|
|
59
59
|
✖ 25 problems (0 errors, 25 warnings)
|
|
60
60
|
|
|
61
|
-
Rule
|
|
62
|
-
|
|
63
|
-
@typescript-eslint/no-unused-vars
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
react-hooks/rules-of-hooks
|
|
67
|
-
no-misleading-character-class
|
|
68
|
-
|
|
69
|
-
no-unexpected-multiline
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
no-
|
|
61
|
+
Rule | Time (ms) | Relative
|
|
62
|
+
:-----------------------------------------|----------:|--------:
|
|
63
|
+
@typescript-eslint/no-unused-vars | 1480.154 | 63.5%
|
|
64
|
+
no-global-assign | 86.111 | 3.7%
|
|
65
|
+
react-hooks/exhaustive-deps | 81.893 | 3.5%
|
|
66
|
+
react-hooks/rules-of-hooks | 58.807 | 2.5%
|
|
67
|
+
no-misleading-character-class | 53.302 | 2.3%
|
|
68
|
+
@typescript-eslint/ban-ts-comment | 49.364 | 2.1%
|
|
69
|
+
no-unexpected-multiline | 36.564 | 1.6%
|
|
70
|
+
no-fallthrough | 32.890 | 1.4%
|
|
71
|
+
@typescript-eslint/triple-slash-reference | 30.004 | 1.3%
|
|
72
|
+
no-regex-spaces | 26.454 | 1.1%
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,43 @@
|
|
|
1
1
|
# @utilitywarehouse/hearth-react-native
|
|
2
2
|
|
|
3
|
+
## 0.22.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#971](https://github.com/utilitywarehouse/hearth/pull/971) [`be1dfeb`](https://github.com/utilitywarehouse/hearth/commit/be1dfebd4b43f2df8ef6c5eaa42a88364e796479) Thanks [@jordmccord](https://github.com/jordmccord)! - 💅 [ENHANCEMENT]: Improve VerificationInput OTP handling and accessibility
|
|
8
|
+
|
|
9
|
+
VerificationInput now uses a single hidden input to manage focus, selection, and paste behaviour across platforms, improving caret handling and bulk entry. Accessibility labels and hints are now derived from the form field to provide clearer screen reader output.
|
|
10
|
+
|
|
11
|
+
**Components affected**:
|
|
12
|
+
- `VerificationInput`
|
|
13
|
+
|
|
14
|
+
**Developer changes**:
|
|
15
|
+
|
|
16
|
+
No changes required.
|
|
17
|
+
|
|
18
|
+
## 0.22.0
|
|
19
|
+
|
|
20
|
+
### Minor Changes
|
|
21
|
+
|
|
22
|
+
- [#968](https://github.com/utilitywarehouse/hearth/pull/968) [`cee5811`](https://github.com/utilitywarehouse/hearth/commit/cee5811020af02fe754d8311ec8313c1793f108a) Thanks [@jordmccord](https://github.com/jordmccord)! - 🌟 [FEATURE]: Add `badge` support to Radio and Checkbox (including tiles).
|
|
23
|
+
|
|
24
|
+
**Components affected**:
|
|
25
|
+
- `Radio`
|
|
26
|
+
- `RadioTile`
|
|
27
|
+
- `Checkbox`
|
|
28
|
+
- `CheckboxTile`
|
|
29
|
+
|
|
30
|
+
**Developer changes**:
|
|
31
|
+
You can now pass a `badge` React node to render beneath helper text. No changes required unless you want to use the new prop.
|
|
32
|
+
|
|
33
|
+
### Patch Changes
|
|
34
|
+
|
|
35
|
+
- [#966](https://github.com/utilitywarehouse/hearth/pull/966) [`4e9f3f0`](https://github.com/utilitywarehouse/hearth/commit/4e9f3f0284e50da5ba4e49e132dac745a1a8a68d) Thanks [@jordmccord](https://github.com/jordmccord)! - 🐛 [FIX]: Allow Card layout props and remove forced alignment
|
|
36
|
+
|
|
37
|
+
Card now accepts flex layout and display props, and it no longer forces `alignItems: flex-start` on the root, so custom alignment works as expected.
|
|
38
|
+
|
|
39
|
+
- [#969](https://github.com/utilitywarehouse/hearth/pull/969) [`c5c988b`](https://github.com/utilitywarehouse/hearth/commit/c5c988b65f1133b85b822037b086a524bc1255e3) Thanks [@jordmccord](https://github.com/jordmccord)! - 🐛 [FIX]: Render the Modal footer in navigation modals
|
|
40
|
+
|
|
3
41
|
## 0.21.0
|
|
4
42
|
|
|
5
43
|
### Minor Changes
|
|
@@ -1,17 +1,13 @@
|
|
|
1
|
-
import { PressableProps
|
|
2
|
-
import { GapProps, MarginProps, SpacingValues } from '../../types';
|
|
3
|
-
interface CardProps extends PressableProps, MarginProps, GapProps {
|
|
1
|
+
import { PressableProps } from 'react-native';
|
|
2
|
+
import { DisplayProps, FlexLayoutProps, GapProps, MarginProps, SpacingValues } from '../../types';
|
|
3
|
+
interface CardProps extends PressableProps, MarginProps, GapProps, FlexLayoutProps, Omit<DisplayProps, 'direction'> {
|
|
4
4
|
variant?: 'emphasis' | 'subtle';
|
|
5
5
|
colorScheme?: 'neutralStrong' | 'neutralSubtle' | 'brand' | 'energy' | 'broadband' | 'mobile' | 'insurance' | 'cashback' | 'pig';
|
|
6
6
|
shadowColor?: 'functional' | 'brand' | 'energy' | 'broadband' | 'mobile' | 'insurance' | 'cashback' | 'pig';
|
|
7
7
|
noPadding?: boolean;
|
|
8
8
|
disabled?: boolean;
|
|
9
9
|
spacing?: SpacingValues;
|
|
10
|
-
/** @deprecated Use `spacing` instead. The `
|
|
10
|
+
/** @deprecated Use `spacing` instead. The `space` prop will be removed in a future release. */
|
|
11
11
|
space?: SpacingValues;
|
|
12
|
-
alignItems?: ViewStyle['alignItems'];
|
|
13
|
-
justifyContent?: ViewStyle['justifyContent'];
|
|
14
|
-
flexDirection?: ViewStyle['flexDirection'];
|
|
15
|
-
flexWrap?: ViewStyle['flexWrap'];
|
|
16
12
|
}
|
|
17
13
|
export default CardProps;
|
|
@@ -10,7 +10,7 @@ declare const CheckboxIcon: import("react").ForwardRefExoticComponent<import("re
|
|
|
10
10
|
}>;
|
|
11
11
|
declare const CheckboxLabel: import("react").ForwardRefExoticComponent<import("react").RefAttributes<import("../Label/Label.props").default> & Omit<import("../Label/Label.props").default, "ref">>;
|
|
12
12
|
declare const Checkbox: {
|
|
13
|
-
({ children, label, disabled, checked, helperIcon, helperText, invalidText, validText, validationStatus: validation, showValidationIcon, type, image, value, ...props }: CheckboxProps): import("react/jsx-runtime").JSX.Element;
|
|
13
|
+
({ children, label, disabled, checked, helperIcon, helperText, badge, invalidText, validText, validationStatus: validation, showValidationIcon, type, image, value, ...props }: CheckboxProps): import("react/jsx-runtime").JSX.Element;
|
|
14
14
|
displayName: string;
|
|
15
15
|
};
|
|
16
16
|
declare const CheckboxTile: {
|
|
@@ -25,12 +25,12 @@ CheckboxGroup.displayName = 'CheckboxGroup';
|
|
|
25
25
|
CheckboxIndicator.displayName = 'CheckboxIndicator';
|
|
26
26
|
CheckboxIcon.displayName = 'CheckboxIcon';
|
|
27
27
|
CheckboxLabel.displayName = 'CheckboxLabel';
|
|
28
|
-
const Checkbox = ({ children, label, disabled, checked, helperIcon, helperText, invalidText, validText, validationStatus: validation, showValidationIcon, type = 'default', image, value, ...props }) => {
|
|
28
|
+
const Checkbox = ({ children, label, disabled, checked, helperIcon, helperText, badge, invalidText, validText, validationStatus: validation, showValidationIcon, type = 'default', image, value, ...props }) => {
|
|
29
29
|
const { validationStatus: fieldValidationStatus } = useFormFieldContext();
|
|
30
30
|
const { validationStatus: groupValidationStatus, type: groupType } = useCheckboxGroupContext();
|
|
31
31
|
const validationStatus = fieldValidationStatus ?? groupValidationStatus ?? validation ?? 'initial';
|
|
32
32
|
const checkboxType = groupType ?? type;
|
|
33
|
-
const checkboxChildren = children ? (children) : (_jsxs(_Fragment, { children: [_jsx(CheckboxIndicator, { children: _jsx(CheckboxIcon, {}) }), image ? image : null, _jsxs(CheckboxTextContent, { children: [!!label && _jsx(CheckboxLabel, { children: label }), !!helperText && _jsx(Helper, { disabled: disabled, icon: helperIcon, text: helperText }), validationStatus === 'invalid' && !!invalidText && (_jsx(Helper, { showIcon: showValidationIcon, disabled: disabled, validationStatus: "invalid", text: invalidText })), validationStatus === 'valid' && !!validText && (_jsx(Helper, { disabled: disabled, showIcon: showValidationIcon, validationStatus: "valid", text: validText }))] })] }));
|
|
33
|
+
const checkboxChildren = children ? (children) : (_jsxs(_Fragment, { children: [_jsx(CheckboxIndicator, { children: _jsx(CheckboxIcon, {}) }), image ? image : null, _jsxs(CheckboxTextContent, { children: [!!label && _jsx(CheckboxLabel, { children: label }), !!helperText && _jsx(Helper, { disabled: disabled, icon: helperIcon, text: helperText }), badge ? badge : null, validationStatus === 'invalid' && !!invalidText && (_jsx(Helper, { showIcon: showValidationIcon, disabled: disabled, validationStatus: "invalid", text: invalidText })), validationStatus === 'valid' && !!validText && (_jsx(Helper, { disabled: disabled, showIcon: showValidationIcon, validationStatus: "valid", text: validText }))] })] }));
|
|
34
34
|
return (_jsx(CheckboxComponent, { ...props, value: (value ?? '').toString(), isDisabled: disabled, isChecked: checked, children: checkboxType === 'tile' ? (_jsx(CheckboxTileRoot, { children: checkboxChildren })) : (checkboxChildren) }));
|
|
35
35
|
};
|
|
36
36
|
const CheckboxTile = ({ type = 'tile', ...props }) => {
|
|
@@ -17,6 +17,7 @@ type CheckboxWithChildrenProps = {
|
|
|
17
17
|
label?: never;
|
|
18
18
|
helperText?: never;
|
|
19
19
|
helperIcon?: never;
|
|
20
|
+
badge?: never;
|
|
20
21
|
invalidText?: never;
|
|
21
22
|
validText?: never;
|
|
22
23
|
showValidationIcon?: never;
|
|
@@ -27,6 +28,7 @@ type CheckboxWithoutChildrenProps = {
|
|
|
27
28
|
label?: string;
|
|
28
29
|
helperText?: string;
|
|
29
30
|
helperIcon?: ComponentType;
|
|
31
|
+
badge?: ReactNode;
|
|
30
32
|
invalidText?: string;
|
|
31
33
|
validText?: string;
|
|
32
34
|
showValidationIcon?: boolean;
|
|
@@ -107,7 +107,7 @@ const Modal = ({ ref, children, heading, description, showCloseButton = true, pr
|
|
|
107
107
|
showHandle: props.showHandle,
|
|
108
108
|
});
|
|
109
109
|
const footer = (_jsxs(View, { style: styles.footer, children: [onPressPrimaryButton && primaryButtonText ? (_jsx(Button, { onPress: handlePrimaryButtonPress, text: primaryButtonText, ...primaryButtonProps, variant: primaryButtonProps?.variant ?? 'solid', colorScheme: primaryButtonProps?.colorScheme ?? 'highlight' })) : null, onPressSecondaryButton && secondaryButtonText ? (_jsx(Button, { onPress: handleSecondaryButtonPress, text: secondaryButtonText, ...secondaryButtonProps, variant: secondaryButtonProps?.variant ?? 'outline', colorScheme: secondaryButtonProps?.colorScheme ?? 'functional' })) : null] }));
|
|
110
|
-
const content = (_jsx(_Fragment, { children: loading ? (_jsxs(View, { style: styles.loadingContainer, accessible: Platform.OS === 'android' ? true : undefined, accessibilityLabel: Platform.OS === 'android' ? 'Loading' : undefined, screenReaderFocusable: true, ref: viewRef, children: [_jsx(Spinner, { size: "lg" }), _jsx(Heading, { size: "lg", textAlign: "center", children: loadingHeading })] })) : (_jsxs(View, { style: styles.container, accessible: Platform.OS === 'android' ? true : undefined, accessibilityLabel: Platform.OS === 'android' ? 'Modal content' : undefined, screenReaderFocusable: true, ref: viewRef, children: [_jsxs(View, { style: styles.header, children: [_jsxs(View, { style: styles.headerTextContent, children: [heading && !image ? (_jsx(Heading, { size: "lg", accessible: true, children: heading })) : null, description && !image ? _jsx(BodyText, { accessible: true, children: description }) : null] }), showCloseButton ? (_jsx(UnstyledIconButton, { icon: CloseMediumIcon, onPress: handleCloseButtonPress, accessibilityLabel: "Close modal", ...closeButtonProps })) : null] }), image ? (_jsxs(View, { style: styles.imageContainer, children: [image, _jsxs(View, { style: styles.textContent, children: [heading ? (_jsx(Heading, { size: "lg", textAlign: "center", accessible: true, children: heading })) : null, description ? (_jsx(BodyText, { textAlign: "center", accessible: true, children: description })) : null] })] })) : null, children, !stickyFooter && !noButtons ? footer : null] })) }));
|
|
110
|
+
const content = (_jsx(_Fragment, { children: loading ? (_jsxs(View, { style: styles.loadingContainer, accessible: Platform.OS === 'android' ? true : undefined, accessibilityLabel: Platform.OS === 'android' ? 'Loading' : undefined, screenReaderFocusable: true, ref: viewRef, children: [_jsx(Spinner, { size: "lg" }), _jsx(Heading, { size: "lg", textAlign: "center", children: loadingHeading })] })) : (_jsxs(View, { style: styles.container, accessible: Platform.OS === 'android' ? true : undefined, accessibilityLabel: Platform.OS === 'android' ? 'Modal content' : undefined, screenReaderFocusable: true, ref: viewRef, children: [_jsxs(View, { style: styles.header, children: [_jsxs(View, { style: styles.headerTextContent, children: [heading && !image ? (_jsx(Heading, { size: "lg", accessible: true, children: heading })) : null, description && !image ? _jsx(BodyText, { accessible: true, children: description }) : null] }), showCloseButton ? (_jsx(UnstyledIconButton, { icon: CloseMediumIcon, onPress: handleCloseButtonPress, accessibilityLabel: "Close modal", ...closeButtonProps })) : null] }), image ? (_jsxs(View, { style: styles.imageContainer, children: [image, _jsxs(View, { style: styles.textContent, children: [heading ? (_jsx(Heading, { size: "lg", textAlign: "center", accessible: true, children: heading })) : null, description ? (_jsx(BodyText, { textAlign: "center", accessible: true, children: description })) : null] })] })) : null, children, (!stickyFooter || inNavModal) && !noButtons ? footer : null] })) }));
|
|
111
111
|
const renderFooter = useCallback((props) => (_jsx(BottomSheetFooter, { ...props, children: _jsx(View, { style: styles.footerWrap, children: footer }) })), [
|
|
112
112
|
onPressPrimaryButton,
|
|
113
113
|
primaryButtonText,
|
|
@@ -10,7 +10,7 @@ declare const RadioIcon: import("react").ForwardRefExoticComponent<import("react
|
|
|
10
10
|
}>;
|
|
11
11
|
declare const RadioLabel: import("react").ForwardRefExoticComponent<import("react").RefAttributes<import("../Label/Label.props").default> & Omit<import("../Label/Label.props").default, "ref">>;
|
|
12
12
|
declare const Radio: {
|
|
13
|
-
({ children, label, disabled, helperIcon, helperText, invalidText, validText, validationStatus: validation, showValidationIcon, type, image, ...props }: RadioProps): import("react/jsx-runtime").JSX.Element;
|
|
13
|
+
({ children, label, disabled, helperIcon, helperText, badge, invalidText, validText, validationStatus: validation, showValidationIcon, type, image, ...props }: RadioProps): import("react/jsx-runtime").JSX.Element;
|
|
14
14
|
displayName: string;
|
|
15
15
|
};
|
|
16
16
|
declare const RadioTile: {
|
|
@@ -25,12 +25,12 @@ RadioGroup.displayName = 'RadioGroup';
|
|
|
25
25
|
RadioIndicator.displayName = 'RadioIndicator';
|
|
26
26
|
RadioIcon.displayName = 'RadioIcon';
|
|
27
27
|
RadioLabel.displayName = 'RadioLabel';
|
|
28
|
-
const Radio = ({ children, label, disabled, helperIcon, helperText, invalidText, validText, validationStatus: validation, showValidationIcon, type = 'default', image, ...props }) => {
|
|
28
|
+
const Radio = ({ children, label, disabled, helperIcon, helperText, badge, invalidText, validText, validationStatus: validation, showValidationIcon, type = 'default', image, ...props }) => {
|
|
29
29
|
const { validationStatus: fieldValidationStatus } = useFormFieldContext();
|
|
30
30
|
const { validationStatus: groupValidationStatus, type: groupType } = useRadioGroupContext();
|
|
31
31
|
const validationStatus = fieldValidationStatus ?? groupValidationStatus ?? validation ?? 'initial';
|
|
32
32
|
const radioType = groupType ?? type;
|
|
33
|
-
const radioChildren = children ? (children) : (_jsxs(_Fragment, { children: [_jsx(RadioIndicator, { children: _jsx(RadioIcon, {}) }), image ? image : null, _jsxs(RadioTextContent, { children: [!!label && _jsx(RadioLabel, { children: label }), !!helperText && _jsx(Helper, { disabled: disabled, icon: helperIcon, text: helperText }), validationStatus === 'invalid' && !!invalidText && (_jsx(Helper, { showIcon: showValidationIcon, disabled: disabled, validationStatus: "invalid", text: invalidText })), validationStatus === 'valid' && !!validText && (_jsx(Helper, { disabled: disabled, showIcon: showValidationIcon, validationStatus: "valid", text: validText }))] })] }));
|
|
33
|
+
const radioChildren = children ? (children) : (_jsxs(_Fragment, { children: [_jsx(RadioIndicator, { children: _jsx(RadioIcon, {}) }), image ? image : null, _jsxs(RadioTextContent, { children: [!!label && _jsx(RadioLabel, { children: label }), !!helperText && _jsx(Helper, { disabled: disabled, icon: helperIcon, text: helperText }), badge ? badge : null, validationStatus === 'invalid' && !!invalidText && (_jsx(Helper, { showIcon: showValidationIcon, disabled: disabled, validationStatus: "invalid", text: invalidText })), validationStatus === 'valid' && !!validText && (_jsx(Helper, { disabled: disabled, showIcon: showValidationIcon, validationStatus: "valid", text: validText }))] })] }));
|
|
34
34
|
return (_jsx(RadioComponent, { ...props, isDisabled: disabled, children: radioType === 'tile' ? _jsx(RadioTileRoot, { children: radioChildren }) : radioChildren }));
|
|
35
35
|
};
|
|
36
36
|
const RadioTile = ({ type = 'tile', ...props }) => _jsx(Radio, { ...props, type: type });
|
|
@@ -12,6 +12,7 @@ interface RadioWithChildrenProps extends RadioBaseProps {
|
|
|
12
12
|
label?: never;
|
|
13
13
|
helperText?: never;
|
|
14
14
|
helperIcon?: never;
|
|
15
|
+
badge?: never;
|
|
15
16
|
invalidText?: never;
|
|
16
17
|
validText?: never;
|
|
17
18
|
showValidationIcon?: never;
|
|
@@ -22,6 +23,7 @@ interface RadioWithoutChildrenProps extends RadioBaseProps {
|
|
|
22
23
|
label?: string;
|
|
23
24
|
helperText?: string;
|
|
24
25
|
helperIcon?: ComponentType;
|
|
26
|
+
badge?: ReactNode;
|
|
25
27
|
invalidText?: string;
|
|
26
28
|
validText?: string;
|
|
27
29
|
showValidationIcon?: boolean;
|
|
@@ -1,36 +1,187 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { forwardRef, useImperativeHandle } from 'react';
|
|
3
|
-
import { View } from 'react-native';
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
|
3
|
+
import { Platform, TextInput, View } from 'react-native';
|
|
4
4
|
import { StyleSheet } from 'react-native-unistyles';
|
|
5
5
|
import { FormField } from '../FormField';
|
|
6
|
-
import { useVerificationInput } from './useVerificationInput';
|
|
7
6
|
import { VerificationInputSlot } from './VerificationInputSlot';
|
|
8
7
|
const VerificationInput = forwardRef(({ value = '', onChangeText, label, labelVariant = 'body', helperText, helperIcon, validationStatus = 'initial', validText, invalidText, disabled = false, readonly = false, secureTextEntry = false, autoFocus = false, style, ...props }, ref) => {
|
|
9
8
|
const length = 6;
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
const inputRef = useRef(null);
|
|
10
|
+
const latestValueRef = useRef(value);
|
|
11
|
+
const [displayValue, setDisplayValue] = useState(value);
|
|
12
|
+
const [focusedIndex, setFocusedIndex] = useState(null);
|
|
13
|
+
const [selection, setSelection] = useState({ start: 0, end: 0 });
|
|
14
|
+
const latestSelectionRef = useRef(selection);
|
|
15
|
+
const ignoreNextSelectionRef = useRef(false);
|
|
16
|
+
const pendingFocusIndexRef = useRef(null);
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (value !== latestValueRef.current) {
|
|
19
|
+
const trimmedValue = value.slice(0, length);
|
|
20
|
+
latestValueRef.current = trimmedValue;
|
|
21
|
+
setDisplayValue(trimmedValue);
|
|
22
|
+
const nextPos = Math.min(trimmedValue.length, length);
|
|
23
|
+
const nextSelection = { start: nextPos, end: nextPos };
|
|
24
|
+
ignoreNextSelectionRef.current = true;
|
|
25
|
+
latestSelectionRef.current = nextSelection;
|
|
26
|
+
setSelection(nextSelection);
|
|
27
|
+
}
|
|
28
|
+
}, [length, value]);
|
|
29
|
+
const updateValue = (nextValue) => {
|
|
30
|
+
const trimmedValue = nextValue.slice(0, length);
|
|
31
|
+
latestValueRef.current = trimmedValue;
|
|
32
|
+
setDisplayValue(trimmedValue);
|
|
33
|
+
onChangeText?.(trimmedValue);
|
|
34
|
+
};
|
|
35
|
+
const setSelectionIndex = (index) => {
|
|
36
|
+
const clampedIndex = Math.max(0, Math.min(index, length));
|
|
37
|
+
const hasChar = !!latestValueRef.current[clampedIndex];
|
|
38
|
+
const endIndex = hasChar ? Math.min(clampedIndex + 1, length) : clampedIndex;
|
|
39
|
+
const nextSelection = { start: clampedIndex, end: endIndex };
|
|
40
|
+
ignoreNextSelectionRef.current = true;
|
|
41
|
+
latestSelectionRef.current = nextSelection;
|
|
42
|
+
setSelection(nextSelection);
|
|
43
|
+
setFocusedIndex(Math.min(clampedIndex, length - 1));
|
|
44
|
+
};
|
|
45
|
+
const setCaretIndex = (index) => {
|
|
46
|
+
const clampedIndex = Math.max(0, Math.min(index, length));
|
|
47
|
+
const nextSelection = { start: clampedIndex, end: clampedIndex };
|
|
48
|
+
ignoreNextSelectionRef.current = true;
|
|
49
|
+
latestSelectionRef.current = nextSelection;
|
|
50
|
+
setSelection(nextSelection);
|
|
51
|
+
setFocusedIndex(Math.min(clampedIndex, length - 1));
|
|
52
|
+
};
|
|
53
|
+
const findDiffIndex = (prevValue, nextValue) => {
|
|
54
|
+
const minLength = Math.min(prevValue.length, nextValue.length);
|
|
55
|
+
for (let i = 0; i < minLength; i += 1) {
|
|
56
|
+
if (prevValue[i] !== nextValue[i]) {
|
|
57
|
+
return i;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return minLength;
|
|
61
|
+
};
|
|
62
|
+
const handleChangeText = (text) => {
|
|
63
|
+
const prevValue = latestValueRef.current;
|
|
64
|
+
const nextValue = text.slice(0, length);
|
|
65
|
+
const prevLength = prevValue.length;
|
|
66
|
+
const nextLength = nextValue.length;
|
|
67
|
+
const diff = nextLength - prevLength;
|
|
68
|
+
const isBulkInsert = text.length > 1 && diff > 1;
|
|
69
|
+
const shouldBlur = nextLength >= length;
|
|
70
|
+
let nextIndex = Math.max(0, Math.min(latestSelectionRef.current.start + (diff >= 0 ? 1 : diff), length));
|
|
71
|
+
if (Platform.OS === 'android') {
|
|
72
|
+
const editedIndex = findDiffIndex(prevValue, nextValue);
|
|
73
|
+
nextIndex = diff >= 0 ? Math.min(editedIndex + 1, length) : Math.max(editedIndex, 0);
|
|
74
|
+
}
|
|
75
|
+
updateValue(nextValue);
|
|
76
|
+
if (isBulkInsert) {
|
|
77
|
+
setCaretIndex(Math.min(nextLength, length));
|
|
78
|
+
if (shouldBlur) {
|
|
79
|
+
inputRef.current?.blur();
|
|
80
|
+
}
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (nextIndex >= length) {
|
|
84
|
+
setCaretIndex(nextIndex);
|
|
85
|
+
if (shouldBlur) {
|
|
86
|
+
inputRef.current?.blur();
|
|
87
|
+
}
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (nextLength >= length) {
|
|
91
|
+
setSelectionIndex(nextIndex);
|
|
92
|
+
inputRef.current?.blur();
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const hasNextChar = !!nextValue[nextIndex];
|
|
96
|
+
if (hasNextChar) {
|
|
97
|
+
setSelectionIndex(nextIndex);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
setCaretIndex(nextIndex);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
const handleFocus = () => {
|
|
104
|
+
if (pendingFocusIndexRef.current !== null) {
|
|
105
|
+
setSelectionIndex(pendingFocusIndexRef.current);
|
|
106
|
+
pendingFocusIndexRef.current = null;
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
setFocusedIndex(Math.min(selection.start, length - 1));
|
|
110
|
+
};
|
|
111
|
+
const handleBlur = () => {
|
|
112
|
+
setFocusedIndex(null);
|
|
113
|
+
};
|
|
14
114
|
useImperativeHandle(ref, () => ({
|
|
15
|
-
focus: () =>
|
|
16
|
-
|
|
17
|
-
|
|
115
|
+
focus: () => {
|
|
116
|
+
inputRef.current?.focus();
|
|
117
|
+
const nextIndex = Math.min(latestValueRef.current.length, length - 1);
|
|
118
|
+
if (latestValueRef.current.length >= length) {
|
|
119
|
+
setSelectionIndex(nextIndex);
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
setCaretIndex(nextIndex);
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
blur: () => inputRef.current?.blur(),
|
|
126
|
+
clear: () => {
|
|
127
|
+
updateValue('');
|
|
128
|
+
setSelectionIndex(0);
|
|
129
|
+
inputRef.current?.blur();
|
|
130
|
+
setFocusedIndex(null);
|
|
18
131
|
},
|
|
19
|
-
clear: () => onChangeText?.(''),
|
|
20
132
|
focusIndex: (index) => {
|
|
21
133
|
if (index >= 0 && index < length) {
|
|
22
|
-
|
|
134
|
+
inputRef.current?.focus();
|
|
135
|
+
setSelectionIndex(index);
|
|
23
136
|
}
|
|
24
137
|
},
|
|
25
138
|
}), [length, onChangeText]);
|
|
26
139
|
const slots = Array.from({ length }, (_, index) => index);
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
140
|
+
const getAccessibilityLabel = () => {
|
|
141
|
+
return label || props.accessibilityLabel;
|
|
142
|
+
};
|
|
143
|
+
const getAccessibilityHint = () => {
|
|
144
|
+
let accessibilityHint = '';
|
|
145
|
+
if (helperText) {
|
|
146
|
+
accessibilityHint = accessibilityHint + helperText;
|
|
147
|
+
}
|
|
148
|
+
if (validationStatus !== 'initial') {
|
|
149
|
+
if (accessibilityHint.length > 0) {
|
|
150
|
+
accessibilityHint = accessibilityHint + ', ';
|
|
151
|
+
}
|
|
152
|
+
if (validationStatus === 'invalid' && invalidText) {
|
|
153
|
+
accessibilityHint = accessibilityHint + invalidText;
|
|
154
|
+
}
|
|
155
|
+
if (validationStatus === 'valid' && validText) {
|
|
156
|
+
accessibilityHint = accessibilityHint + validText;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return accessibilityHint || props.accessibilityHint;
|
|
160
|
+
};
|
|
161
|
+
return (_jsx(FormField, { label: label, labelVariant: labelVariant, helperText: helperText, helperIcon: helperIcon, validationStatus: validationStatus, validText: validText, invalidText: invalidText, disabled: disabled, readonly: readonly, accessibilityHandledByChildren: true, style: [styles.root, style], ...props, children: _jsxs(View, { style: styles.slotsContainer, children: [_jsx(TextInput, { ref: inputRef, value: displayValue, autoFocus: autoFocus, editable: !disabled && !readonly, accessibilityLabel: getAccessibilityLabel(), accessibilityHint: getAccessibilityHint(), accessibilityState: { disabled: disabled || readonly }, importantForAccessibility: "yes", onChangeText: handleChangeText, onSelectionChange: event => {
|
|
162
|
+
const nextSelection = event.nativeEvent.selection;
|
|
163
|
+
if (ignoreNextSelectionRef.current &&
|
|
164
|
+
(nextSelection.start !== latestSelectionRef.current.start ||
|
|
165
|
+
nextSelection.end !== latestSelectionRef.current.end)) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (pendingFocusIndexRef.current !== null) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
ignoreNextSelectionRef.current = false;
|
|
172
|
+
latestSelectionRef.current = nextSelection;
|
|
173
|
+
setSelection(nextSelection);
|
|
174
|
+
setFocusedIndex(Math.min(nextSelection.start, length - 1));
|
|
175
|
+
}, onFocus: handleFocus, onBlur: handleBlur, selection: selection, keyboardType: "number-pad", textContentType: "oneTimeCode", autoComplete: "sms-otp", secureTextEntry: secureTextEntry, maxLength: length, caretHidden: true, style: styles.hiddenInput, pointerEvents: "none" }), slots.map(index => {
|
|
176
|
+
const char = displayValue[index] || '';
|
|
177
|
+
const isActive = focusedIndex === index;
|
|
178
|
+
const displayChar = secureTextEntry && char ? '*' : char;
|
|
179
|
+
return (_jsx(VerificationInputSlot, { value: displayChar, isActive: isActive, showCaret: isActive && !displayChar, validationStatus: validationStatus, disabled: disabled, readonly: readonly, secureTextEntry: secureTextEntry, onPress: () => {
|
|
180
|
+
pendingFocusIndexRef.current = index;
|
|
181
|
+
inputRef.current?.focus();
|
|
182
|
+
setSelectionIndex(index);
|
|
183
|
+
} }, index));
|
|
184
|
+
})] }) }));
|
|
34
185
|
});
|
|
35
186
|
const styles = StyleSheet.create(theme => ({
|
|
36
187
|
root: {
|
|
@@ -42,6 +193,17 @@ const styles = StyleSheet.create(theme => ({
|
|
|
42
193
|
flexDirection: 'row',
|
|
43
194
|
gap: theme.components.input.verification.gap,
|
|
44
195
|
width: '100%',
|
|
196
|
+
position: 'relative',
|
|
197
|
+
},
|
|
198
|
+
hiddenInput: {
|
|
199
|
+
position: 'absolute',
|
|
200
|
+
width: '100%',
|
|
201
|
+
height: '100%',
|
|
202
|
+
left: 0,
|
|
203
|
+
top: 0,
|
|
204
|
+
color: 'transparent',
|
|
205
|
+
fontSize: 1,
|
|
206
|
+
opacity: 0.1,
|
|
45
207
|
},
|
|
46
208
|
}));
|
|
47
209
|
VerificationInput.displayName = 'VerificationInput';
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
interface VerificationInputSlotProps extends
|
|
1
|
+
import { View, ViewProps } from 'react-native';
|
|
2
|
+
interface VerificationInputSlotProps extends ViewProps {
|
|
3
|
+
value: string;
|
|
3
4
|
isActive: boolean;
|
|
5
|
+
showCaret?: boolean;
|
|
4
6
|
validationStatus: 'initial' | 'valid' | 'invalid';
|
|
5
7
|
disabled?: boolean;
|
|
6
8
|
readonly?: boolean;
|
|
9
|
+
onPress?: () => void;
|
|
10
|
+
secureTextEntry?: boolean;
|
|
7
11
|
}
|
|
8
|
-
export declare const VerificationInputSlot: import("react").ForwardRefExoticComponent<VerificationInputSlotProps & import("react").RefAttributes<
|
|
12
|
+
export declare const VerificationInputSlot: import("react").ForwardRefExoticComponent<VerificationInputSlotProps & import("react").RefAttributes<View>>;
|
|
9
13
|
export {};
|
|
@@ -1,28 +1,46 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { forwardRef } from 'react';
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef, useEffect } from 'react';
|
|
3
|
+
import { Pressable, Text } from 'react-native';
|
|
4
|
+
import Animated, { Easing, useAnimatedStyle, useSharedValue, withRepeat, withTiming, } from 'react-native-reanimated';
|
|
3
5
|
import { StyleSheet } from 'react-native-unistyles';
|
|
4
|
-
|
|
5
|
-
export const VerificationInputSlot = forwardRef(({ isActive, validationStatus, disabled, readonly, style, ...props }, ref) => {
|
|
6
|
+
export const VerificationInputSlot = forwardRef(({ value, isActive, showCaret, validationStatus, disabled, readonly, style, onPress, secureTextEntry, ...props }, ref) => {
|
|
6
7
|
styles.useVariants({
|
|
7
8
|
disabled,
|
|
8
9
|
readonly,
|
|
9
10
|
validationStatus,
|
|
10
11
|
active: isActive,
|
|
12
|
+
secureTextEntry,
|
|
11
13
|
});
|
|
12
|
-
|
|
14
|
+
const caretOpacity = useSharedValue(0);
|
|
15
|
+
const animatedCaretStyle = useAnimatedStyle(() => ({
|
|
16
|
+
opacity: caretOpacity.value,
|
|
17
|
+
}));
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (showCaret && !disabled && !readonly) {
|
|
20
|
+
caretOpacity.value = withRepeat(withTiming(1, { duration: 500, easing: Easing.inOut(Easing.ease) }), -1, true);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
caretOpacity.value = withTiming(0, { duration: 150, easing: Easing.out(Easing.ease) });
|
|
24
|
+
}, [caretOpacity, disabled, readonly, showCaret]);
|
|
25
|
+
return (_jsxs(Pressable, { ref: ref, onPress: onPress, disabled: disabled || readonly, style: [styles.slot, style], accessibilityRole: "button", accessible: false, accessibilityElementsHidden: true, importantForAccessibility: "no-hide-descendants", ...props, children: [_jsx(Text, { style: styles.slotText, children: value }), showCaret && !disabled && !readonly && (_jsx(Animated.View, { style: [styles.caret, animatedCaretStyle] }))] }));
|
|
13
26
|
});
|
|
14
27
|
VerificationInputSlot.displayName = 'VerificationInputSlot';
|
|
15
28
|
const styles = StyleSheet.create(theme => ({
|
|
16
29
|
slot: {
|
|
17
|
-
|
|
30
|
+
flexGrow: 0,
|
|
31
|
+
flexShrink: 0,
|
|
18
32
|
width: theme.components.input.height,
|
|
19
33
|
height: theme.components.input.height,
|
|
34
|
+
minWidth: theme.components.input.height,
|
|
35
|
+
minHeight: theme.components.input.height,
|
|
20
36
|
borderWidth: theme.components.input.borderWidth,
|
|
21
37
|
borderColor: theme.color.border.strong,
|
|
22
38
|
borderRadius: theme.components.input.borderRadius,
|
|
23
39
|
backgroundColor: theme.color.surface.neutral.strong,
|
|
24
|
-
|
|
40
|
+
alignItems: 'center',
|
|
41
|
+
justifyContent: 'center',
|
|
25
42
|
padding: 0,
|
|
43
|
+
position: 'relative',
|
|
26
44
|
variants: {
|
|
27
45
|
disabled: {
|
|
28
46
|
true: {
|
|
@@ -69,4 +87,24 @@ const styles = StyleSheet.create(theme => ({
|
|
|
69
87
|
},
|
|
70
88
|
],
|
|
71
89
|
},
|
|
90
|
+
slotText: {
|
|
91
|
+
color: theme.color.text.primary,
|
|
92
|
+
fontSize: theme.typography.mobile.bodyText.md.fontSize,
|
|
93
|
+
fontFamily: theme.typography.mobile.bodyText.fontFamily,
|
|
94
|
+
fontWeight: `${theme.typography.mobile.bodyText.fontWeight}`,
|
|
95
|
+
textAlign: 'center',
|
|
96
|
+
variants: {
|
|
97
|
+
secureTextEntry: {
|
|
98
|
+
true: {
|
|
99
|
+
paddingTop: 5,
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
caret: {
|
|
105
|
+
position: 'absolute',
|
|
106
|
+
width: 2,
|
|
107
|
+
height: '55%',
|
|
108
|
+
backgroundColor: theme.color.text.brand,
|
|
109
|
+
},
|
|
72
110
|
}));
|