@cloud-ru/uikit-product-fields-predefined 0.13.11 → 0.14.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.
Files changed (84) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +28 -103
  3. package/dist/cjs/components/FieldAi/FieldAi.d.ts +1 -1
  4. package/dist/cjs/components/FieldAi/components/MobileFieldAi/MobileFieldAi.d.ts +1 -1
  5. package/dist/cjs/components/FieldDescription/FieldDescription.d.ts +12 -0
  6. package/dist/cjs/components/FieldDescription/FieldDescription.js +68 -0
  7. package/dist/cjs/components/FieldDescription/FieldDescriptionRHF.d.ts +11 -0
  8. package/dist/cjs/components/FieldDescription/FieldDescriptionRHF.js +62 -0
  9. package/dist/cjs/components/FieldDescription/components/FieldWithAddButton.d.ts +7 -0
  10. package/dist/cjs/components/FieldDescription/components/FieldWithAddButton.js +21 -0
  11. package/dist/cjs/components/FieldDescription/components/index.d.ts +1 -0
  12. package/dist/cjs/components/FieldDescription/components/index.js +5 -0
  13. package/dist/cjs/components/FieldDescription/constants.d.ts +1 -0
  14. package/dist/cjs/components/FieldDescription/constants.js +4 -0
  15. package/dist/cjs/components/FieldDescription/index.d.ts +3 -0
  16. package/dist/cjs/components/FieldDescription/index.js +7 -0
  17. package/dist/cjs/components/FieldDescription/types.d.ts +16 -0
  18. package/dist/cjs/components/FieldDescription/types.js +2 -0
  19. package/dist/cjs/components/FieldName/FieldName.d.ts +12 -0
  20. package/dist/cjs/components/FieldName/FieldName.js +95 -0
  21. package/dist/cjs/components/FieldName/FieldNameRHF.d.ts +11 -0
  22. package/dist/cjs/components/FieldName/FieldNameRHF.js +81 -0
  23. package/dist/cjs/components/FieldName/constants.d.ts +1 -0
  24. package/dist/cjs/components/FieldName/constants.js +4 -0
  25. package/dist/cjs/components/FieldName/index.d.ts +3 -0
  26. package/dist/cjs/components/FieldName/index.js +7 -0
  27. package/dist/cjs/components/FieldName/types.d.ts +15 -0
  28. package/dist/cjs/components/FieldName/types.js +2 -0
  29. package/dist/cjs/components/index.d.ts +2 -0
  30. package/dist/cjs/components/index.js +2 -0
  31. package/dist/cjs/hooks/index.d.ts +1 -0
  32. package/dist/cjs/hooks/index.js +1 -0
  33. package/dist/cjs/hooks/useCustomFieldValidation.d.ts +12 -0
  34. package/dist/cjs/hooks/useCustomFieldValidation.js +32 -0
  35. package/dist/esm/components/FieldAi/FieldAi.d.ts +1 -1
  36. package/dist/esm/components/FieldAi/components/MobileFieldAi/MobileFieldAi.d.ts +1 -1
  37. package/dist/esm/components/FieldDescription/FieldDescription.d.ts +12 -0
  38. package/dist/esm/components/FieldDescription/FieldDescription.js +62 -0
  39. package/dist/esm/components/FieldDescription/FieldDescriptionRHF.d.ts +11 -0
  40. package/dist/esm/components/FieldDescription/FieldDescriptionRHF.js +56 -0
  41. package/dist/esm/components/FieldDescription/components/FieldWithAddButton.d.ts +7 -0
  42. package/dist/esm/components/FieldDescription/components/FieldWithAddButton.js +18 -0
  43. package/dist/esm/components/FieldDescription/components/index.d.ts +1 -0
  44. package/dist/esm/components/FieldDescription/components/index.js +1 -0
  45. package/dist/esm/components/FieldDescription/constants.d.ts +1 -0
  46. package/dist/esm/components/FieldDescription/constants.js +1 -0
  47. package/dist/esm/components/FieldDescription/index.d.ts +3 -0
  48. package/dist/esm/components/FieldDescription/index.js +2 -0
  49. package/dist/esm/components/FieldDescription/types.d.ts +16 -0
  50. package/dist/esm/components/FieldDescription/types.js +1 -0
  51. package/dist/esm/components/FieldName/FieldName.d.ts +12 -0
  52. package/dist/esm/components/FieldName/FieldName.js +89 -0
  53. package/dist/esm/components/FieldName/FieldNameRHF.d.ts +11 -0
  54. package/dist/esm/components/FieldName/FieldNameRHF.js +75 -0
  55. package/dist/esm/components/FieldName/constants.d.ts +1 -0
  56. package/dist/esm/components/FieldName/constants.js +1 -0
  57. package/dist/esm/components/FieldName/index.d.ts +3 -0
  58. package/dist/esm/components/FieldName/index.js +2 -0
  59. package/dist/esm/components/FieldName/types.d.ts +15 -0
  60. package/dist/esm/components/FieldName/types.js +1 -0
  61. package/dist/esm/components/index.d.ts +2 -0
  62. package/dist/esm/components/index.js +2 -0
  63. package/dist/esm/hooks/index.d.ts +1 -0
  64. package/dist/esm/hooks/index.js +1 -0
  65. package/dist/esm/hooks/useCustomFieldValidation.d.ts +12 -0
  66. package/dist/esm/hooks/useCustomFieldValidation.js +28 -0
  67. package/dist/tsconfig.cjs.tsbuildinfo +1 -1
  68. package/dist/tsconfig.esm.tsbuildinfo +1 -1
  69. package/package.json +8 -6
  70. package/src/components/FieldDescription/FieldDescription.tsx +97 -0
  71. package/src/components/FieldDescription/FieldDescriptionRHF.tsx +93 -0
  72. package/src/components/FieldDescription/components/FieldWithAddButton.tsx +43 -0
  73. package/src/components/FieldDescription/components/index.ts +1 -0
  74. package/src/components/FieldDescription/constants.ts +1 -0
  75. package/src/components/FieldDescription/index.ts +3 -0
  76. package/src/components/FieldDescription/types.ts +23 -0
  77. package/src/components/FieldName/FieldName.tsx +125 -0
  78. package/src/components/FieldName/FieldNameRHF.tsx +112 -0
  79. package/src/components/FieldName/constants.ts +1 -0
  80. package/src/components/FieldName/index.ts +3 -0
  81. package/src/components/FieldName/types.ts +22 -0
  82. package/src/components/index.ts +2 -0
  83. package/src/hooks/index.ts +1 -0
  84. package/src/hooks/useCustomFieldValidation.ts +38 -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.13.11",
