@snack-uikit/fields 0.13.4-preview-bd4095bc.0 → 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 (36) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +31 -0
  3. package/dist/components/FieldDate/styles.module.css +9 -9
  4. package/dist/components/FieldDecorator/styles.module.css +20 -20
  5. package/dist/components/FieldSelect/styles.module.css +17 -17
  6. package/dist/components/FieldSlider/FieldSlider.d.ts +21 -0
  7. package/dist/components/FieldSlider/FieldSlider.js +93 -0
  8. package/dist/components/FieldSlider/helpers/generateAllowedValues.d.ts +1 -0
  9. package/dist/components/FieldSlider/helpers/generateAllowedValues.js +9 -0
  10. package/dist/components/FieldSlider/helpers/getClosestMark.d.ts +4 -0
  11. package/dist/components/FieldSlider/helpers/getClosestMark.js +14 -0
  12. package/dist/components/FieldSlider/helpers/getTextFieldValue.d.ts +2 -0
  13. package/dist/components/FieldSlider/helpers/getTextFieldValue.js +6 -0
  14. package/dist/components/FieldSlider/helpers/index.d.ts +3 -0
  15. package/dist/components/FieldSlider/helpers/index.js +3 -0
  16. package/dist/components/FieldSlider/index.d.ts +1 -0
  17. package/dist/components/FieldSlider/index.js +1 -0
  18. package/dist/components/FieldSlider/styles.module.css +31 -0
  19. package/dist/components/FieldSlider/types.d.ts +1 -0
  20. package/dist/components/FieldSlider/types.js +1 -0
  21. package/dist/components/index.d.ts +1 -0
  22. package/dist/components/index.js +1 -0
  23. package/dist/helperComponents/ButtonCopyValue/styles.module.css +10 -10
  24. package/dist/helperComponents/ButtonHideValue/styles.module.css +10 -10
  25. package/dist/helperComponents/FieldContainerPrivate/styles.module.css +32 -32
  26. package/dist/helperComponents/TextArea/styles.module.css +6 -6
  27. package/package.json +4 -3
  28. package/src/components/FieldSlider/FieldSlider.tsx +215 -0
  29. package/src/components/FieldSlider/helpers/generateAllowedValues.ts +12 -0
  30. package/src/components/FieldSlider/helpers/getClosestMark.ts +20 -0
  31. package/src/components/FieldSlider/helpers/getTextFieldValue.ts +9 -0
  32. package/src/components/FieldSlider/helpers/index.ts +3 -0
  33. package/src/components/FieldSlider/index.ts +1 -0
  34. package/src/components/FieldSlider/styles.module.scss +29 -0
  35. package/src/components/FieldSlider/types.ts +1 -0
  36. package/src/components/index.ts +1 -0
