@snack-uikit/fields 0.19.0 → 0.19.2

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 (38) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +406 -31
  3. package/dist/components/FieldDate/FieldDate.js +4 -2
  4. package/dist/components/FieldDecorator/FieldDecorator.d.ts +1 -1
  5. package/dist/components/FieldDecorator/FieldDecorator.js +4 -3
  6. package/dist/components/FieldSecure/FieldSecure.js +3 -1
  7. package/dist/components/FieldSelect/FieldSelectMultiple.d.ts +2 -2
  8. package/dist/components/FieldSelect/FieldSelectMultiple.js +4 -2
  9. package/dist/components/FieldSelect/FieldSelectSingle.d.ts +2 -2
  10. package/dist/components/FieldSelect/FieldSelectSingle.js +4 -2
  11. package/dist/components/FieldSelect/types.d.ts +2 -2
  12. package/dist/components/FieldSlider/FieldSlider.js +63 -19
  13. package/dist/components/FieldSlider/helpers/getClosestMark.d.ts +2 -2
  14. package/dist/components/FieldSlider/helpers/getClosestMark.js +3 -3
  15. package/dist/components/FieldSlider/helpers/index.d.ts +1 -0
  16. package/dist/components/FieldSlider/helpers/index.js +1 -0
  17. package/dist/components/FieldSlider/helpers/isMarkObject.d.ts +8 -0
  18. package/dist/components/FieldSlider/helpers/isMarkObject.js +3 -0
  19. package/dist/components/FieldStepper/FieldStepper.js +3 -1
  20. package/dist/components/FieldText/FieldText.js +3 -1
  21. package/dist/components/FieldTextArea/FieldTextArea.js +3 -1
  22. package/dist/utils/getValidationState.d.ts +5 -0
  23. package/dist/utils/getValidationState.js +4 -0
  24. package/package.json +5 -5
  25. package/src/components/FieldDate/FieldDate.tsx +4 -2
  26. package/src/components/FieldDecorator/FieldDecorator.tsx +4 -3
  27. package/src/components/FieldSecure/FieldSecure.tsx +5 -2
  28. package/src/components/FieldSelect/FieldSelectMultiple.tsx +5 -2
  29. package/src/components/FieldSelect/FieldSelectSingle.tsx +5 -2
  30. package/src/components/FieldSelect/types.ts +2 -2
  31. package/src/components/FieldSlider/FieldSlider.tsx +93 -19
  32. package/src/components/FieldSlider/helpers/getClosestMark.ts +7 -3
  33. package/src/components/FieldSlider/helpers/index.ts +1 -0
  34. package/src/components/FieldSlider/helpers/isMarkObject.ts +13 -0
  35. package/src/components/FieldStepper/FieldStepper.tsx +5 -2
  36. package/src/components/FieldText/FieldText.tsx +4 -2
  37. package/src/components/FieldTextArea/FieldTextArea.tsx +5 -2
  38. package/src/utils/getValidationState.ts +6 -0
@@ -9,6 +9,7 @@ import { extractSupportProps } from '@snack-uikit/utils';
9
9
 
10
10
  import { FieldContainerPrivate } from '../../helperComponents';
11
11
  import { useValueControl } from '../../hooks';
12
+ import { getValidationState } from '../../utils/getValidationState';
12
13
  import { FieldDecorator } from '../FieldDecorator';
13
14
  import { extractFieldDecoratorProps } from '../FieldDecorator/utils';
14
15
  import { useButtons, useHandleDeleteItem, useHandleOnKeyDown, useSearchInput } from './hooks';
@@ -164,13 +165,15 @@ export const FieldSelectMultiple = forwardRef<HTMLInputElement, FieldSelectMulti
164
165
  const result =
165
166
  autocomplete || !searchable || prevInputValue.current === inputValue ? items : fuzzySearch(inputValue);
166
167
 