4
+ "version": "0.14.0",
5
5
  "sideEffects": [
6
6
  "*.css",
7
7
  "*.woff",
@@ -34,15 +34,15 @@
34
34
  "dependencies": {
35
35
  "@cloud-ru/uikit-product-icons": "16.0.0",
36
36
  "@cloud-ru/uikit-product-mobile-dropdown": "0.9.28",
37
- "@cloud-ru/uikit-product-mobile-fields": "0.11.31",
37
+ "@cloud-ru/uikit-product-mobile-fields": "0.12.0",
38
38
  "@cloud-ru/uikit-product-mobile-modal": "0.9.25",
39
39
  "@cloud-ru/uikit-product-mobile-tooltip": "0.5.1",
40
40
  "@cloud-ru/uikit-product-utils": "8.0.1",
41
41
  "@snack-uikit/attachment": "0.4.10",
42
- "@snack-uikit/button": "0.19.11",
42
+ "@snack-uikit/button": "0.19.16",
43
43
  "@snack-uikit/drop-zone": "0.9.6",
44
44
  "@snack-uikit/icon-predefined": "0.7.3",
45
- "@snack-uikit/input-private": "4.8.1",
45
+ "@snack-uikit/input-private": "4.8.5",
46
46
  "@snack-uikit/scroll": "0.10.1",
47
47
  "@snack-uikit/tooltip": "0.17.4",
48
48
  "@snack-uikit/typography": "0.8.7",
@@ -57,7 +57,9 @@
57
57
  "@types/merge-refs": "1.0.0"
58
58
  },
59
59
  "peerDependencies": {
60
- "@cloud-ru/uikit-product-locale": "*"
60
+ "@cloud-ru/uikit-product-locale": "*",
61
+ "react-hook-form": ">=7.51.0",
62
+ "yup": ">=0.32.0"
61
63
  },
62
- "gitHead": "f67d8d3987dc49157789aebe9e083c42d0abf5c3"
64
+ "gitHead": "6366d4a4912c09f95af6ba794df7b5a246893762"
63
65
  }
