@cloud-ru/uikit-product-fields-predefined 0.15.3 → 0.17.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/CHANGELOG.md +22 -0
- package/README.md +1 -0
- package/dist/cjs/components/FieldAi/FieldAi.d.ts +4 -0
- package/dist/cjs/components/FieldAi/FieldAi.js +6 -2
- package/dist/cjs/components/FieldAi/components/AlertButton/AlertButton.d.ts +5 -0
- package/dist/cjs/components/FieldAi/components/AlertButton/AlertButton.js +12 -0
- package/dist/cjs/components/FieldAi/components/AlertButton/index.d.ts +1 -0
- package/dist/cjs/components/FieldAi/components/AlertButton/index.js +17 -0
- package/dist/cjs/components/FieldAi/components/AlertButton/styles.module.css +17 -0
- package/dist/cjs/components/FieldAi/styles.module.css +29 -2
- package/dist/cjs/components/FieldPhone/FieldPhone.d.ts +1 -1
- package/dist/cjs/components/FieldPhone/FieldPhone.js +31 -0
- package/dist/cjs/components/FieldPhone/__tests__/constants.d.ts +29 -0
- package/dist/cjs/components/FieldPhone/__tests__/constants.js +175 -1
- package/dist/cjs/components/FieldPhone/__tests__/handleAutoInsert.spec.d.ts +1 -0
- package/dist/cjs/components/FieldPhone/__tests__/handleAutoInsert.spec.js +63 -0
- package/dist/cjs/components/FieldPhone/utils.d.ts +7 -0
- package/dist/cjs/components/FieldPhone/utils.js +30 -1
- package/dist/esm/components/FieldAi/FieldAi.d.ts +4 -0
- package/dist/esm/components/FieldAi/FieldAi.js +8 -4
- package/dist/esm/components/FieldAi/components/AlertButton/AlertButton.d.ts +5 -0
- package/dist/esm/components/FieldAi/components/AlertButton/AlertButton.js +6 -0
- package/dist/esm/components/FieldAi/components/AlertButton/index.d.ts +1 -0
- package/dist/esm/components/FieldAi/components/AlertButton/index.js +1 -0
- package/dist/esm/components/FieldAi/components/AlertButton/styles.module.css +17 -0
- package/dist/esm/components/FieldAi/styles.module.css +29 -2
- package/dist/esm/components/FieldPhone/FieldPhone.d.ts +1 -1
- package/dist/esm/components/FieldPhone/FieldPhone.js +32 -1
- package/dist/esm/components/FieldPhone/__tests__/constants.d.ts +29 -0
- package/dist/esm/components/FieldPhone/__tests__/constants.js +174 -0
- package/dist/esm/components/FieldPhone/__tests__/handleAutoInsert.spec.d.ts +1 -0
- package/dist/esm/components/FieldPhone/__tests__/handleAutoInsert.spec.js +61 -0
- package/dist/esm/components/FieldPhone/utils.d.ts +7 -0
- package/dist/esm/components/FieldPhone/utils.js +28 -0
- package/dist/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/tsconfig.esm.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/components/FieldAi/FieldAi.tsx +28 -3
- package/src/components/FieldAi/components/AlertButton/AlertButton.tsx +16 -0
- package/src/components/FieldAi/components/AlertButton/index.ts +1 -0
- package/src/components/FieldAi/components/AlertButton/styles.module.scss +20 -0
- package/src/components/FieldAi/styles.module.scss +46 -7
- package/src/components/FieldPhone/FieldPhone.tsx +35 -1
- package/src/components/FieldPhone/__tests__/constants.ts +175 -0
- package/src/components/FieldPhone/__tests__/handleAutoInsert.spec.ts +87 -0
- package/src/components/FieldPhone/utils.ts +41 -0
|
@@ -9,24 +9,27 @@ var __rest = (this && this.__rest) || function (s, e) {
|
|
|
9
9
|
}
|
|
10
10
|
return t;
|
|
11
11
|
};
|
|
12
|
-
import { jsx as _jsx,
|
|
12
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
13
13
|
import cn from 'classnames';
|
|
14
14
|
import { forwardRef, useEffect, useMemo, useRef, useState } from 'react';
|
|
15
|
-
import { EyeClosedSVG, EyeSVG } from '@cloud-ru/uikit-product-icons';
|
|
15
|
+
import { EyeClosedSVG, EyeSVG, PasswordLockSVG } from '@cloud-ru/uikit-product-icons';
|
|
16
16
|
import { useLocale } from '@cloud-ru/uikit-product-locale';
|
|
17
17
|
import { AdaptiveFieldTextArea, getAdaptiveFieldProps, } from '@cloud-ru/uikit-product-mobile-fields';
|
|
18
18
|
import { ButtonFunction, ButtonOutline } from '@snack-uikit/button';
|
|
19
|
+
import { themeVars } from '@snack-uikit/figma-tokens';
|
|
19
20
|
import { Tooltip } from '@snack-uikit/tooltip';
|
|
21
|
+
import { Typography } from '@snack-uikit/typography';
|
|
20
22
|
import { FieldSubmitButton } from '../../helperComponents/FieldSubmitButton';
|
|
21
23
|
import { TextAreaActionsFooter } from '../../helperComponents/TextAreaActionsFooter';
|
|
22
24
|
import { isTouchDevice as isTouchDeviceHelper } from '../../helpers';
|
|
23
25
|
import { AIDisclaimer } from '../AIDisclaimer/AIDisclaimer';
|
|
26
|
+
import { AlertButton } from './components/AlertButton';
|
|
24
27
|
import { MobileFieldAi } from './components/MobileFieldAi';
|
|
25
28
|
import { WithPasswordValidation } from './components/WithPasswordValidation';
|
|
26
29
|
import styles from './styles.module.css';
|
|
27
30
|
import { getValidationPassword } from './utils';
|
|
28
31
|
export const FieldAi = forwardRef((_a, ref) => {
|
|
29
|
-
var { secure = false, onSubmit: handleSubmitProp, value, onResetContextClick, disabled, className } = _a, props = __rest(_a, ["secure", "onSubmit", "value", "onResetContextClick", "disabled", "className"]);
|
|
32
|
+
var { secure = false, onSubmit: handleSubmitProp, value, onResetContextClick, onCancelSecure, disabled, className } = _a, props = __rest(_a, ["secure", "onSubmit", "value", "onResetContextClick", "onCancelSecure", "disabled", "className"]);
|
|
30
33
|
const { layoutType, validationState } = props;
|
|
31
34
|
const { t } = useLocale('FieldsPredefined');
|
|
32
35
|
const isTouchDevice = isTouchDeviceHelper(layoutType);
|
|
@@ -38,6 +41,7 @@ export const FieldAi = forwardRef((_a, ref) => {
|
|
|
38
41
|
const passwordValidation = useMemo(() => getValidationPassword(value), [value]);
|
|
39
42
|
const isPasswordValid = isPasswordMode ? Object.values(passwordValidation).every(Boolean) : true;
|
|
40
43
|
const showPasswordError = !isPasswordValid && secure && value;
|
|
44
|
+
const showPasswordAlert = Boolean(onCancelSecure) && secure === 'password';
|
|
41
45
|
useEffect(() => () => {
|
|
42
46
|
if (timerRef.current) {
|
|
43
47
|
clearTimeout(timerRef.current);
|
|
@@ -78,5 +82,5 @@ export const FieldAi = forwardRef((_a, ref) => {
|
|
|
78
82
|
if (isTouchDevice && !secure) {
|
|
79
83
|
return (_jsx(MobileFieldAi, Object.assign({}, props, getAdaptiveFieldProps(props), { onSubmit: handleSubmit, submitEnabled: isValueValid && !disabled, ref: ref, value: value })));
|
|
80
84
|
}
|
|
81
|
-
return (_jsxs("div", { className: cn(styles.wrapper, className), children: [_jsx(WithPasswordValidation, { showValidation: isPasswordMode, passwordValidation: passwordValidation, layoutType: layoutType, animatedKey: animatedValidationKey, children: _jsx(AdaptiveFieldTextArea, Object.assign({}, props, { ref: ref, value: value, size: 'm', minRows: secure ? 1 : 2, maxRows: secure ? 1 : 4, placeholder: secure ? t('FieldAi.secret.placeholder') : t('FieldAi.regular.placeholder'), className: cn(styles.textarea, secure && isValueHidden ? styles.secured : undefined), onKeyDown: handleKeyDown, spellCheck: !secure, validationState: showPasswordError ? 'error' : validationState, footer: _jsx(TextAreaActionsFooter, { left: secure && (_jsx(ButtonFunction, { size: 'xs', icon: isValueHidden ? _jsx(EyeSVG, {}) : _jsx(EyeClosedSVG, {}), onClick: () => setIsValueHidden(prev => !prev) })), right: _jsxs(_Fragment, { children: [secure && onResetContextClick && (_jsx(Tooltip, { tip: t('FieldAi.resetContext.tooltip'), hoverDelayOpen: 600, children: _jsx(ButtonOutline, { size: 'xs', label: t('FieldAi.resetContext.label'), onClick: onResetContextClick, appearance: 'destructive' }) })), _jsx(FieldSubmitButton, { showTooltip: !isTouchDevice, className: cn(styles.submitButton, isTouchDevice ? styles.mobileSubmitButton : undefined), active: isValueValid && !disabled && isPasswordValid, handleClick: handleSubmit, size: isTouchDevice ? 's' : 'xs' })] }) }) })) }), !isPasswordMode && _jsx(AIDisclaimer, { layoutType: layoutType })] }));
|
|
85
|
+
return (_jsxs("div", { className: cn(styles.wrapper, isPasswordMode && styles.passwordWrapper, className), children: [showPasswordAlert && (_jsxs("div", { className: styles.alert, children: [_jsx(PasswordLockSVG, { size: 16, color: themeVars.sys.neutral.textSupport }), _jsx(Typography.SansBodyS, { className: styles.alertText, children: t('FieldAi.secret.alert.text') }), _jsx(AlertButton, { label: t('FieldAi.secret.alert.button'), onClick: onCancelSecure })] })), _jsx(WithPasswordValidation, { showValidation: isPasswordMode, passwordValidation: passwordValidation, layoutType: layoutType, animatedKey: animatedValidationKey, children: _jsx(AdaptiveFieldTextArea, Object.assign({}, props, { ref: ref, value: value, size: 'm', minRows: secure ? 1 : 2, maxRows: secure ? 1 : 4, placeholder: secure ? t('FieldAi.secret.placeholder') : t('FieldAi.regular.placeholder'), className: cn(styles.textarea, secure && isValueHidden ? styles.secured : undefined), onKeyDown: handleKeyDown, spellCheck: !secure, validationState: showPasswordError ? 'error' : validationState, footer: _jsx(TextAreaActionsFooter, { left: secure && (_jsx(ButtonFunction, { size: 'xs', icon: isValueHidden ? _jsx(EyeSVG, {}) : _jsx(EyeClosedSVG, {}), onClick: () => setIsValueHidden(prev => !prev) })), right: _jsxs(_Fragment, { children: [secure && onResetContextClick && (_jsx(Tooltip, { tip: t('FieldAi.resetContext.tooltip'), hoverDelayOpen: 600, children: _jsx(ButtonOutline, { size: 'xs', label: t('FieldAi.resetContext.label'), onClick: onResetContextClick, appearance: 'destructive' }) })), _jsx(FieldSubmitButton, { showTooltip: !isTouchDevice, className: cn(styles.submitButton, isTouchDevice ? styles.mobileSubmitButton : undefined), active: isValueValid && !disabled && isPasswordValid, handleClick: handleSubmit, size: isTouchDevice ? 's' : 'xs' })] }) }) })) }), !isPasswordMode && _jsx(AIDisclaimer, { layoutType: layoutType })] }));
|
|
82
86
|
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Typography } from '@snack-uikit/typography';
|
|
3
|
+
import styles from './styles.module.css';
|
|
4
|
+
export function AlertButton({ label, onClick }) {
|
|
5
|
+
return (_jsx("button", { onClick: onClick, className: styles.button, children: _jsx(Typography.SansLabelM, { className: styles.label, children: label }) }));
|
|
6
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './AlertButton';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './AlertButton';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
.button{
|
|
2
|
+
outline:none;
|
|
3
|
+
border:none;
|
|
4
|
+
background-color:transparent;
|
|
5
|
+
cursor:pointer;
|
|
6
|
+
height:16px;
|
|
7
|
+
display:flex;
|
|
8
|
+
align-items:center;
|
|
9
|
+
justify-content:center;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.label{
|
|
13
|
+
color:var(--sys-blue-text-support, #4877b0);
|
|
14
|
+
}
|
|
15
|
+
.label:hover{
|
|
16
|
+
color:var(--sys-neutral-text-main, #41424e);
|
|
17
|
+
}
|
|
@@ -2,6 +2,33 @@
|
|
|
2
2
|
display:flex;
|
|
3
3
|
flex-direction:column;
|
|
4
4
|
gap:var(--dimension-050m, 4px);
|
|
5
|
+
position:relative;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.passwordWrapper{
|
|
9
|
+
gap:0;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.alert{
|
|
13
|
+
display:flex;
|
|
14
|
+
gap:8px;
|
|
15
|
+
padding:4px 8px;
|
|
16
|
+
border-radius:8px 8px 0 0;
|
|
17
|
+
border:1px solid var(--sys-blue-decor-activated, #aac4ea);
|
|
18
|
+
background-color:var(--sys-blue-decor-default, #d6e2f4);
|
|
19
|
+
box-sizing:border-box;
|
|
20
|
+
height:38px;
|
|
21
|
+
position:absolute;
|
|
22
|
+
left:0;
|
|
23
|
+
top:-26px;
|
|
24
|
+
width:100%;
|
|
25
|
+
color:var(--sys-neutral-text-main, #41424e);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.alertText{
|
|
29
|
+
flex:1;
|
|
30
|
+
min-width:0;
|
|
31
|
+
color:var(--sys-neutral-text-main, #41424e);
|
|
5
32
|
}
|
|
6
33
|
|
|
7
34
|
.secured textarea{
|
|
@@ -25,10 +52,10 @@
|
|
|
25
52
|
}
|
|
26
53
|
|
|
27
54
|
.submitButton:not([data-disabled]){
|
|
28
|
-
background:radial-gradient(92.53% 92.53% at 65.87% 21.69%, #
|
|
55
|
+
background:radial-gradient(92.53% 92.53% at 65.87% 21.69%, #7cb5f2 15%, #5fd7c2 45%, #26d07c 65%);
|
|
29
56
|
}
|
|
30
57
|
|
|
31
58
|
.textarea > :first-child[data-validation=default]:not([data-readonly]):not([data-disable-focus]):focus-within:not([data-disabled]){
|
|
32
59
|
border-color:transparent;
|
|
33
|
-
background:linear-gradient(var(--sys-neutral-background2-level), var(--sys-neutral-background2-level) 0) padding-box, radial-gradient(92.53% 92.53% at 65.87% 21.69%, #
|
|
60
|
+
background:linear-gradient(var(--sys-neutral-background2-level), var(--sys-neutral-background2-level) 0) padding-box, radial-gradient(92.53% 92.53% at 65.87% 21.69%, #7cb5f2 8%, #78b9ec 17%, #70c6dd 28%, #64d8c7 38%, #5fd7c2 44%, #54d5b3 51%, #40d39c 58%, #26d07c 66%) border-box;
|
|
34
61
|
}
|
|
@@ -11,7 +11,7 @@ export type FieldPhoneProps = WithLayoutType<Omit<FieldTextProps, 'prefix' | 'pr
|
|
|
11
11
|
/** options — объект конфигурации для изменения стандартного списка стран */
|
|
12
12
|
options?: CountrySettings;
|
|
13
13
|
}>;
|
|
14
|
-
export declare const FieldPhone: import("react").ForwardRefExoticComponent<Omit<FieldTextProps, "onKeyDown" | "maxLength" | "placeholder" | "inputMode" | "prefix" | "prefixIcon" | "postfix" | "autocomplete" | "decoratorRef" | "allowMoreThanMaxLength"
|
|
14
|
+
export declare const FieldPhone: import("react").ForwardRefExoticComponent<Omit<FieldTextProps, "onKeyDown" | "maxLength" | "placeholder" | "button" | "inputMode" | "prefix" | "prefixIcon" | "postfix" | "autocomplete" | "decoratorRef" | "allowMoreThanMaxLength"> & {
|
|
15
15
|
/** Включить скролл для основной части списка стран */
|
|
16
16
|
scrollList?: boolean;
|
|
17
17
|
onChange?(value: string): void;
|
|
@@ -20,13 +20,15 @@ import { useValueControl } from '@snack-uikit/utils';
|
|
|
20
20
|
import { PLACEHOLDER_CHAR } from './constants';
|
|
21
21
|
import { useCountries } from './hooks';
|
|
22
22
|
import styles from './styles.module.css';
|
|
23
|
-
import { detectCountryByPhone } from './utils';
|
|
23
|
+
import { detectCountryByPhone, handleAutoInsert } from './utils';
|
|
24
24
|
export const FieldPhone = forwardRef((_a, ref) => {
|
|
25
25
|
var { value: valueProp, onChangeCountry, onChange: onChangeProp, showClearButton = true, searchPlaceholder, onPaste, className, scrollList, options: optionsProp } = _a, rest = __rest(_a, ["value", "onChangeCountry", "onChange", "showClearButton", "searchPlaceholder", "onPaste", "className", "scrollList", "options"]);
|
|
26
26
|
const [open, setOpen] = useState(false);
|
|
27
27
|
const localRef = useRef(null);
|
|
28
28
|
const options = useCountries(optionsProp);
|
|
29
29
|
const isOnlyOneCountryAvailable = options.length === 1;
|
|
30
|
+
const rawInsertRef = useRef('');
|
|
31
|
+
const insertSwitchRef = useRef(false);
|
|
30
32
|
const [country, setCountry] = useValueControl({
|
|
31
33
|
defaultValue: options[0],
|
|
32
34
|
onChange: onChangeCountry,
|
|
@@ -45,7 +47,19 @@ export const FieldPhone = forwardRef((_a, ref) => {
|
|
|
45
47
|
definitions: {
|
|
46
48
|
X: /[0-9]/,
|
|
47
49
|
},
|
|
50
|
+
prepare: (str) => {
|
|
51
|
+
if (str.replace(/\D/g, '').length > 1) {
|
|
52
|
+
rawInsertRef.current = str;
|
|
53
|
+
}
|
|
54
|
+
return str;
|
|
55
|
+
},
|
|
48
56
|
}), [country === null || country === void 0 ? void 0 : country.mask]);
|
|
57
|
+
const clearRaw = () => {
|
|
58
|
+
rawInsertRef.current = '';
|
|
59
|
+
};
|
|
60
|
+
const markSwitchRef = () => {
|
|
61
|
+
insertSwitchRef.current = true;
|
|
62
|
+
};
|
|
49
63
|
const { ref: iMaskRef, value: iMaskValue, setValue, unmaskedValue, } = useIMask(maskOptions, {
|
|
50
64
|
onAccept: (_, maskRef) => {
|
|
51
65
|
const unmasked = maskRef.unmaskedValue;
|
|
@@ -54,6 +68,23 @@ export const FieldPhone = forwardRef((_a, ref) => {
|
|
|
54
68
|
if (value !== valueProp) {
|
|
55
69
|
onChangeProp === null || onChangeProp === void 0 ? void 0 : onChangeProp(value);
|
|
56
70
|
}
|
|
71
|
+
if (insertSwitchRef.current) {
|
|
72
|
+
insertSwitchRef.current = false;
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
handleAutoInsert({
|
|
76
|
+
raw: rawInsertRef.current,
|
|
77
|
+
onValueChange: value => {
|
|
78
|
+
setTimeout(() => setValue(value), 0);
|
|
79
|
+
},
|
|
80
|
+
onCountryChange: country => {
|
|
81
|
+
markSwitchRef();
|
|
82
|
+
clearRaw();
|
|
83
|
+
setCountry(country);
|
|
84
|
+
},
|
|
85
|
+
country,
|
|
86
|
+
options,
|
|
87
|
+
});
|
|
57
88
|
},
|
|
58
89
|
});
|
|
59
90
|
useEffect(() => {
|
|
@@ -3,3 +3,32 @@ export declare const phoneFormatCases: {
|
|
|
3
3
|
input: string;
|
|
4
4
|
expected: string;
|
|
5
5
|
}[];
|
|
6
|
+
export declare const handleAutoInsertCases: ({
|
|
7
|
+
name: string;
|
|
8
|
+
raw: string;
|
|
9
|
+
currentId: string;
|
|
10
|
+
expectedCountryId: string;
|
|
11
|
+
expectedValue: string;
|
|
12
|
+
expectedLengthFromCountryId?: undefined;
|
|
13
|
+
} | {
|
|
14
|
+
name: string;
|
|
15
|
+
raw: string;
|
|
16
|
+
currentId: string;
|
|
17
|
+
expectedValue: string;
|
|
18
|
+
expectedCountryId?: undefined;
|
|
19
|
+
expectedLengthFromCountryId?: undefined;
|
|
20
|
+
} | {
|
|
21
|
+
name: string;
|
|
22
|
+
raw: string;
|
|
23
|
+
currentId: string;
|
|
24
|
+
expectedCountryId?: undefined;
|
|
25
|
+
expectedValue?: undefined;
|
|
26
|
+
expectedLengthFromCountryId?: undefined;
|
|
27
|
+
} | {
|
|
28
|
+
name: string;
|
|
29
|
+
raw: string;
|
|
30
|
+
currentId: string;
|
|
31
|
+
expectedCountryId: string;
|
|
32
|
+
expectedLengthFromCountryId: string;
|
|
33
|
+
expectedValue?: undefined;
|
|
34
|
+
})[];
|
|
@@ -24,3 +24,177 @@ export const phoneFormatCases = [
|
|
|
24
24
|
{ country: 'Tajikistan', input: '+992123456789', expected: '+992 12 345-6789' },
|
|
25
25
|
{ country: 'Moldova', input: '+37312345678', expected: '+373 1234 5678' },
|
|
26
26
|
];
|
|
27
|
+
export const handleAutoInsertCases = [
|
|
28
|
+
{
|
|
29
|
+
name: 'Kyrgyzstan switches + exact national',
|
|
30
|
+
raw: '+996 201 666666',
|
|
31
|
+
currentId: 'russia',
|
|
32
|
+
expectedCountryId: 'kyrgyzstan',
|
|
33
|
+
expectedValue: '201666666',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: 'Kyrgyzstan same => exact national',
|
|
37
|
+
raw: '+996201666666',
|
|
38
|
+
currentId: 'kyrgyzstan',
|
|
39
|
+
expectedValue: '201666666',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: 'Russia incomplete => no calls',
|
|
43
|
+
raw: '+7 987',
|
|
44
|
+
currentId: 'russia',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: 'Albania (+355) XXX XXX-XXX',
|
|
48
|
+
raw: '+355 675 123 456',
|
|
49
|
+
currentId: 'russia',
|
|
50
|
+
expectedCountryId: 'albania',
|
|
51
|
+
expectedLengthFromCountryId: 'albania',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: 'Algeria (+213) XX XXX-XXXX',
|
|
55
|
+
raw: '+213 12 345 6789',
|
|
56
|
+
currentId: 'russia',
|
|
57
|
+
expectedCountryId: 'algeria',
|
|
58
|
+
expectedLengthFromCountryId: 'algeria',
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: 'Armenia (+374) XX XXX-XXX',
|
|
62
|
+
raw: '+374 10 123 456',
|
|
63
|
+
currentId: 'russia',
|
|
64
|
+
expectedCountryId: 'armenia',
|
|
65
|
+
expectedLengthFromCountryId: 'armenia',
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: 'Austria (+43) XXX XXX-XXXX',
|
|
69
|
+
raw: '+43 664 123 4567',
|
|
70
|
+
currentId: 'russia',
|
|
71
|
+
expectedCountryId: 'austria',
|
|
72
|
+
expectedLengthFromCountryId: 'austria',
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: 'Australia (+61) X XXXX-XXXX',
|
|
76
|
+
raw: '+61 4 1234 5678',
|
|
77
|
+
currentId: 'russia',
|
|
78
|
+
expectedCountryId: 'australia',
|
|
79
|
+
expectedLengthFromCountryId: 'australia',
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: 'Belarus (+375) XX XXX-XX-XX',
|
|
83
|
+
raw: '+375 29 123 45 67',
|
|
84
|
+
currentId: 'russia',
|
|
85
|
+
expectedCountryId: 'belarus',
|
|
86
|
+
expectedLengthFromCountryId: 'belarus',
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: 'Bangladesh (+880) X XXX-XXXX',
|
|
90
|
+
raw: '+880 1 234 5678',
|
|
91
|
+
currentId: 'russia',
|
|
92
|
+
expectedCountryId: 'bangladesh',
|
|
93
|
+
expectedLengthFromCountryId: 'bangladesh',
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: 'Bahrain (+973) XX XXX-XXX',
|
|
97
|
+
raw: '+973 33 123 456',
|
|
98
|
+
currentId: 'russia',
|
|
99
|
+
expectedCountryId: 'bahrain',
|
|
100
|
+
expectedLengthFromCountryId: 'bahrain',
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: 'Cyprus (+357) XX XXXXXX',
|
|
104
|
+
raw: '+357 99 123456',
|
|
105
|
+
currentId: 'russia',
|
|
106
|
+
expectedCountryId: 'cyprus',
|
|
107
|
+
expectedLengthFromCountryId: 'cyprus',
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: 'Georgia (+995) XXX XX-XX-XX',
|
|
111
|
+
raw: '+995 555 12 34 56',
|
|
112
|
+
currentId: 'russia',
|
|
113
|
+
expectedCountryId: 'georgia',
|
|
114
|
+
expectedLengthFromCountryId: 'georgia',
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
name: 'India (+91) XXXXX-XXXXX',
|
|
118
|
+
raw: '+91 98765 43210',
|
|
119
|
+
currentId: 'russia',
|
|
120
|
+
expectedCountryId: 'india',
|
|
121
|
+
expectedLengthFromCountryId: 'india',
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
name: 'Iran (+98) XXX XXX-XXXX',
|
|
125
|
+
raw: '+98 912 345 6789',
|
|
126
|
+
currentId: 'russia',
|
|
127
|
+
expectedCountryId: 'iran',
|
|
128
|
+
expectedLengthFromCountryId: 'iran',
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
name: 'Netherlands (+31) XX XXX-XXXX',
|
|
132
|
+
raw: '+31 20 123 4567',
|
|
133
|
+
currentId: 'russia',
|
|
134
|
+
expectedCountryId: 'netherlands',
|
|
135
|
+
expectedLengthFromCountryId: 'netherlands',
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: 'Romania (+40) XX XXX-XXXX',
|
|
139
|
+
raw: '+40 21 123 4567',
|
|
140
|
+
currentId: 'russia',
|
|
141
|
+
expectedCountryId: 'romania',
|
|
142
|
+
expectedLengthFromCountryId: 'romania',
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
name: 'Serbia (+381) XX XXX-XXXX',
|
|
146
|
+
raw: '+381 11 123 4567',
|
|
147
|
+
currentId: 'russia',
|
|
148
|
+
expectedCountryId: 'serbia',
|
|
149
|
+
expectedLengthFromCountryId: 'serbia',
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
name: 'Uzbekistan (+998) XX XXX-XX-XX',
|
|
153
|
+
raw: '+998 90 123 45 67',
|
|
154
|
+
currentId: 'russia',
|
|
155
|
+
expectedCountryId: 'uzbekistan',
|
|
156
|
+
expectedLengthFromCountryId: 'uzbekistan',
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
name: 'Tajikistan (+992) XX XXX-XXXX',
|
|
160
|
+
raw: '+992 93 123 4567',
|
|
161
|
+
currentId: 'russia',
|
|
162
|
+
expectedCountryId: 'tajikistan',
|
|
163
|
+
expectedLengthFromCountryId: 'tajikistan',
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
name: 'Moldova (+373) XXXX XXXX',
|
|
167
|
+
raw: '+373 1234 5678',
|
|
168
|
+
currentId: 'russia',
|
|
169
|
+
expectedCountryId: 'moldova',
|
|
170
|
+
expectedLengthFromCountryId: 'moldova',
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
name: 'Netherlands (+31) switches from Moldova',
|
|
174
|
+
raw: '+31 20 123 4567',
|
|
175
|
+
currentId: 'moldova',
|
|
176
|
+
expectedCountryId: 'netherlands',
|
|
177
|
+
expectedLengthFromCountryId: 'netherlands',
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
name: 'Uzbekistan (+998) switches from Tajikistan',
|
|
181
|
+
raw: '+998 90 123 45 67',
|
|
182
|
+
currentId: 'tajikistan',
|
|
183
|
+
expectedCountryId: 'uzbekistan',
|
|
184
|
+
expectedLengthFromCountryId: 'uzbekistan',
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
name: 'Tajikistan (+992) switches from Albania',
|
|
188
|
+
raw: '+992 93 123 4567',
|
|
189
|
+
currentId: 'albania',
|
|
190
|
+
expectedCountryId: 'tajikistan',
|
|
191
|
+
expectedLengthFromCountryId: 'tajikistan',
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
name: 'Moldova (+373) switches from Egypt',
|
|
195
|
+
raw: '+373 1234 5678',
|
|
196
|
+
currentId: 'egypt',
|
|
197
|
+
expectedCountryId: 'moldova',
|
|
198
|
+
expectedLengthFromCountryId: 'moldova',
|
|
199
|
+
},
|
|
200
|
+
];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { ALL_COUNTRY_CODES } from '../countries';
|
|
3
|
+
import { handleAutoInsert } from '../utils';
|
|
4
|
+
import { handleAutoInsertCases } from './constants';
|
|
5
|
+
const OPTIONS = ALL_COUNTRY_CODES.map(c => ({
|
|
6
|
+
id: c.value,
|
|
7
|
+
iso2: c.iso2,
|
|
8
|
+
mask: c.mask,
|
|
9
|
+
content: {
|
|
10
|
+
caption: c.caption,
|
|
11
|
+
option: c.value,
|
|
12
|
+
},
|
|
13
|
+
beforeContent: null,
|
|
14
|
+
}));
|
|
15
|
+
const byId = (id) => {
|
|
16
|
+
const found = OPTIONS.find(c => c.id === id);
|
|
17
|
+
if (!found)
|
|
18
|
+
throw new Error(`Country not found in OPTIONS: ${id}`);
|
|
19
|
+
return found;
|
|
20
|
+
};
|
|
21
|
+
const casesNoCalls = handleAutoInsertCases.filter((c) => !('expectedValue' in c) && !('expectedLengthFromCountryId' in c));
|
|
22
|
+
const casesExact = handleAutoInsertCases.filter((c) => 'expectedValue' in c);
|
|
23
|
+
describe('handleAutoInsert — no calls', () => {
|
|
24
|
+
casesNoCalls.forEach(tc => {
|
|
25
|
+
it(`${tc.name}`, () => {
|
|
26
|
+
const onValueChange = vi.fn();
|
|
27
|
+
const onCountryChange = vi.fn();
|
|
28
|
+
handleAutoInsert({
|
|
29
|
+
raw: tc.raw,
|
|
30
|
+
onValueChange,
|
|
31
|
+
onCountryChange,
|
|
32
|
+
country: byId(tc.currentId),
|
|
33
|
+
options: OPTIONS,
|
|
34
|
+
});
|
|
35
|
+
expect(onCountryChange).not.toHaveBeenCalled();
|
|
36
|
+
expect(onValueChange).not.toHaveBeenCalled();
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
describe('handleAutoInsert — exact value', () => {
|
|
41
|
+
casesExact.forEach(tc => {
|
|
42
|
+
it(`${tc.name}`, () => {
|
|
43
|
+
var _a, _b;
|
|
44
|
+
const onValueChange = vi.fn();
|
|
45
|
+
const onCountryChange = vi.fn();
|
|
46
|
+
handleAutoInsert({
|
|
47
|
+
raw: tc.raw,
|
|
48
|
+
onValueChange,
|
|
49
|
+
onCountryChange,
|
|
50
|
+
country: byId(tc.currentId),
|
|
51
|
+
options: OPTIONS,
|
|
52
|
+
});
|
|
53
|
+
expect(onValueChange).toHaveBeenCalledTimes(1);
|
|
54
|
+
expect(onValueChange).toHaveBeenCalledWith(tc.expectedValue);
|
|
55
|
+
const expectedCountryCalls = tc.expectedCountryId ? 1 : 0;
|
|
56
|
+
expect(onCountryChange).toHaveBeenCalledTimes(expectedCountryCalls);
|
|
57
|
+
const actualCountryId = (_b = (_a = onCountryChange.mock.calls[0]) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.id;
|
|
58
|
+
expect(actualCountryId).toBe(tc.expectedCountryId);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -5,3 +5,10 @@ export declare const formatPhoneNumber: <T extends {
|
|
|
5
5
|
mask: string;
|
|
6
6
|
}>(phone: string, countries: readonly T[]) => string;
|
|
7
7
|
export declare function detectCountryByPhone(text: string, options: FieldPhoneOptionsProps[]): FieldPhoneOptionsProps | undefined;
|
|
8
|
+
export declare const handleAutoInsert: ({ raw, onValueChange, onCountryChange, country, options, }: {
|
|
9
|
+
raw: string;
|
|
10
|
+
onValueChange: (str: string) => void;
|
|
11
|
+
onCountryChange: (country: FieldPhoneOptionsProps) => void;
|
|
12
|
+
country?: FieldPhoneOptionsProps;
|
|
13
|
+
options: FieldPhoneOptionsProps[];
|
|
14
|
+
}) => void;
|
|
@@ -49,3 +49,31 @@ export function detectCountryByPhone(text, options) {
|
|
|
49
49
|
}
|
|
50
50
|
return options.find(opt => opt.iso2 === regionCode);
|
|
51
51
|
}
|
|
52
|
+
export const handleAutoInsert = ({ raw, onValueChange, onCountryChange, country, options, }) => {
|
|
53
|
+
var _a, _b, _c, _d, _e, _f;
|
|
54
|
+
if (!raw)
|
|
55
|
+
return;
|
|
56
|
+
const parsed = parsePhoneNumber(raw);
|
|
57
|
+
const ok = parsed.valid || parsed.possible;
|
|
58
|
+
if (!ok)
|
|
59
|
+
return;
|
|
60
|
+
const detected = detectCountryByPhone(raw, options);
|
|
61
|
+
if (!detected)
|
|
62
|
+
return;
|
|
63
|
+
let national = (_a = parsed.number) === null || _a === void 0 ? void 0 : _a.significant.replace(/\D/g, '');
|
|
64
|
+
if (!national)
|
|
65
|
+
return;
|
|
66
|
+
const nextNationalLength = ((_c = (_b = detected.mask) === null || _b === void 0 ? void 0 : _b.match(/X/g)) !== null && _c !== void 0 ? _c : []).length;
|
|
67
|
+
if (nextNationalLength && national.length > nextNationalLength) {
|
|
68
|
+
national = national.slice(-nextNationalLength);
|
|
69
|
+
}
|
|
70
|
+
const fullDigits = ((_e = (_d = parsed.number) === null || _d === void 0 ? void 0 : _d.e164) !== null && _e !== void 0 ? _e : raw).replace(/\D/g, '');
|
|
71
|
+
const countryCodeDigits = String((_f = parsed.countryCode) !== null && _f !== void 0 ? _f : '');
|
|
72
|
+
const expected = countryCodeDigits.length + nextNationalLength;
|
|
73
|
+
if (fullDigits.length < expected)
|
|
74
|
+
return;
|
|
75
|
+
if (detected.id !== (country === null || country === void 0 ? void 0 : country.id)) {
|
|
76
|
+
onCountryChange(detected);
|
|
77
|
+
}
|
|
78
|
+
onValueChange(national);
|
|
79
|
+
};
|