168
+ const fieldValidationState = getValidationState({ validationState, error: rest.error });
169
+
167
170
  return (
168
171
  <FieldDecorator
169
172
  {...extractSupportProps(rest)}
170
173
  {...extractFieldDecoratorProps(rest)}
171
174
  labelFor={id}
172
175
  size={size}
173
- validationState={validationState}
176
+ validationState={fieldValidationState}
174
177
  >
175
178
  <Droplist
176
179
  {...extractListProps(rest)}
@@ -192,7 +195,7 @@ export const FieldSelectMultiple = forwardRef<HTMLInputElement, FieldSelectMulti
192
195
  {({ onKeyDown }) => (
193
196
  <FieldContainerPrivate
194
197
  className={cn(styles.container, styles.tagContainer)}
195
- validationState={validationState}
198
+ validationState={fieldValidationState}
196
199
  disabled={disabled}
197
200
  readonly={readonly}
198
201
  focused={open}
@@ -18,6 +18,7 @@ import { extractSupportProps } from '@snack-uikit/utils';
18
18
 
19
19
  import { FieldContainerPrivate } from '../../helperComponents';
20
20
  import { useValueControl } from '../../hooks';
21
+ import { getValidationState } from '../../utils/getValidationState';
21
22
  import { FieldDecorator } from '../FieldDecorator';
22
23
  import { extractFieldDecoratorProps } from '../FieldDecorator/utils';
23
24
  import { useButtons, useHandleOnKeyDown, useSearchInput } from './hooks';
@@ -184,11 +185,13 @@ export const FieldSelectSingle = forwardRef<HTMLInputElement, FieldSelectSingleP
184
185
  ? items
185
186
  : fuzzySearch(inputValue);
186
187
 
188
+ const fieldValidationState = getValidationState({ validationState, error: rest.error });
189
+
187
190
  return (
188
191
  <FieldDecorator
189
192
  {...extractSupportProps(rest)}
190
193
  {...extractFieldDecoratorProps(rest)}
191
- validationState={validationState}
194
+ validationState={fieldValidationState}
192
195
  required={required}
193
196
  readonly={readonly}
194
197
  labelFor={id}
@@ -211,7 +214,7 @@ export const FieldSelectSingle = forwardRef<HTMLInputElement, FieldSelectSingleP
211
214
  {({ onKeyDown }) => (
212
215
  <FieldContainerPrivate
213
216
  className={styles.container}
214
- validationState={validationState}
217
+ validationState={fieldValidationState}
215
218
  disabled={disabled}
216
219
  readonly={readonly}
217
220
  focused={open}
@@ -83,8 +83,8 @@ export type FieldSelectPrivateProps = InputProps & WrapperProps & { options: Opt
83
83
  type FiledSelectCommonProps = WithSupportProps<{
84
84
  options: OptionProps[];
85
85
 
86
- pinTop: OptionProps[];
87
- pinBottom: OptionProps[];
86
+ pinTop?: OptionProps[];
87
+ pinBottom?: OptionProps[];
88
88
 
89
89
  searchable?: boolean;
90
90
  /** Отображение кнопки Копировать для поля (актуально только для `readonly = true`) */
@@ -1,5 +1,15 @@
1
1
  import mergeRefs from 'merge-refs';
2
- import { FocusEvent, forwardRef, KeyboardEvent, ReactElement, useEffect, useRef, useState } from 'react';
2
+ import {
3
+ FocusEvent,
4
+ forwardRef,
5
+ KeyboardEvent,
6
+ ReactElement,
7
+ useCallback,
8
+ useEffect,
9
+ useMemo,
10
+ useRef,
11
+ useState,
12
+ } from 'react';
3
13
 
4
14
  import { InputPrivate, InputPrivateProps, SIZE } from '@snack-uikit/input-private';
5
15
  import { Slider, SliderProps as SliderComponentProps } from '@snack-uikit/slider';
@@ -9,7 +19,7 @@ import { CONTAINER_VARIANT, VALIDATION_STATE } from '../../constants';
9
19
  import { FieldContainerPrivate } from '../../helperComponents';
10
20
  import { useValueControl } from '../../hooks';
11
21
  import { FieldDecorator, FieldDecoratorProps } from '../FieldDecorator';
12
- import { generateAllowedValues, getClosestMark, getTextFieldValue } from './helpers';
22
+ import { generateAllowedValues, getClosestMark, getTextFieldValue, isMarkObject } from './helpers';
13
23
  import styles from './styles.module.scss';
14
24
  import { TextInputFormatter } from './types';
15
25
 
@@ -81,6 +91,24 @@ export const FieldSlider = forwardRef<HTMLInputElement, FieldSliderProps>(
81
91
  },
82
92
  ref,
83
93
  ) => {
94
+ const getMarkValue = useCallback(
95
+ (key: keyof SliderProps['marks']) => {
96
+ const mark = marks[key];
97
+
98
+ if (isMarkObject(mark)) {
99
+ return mark.label;
100
+ }
101
+
102
+ return mark;
103
+ },
104
+ [marks],
105
+ );
106
+
107
+ const hasMarksEqualToValues = useMemo(
108
+ () => Object.keys(marks).every(key => key === getMarkValue(key)),
109
+ [getMarkValue, marks],
110
+ );
111
+
84
112
  const [value = getDefaultValue(range, min, max, valueProp), onChange] = useValueControl<number | number[]>({
85
113
  value: valueProp,
86
114
  defaultValue: getDefaultValue(range, min, max, valueProp),
@@ -93,54 +121,100 @@ export const FieldSlider = forwardRef<HTMLInputElement, FieldSliderProps>(
93
121
  const localRef = useRef<HTMLInputElement>(null);
94
122
 
95
123
  const onTextFieldChange = (textFieldValue: string) => {
96
- const numValue = Number(textFieldValue);
97
- if (Number.isNaN(numValue)) {
124
+ const numValue = parseInt(textFieldValue);
125
+
126
+ if (textFieldValue && Number.isNaN(numValue)) {
98
127
  return;
99
128
  }
100
129
 
101
130
  setTextFieldInputValue(textFieldValue);
102
131
  };
103
132
 
104
- const handleTextValueChange = () => {
105
- const textFieldNumValue = Number(textFieldInputValue);
106
- if (Number.isNaN(textFieldNumValue)) {
133
+ const handleNonEqualMarksSliderChange = (textFieldNumValue: number) => {
134
+ const handleChange = (key: string | number) => {
135
+ setTextFieldInputValue(String(getMarkValue(key)));
136
+ onChange(Number(key));
137
+ };
138
+
139
+ const allowedValues = Object.keys(marks).map(key => ({
140
+ key,
141
+ value: parseInt(String(getMarkValue(key))),
142
+ }));
143
+ const suitableEntry = allowedValues.find(entry => entry.value === textFieldNumValue);
144
+
145
+ if (suitableEntry) {
146
+ handleChange(suitableEntry.key);
147
+ return;
148
+ }
149
+
150
+ const actualMin = parseInt(String(getMarkValue(min)));
151
+ const actualMax = parseInt(String(getMarkValue(max)));
152
+
153
+ if (textFieldNumValue < actualMin) {
154
+ handleChange(min);
107
155
  return;
108
156
  }
109
157
 
158
+ if (textFieldNumValue > actualMax) {
159
+ handleChange(max);
160
+ return;
161
+ }
162
+
163
+ const { mark } = getClosestMark(textFieldNumValue, allowedValues, mark => mark.value);
164
+ handleChange(mark.key);
165
+ };
166
+
167
+ const handleEqualMarksSliderChange = (textFieldNumValue: number) => {
168
+ const handleChange = (value: number) => {
169
+ setTextFieldInputValue(String(value));
170
+ onChange(value);
171
+ };
172
+
110
173
  if (textFieldNumValue < min) {
111
- setTextFieldInputValue(String(min));
112
- onChange(min);
174
+ handleChange(min);
113
175
  return;
114
176
  }
115
177
 
116
178
  if (textFieldNumValue > max) {
117
- setTextFieldInputValue(String(max));
118
- onChange(max);
179
+ handleChange(max);
119
180
  return;
120
181
  }
121
182
 
122
183
  if (step === null) {
123
184
  const allowedValues = Object.keys(marks).map(Number);
124
185
  if (allowedValues.includes(textFieldNumValue)) {
125
- onChange(textFieldNumValue);
186
+ setTextFieldInputValue(String(textFieldNumValue));
187
+ handleChange(textFieldNumValue);
126
188
  return;
127
189
  }
128
190
 
129
- const { mark } = getClosestMark(textFieldNumValue, allowedValues);
130
- setTextFieldInputValue(String(mark));
131
- onChange(mark);
191
+ const { mark } = getClosestMark(textFieldNumValue, allowedValues, mark => mark);
192
+ handleChange(mark);
132
193
  return;
133
194
  }
134
195
 
135
196
  const allowedValues = generateAllowedValues(min, max, step);
136
197
  if (allowedValues.includes(textFieldNumValue)) {
137
- onChange(textFieldNumValue);
198
+ handleChange(textFieldNumValue);
138
199
  return;
139
200
  }
140
201
 
141
- const { mark } = getClosestMark(textFieldNumValue, allowedValues);
142
- setTextFieldInputValue(String(mark));
143
- onChange(mark);
202
+ const { mark } = getClosestMark(textFieldNumValue, allowedValues, mark => mark);
203
+ handleChange(mark);
204
+ };
205
+
206
+ const handleTextValueChange = () => {
207
+ const textFieldNumValue = parseInt(textFieldInputValue);
208
+
209
+ if (Number.isNaN(textFieldNumValue)) {
210
+ return;
211
+ }
212
+
213
+ if (hasMarksEqualToValues) {
214
+ handleEqualMarksSliderChange(textFieldNumValue);
215
+ } else {
216
+ handleNonEqualMarksSliderChange(textFieldNumValue);
217
+ }
144
218
  };
145
219
 
146
220
  const onTextFieldBlur = (e: FocusEvent<HTMLInputElement, Element>) => {
@@ -1,9 +1,13 @@
1
1
  const getDiff = (value: number, mark: number): number => Math.abs(mark - value);
2
2
 
3
- export const getClosestMark = (value: number, marks: number[]): { lowestDiff: number; mark: number } =>
3
+ export const getClosestMark = <T>(
4
+ value: number,
5
+ marks: T[],
6
+ getMarkValue: (value: T) => number,
7
+ ): { lowestDiff: number; mark: T } =>
4
8
  marks.reduce(
5
9
  (accResult, mark) => {
6
- const diff = getDiff(value, mark);
10
+ const diff = getDiff(value, getMarkValue(mark));
7
11
  if (diff < accResult.lowestDiff) {
8
12
  return {
9
13
  lowestDiff: diff,
@@ -14,7 +18,7 @@ export const getClosestMark = (value: number, marks: number[]): { lowestDiff: nu
14
18
  return accResult;
15
19
  },
16
20
  {
17
- lowestDiff: getDiff(value, marks[0]),
21
+ lowestDiff: getDiff(value, getMarkValue(marks[0])),
18
22
  mark: marks[0],
19
23
  },
20
24
  );
@@ -1,3 +1,4 @@
1
1
  export * from './getTextFieldValue';
2
2
  export * from './getClosestMark';
3
3
  export * from './generateAllowedValues';
4
+ export * from './isMarkObject';
@@ -0,0 +1,13 @@
1
+ import { ReactNode } from 'react';
2
+
3
+ import { SliderProps as SliderComponentProps } from '@snack-uikit/slider';
4
+
5
+ type Marks = NonNullable<SliderComponentProps['marks']>;
6
+
7
+ type MarkObject = {
8
+ label: ReactNode;
9
+ };
10
+
11
+ export function isMarkObject(mark: Marks[keyof Marks]): mark is MarkObject {
12
+ return Boolean(mark && typeof mark === 'object' && 'label' in mark);
13
+ }
@@ -20,6 +20,7 @@ import { extractSupportProps, WithSupportProps } from '@snack-uikit/utils';
20
20
  import { CONTAINER_VARIANT, VALIDATION_STATE } from '../../constants';
21
21
  import { FieldContainerPrivate } from '../../helperComponents';
22
22
  import { useValueControl } from '../../hooks';
23
+ import { getValidationState } from '../../utils/getValidationState';
23
24
  import { FieldDecorator, FieldDecoratorProps } from '../FieldDecorator';
24
25
  import styles from './styles.module.scss';
25
26
 
@@ -113,6 +114,8 @@ export const FieldStepper = forwardRef<HTMLInputElement, FieldStepperProps>(
113
114
  const isMinusButtonDisabled = (typeof min === 'number' && value <= min) || readonly || disabled;
114
115
  const isPlusButtonDisabled = (typeof max === 'number' && value >= max) || readonly || disabled;
115
116
 
117
+ const fieldValidationState = getValidationState({ validationState, error });
118
+
116
119
  const adjustValue = (value: number) => {
117
120
  setValue(value);
118
121
  setTooltipOpen(true);
@@ -217,7 +220,7 @@ export const FieldStepper = forwardRef<HTMLInputElement, FieldStepperProps>(
217
220
  readonly={readonly}
218
221
  showHintIcon={showHintIcon}
219
222
  size={size}
220
- validationState={validationState}
223
+ validationState={fieldValidationState}
221
224
  error={error}
222
225
  {...extractSupportProps(rest)}
223
226
  >
@@ -228,7 +231,7 @@ export const FieldStepper = forwardRef<HTMLInputElement, FieldStepperProps>(
228
231
  >
229
232
  <FieldContainerPrivate
230
233
  size={size}
231
- validationState={validationState}
234
+ validationState={fieldValidationState}
232
235
  disabled={disabled}
233
236
  readonly={readonly}
234
237
  variant={CONTAINER_VARIANT.SingleLine}
@@ -7,6 +7,7 @@ import { extractSupportProps, WithSupportProps } from '@snack-uikit/utils';
7
7
  import { CONTAINER_VARIANT, VALIDATION_STATE } from '../../constants';
8
8
  import { FieldContainerPrivate } from '../../helperComponents';
9
9
  import { useCopyButton, useValueControl } from '../../hooks';
10
+ import { getValidationState } from '../../utils/getValidationState';
10
11
  import { FieldDecorator, FieldDecoratorProps } from '../FieldDecorator';
11
12
 
12
13
  type InputProps = Pick<Partial<InputPrivateProps>, 'value' | 'onChange'> &
@@ -84,6 +85,7 @@ export const FieldText = forwardRef<HTMLInputElement, FieldTextProps>(
84
85
  const showAdditionalButton = Boolean(value && !disabled);
85
86
  const showClearButton = showClearButtonProp && showAdditionalButton && !readonly;
86
87
  const showCopyButton = showCopyButtonProp && showAdditionalButton && readonly;
88
+ const fieldValidationState = getValidationState({ validationState, error });
87
89
 
88
90
  const onClear = () => {
89
91
  onChange('');
@@ -116,13 +118,13 @@ export const FieldText = forwardRef<HTMLInputElement, FieldTextProps>(
116
118
  readonly={readonly}
117
119
  showHintIcon={showHintIcon}
118
120
  size={size}
119
- validationState={validationState}
121
+ validationState={fieldValidationState}
120
122
  error={error}
121
123
  {...extractSupportProps(rest)}
122
124
  >
123
125
  <FieldContainerPrivate
124
126
  size={size}
125
- validationState={validationState}
127
+ validationState={fieldValidationState}
126
128
  disabled={disabled}
127
129
  readonly={readonly}
128
130
  variant={CONTAINER_VARIANT.SingleLine}
@@ -8,6 +8,7 @@ import { extractSupportProps, WithSupportProps } from '@snack-uikit/utils';
8
8
  import { CONTAINER_VARIANT, VALIDATION_STATE } from '../../constants';
9
9
  import { FieldContainerPrivate, TextArea, TextAreaProps } from '../../helperComponents';
10
10
  import { useCopyButton, useValueControl } from '../../hooks';
11
+ import { getValidationState } from '../../utils/getValidationState';
11
12
  import { FieldDecorator, FieldDecoratorProps } from '../FieldDecorator';
12
13
  import styles from './styles.module.scss';
13
14
 
@@ -93,6 +94,8 @@ export const FieldTextArea = forwardRef<HTMLTextAreaElement, FieldTextAreaProps>
93
94
  const showCopyButton = showCopyButtonProp && Boolean(value) && !disabled && readonly;
94
95
  const showClearButton = showClearButtonProp && Boolean(value) && !disabled && !readonly;
95
96
 
97
+ const fieldValidationState = getValidationState({ validationState, error });
98
+
96
99
  const onClear = () => {
97
100
  onChange('');
98
101
 
@@ -125,13 +128,13 @@ export const FieldTextArea = forwardRef<HTMLTextAreaElement, FieldTextAreaProps>
125
128
  showHintIcon={showHintIcon}
126
129
  size={size}
127
130
  error={error}
128
- validationState={validationState}
131
+ validationState={fieldValidationState}
129
132
  {...extractSupportProps(rest)}
130
133
  >
131
134
  <FieldContainerPrivate
132
135
  className={styles.container}
133
136
  size={size}
134
- validationState={validationState}
137
+ validationState={fieldValidationState}
135
138
  disabled={disabled}
136
139
  readonly={readonly}
137
140
  data-resizable={isResizable || undefined}
@@ -0,0 +1,6 @@
1
+ import { VALIDATION_STATE } from '../constants';
2
+ import { ValidationState } from '../types';
3
+
4
+ export function getValidationState({ validationState, error }: { validationState?: ValidationState; error?: string }) {
5
+ return error ? VALIDATION_STATE.Error : validationState ?? VALIDATION_STATE.Default;
6
+ }