package/CHANGELOG.md CHANGED
@@ -3,6 +3,28 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # 0.14.0 (2024-02-16)
7
+
8
+
9
+ ### Features
10
+
11
+ * **FF-4218:** Field slider ([7a853bf](https://github.com/cloud-ru-tech/snack-uikit/commit/7a853bf8807ae595b2a8a635d754825305c07d6a))
12
+
13
+
14
+
15
+
16
+
17
+ ## 0.13.4 (2024-02-12)
18
+
19
+
20
+ ### Bug Fixes
21
+
22
+ * **FF-4205:** update locale usage ([bd4095b](https://github.com/cloud-ru-tech/snack-uikit/commit/bd4095bc875c2efc95a0549a366d5b40dd424741))
23
+
24
+
25
+
26
+
27
+
6
28
  ## 0.13.3 (2024-02-09)
7
29
 
8
30
  ### Only dependencies have been changed
package/README.md CHANGED
@@ -360,6 +360,37 @@ const [isOpen, setIsOpen] = useState(false);
360
360
  | showHintIcon | `boolean` | - | Отображать иконку подсказки |
361
361
  | ref | `Ref<HTMLInputElement>` | - | Allows getting a ref to the component instance. Once the component unmounts, React will set `ref.current` to `null` (or call the ref with `null` if you passed a callback ref). @see https://react.dev/learn/referencing-values-with-refs#refs-and-the-dom |
362
362
  | key | `Key` | - | |
363
+ ## FieldSlider
364
+ ### Props
365
+ | name | type | default value | description |
366
+ |------|------|---------------|-------------|
367
+ | postfixIcon | `ReactElement<any, string \| JSXElementConstructor<any>>` | - | Иконка-постфикс для поля |
368
+ | showScaleBar | `boolean` | true | Отображение линейки |
369
+ | textInputFormatter | `TextInputFormatter` | - | Функция для форматирования значений в текстовом поле |
370
+ | disabled | `boolean` | - | Является ли поле деактивированным |
371
+ | readonly | `boolean` | - | Является ли поле доступным только для чтения |
372
+ | id | `string` | - | Значение html-атрибута id |
373
+ | name | `string` | - | Значение html-атрибута name |
374
+ | onFocus | `FocusEventHandler<HTMLInputElement>` | - | Колбек обработки получения фокуса |
375
+ | onBlur | `FocusEventHandler<HTMLInputElement>` | - | Колбек обработки потери фокуса |
376
+ | value | `number \| number[]` | - | |
377
+ | onChange | `(value: number \| number[]) => void` | - | |
378
+ | range | `boolean` | - | |
379
+ | tipFormatter | `(value: string \| number) => ReactNode` | - | |
380
+ | step | `number` | - | |
381
+ | min | `number` | - | |
382
+ | max | `number` | - | |
383
+ | marks | `Record<string \| number, ReactNode \| MarkObj>` | - | |
384
+ | className | `string` | - | CSS-класс |
385
+ | label | `string` | - | Лейбл |
386
+ | labelTooltip | `string` | - | Всплывающая подсказка лейбла |
387
+ | required | `boolean` | - | Является ли поле обязательным |
388
+ | size | enum Size: `"s"`, `"m"`, `"l"` | SIZE.S | Размер |
389
+ | labelTooltipPlacement | enum Placement: `"left"`, `"left-start"`, `"left-end"`, `"right"`, `"right-start"`, `"right-end"`, `"top"`, `"top-start"`, `"top-end"`, `"bottom"`, `"bottom-start"`, `"bottom-end"` | top | Расположение подсказки лейбла |
390
+ | hint | `string` | - | Подсказка внизу |
391
+ | showHintIcon | `boolean` | - | Отображать иконку подсказки |
392
+ | ref | `Ref<HTMLInputElement>` | - | Allows getting a ref to the component instance. Once the component unmounts, React will set `ref.current` to `null` (or call the ref with `null` if you passed a callback ref). @see https://react.dev/learn/referencing-values-with-refs#refs-and-the-dom |
393
+ | key | `Key` | - | |
363
394
 
364
395
 
365
396
  [//]: DOCUMENTATION_SECTION_END
@@ -22,23 +22,23 @@
22
22
  }
23
23
 
24
24
  .container .calendarIcon{
25
- color:var(--sys-neutral-text-light, #898989);
25
+ color:var(--sys-neutral-text-light, #868892);
26
26
  }
27
27
  .container .calendarIcon[data-size=s]{
28
- width:var(--dimension-2m, 16px) !important;
29
- height:var(--dimension-2m, 16px) !important;
28
+ width:var(--size-icon-container-xs, 16px) !important;
29
+ height:var(--size-icon-container-xs, 16px) !important;
30
30
  }
31
31
  .container .calendarIcon[data-size=m]{
32
- width:var(--dimension-3m, 24px) !important;
33
- height:var(--dimension-3m, 24px) !important;
32
+ width:var(--size-icon-container-s, 24px) !important;
33
+ height:var(--size-icon-container-s, 24px) !important;
34
34
  }
35
35
  .container .calendarIcon[data-size=l]{
36
- width:var(--dimension-3m, 24px) !important;
37
- height:var(--dimension-3m, 24px) !important;
36
+ width:var(--size-icon-container-s, 24px) !important;
37
+ height:var(--size-icon-container-s, 24px) !important;
38
38
  }
39
39
  .container:hover .calendarIcon, .container:focus-within .calendarIcon, .container[data-focused] .calendarIcon{
40
- color:var(--sys-neutral-text-support, #565656);
40
+ color:var(--sys-neutral-text-support, #656771);
41
41
  }
42
42
  .container[data-disabled] .calendarIcon, .container[data-readonly] .calendarIcon{
43
- color:var(--sys-neutral-text-disabled, #9e9e9e);
43
+ color:var(--sys-neutral-text-disabled, #b3b6bf);
44
44
  }
@@ -18,7 +18,7 @@
18
18
  .header{
19
19
  display:flex;
20
20
  box-sizing:border-box;
21
- color:var(--sys-neutral-text-support, #565656);
21
+ color:var(--sys-neutral-text-support, #656771);
22
22
  }
23
23
  .header[data-size=s]{
24
24
  font-family:var(--sans-label-m-font-family, SB Sans Interface);
@@ -46,7 +46,7 @@
46
46
  }
47
47
 
48
48
  .labelLayout{
49
- gap:var(--dimension-theme-general-2px, 2px);
49
+ gap:var(--space-fields-label-gap, 2px);
50
50
  display:inline-flex;
51
51
  align-items:center;
52
52
  }
@@ -89,7 +89,7 @@
89
89
  }
90
90
 
91
91
  .hintLayout{
92
- gap:var(--dimension-theme-general-2px, 2px);
92
+ gap:var(--space-fields-hint-gap, 2px);
93
93
  display:inline-flex;
94
94
  align-items:flex-start;
95
95
  }
@@ -118,22 +118,22 @@
118
118
  flex-grow:1;
119
119
  }
120
120
  .hint[data-validation=default]{
121
- color:var(--sys-neutral-text-light, #898989);
121
+ color:var(--sys-neutral-text-light, #868892);
122
122
  }
123
123
  .hint[data-validation=error]{
124
- color:var(--sys-red-text-main, #621c1e);
124
+ color:var(--sys-red-text-main, #621c1b);
125
125
  }
126
126
  .hint[data-validation=warning]{
127
- color:var(--sys-yellow-text-main, #54290f);
127
+ color:var(--sys-yellow-text-main, #5e3d06);
128
128
  }
129
129
  .hint[data-validation=success]{
130
- color:var(--sys-green-text-main, #1f392a);
130
+ color:var(--sys-green-text-main, #323f27);
131
131
  }
132
132
 
133
133
  .icon{
134
134
  flex-shrink:0;
135
135
  box-sizing:content-box;
136
- color:var(--sys-neutral-text-light, #898989);
136
+ color:var(--sys-neutral-text-light, #868892);
137
137
  }
138
138
 
139
139
  .hintIcon{
@@ -141,40 +141,40 @@
141
141
  box-sizing:content-box;
142
142
  }
143
143
  .hintIcon[data-validation=default]{
144
- color:var(--sys-neutral-text-light, #898989);
144
+ color:var(--sys-neutral-text-light, #868892);
145
145
  }
146
146
  .hintIcon[data-validation=error]{
147
- color:var(--sys-red-accent-default, #d93f2f);
147
+ color:var(--sys-red-accent-default, #cd3c3c);
148
148
  }
149
149
  .hintIcon[data-validation=warning]{
150
- color:var(--sys-yellow-accent-default, #feb528);
150
+ color:var(--sys-yellow-accent-default, #fdca46);
151
151
  }
152
152
  .hintIcon[data-validation=success]{
153
- color:var(--sys-green-accent-default, #0bbe8f);
153
+ color:var(--sys-green-accent-default, #57b762);
154
154
  }
155
155
 
156
156
  .counterLimit > span[data-validation=default]{
157
- color:var(--sys-neutral-text-light, #898989);
157
+ color:var(--sys-neutral-text-light, #868892);
158
158
  }
159
159
  .counterLimit > span[data-limit-exceeded], .counterLimit > span[data-validation=error]{
160
- color:var(--sys-red-text-light, #ec7465);
160
+ color:var(--sys-red-text-light, #ea6658);
161
161
  }
162
162
  .counterLimit > span[data-validation=warning]{
163
- color:var(--sys-yellow-text-light, #be7d3c);
163
+ color:var(--sys-yellow-text-light, #ddb035);
164
164
  }
165
165
  .counterLimit > span[data-validation=success]{
166
- color:var(--sys-green-text-light, #4d9e80);
166
+ color:var(--sys-green-text-light, #67ba6e);
167
167
  }
168
168
 
169
169
  .counterCurrentValue[data-limit-exceeded][data-validation=default]{
170
- color:var(--sys-neutral-text-main, #333333);
170
+ color:var(--sys-neutral-text-main, #33333b);
171
171
  }
172
172
  .counterCurrentValue[data-limit-exceeded][data-validation=error]{
173
- color:var(--sys-red-text-main, #621c1e);
173
+ color:var(--sys-red-text-main, #621c1b);
174
174
  }
175
175
  .counterCurrentValue[data-limit-exceeded][data-validation=warning]{
176
- color:var(--sys-yellow-text-main, #54290f);
176
+ color:var(--sys-yellow-text-main, #5e3d06);
177
177
  }
178
178
  .counterCurrentValue[data-limit-exceeded][data-validation=success]{
179
- color:var(--sys-green-text-light, #4d9e80);
179
+ color:var(--sys-green-text-light, #67ba6e);
180
180
  }
@@ -34,41 +34,41 @@
34
34
  flex:1 1 0;
35
35
  }
36
36
  .container[data-size=s] .arrowIcon{
37
- width:var(--dimension-2m, 16px) !important;
38
- height:var(--dimension-2m, 16px) !important;
39
- color:var(--sys-neutral-text-light, #898989);
37
+ width:var(--size-icon-container-xs, 16px) !important;
38
+ height:var(--size-icon-container-xs, 16px) !important;
39
+ color:var(--sys-neutral-text-light, #868892);
40
40
  }
41
41
  .container[data-size=s][data-variant=single-line-container] .displayValue{
42
- width:calc(100% - (var(--space-fields-single-line-container-s-right, 6px) + var(--space-fields-single-line-container-s-gap, 4px) + calc(var(var(--space-fields-postfix-gap, 4px)) + var(--dimension-2m, 16px) * 2)));
43
- margin-right:calc(var(--space-fields-single-line-container-s-right, 6px) + var(--space-fields-single-line-container-s-gap, 4px) + calc(var(var(--space-fields-postfix-gap, 4px)) + var(--dimension-2m, 16px) * 2));
42
+ width:calc(100% - (var(--space-fields-single-line-container-s-right, 6px) + var(--space-fields-single-line-container-s-gap, 4px) + calc(var(var(--space-fields-postfix-gap, 4px)) + var(--size-icon-container-xs, 16px) * 2)));
43
+ margin-right:calc(var(--space-fields-single-line-container-s-right, 6px) + var(--space-fields-single-line-container-s-gap, 4px) + calc(var(var(--space-fields-postfix-gap, 4px)) + var(--size-icon-container-xs, 16px) * 2));
44
44
  padding-left:var(--space-fields-single-line-container-s-left, 6px);
45
45
  border-radius:var(--radius-fields-s, 12px);
46
46
  }
47
47
  .container[data-size=m] .arrowIcon{
48
- width:var(--dimension-3m, 24px) !important;
49
- height:var(--dimension-3m, 24px) !important;
50
- color:var(--sys-neutral-text-light, #898989);
48
+ width:var(--size-icon-container-s, 24px) !important;
49
+ height:var(--size-icon-container-s, 24px) !important;
50
+ color:var(--sys-neutral-text-light, #868892);
51
51
  }
52
52
  .container[data-size=m][data-variant=single-line-container] .displayValue{
53
- width:calc(100% - (var(--space-fields-single-line-container-m-right, 8px) + var(--space-fields-single-line-container-m-gap, 4px) + calc(var(var(--space-fields-postfix-gap, 4px)) + var(--dimension-3m, 24px) * 2)));
54
- margin-right:calc(var(--space-fields-single-line-container-m-right, 8px) + var(--space-fields-single-line-container-m-gap, 4px) + calc(var(var(--space-fields-postfix-gap, 4px)) + var(--dimension-3m, 24px) * 2));
53
+ width:calc(100% - (var(--space-fields-single-line-container-m-right, 8px) + var(--space-fields-single-line-container-m-gap, 4px) + calc(var(var(--space-fields-postfix-gap, 4px)) + var(--size-icon-container-s, 24px) * 2)));
54
+ margin-right:calc(var(--space-fields-single-line-container-m-right, 8px) + var(--space-fields-single-line-container-m-gap, 4px) + calc(var(var(--space-fields-postfix-gap, 4px)) + var(--size-icon-container-s, 24px) * 2));
55
55
  padding-left:var(--space-fields-single-line-container-m-left, 8px);
56
56
  border-radius:var(--radius-fields-m, 14px);
57
57
  }
58
58
  .container[data-size=l] .arrowIcon{
59
- width:var(--dimension-3m, 24px) !important;
60
- height:var(--dimension-3m, 24px) !important;
61
- color:var(--sys-neutral-text-light, #898989);
59
+ width:var(--size-icon-container-s, 24px) !important;
60
+ height:var(--size-icon-container-s, 24px) !important;
61
+ color:var(--sys-neutral-text-light, #868892);
62
62
  }
63
63
  .container[data-size=l][data-variant=single-line-container] .displayValue{
64
- width:calc(100% - (var(--space-fields-single-line-container-l-right, 10px) + var(--space-fields-single-line-container-l-gap, 8px) + calc(var(var(--space-fields-postfix-gap, 4px)) + var(--dimension-3m, 24px) * 2)));
65
- margin-right:calc(var(--space-fields-single-line-container-l-right, 10px) + var(--space-fields-single-line-container-l-gap, 8px) + calc(var(var(--space-fields-postfix-gap, 4px)) + var(--dimension-3m, 24px) * 2));
64
+ width:calc(100% - (var(--space-fields-single-line-container-l-right, 10px) + var(--space-fields-single-line-container-l-gap, 8px) + calc(var(var(--space-fields-postfix-gap, 4px)) + var(--size-icon-container-s, 24px) * 2)));
65
+ margin-right:calc(var(--space-fields-single-line-container-l-right, 10px) + var(--space-fields-single-line-container-l-gap, 8px) + calc(var(var(--space-fields-postfix-gap, 4px)) + var(--size-icon-container-s, 24px) * 2));
66
66
  padding-left:var(--space-fields-single-line-container-l-left, 10px);
67
67
  border-radius:var(--radius-fields-l, 16px);
68
68
  }
69
69
  .container:hover .arrowIcon, .container:focus-within .arrowIcon, .container[data-focused] .arrowIcon{
70
- color:var(--sys-neutral-text-support, #565656);
70
+ color:var(--sys-neutral-text-support, #656771);
71
71
  }
72
72
  .container[data-disabled] .arrowIcon, .container[data-readonly] .arrowIcon{
73
- color:var(--sys-neutral-text-disabled, #9e9e9e);
73
+ color:var(--sys-neutral-text-disabled, #b3b6bf);
74
74
  }
@@ -0,0 +1,21 @@
1
+ import { ReactElement } from 'react';
2
+ import { InputPrivateProps } from '@snack-uikit/input-private';
3
+ import { SliderProps as SliderComponentProps } from '@snack-uikit/slider';
4
+ import { WithSupportProps } from '@snack-uikit/utils';
5
+ import { FieldDecoratorProps } from '../FieldDecorator';
6
+ import { TextInputFormatter } from './types';
7
+ type SliderProps = Pick<InputPrivateProps, 'id' | 'name' | 'disabled' | 'readonly' | 'onFocus' | 'onBlur'> & Pick<SliderComponentProps, 'range' | 'value' | 'onChange' | 'tipFormatter'> & Required<Pick<SliderComponentProps, 'min' | 'max' | 'step' | 'marks'>>;
8
+ type WrapperProps = Pick<FieldDecoratorProps, 'className' | 'label' | 'labelTooltip' | 'required' | 'hint' | 'showHintIcon' | 'size' | 'labelTooltipPlacement'>;
9
+ type FieldSliderOwnProps = {
10
+ /** Иконка-постфикс для поля */
11
+ postfixIcon?: ReactElement;
12
+ /** Отображение линейки */
13
+ showScaleBar?: boolean;
14
+ /** Функция для форматирования значений в текстовом поле */
15
+ textInputFormatter?: TextInputFormatter;
16
+ };
17
+ export type FieldSliderProps = WithSupportProps<FieldSliderOwnProps & SliderProps & WrapperProps>;
18
+ export declare const FieldSlider: import("react").ForwardRefExoticComponent<{
19
+ 'data-test-id'?: string | undefined;
20
+ } & import("react").AriaAttributes & FieldSliderOwnProps & Pick<InputPrivateProps, "disabled" | "readonly" | "name" | "id" | "onFocus" | "onBlur"> & Pick<SliderComponentProps, "value" | "onChange" | "range" | "tipFormatter"> & Required<Pick<SliderComponentProps, "max" | "step" | "min" | "marks">> & WrapperProps & import("react").RefAttributes<HTMLInputElement>>;
21
+ export {};
@@ -0,0 +1,93 @@
1
+ var __rest = (this && this.__rest) || function (s, e) {
2
+ var t = {};
3
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
4
+ t[p] = s[p];
5
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
6
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
7
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
8
+ t[p[i]] = s[p[i]];
9
+ }
10
+ return t;
11
+ };
12
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
13
+ import mergeRefs from 'merge-refs';
14
+ import { forwardRef, useEffect, useRef, useState } from 'react';
15
+ import { useUncontrolledProp } from 'uncontrollable';
16
+ import { InputPrivate, SIZE } from '@snack-uikit/input-private';
17
+ import { Slider } from '@snack-uikit/slider';
18
+ import { extractSupportProps } from '@snack-uikit/utils';
19
+ import { CONTAINER_VARIANT, VALIDATION_STATE } from '../../constants';
20
+ import { FieldContainerPrivate } from '../../helperComponents';
21
+ import { FieldDecorator } from '../FieldDecorator';
22
+ import { generateAllowedValues, getClosestMark, getTextFieldValue } from './helpers';
23
+ import styles from './styles.module.css';
24
+ const getDefaultValue = (range, min, max, value) => {
25
+ if (range) {
26
+ if (value) {
27
+ return value;
28
+ }
29
+ return [min, max];
30
+ }
31
+ return value !== null && value !== void 0 ? value : min;
32
+ };
33
+ export const FieldSlider = forwardRef((_a, ref) => {
34
+ var { id, name, min, max, step, marks, showScaleBar = true, value: valueProp, range = false, disabled = false, readonly = false, onChange: onChangeProp, onFocus, onBlur, className, label, labelTooltip, labelTooltipPlacement, required, hint, showHintIcon, size = SIZE.S, postfixIcon, textInputFormatter } = _a, rest = __rest(_a, ["id", "name", "min", "max", "step", "marks", "showScaleBar", "value", "range", "disabled", "readonly", "onChange", "onFocus", "onBlur", "className", "label", "labelTooltip", "labelTooltipPlacement", "required", "hint", "showHintIcon", "size", "postfixIcon", "textInputFormatter"]);
35
+ const [value, onChange] = useUncontrolledProp(valueProp, getDefaultValue(range, min, max, valueProp), onChangeProp);
36
+ const [textFieldInputValue, setTextFieldInputValue] = useState(getTextFieldValue(value, textInputFormatter));
37
+ const localRef = useRef(null);
38
+ const onTextFieldChange = (textFieldValue) => {
39
+ const numValue = Number(textFieldValue);
40
+ if (Number.isNaN(numValue)) {
41
+ return;
42
+ }
43
+ setTextFieldInputValue(textFieldValue);
44
+ };
45
+ const handleTextValueChange = () => {
46
+ const textFieldNumValue = Number(textFieldInputValue);
47
+ if (Number.isNaN(textFieldNumValue)) {
48
+ return;
49
+ }
50
+ if (textFieldNumValue < min) {
51
+ setTextFieldInputValue(String(min));
52
+ onChange(min);
53
+ return;
54
+ }
55
+ if (textFieldNumValue > max) {
56
+ setTextFieldInputValue(String(max));
57
+ onChange(max);
58
+ return;
59
+ }
60
+ if (step === null) {
61
+ const allowedValues = Object.keys(marks).map(Number);
62
+ if (allowedValues.includes(textFieldNumValue)) {
63
+ onChange(textFieldNumValue);
64
+ return;
65
+ }
66
+ const { mark } = getClosestMark(textFieldNumValue, allowedValues);
67
+ setTextFieldInputValue(String(mark));
68
+ onChange(mark);
69
+ return;
70
+ }
71
+ const allowedValues = generateAllowedValues(min, max, step);
72
+ if (allowedValues.includes(textFieldNumValue)) {
73
+ onChange(textFieldNumValue);
74
+ return;
75
+ }
76
+ const { mark } = getClosestMark(textFieldNumValue, allowedValues);
77
+ setTextFieldInputValue(String(mark));
78
+ onChange(mark);
79
+ };
80
+ const onTextFieldBlur = (e) => {
81
+ onBlur === null || onBlur === void 0 ? void 0 : onBlur(e);
82
+ handleTextValueChange();
83
+ };
84
+ const handleTextFieldKeyChange = (e) => {
85
+ if (e.key === 'Enter') {
86
+ handleTextValueChange();
87
+ }
88
+ };
89
+ useEffect(() => {
90
+ setTextFieldInputValue(getTextFieldValue(value, textInputFormatter));
91
+ }, [value, textInputFormatter]);
92
+ return (_jsxs(FieldDecorator, Object.assign({ className: className, label: label, labelTooltip: labelTooltip, labelTooltipPlacement: labelTooltipPlacement, labelFor: id, disabled: disabled, required: required, hint: hint, showHintIcon: showHintIcon, readonly: readonly, size: size }, extractSupportProps(rest), { children: [_jsx(FieldContainerPrivate, { className: styles.fieldContainer, size: size, validationState: VALIDATION_STATE.Default, disabled: disabled, readonly: readonly, variant: CONTAINER_VARIANT.SingleLine, inputRef: localRef, postfix: postfixIcon, children: _jsx(InputPrivate, { ref: mergeRefs(ref, localRef), "data-size": size, value: textFieldInputValue, onChange: range ? undefined : onTextFieldChange, onFocus: onFocus, onBlur: range ? onBlur : onTextFieldBlur, onKeyDown: handleTextFieldKeyChange, disabled: disabled, readonly: range ? true : readonly, type: 'text', id: id, name: name, "data-test-id": 'field-slider__input' }) }), _jsx("div", { className: styles.sliderWrapper, children: _jsx("div", { className: styles.slider, "data-size": size, children: _jsx(Slider, { range: range, min: min, max: max, step: step, value: value, onChange: onChange, marks: showScaleBar ? marks : undefined, disabled: readonly || disabled, "data-test-id": 'field-slider__slider' }) }) })] })));
93
+ });
@@ -0,0 +1 @@
1
+ export declare const generateAllowedValues: (min: number, max: number, step: number) => number[];
@@ -0,0 +1,9 @@
1
+ export const generateAllowedValues = (min, max, step) => {
2
+ const values = [];
3
+ let current = min;
4
+ while (current < max) {
5
+ values.push(current);
6
+ current += step;
7
+ }
8
+ return values;
9
+ };
@@ -0,0 +1,4 @@
1
+ export declare const getClosestMark: (value: number, marks: number[]) => {
2
+ lowestDiff: number;
3
+ mark: number;
4
+ };
@@ -0,0 +1,14 @@
1
+ const getDiff = (value, mark) => Math.abs(mark - value);
2
+ export const getClosestMark = (value, marks) => marks.reduce((accResult, mark) => {
3
+ const diff = getDiff(value, mark);
4
+ if (diff < accResult.lowestDiff) {
5
+ return {
6
+ lowestDiff: diff,
7
+ mark,
8
+ };
9
+ }
10
+ return accResult;
11
+ }, {
12
+ lowestDiff: getDiff(value, marks[0]),
13
+ mark: marks[0],
14
+ });
@@ -0,0 +1,2 @@
1
+ import { TextInputFormatter } from '../types';
2
+ export declare const getTextFieldValue: (value: number | number[], textInputFormatter?: TextInputFormatter) => string;
@@ -0,0 +1,6 @@
1
+ export const getTextFieldValue = (value, textInputFormatter) => {
2
+ if (!textInputFormatter) {
3
+ return typeof value === 'number' ? String(value) : value.join(' – ');
4
+ }
5
+ return typeof value === 'number' ? textInputFormatter(value) : value.map(textInputFormatter).join(' – ');
6
+ };
@@ -0,0 +1,3 @@
1
+ export * from './getTextFieldValue';
2
+ export * from './getClosestMark';
3
+ export * from './generateAllowedValues';
@@ -0,0 +1,3 @@
1
+ export * from './getTextFieldValue';
2
+ export * from './getClosestMark';
3
+ export * from './generateAllowedValues';
@@ -0,0 +1 @@
1
+ export * from './FieldSlider';
@@ -0,0 +1 @@
1
+ export * from './FieldSlider';
@@ -0,0 +1,31 @@
1
+ .sliderWrapper{
2
+ display:flex;
3
+ justify-content:center;
4
+ width:100%;
5
+ margin-top:-13px;
6
+ }
7
+ .sliderWrapper .slider{
8
+ flex:1;
9
+ }
10
+ .sliderWrapper .slider[data-size=s]{
11
+ height:var(--size-slider-track-line, 2px);
12
+ padding-left:var(--space-fields-slider-padding-s, 8px);
13
+ padding-right:var(--space-fields-slider-padding-s, 8px);
14
+ height:inherit;
15
+ }
16
+ .sliderWrapper .slider[data-size=m]{
17
+ height:var(--size-slider-track-line, 2px);
18
+ padding-left:var(--space-fields-slider-padding-m, 10px);
19
+ padding-right:var(--space-fields-slider-padding-m, 10px);
20
+ height:inherit;
21
+ }
22
+ .sliderWrapper .slider[data-size=l]{
23
+ height:var(--size-slider-track-line, 2px);
24
+ padding-left:var(--space-fields-slider-padding-l, 12px);
25
+ padding-right:var(--space-fields-slider-padding-l, 12px);
26
+ height:inherit;
27
+ }
28
+
29
+ .fieldContainer svg{
30
+ color:var(--sys-neutral-text-disabled, #b3b6bf);
31
+ }
@@ -0,0 +1 @@
1
+ export type TextInputFormatter = (value: number) => string;
@@ -0,0 +1 @@
1
+ export {};
@@ -5,3 +5,4 @@ export * from './FieldSecure';
5
5
  export * from './FieldDate';
6
6
  export * from './FieldSelect';
7
7
  export * from './FieldStepper';
8
+ export * from './FieldSlider';
@@ -5,3 +5,4 @@ export * from './FieldSecure';
5
5
  export * from './FieldDate';
6
6
  export * from './FieldSelect';
7
7
  export * from './FieldStepper';
8
+ export * from './FieldSlider';
@@ -4,7 +4,7 @@
4
4
  justify-content:center;
5
5
  margin:0;
6
6
  padding:0;
7
- color:var(--sys-neutral-text-light, #898989);
7
+ color:var(--sys-neutral-text-light, #868892);
8
8
  background-color:transparent;
9
9
  border:none;
10
10
  }
@@ -14,8 +14,8 @@
14
14
  border-radius:var(--radius-fields-buttons-s, 8px);
15
15
  }
16
16
  .buttonCopyValue[data-size=s] svg{
17
- width:var(--dimension-2m, 16px) !important;
18
- height:var(--dimension-2m, 16px) !important;
17
+ width:var(--size-icon-container-xs, 16px) !important;
18
+ height:var(--size-icon-container-xs, 16px) !important;
19
19
  }
20
20
  .buttonCopyValue[data-size=m]{
21
21
  width:var(--size-fields-buttons-m, 24px);
@@ -23,25 +23,25 @@
23
23
  border-radius:var(--radius-fields-buttons-m, 12px);
24
24
  }
25
25
  .buttonCopyValue[data-size=m] svg{
26
- width:var(--dimension-3m, 24px) !important;
27
- height:var(--dimension-3m, 24px) !important;
26
+ width:var(--size-icon-container-s, 24px) !important;
27
+ height:var(--size-icon-container-s, 24px) !important;
28
28
  }
29
29
  .buttonCopyValue:hover{
30
30
  cursor:pointer;
31
- color:var(--sys-neutral-text-support, #565656);
31
+ color:var(--sys-neutral-text-support, #656771);
32
32
  }
33
33
  .buttonCopyValue:focus-visible{
34
34
  outline-width:var(--border-state-focus-s-border-width, 2px);
35
35
  outline-style:var(--border-state-focus-s-border-style, solid);
36
36
  outline-color:var(--border-state-focus-s-border-color, );
37
- color:var(--sys-neutral-text-support, #565656);
38
- outline-color:var(--sys-available-complementary, #131313);
37
+ color:var(--sys-neutral-text-support, #656771);
38
+ outline-color:var(--sys-available-complementary, #141415);
39
39
  outline-offset:var(--spacing-state-focus-offset, 2px);
40
40
  }
41
41
  .buttonCopyValue:active{
42
- color:var(--sys-neutral-text-main, #333333);
42
+ color:var(--sys-neutral-text-main, #33333b);
43
43
  }
44
44
  .buttonCopyValue[data-disabled]{
45
45
  cursor:not-allowed;
46
- color:var(--sys-neutral-text-disabled, #9e9e9e);
46
+ color:var(--sys-neutral-text-disabled, #b3b6bf);
47
47
  }
@@ -4,7 +4,7 @@
4
4
  justify-content:center;
5
5
  margin:0;
6
6
  padding:0;
7
- color:var(--sys-neutral-text-light, #898989);
7
+ color:var(--sys-neutral-text-light, #868892);
8
8
  background-color:transparent;
9
9
  border:none;
10
10
  }
@@ -14,8 +14,8 @@
14
14
  border-radius:var(--radius-fields-buttons-s, 8px);
15
15
  }
16
16
  .buttonHideValue[data-size=s] svg{
17
- width:var(--dimension-2m, 16px) !important;
18
- height:var(--dimension-2m, 16px) !important;
17
+ width:var(--size-icon-container-xs, 16px) !important;
18
+ height:var(--size-icon-container-xs, 16px) !important;
19
19
  }
20
20
  .buttonHideValue[data-size=m]{
21
21
  width:var(--size-fields-buttons-m, 24px);
@@ -23,25 +23,25 @@
23
23
  border-radius:var(--radius-fields-buttons-m, 12px);
24
24
  }
25
25
  .buttonHideValue[data-size=m] svg{
26
- width:var(--dimension-3m, 24px) !important;
27
- height:var(--dimension-3m, 24px) !important;
26
+ width:var(--size-icon-container-s, 24px) !important;
27
+ height:var(--size-icon-container-s, 24px) !important;
28
28
  }
29
29
  .buttonHideValue:hover{
30
30
  cursor:pointer;
31
- color:var(--sys-neutral-text-support, #565656);
31
+ color:var(--sys-neutral-text-support, #656771);
32
32
  }
33
33
  .buttonHideValue:focus-visible{
34
34
  outline-width:var(--border-state-focus-s-border-width, 2px);
35
35
  outline-style:var(--border-state-focus-s-border-style, solid);
36
36
  outline-color:var(--border-state-focus-s-border-color, );
37
- color:var(--sys-neutral-text-support, #565656);
38
- outline-color:var(--sys-available-complementary, #131313);
37
+ color:var(--sys-neutral-text-support, #656771);
38
+ outline-color:var(--sys-available-complementary, #141415);
39
39
  outline-offset:var(--spacing-state-focus-offset, 2px);
40
40
  }
41
41
  .buttonHideValue:active{
42
- color:var(--sys-neutral-text-main, #333333);
42
+ color:var(--sys-neutral-text-main, #33333b);
43
43
  }
44
44
  .buttonHideValue[data-disabled]{
45
45
  cursor:not-allowed;
46
- color:var(--sys-neutral-text-disabled, #9e9e9e);
46
+ color:var(--sys-neutral-text-disabled, #b3b6bf);
47
47
  }
@@ -94,68 +94,68 @@
94
94
  padding-right:var(--space-fields-multi-line-container-l-right, 2px);
95
95
  }
96
96
  .container[data-validation=default]{
97
- background-color:var(--sys-neutral-background1-level, #f9f9f9);
98
- border-color:var(--sys-neutral-decor-default, #dedede);
97
+ background-color:var(--sys-neutral-background1-level, #fafafc);
98
+ border-color:var(--sys-neutral-decor-default, #dfe2ec);
99
99
  }
100
100
  .container[data-validation=default]:hover{
101
- background-color:var(--sys-neutral-background2-level, #fdfdfd);
101
+ background-color:var(--sys-neutral-background2-level, #ffffff);
102
102
  border-color:var(--sys-primary-decor-hovered, #decdfb);
103
103
  }
104
104
  .container[data-validation=default]:focus-within, .container[data-validation=default][data-focused]{
105
105
  outline-width:var(--border-state-focus-m-border-width, 3px);
106
106
  outline-style:var(--border-state-focus-m-border-style, solid);
107
107
  outline-color:var(--border-state-focus-m-border-color, );
108
- background-color:var(--sys-neutral-background2-level, #fdfdfd);
108
+ background-color:var(--sys-neutral-background2-level, #ffffff);
109
109
  border-color:var(--sys-primary-accent-default, #794ed3);
110
110
  outline-color:var(--sys-primary-decor-activated, #c5b2f1);
111
111
  }
112
112
  .container[data-validation=error]{
113
- background-color:var(--sys-red-background1-level, #fcf6f4);
114
- border-color:var(--sys-red-decor-default, #ffd8d0);
113
+ background-color:var(--sys-red-background1-level, #fcf6f5);
114
+ border-color:var(--sys-red-decor-default, #fdd6cd);
115
115
  }
116
116
  .container[data-validation=error]:hover{
117
- background-color:var(--sys-neutral-background2-level, #fdfdfd);
118
- border-color:var(--sys-red-decor-hovered, #fcc4b6);
117
+ background-color:var(--sys-neutral-background2-level, #ffffff);
118
+ border-color:var(--sys-red-decor-hovered, #fac1b3);
119
119
  }
120
120
  .container[data-validation=error]:focus-within, .container[data-validation=error][data-focused]{
121
121
  outline-width:var(--border-state-focus-m-border-width, 3px);
122
122
  outline-style:var(--border-state-focus-m-border-style, solid);
123
123
  outline-color:var(--border-state-focus-m-border-color, );
124
- background-color:var(--sys-neutral-background2-level, #fdfdfd);
125
- border-color:var(--sys-red-accent-default, #d93f2f);
126
- outline-color:var(--sys-red-decor-activated, #ffa491);
124
+ background-color:var(--sys-neutral-background2-level, #ffffff);
125
+ border-color:var(--sys-red-accent-default, #cd3c3c);
126
+ outline-color:var(--sys-red-decor-activated, #fbab99);
127
127
  }
128
128
  .container[data-validation=warning]{
129
- background-color:var(--sys-yellow-background1-level, #fdfae6);
130
- border-color:var(--sys-yellow-decor-default, #f9e5ad);
129
+ background-color:var(--sys-yellow-background1-level, #fefae6);
130
+ border-color:var(--sys-yellow-decor-default, #f0dfb1);
131
131
  }
132
132
  .container[data-validation=warning]:hover{
133
- background-color:var(--sys-neutral-background2-level, #fdfdfd);
134
- border-color:var(--sys-yellow-decor-hovered, #f6d997);
133
+ background-color:var(--sys-neutral-background2-level, #ffffff);
134
+ border-color:var(--sys-yellow-decor-hovered, #ead49a);
135
135
  }
136
136
  .container[data-validation=warning]:focus-within, .container[data-validation=warning][data-focused]{
137
137
  outline-width:var(--border-state-focus-m-border-width, 3px);
138
138
  outline-style:var(--border-state-focus-m-border-style, solid);
139
139
  outline-color:var(--border-state-focus-m-border-color, );
140
- background-color:var(--sys-neutral-background2-level, #fdfdfd);
141
- border-color:var(--sys-yellow-accent-default, #feb528);
142
- outline-color:var(--sys-yellow-decor-activated, #f3cd81);
140
+ background-color:var(--sys-neutral-background2-level, #ffffff);
141
+ border-color:var(--sys-yellow-accent-default, #fdca46);
142
+ outline-color:var(--sys-yellow-decor-activated, #e5c878);
143
143
  }
144
144
  .container[data-validation=success]{
145
- background-color:var(--sys-green-background1-level, #f1fbf9);
146
- border-color:var(--sys-green-decor-default, #cdebe3);
145
+ background-color:var(--sys-green-background1-level, #f6faf3);
146
+ border-color:var(--sys-green-decor-default, #d2ead0);
147
147
  }
148
148
  .container[data-validation=success]:hover{
149
- background-color:var(--sys-neutral-background2-level, #fdfdfd);
150
- border-color:var(--sys-green-decor-hovered, #b6e1d5);
149
+ background-color:var(--sys-neutral-background2-level, #ffffff);
150
+ border-color:var(--sys-green-decor-hovered, #c0e1ba);
151
151
  }
152
152
  .container[data-validation=success]:focus-within, .container[data-validation=success][data-focused]{
153
153
  outline-width:var(--border-state-focus-m-border-width, 3px);
154
154
  outline-style:var(--border-state-focus-m-border-style, solid);
155
155
  outline-color:var(--border-state-focus-m-border-color, );
156
- background-color:var(--sys-neutral-background2-level, #fdfdfd);
157
- border-color:var(--sys-green-accent-default, #0bbe8f);
158
- outline-color:var(--sys-green-decor-activated, #9fd7c4);
156
+ background-color:var(--sys-neutral-background2-level, #ffffff);
157
+ border-color:var(--sys-green-accent-default, #57b762);
158
+ outline-color:var(--sys-green-decor-activated, #a8d1a2);
159
159
  }
160
160
  .container[data-selectable], .container[data-selectable] input, .container[data-selectable] select, .container[data-selectable] textarea, .container[data-selectable] span{
161
161
  cursor:pointer;
@@ -164,30 +164,30 @@
164
164
  cursor:default;
165
165
  }
166
166
  .container[data-readonly], .container[data-readonly]:hover{
167
- background-color:var(--sys-neutral-decor-disabled, #e7e7e7);
168
- border-color:var(--sys-neutral-decor-disabled, #e7e7e7);
167
+ background-color:var(--sys-neutral-decor-disabled, #e8eaf1);
168
+ border-color:var(--sys-neutral-decor-disabled, #e8eaf1);
169
169
  }
170
170
  .container[data-readonly]:focus-within, .container[data-readonly][data-focused]{
171
171
  outline-width:var(--border-state-focus-m-border-width, 3px);
172
172
  outline-style:var(--border-state-focus-m-border-style, solid);
173
173
  outline-color:var(--border-state-focus-m-border-color, );
174
- background-color:var(--sys-neutral-decor-disabled, #e7e7e7);
175
- border-color:var(--sys-neutral-decor-disabled, #e7e7e7);
174
+ background-color:var(--sys-neutral-decor-disabled, #e8eaf1);
175
+ border-color:var(--sys-neutral-decor-disabled, #e8eaf1);
176
176
  outline-color:var(--sys-primary-decor-activated, #c5b2f1);
177
177
  }
178
178
  .container[data-disabled], .container[data-disabled] input, .container[data-disabled] select, .container[data-disabled] textarea, .container[data-disabled] span{
179
179
  cursor:not-allowed;
180
180
  }
181
181
  .container[data-disabled], .container[data-disabled]:focus-within, .container[data-disabled][data-focused], .container[data-disabled]:hover{
182
- background-color:var(--sys-neutral-background, #f0f0f0);
183
- border-color:var(--sys-neutral-decor-disabled, #e7e7e7);
182
+ background-color:var(--sys-neutral-background, #f1f2f6);
183
+ border-color:var(--sys-neutral-decor-disabled, #e8eaf1);
184
184
  outline:none;
185
185
  }
186
186
 
187
187
  .prefix{
188
188
  display:inline-flex;
189
189
  flex-shrink:0;
190
- color:var(--sys-neutral-text-disabled, #9e9e9e);
190
+ color:var(--sys-neutral-text-disabled, #b3b6bf);
191
191
  }
192
192
 
193
193
  .postfix{
@@ -5,28 +5,28 @@
5
5
  max-width:100%;
6
6
  margin:0;
7
7
  padding:0;
8
- color:var(--sys-neutral-text-main, #333333);
8
+ color:var(--sys-neutral-text-main, #33333b);
9
9
  background-color:transparent;
10
10
  border:none;
11
11
  border-radius:0;
12
12
  outline:0;
13
13
  }
14
14
  .textarea::-moz-placeholder{
15
- color:var(--sys-neutral-text-disabled, #9e9e9e);
15
+ color:var(--sys-neutral-text-disabled, #b3b6bf);
16
16
  }
17
17
  .textarea::placeholder{
18
- color:var(--sys-neutral-text-disabled, #9e9e9e);
18
+ color:var(--sys-neutral-text-disabled, #b3b6bf);
19
19
  }
20
20
  .textarea::-webkit-scrollbar{
21
21
  width:0;
22
22
  max-width:0;
23
23
  }
24
24
  .textarea:-moz-read-only{
25
- color:var(--sys-neutral-text-support, #565656);
25
+ color:var(--sys-neutral-text-support, #656771);
26
26
  }
27
27
  .textarea:read-only{
28
- color:var(--sys-neutral-text-support, #565656);
28
+ color:var(--sys-neutral-text-support, #656771);
29
29
  }
30
30
  .textarea[disabled]{
31
- color:var(--sys-neutral-text-disabled, #9e9e9e);
31
+ color:var(--sys-neutral-text-disabled, #b3b6bf);
32
32
  }
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "access": "public"
5
5
  },
6
6
  "title": "Fields",
7
- "version": "0.13.4-preview-bd4095bc.0",
7
+ "version": "0.14.0",
8
8
  "sideEffects": [
9
9
  "*.css",
10
10
  "*.woff",
@@ -33,11 +33,12 @@
33
33
  "scripts": {},
34
34
  "dependencies": {
35
35
  "@snack-uikit/button": "0.16.0",
36
- "@snack-uikit/calendar": "0.7.5-preview-bd4095bc.0",
36
+ "@snack-uikit/calendar": "0.7.5",
37
37
  "@snack-uikit/droplist": "0.13.4",
38
38
  "@snack-uikit/icons": "0.20.1",
39
39
  "@snack-uikit/input-private": "3.1.0",
40
40
  "@snack-uikit/scroll": "0.5.1",
41
+ "@snack-uikit/slider": "0.1.1",
41
42
  "@snack-uikit/tooltip": "0.11.1",
42
43
  "@snack-uikit/truncate-string": "0.4.7",
43
44
  "@snack-uikit/utils": "3.2.0",
@@ -53,5 +54,5 @@
53
54
  "peerDependencies": {
54
55
  "@snack-uikit/locale": "*"
55
56
  },
56
- "gitHead": "53065276382788f1cae880a1303f6f5a47cdd750"
57
+ "gitHead": "a1d688a1f1f70b6807cd05e2514cd1636c3e925a"
57
58
  }
@@ -0,0 +1,215 @@
1
+ import mergeRefs from 'merge-refs';
2
+ import { FocusEvent, forwardRef, KeyboardEvent, ReactElement, useEffect, useRef, useState } from 'react';
3
+ import { useUncontrolledProp } from 'uncontrollable';
4
+
5
+ import { InputPrivate, InputPrivateProps, SIZE } from '@snack-uikit/input-private';
6
+ import { Slider, SliderProps as SliderComponentProps } from '@snack-uikit/slider';
7
+ import { extractSupportProps, WithSupportProps } from '@snack-uikit/utils';
8
+
9
+ import { CONTAINER_VARIANT, VALIDATION_STATE } from '../../constants';
10
+ import { FieldContainerPrivate } from '../../helperComponents';
11
+ import { FieldDecorator, FieldDecoratorProps } from '../FieldDecorator';
12
+ import { generateAllowedValues, getClosestMark, getTextFieldValue } from './helpers';
13
+ import styles from './styles.module.scss';
14
+ import { TextInputFormatter } from './types';
15
+
16
+ type SliderProps = Pick<InputPrivateProps, 'id' | 'name' | 'disabled' | 'readonly' | 'onFocus' | 'onBlur'> &
17
+ Pick<SliderComponentProps, 'range' | 'value' | 'onChange' | 'tipFormatter'> &
18
+ Required<Pick<SliderComponentProps, 'min' | 'max' | 'step' | 'marks'>>;
19
+
20
+ type WrapperProps = Pick<
21
+ FieldDecoratorProps,
22
+ 'className' | 'label' | 'labelTooltip' | 'required' | 'hint' | 'showHintIcon' | 'size' | 'labelTooltipPlacement'
23
+ >;
24
+
25
+ type FieldSliderOwnProps = {
26
+ /** Иконка-постфикс для поля */
27
+ postfixIcon?: ReactElement;
28
+ /** Отображение линейки */
29
+ showScaleBar?: boolean;
30
+ /** Функция для форматирования значений в текстовом поле */
31
+ textInputFormatter?: TextInputFormatter;
32
+ };
33
+
34
+ export type FieldSliderProps = WithSupportProps<FieldSliderOwnProps & SliderProps & WrapperProps>;
35
+
36
+ const getDefaultValue = (
37
+ range: boolean,
38
+ min: FieldSliderProps['min'],
39
+ max: FieldSliderProps['max'],
40
+ value: FieldSliderProps['value'],
41
+ ): number | number[] => {
42
+ if (range) {
43
+ if (value) {
44
+ return value;
45
+ }
46
+
47
+ return [min, max];
48
+ }
49
+
50
+ return value ?? min;
51
+ };
52
+
53
+ export const FieldSlider = forwardRef<HTMLInputElement, FieldSliderProps>(
54
+ (
55
+ {
56
+ id,
57
+ name,
58
+ min,
59
+ max,
60
+ step,
61
+ marks,
62
+ showScaleBar = true,
63
+ value: valueProp,
64
+ range = false,
65
+ disabled = false,
66
+ readonly = false,
67
+ onChange: onChangeProp,
68
+ onFocus,
69
+ onBlur,
70
+ className,
71
+ label,
72
+ labelTooltip,
73
+ labelTooltipPlacement,
74
+ required,
75
+ hint,
76
+ showHintIcon,
77
+ size = SIZE.S,
78
+ postfixIcon,
79
+ textInputFormatter,
80
+ ...rest
81
+ },
82
+ ref,
83
+ ) => {
84
+ const [value, onChange] = useUncontrolledProp(valueProp, getDefaultValue(range, min, max, valueProp), onChangeProp);
85
+ const [textFieldInputValue, setTextFieldInputValue] = useState<string>(
86
+ getTextFieldValue(value, textInputFormatter),
87
+ );
88
+ const localRef = useRef<HTMLInputElement>(null);
89
+
90
+ const onTextFieldChange = (textFieldValue: string) => {
91
+ const numValue = Number(textFieldValue);
92
+ if (Number.isNaN(numValue)) {
93
+ return;
94
+ }
95
+
96
+ setTextFieldInputValue(textFieldValue);
97
+ };
98
+
99
+ const handleTextValueChange = () => {
100
+ const textFieldNumValue = Number(textFieldInputValue);
101
+ if (Number.isNaN(textFieldNumValue)) {
102
+ return;
103
+ }
104
+
105
+ if (textFieldNumValue < min) {
106
+ setTextFieldInputValue(String(min));
107
+ onChange(min);
108
+ return;
109
+ }
110
+
111
+ if (textFieldNumValue > max) {
112
+ setTextFieldInputValue(String(max));
113
+ onChange(max);
114
+ return;
115
+ }
116
+
117
+ if (step === null) {
118
+ const allowedValues = Object.keys(marks).map(Number);
119
+ if (allowedValues.includes(textFieldNumValue)) {
120
+ onChange(textFieldNumValue);
121
+ return;
122
+ }
123
+
124
+ const { mark } = getClosestMark(textFieldNumValue, allowedValues);
125
+ setTextFieldInputValue(String(mark));
126
+ onChange(mark);
127
+ return;
128
+ }
129
+
130
+ const allowedValues = generateAllowedValues(min, max, step);
131
+ if (allowedValues.includes(textFieldNumValue)) {
132
+ onChange(textFieldNumValue);
133
+ return;
134
+ }
135
+
136
+ const { mark } = getClosestMark(textFieldNumValue, allowedValues);
137
+ setTextFieldInputValue(String(mark));
138
+ onChange(mark);
139
+ };
140
+
141
+ const onTextFieldBlur = (e: FocusEvent<HTMLInputElement, Element>) => {
142
+ onBlur?.(e);
143
+ handleTextValueChange();
144
+ };
145
+
146
+ const handleTextFieldKeyChange = (e: KeyboardEvent<HTMLInputElement>) => {
147
+ if (e.key === 'Enter') {
148
+ handleTextValueChange();
149
+ }
150
+ };
151
+
152
+ useEffect(() => {
153
+ setTextFieldInputValue(getTextFieldValue(value, textInputFormatter));
154
+ }, [value, textInputFormatter]);
155
+
156
+ return (
157
+ <FieldDecorator
158
+ className={className}
159
+ label={label}
160
+ labelTooltip={labelTooltip}
161
+ labelTooltipPlacement={labelTooltipPlacement}
162
+ labelFor={id}
163
+ disabled={disabled}
164
+ required={required}
165
+ hint={hint}
166
+ showHintIcon={showHintIcon}
167
+ readonly={readonly}
168
+ size={size}
169
+ {...extractSupportProps(rest)}
170
+ >
171
+ <FieldContainerPrivate
172
+ className={styles.fieldContainer}
173
+ size={size}
174
+ validationState={VALIDATION_STATE.Default}
175
+ disabled={disabled}
176
+ readonly={readonly}
177
+ variant={CONTAINER_VARIANT.SingleLine}
178
+ inputRef={localRef}
179
+ postfix={postfixIcon}
180
+ >
181
+ <InputPrivate
182
+ ref={mergeRefs(ref, localRef)}
183
+ data-size={size}
184
+ value={textFieldInputValue}
185
+ onChange={range ? undefined : onTextFieldChange}
186
+ onFocus={onFocus}
187
+ onBlur={range ? onBlur : onTextFieldBlur}
188
+ onKeyDown={handleTextFieldKeyChange}
189
+ disabled={disabled}
190
+ readonly={range ? true : readonly}
191
+ type='text'
192
+ id={id}
193
+ name={name}
194
+ data-test-id='field-slider__input'
195
+ />
196
+ </FieldContainerPrivate>
197
+ <div className={styles.sliderWrapper}>
198
+ <div className={styles.slider} data-size={size}>
199
+ <Slider
200
+ range={range}
201
+ min={min}
202
+ max={max}
203
+ step={step}
204
+ value={value}
205
+ onChange={onChange}
206
+ marks={showScaleBar ? marks : undefined}
207
+ disabled={readonly || disabled}
208
+ data-test-id='field-slider__slider'
209
+ />
210
+ </div>
211
+ </div>
212
+ </FieldDecorator>
213
+ );
214
+ },
215
+ );
@@ -0,0 +1,12 @@
1
+ export const generateAllowedValues = (min: number, max: number, step: number): number[] => {
2
+ const values: number[] = [];
3
+
4
+ let current = min;
5
+
6
+ while (current < max) {
7
+ values.push(current);
8
+ current += step;
9
+ }
10
+
11
+ return values;
12
+ };
@@ -0,0 +1,20 @@
1
+ const getDiff = (value: number, mark: number): number => Math.abs(mark - value);
2
+
3
+ export const getClosestMark = (value: number, marks: number[]): { lowestDiff: number; mark: number } =>
4
+ marks.reduce(
5
+ (accResult, mark) => {
6
+ const diff = getDiff(value, mark);
7
+ if (diff < accResult.lowestDiff) {
8
+ return {
9
+ lowestDiff: diff,
10
+ mark,
11
+ };
12
+ }
13
+
14
+ return accResult;
15
+ },
16
+ {
17
+ lowestDiff: getDiff(value, marks[0]),
18
+ mark: marks[0],
19
+ },
20
+ );
@@ -0,0 +1,9 @@
1
+ import { TextInputFormatter } from '../types';
2
+
3
+ export const getTextFieldValue = (value: number | number[], textInputFormatter?: TextInputFormatter): string => {
4
+ if (!textInputFormatter) {
5
+ return typeof value === 'number' ? String(value) : value.join(' – ');
6
+ }
7
+
8
+ return typeof value === 'number' ? textInputFormatter(value) : value.map(textInputFormatter).join(' – ');
9
+ };
@@ -0,0 +1,3 @@
1
+ export * from './getTextFieldValue';
2
+ export * from './getClosestMark';
3
+ export * from './generateAllowedValues';
@@ -0,0 +1 @@
1
+ export * from './FieldSlider';
@@ -0,0 +1,29 @@
1
+ @import '@snack-uikit/figma-tokens/build/scss/components/styles-tokens-fields';
2
+ @import '@snack-uikit/figma-tokens/build/scss/styles-theme-variables';
3
+
4
+ $sizes: 's', 'm', 'l';
5
+
6
+ .sliderWrapper {
7
+ display: flex;
8
+ justify-content: center;
9
+ width: 100%;
10
+ margin-top: -13px;
11
+
12
+ .slider {
13
+ flex: 1;
14
+
15
+ @each $size in $sizes {
16
+ &[data-size='#{$size}'] {
17
+ @include composite-var($fields, 'slider-wrap', $size);
18
+
19
+ height: inherit;
20
+ }
21
+ }
22
+ }
23
+ }
24
+
25
+ .fieldContainer {
26
+ svg {
27
+ color: simple-var($theme-variables, 'sys', 'neutral', 'text-disabled');
28
+ }
29
+ }
@@ -0,0 +1 @@
1
+ export type TextInputFormatter = (value: number) => string;
@@ -5,3 +5,4 @@ export * from './FieldSecure';
5
5
  export * from './FieldDate';
6
6
  export * from './FieldSelect';
7
7
  export * from './FieldStepper';
8
+ export * from './FieldSlider';