@@ -0,0 +1,97 @@
1
+ import mergeRefs from 'merge-refs';
2
+ import { FocusEvent, forwardRef, useMemo, useRef, useState } from 'react';
3
+ import { string, ValidationError } from 'yup';
4
+
5
+ import { useLocale } from '@cloud-ru/uikit-product-locale';
6
+ import { AdaptiveFieldTextArea } from '@cloud-ru/uikit-product-mobile-fields';
7
+ import { WithLayoutType } from '@cloud-ru/uikit-product-utils';
8
+
9
+ import { useCustomFieldValidation } from '../../hooks';
10
+ import { FieldWithAddButton } from './components';
11
+ import { DEFAULT_MAX_LENGTH } from './constants';
12
+ import { FieldDescriptionProps } from './types';
13
+
14
+ /**
15
+ * Поле описание c локальным стейтом и валидацией
16
+ */
17
+ export const FieldDescription = forwardRef<HTMLTextAreaElement, WithLayoutType<FieldDescriptionProps>>(
18
+ (
19
+ {
20
+ size = 'm',
21
+ required = false,
22
+ maxLength = DEFAULT_MAX_LENGTH,
23
+ customSchema,
24
+ resizable = true,
25
+ addButton,
26
+ onValidationError,
27
+ ...props
28
+ },
29
+ ref,
30
+ ) => {
31
+ const { t } = useLocale('FieldsPredefined');
32
+
33
+ const textareaRef = useRef<HTMLTextAreaElement | null>(null);
34
+
35
+ const validationSchema = useMemo(() => {
36
+ let baseSchema = string()
37
+ .trim()
38
+ .max(maxLength, t('FieldDescription.maxSymbols', { max: maxLength }));
39
+
40
+ if (customSchema) {
41
+ baseSchema = baseSchema.concat(customSchema);
42
+ }
43
+
44
+ return required ? baseSchema.required(t('FieldDescription.required')) : baseSchema;
45
+ }, [customSchema, maxLength, required, t]);
46
+
47
+ const { validate } = useCustomFieldValidation({ schema: validationSchema });
48
+
49
+ const [value, setValue] = useState('');
50
+ const [error, setError] = useState<ValidationError | null>(null);
51
+
52
+ const handleChange = (newValue: string) => {
53
+ setValue(newValue);
54
+ props.onChange?.(newValue);
55
+ const result = validate(newValue);
56
+ setError(result.error);
57
+ onValidationError?.(result.error);
58
+ };
59
+
60
+ const handleBlur = (newValue: FocusEvent<HTMLTextAreaElement, Element>) => {
61
+ props.onBlur?.(newValue);
62
+ const result = validate(value);
63
+ setError(result.error);
64
+ onValidationError?.(result.error);
65
+ };
66
+
67
+ const errorMes = props.error ?? error?.message;
68
+
69
+ const standaloneComponent = (
70
+ <AdaptiveFieldTextArea
71
+ {...props}
72
+ resizable={resizable}
73
+ label={t('FieldDescription.label')}
74
+ inputMode='text'
75
+ ref={mergeRefs(ref, textareaRef)}
76
+ size={size}
77
+ maxLength={maxLength}
78
+ value={value}
79
+ onChange={handleChange}
80
+ onBlur={handleBlur}
81
+ validationState={errorMes ? 'error' : props.validationState}
82
+ hint={errorMes}
83
+ caption={!required ? t('FieldDescription.optional') : undefined}
84
+ />
85
+ );
86
+
87
+ if (addButton && !required) {
88
+ return (
89
+ <FieldWithAddButton autoFocusRef={textareaRef} size={size}>
90
+ {standaloneComponent}
91
+ </FieldWithAddButton>
92
+ );
93
+ }
94
+
95
+ return standaloneComponent;
96
+ },
97
+ );
@@ -0,0 +1,93 @@
1
+ import mergeRefs from 'merge-refs';
2
+ import { forwardRef, useMemo, useRef } from 'react';
3
+ import { Controller } from 'react-hook-form';
4
+ import { string } from 'yup';
5
+
6
+ import { useLocale } from '@cloud-ru/uikit-product-locale';
7
+ import { AdaptiveFieldTextArea } from '@cloud-ru/uikit-product-mobile-fields';
8
+ import { WithLayoutType } from '@cloud-ru/uikit-product-utils';
9
+
10
+ import { useCustomFieldValidation } from '../../hooks';
11
+ import { FieldWithAddButton } from './components/FieldWithAddButton';
12
+ import { DEFAULT_MAX_LENGTH } from './constants';
13
+ import { FieldDescriptionRHFProps } from './types';
14
+
15
+ /**
16
+ * Поле описание c оберткой для React Hook Form
17
+ */
18
+ export const FieldDescriptionRHF = forwardRef<HTMLTextAreaElement, WithLayoutType<FieldDescriptionRHFProps>>(
19
+ (
20
+ {
21
+ controllerProps,
22
+ customSchema,
23
+ size = 'm',
24
+ required = false,
25
+ maxLength = DEFAULT_MAX_LENGTH,
26
+ addButton,
27
+ resizable = true,
28
+ ...props
29
+ },
30
+ ref,
31
+ ) => {
32
+ const { t } = useLocale('FieldsPredefined');
33
+ const textareaRef = useRef<HTMLTextAreaElement | null>(null);
34
+
35
+ const validationSchema = useMemo(() => {
36
+ let baseSchema = string()
37
+ .trim()
38
+ .max(maxLength, t('FieldDescription.maxSymbols', { max: maxLength }));
39
+
40
+ if (customSchema) {
41
+ baseSchema = baseSchema.concat(customSchema);
42
+ }
43
+
44
+ return required ? baseSchema.required(t('FieldDescription.required')) : baseSchema;
45
+ }, [customSchema, maxLength, required, t]);
46
+
47
+ const { validateRHF } = useCustomFieldValidation({ schema: validationSchema });
48
+
49
+ const controllerComponent = (
50
+ <Controller
51
+ {...controllerProps}
52
+ rules={{ validate: validateRHF }}
53
+ render={({ field: { value, ref: localRef, onBlur, onChange }, fieldState: { error } }) => {
54
+ const errorMes = props.error ?? error?.message;
55
+
56
+ return (
57
+ <AdaptiveFieldTextArea
58
+ {...props}
59
+ resizable={resizable}
60
+ size={size}
61
+ label={t('FieldDescription.label')}
62
+ inputMode='text'
63
+ ref={mergeRefs(ref, localRef, textareaRef)}
64
+ maxLength={maxLength}
65
+ value={value}
66
+ onChange={newValue => {
67
+ props.onChange?.(newValue);
68
+ onChange(newValue);
69
+ }}
70
+ onBlur={value => {
71
+ props.onBlur?.(value);
72
+ onBlur();
73
+ }}
74
+ validationState={errorMes ? 'error' : props.validationState}
75
+ hint={errorMes}
76
+ caption={!required ? t('FieldDescription.optional') : undefined}
77
+ />
78
+ );
79
+ }}
80
+ />
81
+ );
82
+
83
+ if (addButton && !required) {
84
+ return (
85
+ <FieldWithAddButton autoFocusRef={textareaRef} size={size}>
86
+ {controllerComponent}
87
+ </FieldWithAddButton>
88
+ );
89
+ }
90
+
91
+ return controllerComponent;
92
+ },
93
+ );
@@ -0,0 +1,43 @@
1
+ import { RefObject, useEffect, useState } from 'react';
2
+
3
+ import { PlusSVG } from '@cloud-ru/uikit-product-icons';
4
+ import { useLocale } from '@cloud-ru/uikit-product-locale';
5
+ import { FieldTextAreaProps } from '@cloud-ru/uikit-product-mobile-fields';
6
+ import { ButtonFunction } from '@snack-uikit/button';
7
+
8
+ export function FieldWithAddButton({
9
+ children,
10
+ size,
11
+ autoFocusRef,
12
+ }: {
13
+ children: React.ReactNode;
14
+ size?: FieldTextAreaProps['size'];
15
+ autoFocusRef: RefObject<HTMLTextAreaElement | null>;
16
+ }) {
17
+ const { t } = useLocale('FieldsPredefined');
18
+ const [showField, setShowField] = useState(false);
19
+
20
+ useEffect(() => {
21
+ if (showField && autoFocusRef.current) {
22
+ autoFocusRef.current.focus();
23
+ }
24
+ }, [showField, autoFocusRef]);
25
+
26
+ if (showField) {
27
+ return children;
28
+ }
29
+
30
+ return (
31
+ <div>
32
+ {!showField && (
33
+ <ButtonFunction
34
+ icon={<PlusSVG />}
35
+ iconPosition='before'
36
+ label={t('FieldDescription.addButton')}
37
+ onClick={() => setShowField(true)}
38
+ size={size}
39
+ />
40
+ )}
41
+ </div>
42
+ );
43
+ }
@@ -0,0 +1 @@
1
+ export { FieldWithAddButton } from './FieldWithAddButton';
@@ -0,0 +1 @@
1
+ export const DEFAULT_MAX_LENGTH = 255;
@@ -0,0 +1,3 @@
1
+ export { FieldDescription } from './FieldDescription';
2
+ export { FieldDescriptionRHF } from './FieldDescriptionRHF';
3
+ export type { FieldDescriptionProps, FieldDescriptionPropsBase, FieldDescriptionRHFProps } from './types';
@@ -0,0 +1,23 @@
1
+ import { ControllerProps, FieldValues } from 'react-hook-form';
2
+ import { StringSchema, ValidationError } from 'yup';
3
+
4
+ import { FieldTextAreaProps } from '@cloud-ru/uikit-product-mobile-fields';
5
+
6
+ export type FieldDescriptionPropsBase = Omit<
7
+ FieldTextAreaProps,
8
+ 'placeholder' | 'label' | 'footer' | 'searchPlaceholder' | 'hint' | 'inputMode' | 'caption' | 'name'
9
+ > & {
10
+ customSchema?: StringSchema;
11
+ /** Поле появляется по кнопке "Добавить описание" (только для опционального поля) */
12
+ addButton?: boolean;
13
+ };
14
+
15
+ export type FieldDescriptionProps = FieldDescriptionPropsBase & {
16
+ /** Колбэк, вызываемый при изменении ошибки валидации (только в standalone режиме) */
17
+ onValidationError?: (error: ValidationError | null) => void;
18
+ };
19
+
20
+ export type FieldDescriptionRHFProps = FieldDescriptionPropsBase & {
21
+ /** Режим контроллера с использованием react-hook-form */
22
+ controllerProps: Omit<ControllerProps<FieldValues>, 'render' | 'rules' | 'disabled'>;
23
+ };
@@ -0,0 +1,125 @@
1
+ import mergeRefs from 'merge-refs';
2
+ import { FocusEvent, forwardRef, useMemo, useRef, useState } from 'react';
3
+ import { string, ValidationError } from 'yup';
4
+
5
+ import { useLocale } from '@cloud-ru/uikit-product-locale';
6
+ import { AdaptiveFieldText } from '@cloud-ru/uikit-product-mobile-fields';
7
+ import { WithLayoutType } from '@cloud-ru/uikit-product-utils';
8
+ import { runAfterRerender } from '@snack-uikit/input-private';
9
+
10
+ import { useCustomFieldValidation } from '../../hooks';
11
+ import { DEFAULT_MAX_NAME_LENGTH } from './constants';
12
+ import { FieldNameProps } from './types';
13
+
14
+ /**
15
+ * Поле имя c локальным стейтом и валидацией
16
+ */
17
+ export const FieldName = forwardRef<HTMLInputElement, WithLayoutType<FieldNameProps>>((props, ref) => {
18
+ const { t } = useLocale('FieldsPredefined');
19
+
20
+ const {
21
+ value: propValue = '',
22
+ onChange: propOnChange,
23
+ onBlur: propOnBlur,
24
+ error: propError,
25
+ maxLength = DEFAULT_MAX_NAME_LENGTH,
26
+ required = true,
27
+ customSchema,
28
+ showLabel = true,
29
+ allowMoreThanMaxLength = true,
30
+ size = 'm',
31
+ ...inputProps
32
+ } = props;
33
+
34
+ const [internalValue, setInternalValue] = useState(propValue);
35
+ const [validationError, setValidationError] = useState<ValidationError | null>(null);
36
+ const [isFocused, setIsFocused] = useState(false);
37
+
38
+ const inputRef = useRef<HTMLInputElement>(null);
39
+
40
+ const validationSchema = useMemo(() => {
41
+ let baseSchema = string()
42
+ .test('maxLength', t('FieldName.maxSymbols', { max: maxLength }), value => {
43
+ if (!value) return true;
44
+ return value.length <= maxLength;
45
+ })
46
+ .matches(/^[a-zA-Z0-9.\-_]*$/, {
47
+ message: t('FieldName.wrongSymbols'),
48
+ name: 'allowedSymbols',
49
+ excludeEmptyString: true,
50
+ });
51
+
52
+ if (customSchema) {
53
+ baseSchema = baseSchema.concat(customSchema);
54
+ }
55
+
56
+ return required ? baseSchema.required(t('FieldName.required')) : baseSchema;
57
+ }, [customSchema, maxLength, required, t]);
58
+
59
+ const { validate } = useCustomFieldValidation({ schema: validationSchema });
60
+
61
+ const currentValue = 'value' in props ? propValue : internalValue;
62
+ const currentError = propError || validationError?.message;
63
+ const isRequiredError = currentError?.match(t('FieldName.required'));
64
+
65
+ const handleChange = (newValue: string) => {
66
+ const result = validate(newValue);
67
+ // Если была required-ошибка и пользователь начал ввод, очищаем ее
68
+ if (isRequiredError && !isFocused) {
69
+ setValidationError(null);
70
+ } else if (result.error && result.error.message !== t('FieldName.required')) {
71
+ // Любую НЕ required ошибку показываем сразу
72
+ setValidationError(result.error);
73
+ } else {
74
+ setValidationError(null);
75
+ }
76
+
77
+ setInternalValue(newValue);
78
+ propOnChange?.(newValue);
79
+ };
80
+
81
+ const handleBlur = (e: FocusEvent<HTMLInputElement>) => {
82
+ runAfterRerender(() => setIsFocused(false));
83
+
84
+ const result = validate(currentValue);
85
+ setValidationError(result.error);
86
+ props.onValidationError?.(result.error);
87
+ propOnBlur?.(e);
88
+ };
89
+
90
+ const handleFocus = () => {
91
+ setIsFocused(true);
92
+ };
93
+
94
+ const handleClear = () => {
95
+ const result = validate('');
96
+ setValidationError(result.error);
97
+ props.onValidationError?.(result.error);
98
+ };
99
+
100
+ const shouldShowCounter =
101
+ currentError && (currentError?.match(t('FieldName.maxSymbols', { max: maxLength })) || isRequiredError);
102
+
103
+ // Ошибка обязательного поля появляется после blur
104
+ const showError = currentError && ((isRequiredError && !isFocused) || !isRequiredError);
105
+
106
+ return (
107
+ <AdaptiveFieldText
108
+ {...inputProps}
109
+ inputMode='text'
110
+ onClearButtonClick={handleClear}
111
+ allowMoreThanMaxLength={allowMoreThanMaxLength}
112
+ ref={mergeRefs(ref, inputRef)}
113
+ value={currentValue}
114
+ onChange={handleChange}
115
+ onFocus={handleFocus}
116
+ onBlur={handleBlur}
117
+ validationState={showError ? 'error' : inputProps.validationState}
118
+ hint={showError ? currentError : undefined}
119
+ maxLength={shouldShowCounter && showError ? maxLength : undefined}
120
+ size={size}
121
+ label={showLabel ? t('FieldName.label') : undefined}
122
+ caption={!required ? t('FieldDescription.optional') : undefined}
123
+ />
124
+ );
125
+ });
@@ -0,0 +1,112 @@
1
+ import mergeRefs from 'merge-refs';
2
+ import { FocusEventHandler, forwardRef, useMemo, useState } from 'react';
3
+ import { Controller, useFormContext } from 'react-hook-form';
4
+ import { string } from 'yup';
5
+
6
+ import { useLocale } from '@cloud-ru/uikit-product-locale';
7
+ import { AdaptiveFieldText } from '@cloud-ru/uikit-product-mobile-fields';
8
+ import { WithLayoutType } from '@cloud-ru/uikit-product-utils';
9
+ import { runAfterRerender } from '@snack-uikit/input-private';
10
+
11
+ import { useCustomFieldValidation } from '../../hooks';
12
+ import { DEFAULT_MAX_NAME_LENGTH } from './constants';
13
+ import { FieldNameRHFProps } from './types';
14
+
15
+ /**
16
+ * Поле имя c оберткой для React Hook Form
17
+ */
18
+ export const FieldNameRHF = forwardRef<HTMLInputElement, WithLayoutType<FieldNameRHFProps>>((props, ref) => {
19
+ const { t } = useLocale('FieldsPredefined');
20
+
21
+ const {
22
+ controllerProps,
23
+ maxLength = DEFAULT_MAX_NAME_LENGTH,
24
+ required = true,
25
+ customSchema,
26
+ showLabel = true,
27
+ size = 'm',
28
+ allowMoreThanMaxLength = true,
29
+ error: propError,
30
+ ...inputProps
31
+ } = props;
32
+
33
+ const [isFocused, setIsFocused] = useState(false);
34
+ const { trigger } = useFormContext();
35
+
36
+ const validationSchema = useMemo(() => {
37
+ let baseSchema = string()
38
+ .test('maxLength', t('FieldName.maxSymbols', { max: maxLength }), value => {
39
+ if (!value) return true;
40
+ return value.length <= maxLength;
41
+ })
42
+ .matches(/^[a-zA-Z0-9.\-_]*$/, {
43
+ message: t('FieldName.wrongSymbols'),
44
+ name: 'allowedSymbols',
45
+ excludeEmptyString: true,
46
+ });
47
+
48
+ if (customSchema) {
49
+ baseSchema = baseSchema.concat(customSchema);
50
+ }
51
+
52
+ return required ? baseSchema.required(t('FieldName.required')) : baseSchema;
53
+ }, [customSchema, maxLength, required, t]);
54
+
55
+ const { validateRHF } = useCustomFieldValidation({ schema: validationSchema });
56
+
57
+ const handleFocus: FocusEventHandler<HTMLInputElement> = value => {
58
+ setIsFocused(true);
59
+ inputProps.onFocus?.(value);
60
+ };
61
+
62
+ return (
63
+ <Controller
64
+ {...controllerProps}
65
+ rules={{ validate: validateRHF }}
66
+ render={({ field: { value, ref: localRef, onBlur, onChange }, fieldState: { error } }) => {
67
+ const isRequiredError = Boolean(error && error.message?.match(t('FieldName.required')));
68
+ const shouldShowCounter =
69
+ error && (error.message?.match(t('FieldName.maxSymbols', { max: maxLength })) || isRequiredError);
70
+
71
+ // - Есть ошибка обязательного поля и поле не в фокусе
72
+ // - Или другая ошибка
73
+ // - Или принудительно показываем ошибку
74
+ const showError = error && ((isRequiredError && !isFocused) || !isRequiredError);
75
+
76
+ const errorMes = propError ?? error?.message;
77
+
78
+ const handleChange = (newValue: string) => {
79
+ inputProps.onChange?.(newValue);
80
+ onChange(newValue);
81
+ };
82
+
83
+ const handleBlur: FocusEventHandler<HTMLInputElement> = value => {
84
+ runAfterRerender(() => setIsFocused(false));
85
+
86
+ inputProps.onBlur?.(value);
87
+ onBlur();
88
+ };
89
+
90
+ return (
91
+ <AdaptiveFieldText
92
+ {...inputProps}
93
+ inputMode='text'
94
+ onClearButtonClick={() => trigger(controllerProps.name)}
95
+ allowMoreThanMaxLength={allowMoreThanMaxLength}
96
+ ref={mergeRefs(ref, localRef)}
97
+ value={value}
98
+ onChange={handleChange}
99
+ onFocus={handleFocus}
100
+ onBlur={handleBlur}
101
+ validationState={showError ? 'error' : inputProps.validationState}
102
+ hint={showError ? errorMes : undefined}
103
+ maxLength={shouldShowCounter && showError ? maxLength : undefined}
104
+ size={size}
105
+ label={showLabel ? t('FieldName.label') : undefined}
106
+ caption={!required ? t('FieldDescription.optional') : undefined}
107
+ />
108
+ );
109
+ }}
110
+ />
111
+ );
112
+ });
@@ -0,0 +1 @@
1
+ export const DEFAULT_MAX_NAME_LENGTH = 64;
@@ -0,0 +1,3 @@
1
+ export { FieldName } from './FieldName';
2
+ export { FieldNameRHF } from './FieldNameRHF';
3
+ export type { BaseFieldNameProps, FieldNameRHFProps, FieldNameProps } from './types';
@@ -0,0 +1,22 @@
1
+ import { ControllerProps, FieldValues } from 'react-hook-form';
2
+ import { StringSchema, ValidationError } from 'yup';
3
+
4
+ import { FieldTextProps } from '@cloud-ru/uikit-product-mobile-fields';
5
+
6
+ export type BaseFieldNameProps = Omit<
7
+ FieldTextProps,
8
+ 'placeholder' | 'label' | 'footer' | 'type' | 'inputMode' | 'caption' | 'hint'
9
+ > & {
10
+ showLabel?: boolean;
11
+ customSchema?: StringSchema;
12
+ };
13
+
14
+ export type FieldNameProps = BaseFieldNameProps & {
15
+ /** Колбэк, вызываемый при изменении ошибки валидации */
16
+ onValidationError?: (error: ValidationError | null) => void;
17
+ };
18
+
19
+ export type FieldNameRHFProps = BaseFieldNameProps & {
20
+ /** Режим контроллера с использованием react-hook-form */
21
+ controllerProps: Omit<ControllerProps<FieldValues>, 'render' | 'rules' | 'disabled'>;
22
+ };
@@ -3,3 +3,5 @@ export * from './SelectCreate';
3
3
  export * from './FieldAi';
