@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
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloud-ru/uikit-product-fields-predefined",
|
|
3
3
|
"title": "Fields Predefined",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.17.0",
|
|
5
5
|
"sideEffects": [
|
|
6
6
|
"*.css",
|
|
7
7
|
"*.woff",
|
|
@@ -61,5 +61,5 @@
|
|
|
61
61
|
"react-hook-form": ">=7.51.0",
|
|
62
62
|
"yup": ">=0.32.0"
|
|
63
63
|
},
|
|
64
|
-
"gitHead": "
|
|
64
|
+
"gitHead": "649ac65d527094cc472552ec150ef7ba66dfcb78"
|
|
65
65
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import cn from 'classnames';
|
|
2
2
|
import { forwardRef, KeyboardEventHandler, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
3
|
|
|
4
|
-
import { EyeClosedSVG, EyeSVG } from '@cloud-ru/uikit-product-icons';
|
|
4
|
+
import { EyeClosedSVG, EyeSVG, PasswordLockSVG } from '@cloud-ru/uikit-product-icons';
|
|
5
5
|
import { useLocale } from '@cloud-ru/uikit-product-locale';
|
|
6
6
|
import {
|
|
7
7
|
AdaptiveFieldTextArea,
|
|
@@ -10,12 +10,15 @@ import {
|
|
|
10
10
|
} from '@cloud-ru/uikit-product-mobile-fields';
|
|
11
11
|
import { WithLayoutType } from '@cloud-ru/uikit-product-utils';
|
|
12
12
|
import { ButtonFunction, ButtonOutline } from '@snack-uikit/button';
|
|
13
|
+
import { themeVars } from '@snack-uikit/figma-tokens';
|
|
13
14
|
import { Tooltip } from '@snack-uikit/tooltip';
|
|
15
|
+
import { Typography } from '@snack-uikit/typography';
|
|
14
16
|
|
|
15
17
|
import { FieldSubmitButton } from '../../helperComponents/FieldSubmitButton';
|
|
16
18
|
import { TextAreaActionsFooter } from '../../helperComponents/TextAreaActionsFooter';
|
|
17
19
|
import { isTouchDevice as isTouchDeviceHelper } from '../../helpers';
|
|
18
20
|
import { AIDisclaimer } from '../AIDisclaimer/AIDisclaimer';
|
|
21
|
+
import { AlertButton } from './components/AlertButton';
|
|
19
22
|
import { MobileFieldAi } from './components/MobileFieldAi';
|
|
20
23
|
import { WithPasswordValidation } from './components/WithPasswordValidation';
|
|
21
24
|
import styles from './styles.module.scss';
|
|
@@ -29,11 +32,25 @@ export type FieldAiProps = WithLayoutType<
|
|
|
29
32
|
onSubmit(value: string): void;
|
|
30
33
|
/** Действие при клике по кнопке сброса контекста */
|
|
31
34
|
onResetContextClick?(): void;
|
|
35
|
+
/** Действие для отмены секьюрности поля */
|
|
36
|
+
onCancelSecure?(): void;
|
|
32
37
|
}
|
|
33
38
|
>;
|
|
34
39
|
|
|
35
40
|
export const FieldAi = forwardRef<HTMLTextAreaElement, FieldAiProps>(
|
|
36
|
-
(
|
|
41
|
+
(
|
|
42
|
+
{
|
|
43
|
+
secure = false,
|
|
44
|
+
onSubmit: handleSubmitProp,
|
|
45
|
+
value,
|
|
46
|
+
onResetContextClick,
|
|
47
|
+
onCancelSecure,
|
|
48
|
+
disabled,
|
|
49
|
+
className,
|
|
50
|
+
...props
|
|
51
|
+
},
|
|
52
|
+
ref,
|
|
53
|
+
) => {
|
|
37
54
|
const { layoutType, validationState } = props;
|
|
38
55
|
const { t } = useLocale('FieldsPredefined');
|
|
39
56
|
const isTouchDevice = isTouchDeviceHelper(layoutType);
|
|
@@ -48,6 +65,7 @@ export const FieldAi = forwardRef<HTMLTextAreaElement, FieldAiProps>(
|
|
|
48
65
|
const passwordValidation = useMemo(() => getValidationPassword(value), [value]);
|
|
49
66
|
const isPasswordValid = isPasswordMode ? Object.values(passwordValidation).every(Boolean) : true;
|
|
50
67
|
const showPasswordError = !isPasswordValid && secure && value;
|
|
68
|
+
const showPasswordAlert = Boolean(onCancelSecure) && secure === 'password';
|
|
51
69
|
|
|
52
70
|
useEffect(
|
|
53
71
|
() => () => {
|
|
@@ -113,7 +131,14 @@ export const FieldAi = forwardRef<HTMLTextAreaElement, FieldAiProps>(
|
|
|
113
131
|
}
|
|
114
132
|
|
|
115
133
|
return (
|
|
116
|
-
<div className={cn(styles.wrapper, className)}>
|
|
134
|
+
<div className={cn(styles.wrapper, isPasswordMode && styles.passwordWrapper, className)}>
|
|
135
|
+
{showPasswordAlert && (
|
|
136
|
+
<div className={styles.alert}>
|
|
137
|
+
<PasswordLockSVG size={16} color={themeVars.sys.neutral.textSupport} />
|
|
138
|
+
<Typography.SansBodyS className={styles.alertText}>{t('FieldAi.secret.alert.text')}</Typography.SansBodyS>
|
|
139
|
+
<AlertButton label={t('FieldAi.secret.alert.button')} onClick={onCancelSecure} />
|
|
140
|
+
</div>
|
|
141
|
+
)}
|
|
117
142
|
<WithPasswordValidation
|
|
118
143
|
showValidation={isPasswordMode}
|
|
119
144
|
passwordValidation={passwordValidation}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Typography } from '@snack-uikit/typography';
|
|
2
|
+
|
|
3
|
+
import styles from './styles.module.scss';
|
|
4
|
+
|
|
5
|
+
export type AlertButtonProps = {
|
|
6
|
+
label: string;
|
|
7
|
+
onClick?(): void;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function AlertButton({ label, onClick }: AlertButtonProps) {
|
|
11
|
+
return (
|
|
12
|
+
<button onClick={onClick} className={styles.button}>
|
|
13
|
+
<Typography.SansLabelM className={styles.label}>{label}</Typography.SansLabelM>
|
|
14
|
+
</button>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './AlertButton';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
@use '@sbercloud/figma-tokens-cloud-platform/build/scss/styles-theme-variables' as stv;
|
|
2
|
+
|
|
3
|
+
.button {
|
|
4
|
+
outline: none;
|
|
5
|
+
border: none;
|
|
6
|
+
background-color: transparent;
|
|
7
|
+
cursor: pointer;
|
|
8
|
+
height: 16px;
|
|
9
|
+
display: flex;
|
|
10
|
+
align-items: center;
|
|
11
|
+
justify-content: center;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.label {
|
|
15
|
+
color: stv.$sys-blue-text-support;
|
|
16
|
+
|
|
17
|
+
&:hover {
|
|
18
|
+
color: stv.$sys-neutral-text-main;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -1,9 +1,38 @@
|
|
|
1
|
+
/* stylelint-disable color-no-hex */
|
|
1
2
|
@use '@sbercloud/figma-tokens-cloud-platform/build/scss/components/styles-tokens-fields' as ste;
|
|
3
|
+
@use '@sbercloud/figma-tokens-cloud-platform/build/scss/styles-theme-variables' as stv;
|
|
2
4
|
|
|
3
5
|
.wrapper {
|
|
4
6
|
display: flex;
|
|
5
7
|
flex-direction: column;
|
|
6
8
|
gap: ste.$dimension-050m;
|
|
9
|
+
position: relative;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.passwordWrapper {
|
|
13
|
+
gap: 0;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.alert {
|
|
17
|
+
display: flex;
|
|
18
|
+
gap: 8px;
|
|
19
|
+
padding: 4px 8px;
|
|
20
|
+
border-radius: 8px 8px 0 0;
|
|
21
|
+
border: 1px solid stv.$sys-blue-decor-activated;
|
|
22
|
+
background-color: stv.$sys-blue-decor-default;
|
|
23
|
+
box-sizing: border-box;
|
|
24
|
+
height: 38px;
|
|
25
|
+
position: absolute;
|
|
26
|
+
left: 0;
|
|
27
|
+
top: -26px;
|
|
28
|
+
width: 100%;
|
|
29
|
+
color: stv.$sys-neutral-text-main;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.alertText {
|
|
33
|
+
flex: 1;
|
|
34
|
+
min-width: 0;
|
|
35
|
+
color: stv.$sys-neutral-text-main;
|
|
7
36
|
}
|
|
8
37
|
|
|
9
38
|
.secured {
|
|
@@ -34,18 +63,28 @@
|
|
|
34
63
|
|
|
35
64
|
.submitButton {
|
|
36
65
|
&:not([data-disabled]) {
|
|
37
|
-
|
|
38
|
-
background: radial-gradient(92.53% 92.53% at 65.87% 21.69%, #7CB5F2 15%, #5FD7C2 45%, #26D07C 65%);
|
|
66
|
+
background: radial-gradient(92.53% 92.53% at 65.87% 21.69%, #7cb5f2 15%, #5fd7c2 45%, #26d07c 65%);
|
|
39
67
|
}
|
|
40
68
|
}
|
|
41
69
|
|
|
42
70
|
.textarea {
|
|
43
|
-
|
|
44
|
-
&[data-validation=default]:not([data-readonly]):not([data-disable-focus]):focus-within:not([data-disabled]) {
|
|
71
|
+
& > :first-child {
|
|
72
|
+
&[data-validation='default']:not([data-readonly]):not([data-disable-focus]):focus-within:not([data-disabled]) {
|
|
45
73
|
border-color: transparent;
|
|
46
|
-
|
|
47
|
-
|
|
74
|
+
background:
|
|
75
|
+
linear-gradient(var(--sys-neutral-background2-level), var(--sys-neutral-background2-level) 0) padding-box,
|
|
76
|
+
radial-gradient(
|
|
77
|
+
92.53% 92.53% at 65.87% 21.69%,
|
|
78
|
+
#7cb5f2 8%,
|
|
79
|
+
#78b9ec 17%,
|
|
80
|
+
#70c6dd 28%,
|
|
81
|
+
#64d8c7 38%,
|
|
82
|
+
#5fd7c2 44%,
|
|
83
|
+
#54d5b3 51%,
|
|
84
|
+
#40d39c 58%,
|
|
85
|
+
#26d07c 66%
|
|
86
|
+
)
|
|
87
|
+
border-box;
|
|
48
88
|
}
|
|
49
89
|
}
|
|
50
90
|
}
|
|
51
|
-
|
|
@@ -12,7 +12,7 @@ import { PLACEHOLDER_CHAR } from './constants';
|
|
|
12
12
|
import { useCountries } from './hooks';
|
|
13
13
|
import styles from './styles.module.scss';
|
|
14
14
|
import { CountrySettings, FieldPhoneOptionsProps, MaskOptions } from './types';
|
|
15
|
-
import { detectCountryByPhone } from './utils';
|
|
15
|
+
import { detectCountryByPhone, handleAutoInsert } from './utils';
|
|
16
16
|
|
|
17
17
|
export type FieldPhoneProps = WithLayoutType<
|
|
18
18
|
Omit<
|
|
@@ -63,6 +63,9 @@ export const FieldPhone = forwardRef<HTMLInputElement, FieldPhoneProps>(
|
|
|
63
63
|
const options = useCountries(optionsProp);
|
|
64
64
|
const isOnlyOneCountryAvailable = options.length === 1;
|
|
65
65
|
|
|
66
|
+
const rawInsertRef = useRef('');
|
|
67
|
+
const insertSwitchRef = useRef(false);
|
|
68
|
+
|
|
66
69
|
const [country, setCountry] = useValueControl<FieldPhoneOptionsProps>({
|
|
67
70
|
defaultValue: options[0],
|
|
68
71
|
onChange: onChangeCountry,
|
|
@@ -90,10 +93,23 @@ export const FieldPhone = forwardRef<HTMLInputElement, FieldPhoneProps>(
|
|
|
90
93
|
definitions: {
|
|
91
94
|
X: /[0-9]/,
|
|
92
95
|
},
|
|
96
|
+
prepare: (str: string) => {
|
|
97
|
+
if (str.replace(/\D/g, '').length > 1) {
|
|
98
|
+
rawInsertRef.current = str;
|
|
99
|
+
}
|
|
100
|
+
return str;
|
|
101
|
+
},
|
|
93
102
|
}),
|
|
94
103
|
[country?.mask],
|
|
95
104
|
);
|
|
96
105
|
|
|
106
|
+
const clearRaw = () => {
|
|
107
|
+
rawInsertRef.current = '';
|
|
108
|
+
};
|
|
109
|
+
const markSwitchRef = () => {
|
|
110
|
+
insertSwitchRef.current = true;
|
|
111
|
+
};
|
|
112
|
+
|
|
97
113
|
const {
|
|
98
114
|
ref: iMaskRef,
|
|
99
115
|
value: iMaskValue,
|
|
@@ -110,6 +126,24 @@ export const FieldPhone = forwardRef<HTMLInputElement, FieldPhoneProps>(
|
|
|
110
126
|
if (value !== valueProp) {
|
|
111
127
|
onChangeProp?.(value);
|
|
112
128
|
}
|
|
129
|
+
|
|
130
|
+
if (insertSwitchRef.current) {
|
|
131
|
+
insertSwitchRef.current = false;
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
handleAutoInsert({
|
|
135
|
+
raw: rawInsertRef.current,
|
|
136
|
+
onValueChange: value => {
|
|
137
|
+
setTimeout(() => setValue(value), 0);
|
|
138
|
+
},
|
|
139
|
+
onCountryChange: country => {
|
|
140
|
+
markSwitchRef();
|
|
141
|
+
clearRaw();
|
|
142
|
+
setCountry(country);
|
|
143
|
+
},
|
|
144
|
+
country,
|
|
145
|
+
options,
|
|
146
|
+
});
|
|
113
147
|
},
|
|
114
148
|
});
|
|
115
149
|
|
|
@@ -24,3 +24,178 @@ 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
|
+
|
|
28
|
+
export const handleAutoInsertCases = [
|
|
29
|
+
{
|
|
30
|
+
name: 'Kyrgyzstan switches + exact national',
|
|
31
|
+
raw: '+996 201 666666',
|
|
32
|
+
currentId: 'russia',
|
|
33
|
+
expectedCountryId: 'kyrgyzstan',
|
|
34
|
+
expectedValue: '201666666',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: 'Kyrgyzstan same => exact national',
|
|
38
|
+
raw: '+996201666666',
|
|
39
|
+
currentId: 'kyrgyzstan',
|
|
40
|
+
expectedValue: '201666666',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: 'Russia incomplete => no calls',
|
|
44
|
+
raw: '+7 987',
|
|
45
|
+
currentId: 'russia',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: 'Albania (+355) XXX XXX-XXX',
|
|
49
|
+
raw: '+355 675 123 456',
|
|
50
|
+
currentId: 'russia',
|
|
51
|
+
expectedCountryId: 'albania',
|
|
52
|
+
expectedLengthFromCountryId: 'albania',
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: 'Algeria (+213) XX XXX-XXXX',
|
|
56
|
+
raw: '+213 12 345 6789',
|
|
57
|
+
currentId: 'russia',
|
|
58
|
+
expectedCountryId: 'algeria',
|
|
59
|
+
expectedLengthFromCountryId: 'algeria',
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: 'Armenia (+374) XX XXX-XXX',
|
|
63
|
+
raw: '+374 10 123 456',
|
|
64
|
+
currentId: 'russia',
|
|
65
|
+
expectedCountryId: 'armenia',
|
|
66
|
+
expectedLengthFromCountryId: 'armenia',
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: 'Austria (+43) XXX XXX-XXXX',
|
|
70
|
+
raw: '+43 664 123 4567',
|
|
71
|
+
currentId: 'russia',
|
|
72
|
+
expectedCountryId: 'austria',
|
|
73
|
+
expectedLengthFromCountryId: 'austria',
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: 'Australia (+61) X XXXX-XXXX',
|
|
77
|
+
raw: '+61 4 1234 5678',
|
|
78
|
+
currentId: 'russia',
|
|
79
|
+
expectedCountryId: 'australia',
|
|
80
|
+
expectedLengthFromCountryId: 'australia',
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: 'Belarus (+375) XX XXX-XX-XX',
|
|
84
|
+
raw: '+375 29 123 45 67',
|
|
85
|
+
currentId: 'russia',
|
|
86
|
+
expectedCountryId: 'belarus',
|
|
87
|
+
expectedLengthFromCountryId: 'belarus',
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: 'Bangladesh (+880) X XXX-XXXX',
|
|
91
|
+
raw: '+880 1 234 5678',
|
|
92
|
+
currentId: 'russia',
|
|
93
|
+
expectedCountryId: 'bangladesh',
|
|
94
|
+
expectedLengthFromCountryId: 'bangladesh',
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: 'Bahrain (+973) XX XXX-XXX',
|
|
98
|
+
raw: '+973 33 123 456',
|
|
99
|
+
currentId: 'russia',
|
|
100
|
+
expectedCountryId: 'bahrain',
|
|
101
|
+
expectedLengthFromCountryId: 'bahrain',
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
name: 'Cyprus (+357) XX XXXXXX',
|
|
105
|
+
raw: '+357 99 123456',
|
|
106
|
+
currentId: 'russia',
|
|
107
|
+
expectedCountryId: 'cyprus',
|
|
108
|
+
expectedLengthFromCountryId: 'cyprus',
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: 'Georgia (+995) XXX XX-XX-XX',
|
|
112
|
+
raw: '+995 555 12 34 56',
|
|
113
|
+
currentId: 'russia',
|
|
114
|
+
expectedCountryId: 'georgia',
|
|
115
|
+
expectedLengthFromCountryId: 'georgia',
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: 'India (+91) XXXXX-XXXXX',
|
|
119
|
+
raw: '+91 98765 43210',
|
|
120
|
+
currentId: 'russia',
|
|
121
|
+
expectedCountryId: 'india',
|
|
122
|
+
expectedLengthFromCountryId: 'india',
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
name: 'Iran (+98) XXX XXX-XXXX',
|
|
126
|
+
raw: '+98 912 345 6789',
|
|
127
|
+
currentId: 'russia',
|
|
128
|
+
expectedCountryId: 'iran',
|
|
129
|
+
expectedLengthFromCountryId: 'iran',
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
name: 'Netherlands (+31) XX XXX-XXXX',
|
|
133
|
+
raw: '+31 20 123 4567',
|
|
134
|
+
currentId: 'russia',
|
|
135
|
+
expectedCountryId: 'netherlands',
|
|
136
|
+
expectedLengthFromCountryId: 'netherlands',
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
name: 'Romania (+40) XX XXX-XXXX',
|
|
140
|
+
raw: '+40 21 123 4567',
|
|
141
|
+
currentId: 'russia',
|
|
142
|
+
expectedCountryId: 'romania',
|
|
143
|
+
expectedLengthFromCountryId: 'romania',
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
name: 'Serbia (+381) XX XXX-XXXX',
|
|
147
|
+
raw: '+381 11 123 4567',
|
|
148
|
+
currentId: 'russia',
|
|
149
|
+
expectedCountryId: 'serbia',
|
|
150
|
+
expectedLengthFromCountryId: 'serbia',
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
name: 'Uzbekistan (+998) XX XXX-XX-XX',
|
|
154
|
+
raw: '+998 90 123 45 67',
|
|
155
|
+
currentId: 'russia',
|
|
156
|
+
expectedCountryId: 'uzbekistan',
|
|
157
|
+
expectedLengthFromCountryId: 'uzbekistan',
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
name: 'Tajikistan (+992) XX XXX-XXXX',
|
|
161
|
+
raw: '+992 93 123 4567',
|
|
162
|
+
currentId: 'russia',
|
|
163
|
+
expectedCountryId: 'tajikistan',
|
|
164
|
+
expectedLengthFromCountryId: 'tajikistan',
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
name: 'Moldova (+373) XXXX XXXX',
|
|
168
|
+
raw: '+373 1234 5678',
|
|
169
|
+
currentId: 'russia',
|
|
170
|
+
expectedCountryId: 'moldova',
|
|
171
|
+
expectedLengthFromCountryId: 'moldova',
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
name: 'Netherlands (+31) switches from Moldova',
|
|
175
|
+
raw: '+31 20 123 4567',
|
|
176
|
+
currentId: 'moldova',
|
|
177
|
+
expectedCountryId: 'netherlands',
|
|
178
|
+
expectedLengthFromCountryId: 'netherlands',
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
name: 'Uzbekistan (+998) switches from Tajikistan',
|
|
182
|
+
raw: '+998 90 123 45 67',
|
|
183
|
+
currentId: 'tajikistan',
|
|
184
|
+
expectedCountryId: 'uzbekistan',
|
|
185
|
+
expectedLengthFromCountryId: 'uzbekistan',
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
name: 'Tajikistan (+992) switches from Albania',
|
|
189
|
+
raw: '+992 93 123 4567',
|
|
190
|
+
currentId: 'albania',
|
|
191
|
+
expectedCountryId: 'tajikistan',
|
|
192
|
+
expectedLengthFromCountryId: 'tajikistan',
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
name: 'Moldova (+373) switches from Egypt',
|
|
196
|
+
raw: '+373 1234 5678',
|
|
197
|
+
currentId: 'egypt',
|
|
198
|
+
expectedCountryId: 'moldova',
|
|
199
|
+
expectedLengthFromCountryId: 'moldova',
|
|
200
|
+
},
|
|
201
|
+
];
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { ALL_COUNTRY_CODES } from '../countries';
|
|
4
|
+
import { handleAutoInsert } from '../utils';
|
|
5
|
+
import { handleAutoInsertCases } from './constants';
|
|
6
|
+
|
|
7
|
+
const OPTIONS = ALL_COUNTRY_CODES.map(c => ({
|
|
8
|
+
id: c.value,
|
|
9
|
+
iso2: c.iso2,
|
|
10
|
+
mask: c.mask,
|
|
11
|
+
content: {
|
|
12
|
+
caption: c.caption,
|
|
13
|
+
option: c.value,
|
|
14
|
+
},
|
|
15
|
+
beforeContent: null,
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
const byId = (id: string) => {
|
|
19
|
+
const found = OPTIONS.find(c => c.id === id);
|
|
20
|
+
if (!found) throw new Error(`Country not found in OPTIONS: ${id}`);
|
|
21
|
+
return found;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type BaseCase = {
|
|
25
|
+
name: string;
|
|
26
|
+
raw: string;
|
|
27
|
+
currentId: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
type NoCallsCase = BaseCase;
|
|
31
|
+
|
|
32
|
+
type ExactCase = BaseCase & {
|
|
33
|
+
expectedValue: string;
|
|
34
|
+
expectedCountryId?: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const casesNoCalls: NoCallsCase[] = handleAutoInsertCases.filter(
|
|
38
|
+
(c): c is NoCallsCase => !('expectedValue' in c) && !('expectedLengthFromCountryId' in c),
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const casesExact: ExactCase[] = handleAutoInsertCases.filter((c): c is ExactCase => 'expectedValue' in c);
|
|
42
|
+
|
|
43
|
+
describe('handleAutoInsert — no calls', () => {
|
|
44
|
+
casesNoCalls.forEach(tc => {
|
|
45
|
+
it(`${tc.name}`, () => {
|
|
46
|
+
const onValueChange = vi.fn();
|
|
47
|
+
const onCountryChange = vi.fn();
|
|
48
|
+
|
|
49
|
+
handleAutoInsert({
|
|
50
|
+
raw: tc.raw,
|
|
51
|
+
onValueChange,
|
|
52
|
+
onCountryChange,
|
|
53
|
+
country: byId(tc.currentId),
|
|
54
|
+
options: OPTIONS,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
expect(onCountryChange).not.toHaveBeenCalled();
|
|
58
|
+
expect(onValueChange).not.toHaveBeenCalled();
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('handleAutoInsert — exact value', () => {
|
|
64
|
+
casesExact.forEach(tc => {
|
|
65
|
+
it(`${tc.name}`, () => {
|
|
66
|
+
const onValueChange = vi.fn();
|
|
67
|
+
const onCountryChange = vi.fn();
|
|
68
|
+
|
|
69
|
+
handleAutoInsert({
|
|
70
|
+
raw: tc.raw,
|
|
71
|
+
onValueChange,
|
|
72
|
+
onCountryChange,
|
|
73
|
+
country: byId(tc.currentId),
|
|
74
|
+
options: OPTIONS,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
expect(onValueChange).toHaveBeenCalledTimes(1);
|
|
78
|
+
expect(onValueChange).toHaveBeenCalledWith(tc.expectedValue);
|
|
79
|
+
|
|
80
|
+
const expectedCountryCalls = tc.expectedCountryId ? 1 : 0;
|
|
81
|
+
expect(onCountryChange).toHaveBeenCalledTimes(expectedCountryCalls);
|
|
82
|
+
|
|
83
|
+
const actualCountryId = onCountryChange.mock.calls[0]?.[0]?.id;
|
|
84
|
+
expect(actualCountryId).toBe(tc.expectedCountryId);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -69,3 +69,44 @@ export function detectCountryByPhone(text: string, options: FieldPhoneOptionsPro
|
|
|
69
69
|
|
|
70
70
|
return options.find(opt => opt.iso2 === regionCode);
|
|
71
71
|
}
|
|
72
|
+
|
|
73
|
+
export const handleAutoInsert = ({
|
|
74
|
+
raw,
|
|
75
|
+
onValueChange,
|
|
76
|
+
onCountryChange,
|
|
77
|
+
country,
|
|
78
|
+
options,
|
|
79
|
+
}: {
|
|
80
|
+
raw: string;
|
|
81
|
+
onValueChange: (str: string) => void;
|
|
82
|
+
onCountryChange: (country: FieldPhoneOptionsProps) => void;
|
|
83
|
+
country?: FieldPhoneOptionsProps;
|
|
84
|
+
options: FieldPhoneOptionsProps[];
|
|
85
|
+
}) => {
|
|
86
|
+
if (!raw) return;
|
|
87
|
+
|
|
88
|
+
const parsed = parsePhoneNumber(raw);
|
|
89
|
+
const ok = parsed.valid || parsed.possible;
|
|
90
|
+
if (!ok) return;
|
|
91
|
+
|
|
92
|
+
const detected = detectCountryByPhone(raw, options);
|
|
93
|
+
if (!detected) return;
|
|
94
|
+
|
|
95
|
+
let national = parsed.number?.significant.replace(/\D/g, '');
|
|
96
|
+
if (!national) return;
|
|
97
|
+
|
|
98
|
+
const nextNationalLength = (detected.mask?.match(/X/g) ?? []).length;
|
|
99
|
+
if (nextNationalLength && national.length > nextNationalLength) {
|
|
100
|
+
national = national.slice(-nextNationalLength);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const fullDigits = (parsed.number?.e164 ?? raw).replace(/\D/g, '');
|
|
104
|
+
const countryCodeDigits = String(parsed.countryCode ?? '');
|
|
105
|
+
const expected = countryCodeDigits.length + nextNationalLength;
|
|
106
|
+
if (fullDigits.length < expected) return;
|
|
107
|
+
|
|
108
|
+
if (detected.id !== country?.id) {
|
|
109
|
+
onCountryChange(detected);
|
|
110
|
+
}
|
|
111
|
+
onValueChange(national);
|
|
112
|
+
};
|