4
4
  export * from './FieldChat';
5
5
  export * from './AIDisclaimer';
6
+ export * from './FieldName';
7
+ export * from './FieldDescription';
@@ -1 +1,2 @@
1
1
  export * from './useOpen';
2
+ export * from './useCustomFieldValidation';
@@ -0,0 +1,38 @@
1
+ import { useCallback } from 'react';
2
+ import { AnySchema, ValidationError } from 'yup';
3
+
4
+ export type UseFieldValidationProps = {
5
+ schema: AnySchema;
6
+ };
7
+
8
+ export const useCustomFieldValidation = ({ schema }: UseFieldValidationProps) => {
9
+ const validate = useCallback(
10
+ (value: string) => {
11
+ try {
12
+ schema.validateSync(value);
13
+ return { error: null };
14
+ } catch (err: unknown) {
15
+ if (err instanceof ValidationError) {
16
+ return { error: err };
17
+ }
18
+ return { error: null };
19
+ }
20
+ },
21
+ [schema],
22
+ );
23
+
24
+ const validateRHF = useCallback(
25
+ (value: string) => {
26
+ try {
27
+ schema.validateSync(value);
28
+ return true;
29
+ } catch (err: unknown) {
30
+ if (err instanceof ValidationError) return err.message;
31
+ return;
32
+ }
33
+ },
34
+ [schema],
35
+ );
36
+
37
+ return { validate, validateRHF };
38
+ };