@snack-uikit/fields 0.18.2 → 0.18.4-preview-5d3667ec.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,18 @@
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.18.3 (2024-04-22)
7
+
8
+
9
+ ### Bug Fixes
10
+
11
+ * **FF-4649:** container width of multiselect ([ca5702d](https://github.com/cloud-ru-tech/snack-uikit/commit/ca5702dbb4ae2574b5b312c926663cc67bbe694c))
12
+ * **FF-4649:** pass props footer and widthStrategy for Droplist ([c4a78cb](https://github.com/cloud-ru-tech/snack-uikit/commit/c4a78cbaddd6b8e6718b7a10614e56b5d618332e))
13
+
14
+
15
+
16
+
17
+
6
18
  ## 0.18.2 (2024-04-22)
7
19
 
8
20
  ### Only dependencies have been changed
package/README.md CHANGED
@@ -340,19 +340,20 @@ const [isOpen, setIsOpen] = useState(false);
340
340
  | showClearButton | `boolean` | true | Отображение кнопки очистки поля |
341
341
  | prefixIcon | `ReactElement<any, string \| JSXElementConstructor<any>>` | - | Иконка-префикс для поля |
342
342
  | footer | `ReactNode` | - | |
343
+ | widthStrategy | enum PopoverWidthStrategy: `"auto"`, `"gte"`, `"eq"` | - | |
343
344
  | search | `SearchState` | - | |
344
345
  | autocomplete | `boolean` | - | |
345
346
  | addOptionByEnter | `boolean` | - | |
346
347
  | open | `boolean` | - | |
347
348
  | onOpenChange | `(open: boolean) => void` | - | |
348
349
  | selectedOptionFormatter | `SelectedOptionFormatter` | - | |
350
+ | pinTop | `ItemProps[]` | - | Элементы списка, закрепленные сверху |
351
+ | pinBottom | `ItemProps[]` | - | Элементы списка, закрепленные снизу |
352
+ | dataFiltered | `boolean` | - | |
349
353
  | dataError | `boolean` | - | |
350
354
  | noDataState | `EmptyStateProps` | - | Экран при отстутствии данных |
351
355
  | noResultsState | `EmptyStateProps` | - | Экран при отстутствии результатов поиска или фильтров |
352
356
  | errorDataState | `EmptyStateProps` | - | Экран при ошибке запроса |
353
- | pinTop | `ItemProps[]` | - | Элементы списка, закрепленные сверху |
354
- | pinBottom | `ItemProps[]` | - | Элементы списка, закрепленные снизу |
355
- | dataFiltered | `boolean` | - | |
356
357
  | selection | "single" \| "multiple" | - | |
357
358
  | 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 |
358
359
  | key | `Key` | - | |
@@ -15,10 +15,11 @@ export declare const FieldSelectMultiple: import("react").ForwardRefExoticCompon
15
15
  readonly?: boolean | undefined;
16
16
  prefixIcon?: import("react").ReactElement<any, string | import("react").JSXElementConstructor<any>> | undefined;
17
17
  footer?: import("react").ReactNode;
18
+ widthStrategy?: import("@snack-uikit/popover-private/dist/types").PopoverWidthStrategy | undefined;
18
19
  search?: import("./types").SearchState | undefined;
19
20
  autocomplete?: boolean | undefined;
20
21
  addOptionByEnter?: boolean | undefined;
21
22
  open?: boolean | undefined;
22
23
  onOpenChange?(open: boolean): void;
23
24
  selectedOptionFormatter?: SelectedOptionFormatter | undefined;
24
- } & Pick<import("@snack-uikit/list").ListProps, "dataError" | "noDataState" | "noResultsState" | "errorDataState" | "pinTop" | "pinBottom" | "dataFiltered">, "showCopyButton"> & import("react").RefAttributes<HTMLInputElement>>;
25
+ } & Pick<import("@snack-uikit/list").DroplistProps, "pinTop" | "pinBottom" | "dataFiltered" | "dataError" | "noDataState" | "noResultsState" | "errorDataState">, "showCopyButton"> & import("react").RefAttributes<HTMLInputElement>>;
@@ -13,10 +13,11 @@ export declare const FieldSelectSingle: import("react").ForwardRefExoticComponen
13
13
  readonly?: boolean | undefined;
14
14
  prefixIcon?: import("react").ReactElement<any, string | import("react").JSXElementConstructor<any>> | undefined;
15
15
  footer?: import("react").ReactNode;
16
+ widthStrategy?: import("@snack-uikit/popover-private/dist/types").PopoverWidthStrategy | undefined;
16
17
  search?: import("./types").SearchState | undefined;
17
18
  autocomplete?: boolean | undefined;
18
19
  addOptionByEnter?: boolean | undefined;
19
20
  open?: boolean | undefined;
20
21
  onOpenChange?(open: boolean): void;
21
22
  selectedOptionFormatter?: SelectedOptionFormatter | undefined;
22
- } & Pick<import("@snack-uikit/list").ListProps, "dataError" | "noDataState" | "noResultsState" | "errorDataState" | "pinTop" | "pinBottom" | "dataFiltered"> & import("react").RefAttributes<HTMLInputElement>>;
23
+ } & Pick<import("@snack-uikit/list").DroplistProps, "pinTop" | "pinBottom" | "dataFiltered" | "dataError" | "noDataState" | "noResultsState" | "errorDataState"> & import("react").RefAttributes<HTMLInputElement>>;
@@ -4,6 +4,8 @@
4
4
  }
5
5
 
6
6
  .contentWrapper{
7
+ position:relative;
8
+ overflow:hidden;
7
9
  display:flex;
8
10
  flex-wrap:wrap;
9
11
  width:100%;
@@ -71,14 +73,10 @@
71
73
  }
72
74
  .container[data-size=s][data-variant=single-line-container] .displayValue{
73
75
  width:calc(100% - (
74
- var(--space-fields-single-line-container-s-right, 6px) +
75
- var(--space-fields-single-line-container-s-gap, 4px) +
76
- calc(var(var(--space-fields-postfix-gap, 4px)) + var(--size-icon-container-xs, 16px) * 2)
76
+ 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)
77
77
  ));
78
78
  margin-right:calc(
79
- var(--space-fields-single-line-container-s-right, 6px) +
80
- var(--space-fields-single-line-container-s-gap, 4px) +
81
- calc(var(var(--space-fields-postfix-gap, 4px)) + var(--size-icon-container-xs, 16px) * 2)
79
+ 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)
82
80
  );
83
81
  padding-left:var(--space-fields-single-line-container-s-left, 6px);
84
82
  border-radius:var(--radius-fields-s, 12px);
@@ -98,14 +96,10 @@
98
96
  }
99
97
  .container[data-size=m][data-variant=single-line-container] .displayValue{
100
98
  width:calc(100% - (
101
- var(--space-fields-single-line-container-m-right, 8px) +
102
- var(--space-fields-single-line-container-m-gap, 4px) +
103
- calc(var(var(--space-fields-postfix-gap, 4px)) + var(--size-icon-container-s, 24px) * 2)
99
+ 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)
104
100
  ));
105
101
  margin-right:calc(
106
- var(--space-fields-single-line-container-m-right, 8px) +
107
- var(--space-fields-single-line-container-m-gap, 4px) +
108
- calc(var(var(--space-fields-postfix-gap, 4px)) + var(--size-icon-container-s, 24px) * 2)
102
+ 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)
109
103
  );
110
104
  padding-left:var(--space-fields-single-line-container-m-left, 8px);
111
105
  border-radius:var(--radius-fields-m, 14px);
@@ -125,14 +119,10 @@
125
119
  }
126
120
  .container[data-size=l][data-variant=single-line-container] .displayValue{
127
121
  width:calc(100% - (
128
- var(--space-fields-single-line-container-l-right, 10px) +
129
- var(--space-fields-single-line-container-l-gap, 8px) +
130
- calc(var(var(--space-fields-postfix-gap, 4px)) + var(--size-icon-container-s, 24px) * 2)
122
+ 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)
131
123
  ));
132
124
  margin-right:calc(
133
- var(--space-fields-single-line-container-l-right, 10px) +
134
- var(--space-fields-single-line-container-l-gap, 8px) +
135
- calc(var(var(--space-fields-postfix-gap, 4px)) + var(--size-icon-container-s, 24px) * 2)
125
+ 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)
136
126
  );
137
127
  padding-left:var(--space-fields-single-line-container-l-left, 10px);
138
128
  border-radius:var(--radius-fields-l, 16px);
@@ -1,6 +1,6 @@
1
1
  import { ReactElement } from 'react';
2
2
  import { InputPrivateProps } from '@snack-uikit/input-private';
3
- import { AccordionItemProps, BaseItemProps, GroupItemProps, ListProps, NextListItemProps, SelectionMultipleState, SelectionSingleState } from '@snack-uikit/list';
3
+ import { AccordionItemProps, BaseItemProps, DroplistProps, GroupItemProps, NextListItemProps, SelectionMultipleState, SelectionSingleState } from '@snack-uikit/list';
4
4
  import { TagProps } from '@snack-uikit/tag';
5
5
  import { WithSupportProps } from '@snack-uikit/utils';
6
6
  import { FieldDecoratorProps } from '../FieldDecorator';
@@ -51,14 +51,15 @@ type FiledSelectCommonProps = WithSupportProps<{
51
51
  readonly?: boolean;
52
52
  /** Иконка-префикс для поля */
53
53
  prefixIcon?: ReactElement;
54
- footer?: ListProps['footer'];
54
+ footer?: DroplistProps['footer'];
55
+ widthStrategy?: DroplistProps['widthStrategy'];
55
56
  search?: SearchState;
56
57
  autocomplete?: boolean;
57
58
  addOptionByEnter?: boolean;
58
59
  open?: boolean;
59
60
  onOpenChange?(open: boolean): void;
60
61
  selectedOptionFormatter?: SelectedOptionFormatter;
61
- }> & Pick<ListProps, 'dataError' | 'noDataState' | 'noResultsState' | 'errorDataState' | 'pinTop' | 'pinBottom' | 'dataFiltered'>;
62
+ }> & Pick<DroplistProps, 'dataError' | 'noDataState' | 'noResultsState' | 'errorDataState' | 'pinTop' | 'pinBottom' | 'dataFiltered'>;
62
63
  export type FieldSelectSingleProps = FieldSelectPrivateProps & Omit<SelectionSingleState, 'mode'> & WrapperProps & FiledSelectCommonProps;
63
64
  export type FieldSelectMultipleProps = FieldSelectPrivateProps & {
64
65
  removeByBackspace?: boolean;
@@ -1,3 +1,3 @@
1
1
  import { DroplistProps } from '@snack-uikit/list';
2
2
  import { FieldSelectProps } from '../types';
3
- export declare function extractListProps({ dataError, noDataState, noResultsState, errorDataState, pinTop, pinBottom, dataFiltered, loading, }: Partial<FieldSelectProps>): Partial<DroplistProps>;
3
+ export declare function extractListProps({ dataError, noDataState, noResultsState, errorDataState, pinTop, pinBottom, dataFiltered, loading, footer, widthStrategy, }: Partial<FieldSelectProps>): Partial<DroplistProps>;
@@ -1,4 +1,4 @@
1
- export function extractListProps({ dataError, noDataState, noResultsState, errorDataState, pinTop, pinBottom, dataFiltered, loading, }) {
1
+ export function extractListProps({ dataError, noDataState, noResultsState, errorDataState, pinTop, pinBottom, dataFiltered, loading, footer, widthStrategy, }) {
2
2
  return {
3
3
  dataError,
4
4
  noDataState,
@@ -8,6 +8,8 @@ export function extractListProps({ dataError, noDataState, noResultsState, error
8
8
  pinBottom,
9
9
  dataFiltered,
10
10
  loading,
11
+ footer,
12
+ widthStrategy,
11
13
  trigger: 'clickAndFocusVisible',
12
14
  placement: 'bottom',
13
15
  'data-test-id': 'field-select__list',
@@ -11,7 +11,7 @@ var __rest = (this && this.__rest) || function (s, e) {
11
11
  };
12
12
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
13
13
  import mergeRefs from 'merge-refs';
14
- import { forwardRef, useEffect, useRef, useState } from 'react';
14
+ import { forwardRef, useCallback, useEffect, useMemo, useRef, useState, } from 'react';
15
15
  import { InputPrivate, SIZE } from '@snack-uikit/input-private';
16
16
  import { Slider } from '@snack-uikit/slider';
17
17
  import { extractSupportProps } from '@snack-uikit/utils';
@@ -19,7 +19,7 @@ import { CONTAINER_VARIANT, VALIDATION_STATE } from '../../constants';
19
19
  import { FieldContainerPrivate } from '../../helperComponents';
20
20
  import { useValueControl } from '../../hooks';
21
21
  import { FieldDecorator } from '../FieldDecorator';
22
- import { generateAllowedValues, getClosestMark, getTextFieldValue } from './helpers';
22
+ import { generateAllowedValues, getClosestMark, getTextFieldValue, isMarkObject } from './helpers';
23
23
  import styles from './styles.module.css';
24
24
  const getDefaultValue = (range, min, max, value) => {
25
25
  if (range) {
@@ -32,6 +32,14 @@ const getDefaultValue = (range, min, max, value) => {
32
32
  };
33
33
  export const FieldSlider = forwardRef((_a, ref) => {
34
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 getMarkValue = useCallback((key) => {
36
+ const mark = marks[key];
37
+ if (isMarkObject(mark)) {
38
+ return mark.label;
39
+ }
40
+ return mark;
41
+ }, [marks]);
42
+ const hasMarksEqualToValues = useMemo(() => Object.keys(marks).every(key => key === getMarkValue(key)), [getMarkValue, marks]);
35
43
  const [value = getDefaultValue(range, min, max, valueProp), onChange] = useValueControl({
36
44
  value: valueProp,
37
45
  defaultValue: getDefaultValue(range, min, max, valueProp),
@@ -40,46 +48,82 @@ export const FieldSlider = forwardRef((_a, ref) => {
40
48
  const [textFieldInputValue, setTextFieldInputValue] = useState(getTextFieldValue(value, textInputFormatter));
41
49
  const localRef = useRef(null);
42
50
  const onTextFieldChange = (textFieldValue) => {
43
- const numValue = Number(textFieldValue);
44
- if (Number.isNaN(numValue)) {
51
+ const numValue = parseInt(textFieldValue);
52
+ if (textFieldValue && Number.isNaN(numValue)) {
45
53
  return;
46
54
  }
47
55
  setTextFieldInputValue(textFieldValue);
48
56
  };
49
- const handleTextValueChange = () => {
50
- const textFieldNumValue = Number(textFieldInputValue);
51
- if (Number.isNaN(textFieldNumValue)) {
57
+ const handleNonEqualMarksSliderChange = (textFieldNumValue) => {
58
+ const handleChange = (key) => {
59
+ setTextFieldInputValue(String(getMarkValue(key)));
60
+ onChange(Number(key));
61
+ };
62
+ const allowedValues = Object.keys(marks).map(key => ({
63
+ key,
64
+ value: parseInt(String(getMarkValue(key))),
65
+ }));
66
+ const suitableEntry = allowedValues.find(entry => entry.value === textFieldNumValue);
67
+ if (suitableEntry) {
68
+ handleChange(suitableEntry.key);
69
+ return;
70
+ }
71
+ const actualMin = parseInt(String(getMarkValue(min)));
72
+ const actualMax = parseInt(String(getMarkValue(max)));
73
+ if (textFieldNumValue < actualMin) {
74
+ handleChange(min);
75
+ return;
76
+ }
77
+ if (textFieldNumValue > actualMax) {
78
+ handleChange(max);
52
79
  return;
53
80
  }
81
+ const { mark } = getClosestMark(textFieldNumValue, allowedValues, mark => mark.value);
82
+ handleChange(mark.key);
83
+ };
84
+ const handleEqualMarksSliderChange = (textFieldNumValue) => {
85
+ const handleChange = (value) => {
86
+ setTextFieldInputValue(String(value));
87
+ onChange(value);
88
+ };
54
89
  if (textFieldNumValue < min) {
55
- setTextFieldInputValue(String(min));
56
- onChange(min);
90
+ handleChange(min);
57
91
  return;
58
92
  }
59
93
  if (textFieldNumValue > max) {
60
- setTextFieldInputValue(String(max));
61
- onChange(max);
94
+ handleChange(max);
62
95
  return;
63
96
  }
64
97
  if (step === null) {
65
98
  const allowedValues = Object.keys(marks).map(Number);
66
99
  if (allowedValues.includes(textFieldNumValue)) {
67
- onChange(textFieldNumValue);
100
+ setTextFieldInputValue(String(textFieldNumValue));
101
+ handleChange(textFieldNumValue);
68
102
  return;
69
103
  }
70
- const { mark } = getClosestMark(textFieldNumValue, allowedValues);
71
- setTextFieldInputValue(String(mark));
72
- onChange(mark);
104
+ const { mark } = getClosestMark(textFieldNumValue, allowedValues, mark => mark);
105
+ handleChange(mark);
73
106
  return;
74
107
  }
75
108
  const allowedValues = generateAllowedValues(min, max, step);
76
109
  if (allowedValues.includes(textFieldNumValue)) {
77
- onChange(textFieldNumValue);
110
+ handleChange(textFieldNumValue);
78
111
  return;
79
112
  }
80
- const { mark } = getClosestMark(textFieldNumValue, allowedValues);
81
- setTextFieldInputValue(String(mark));
82
- onChange(mark);
113
+ const { mark } = getClosestMark(textFieldNumValue, allowedValues, mark => mark);
114
+ handleChange(mark);
115
+ };
116
+ const handleTextValueChange = () => {
117
+ const textFieldNumValue = parseInt(textFieldInputValue);
118
+ if (Number.isNaN(textFieldNumValue)) {
119
+ return;
120
+ }
121
+ if (hasMarksEqualToValues) {
122
+ handleEqualMarksSliderChange(textFieldNumValue);
123
+ }
124
+ else {
125
+ handleNonEqualMarksSliderChange(textFieldNumValue);
126
+ }
83
127
  };
84
128
  const onTextFieldBlur = (e) => {
85
129
  onBlur === null || onBlur === void 0 ? void 0 : onBlur(e);
@@ -1,4 +1,4 @@
1
- export declare const getClosestMark: (value: number, marks: number[]) => {
1
+ export declare const getClosestMark: <T>(value: number, marks: T[], getMarkValue: (value: T) => number) => {
2
2
  lowestDiff: number;
3
- mark: number;
3
+ mark: T;
4
4
  };
@@ -1,6 +1,6 @@
1
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);
2
+ export const getClosestMark = (value, marks, getMarkValue) => marks.reduce((accResult, mark) => {
3
+ const diff = getDiff(value, getMarkValue(mark));
4
4
  if (diff < accResult.lowestDiff) {
5
5
  return {
6
6
  lowestDiff: diff,
@@ -9,6 +9,6 @@ export const getClosestMark = (value, marks) => marks.reduce((accResult, mark) =
9
9
  }
10
10
  return accResult;
11
11
  }, {
12
- lowestDiff: getDiff(value, marks[0]),
12
+ lowestDiff: getDiff(value, getMarkValue(marks[0])),
13
13
  mark: marks[0],
14
14
  });
@@ -1,3 +1,4 @@
1
1
  export * from './getTextFieldValue';
2
2
  export * from './getClosestMark';
3
3
  export * from './generateAllowedValues';
4
+ export * from './isMarkObject';
@@ -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,8 @@
1
+ import { ReactNode } from 'react';
2
+ import { SliderProps as SliderComponentProps } from '@snack-uikit/slider';
3
+ type Marks = NonNullable<SliderComponentProps['marks']>;
4
+ type MarkObject = {
5
+ label: ReactNode;
6
+ };
7
+ export declare function isMarkObject(mark: Marks[keyof Marks]): mark is MarkObject;
8
+ export {};
@@ -0,0 +1,3 @@
1
+ export function isMarkObject(mark) {
2
+ return Boolean(mark && typeof mark === 'object' && 'label' in mark);
3
+ }
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "access": "public"
5
5
  },
6
6
  "title": "Fields",
7
- "version": "0.18.2",
7
+ "version": "0.18.4-preview-5d3667ec.0",
8
8
  "sideEffects": [
9
9
  "*.css",
10
10
  "*.woff",
@@ -34,13 +34,13 @@
34
34
  "dependencies": {
35
35
  "@snack-uikit/button": "0.17.0",
36
36
  "@snack-uikit/calendar": "0.7.6",
37
- "@snack-uikit/droplist": "0.13.14",
37
+ "@snack-uikit/droplist": "0.13.15",
38
38
  "@snack-uikit/icons": "0.20.1",
39
39
  "@snack-uikit/input-private": "3.1.1",
40
- "@snack-uikit/list": "0.11.0",
40
+ "@snack-uikit/list": "0.11.1-preview-5d3667ec.0",
41
41
  "@snack-uikit/scroll": "0.5.2",
42
- "@snack-uikit/slider": "0.1.7",
43
- "@snack-uikit/tag": "0.8.2",
42
+ "@snack-uikit/slider": "0.1.8-preview-5d3667ec.0",
43
+ "@snack-uikit/tag": "0.9.0",
44
44
  "@snack-uikit/tooltip": "0.13.1",
45
45
  "@snack-uikit/truncate-string": "0.4.12",
46
46
  "@snack-uikit/utils": "3.2.0",
@@ -56,5 +56,5 @@
56
56
  "peerDependencies": {
57
57
  "@snack-uikit/locale": "*"
58
58
  },
59
- "gitHead": "082743508cd64cdc90ed11d2ea51280b5cab19de"
59
+ "gitHead": "311bf31807d9b8cc975e80caf14ec35108256337"
60
60
  }
@@ -18,8 +18,12 @@ $base-min-width: 4px;
18
18
  }
19
19
 
20
20
  .contentWrapper {
21
+ position: relative;
22
+
23
+ overflow: hidden;
21
24
  display: flex;
22
25
  flex-wrap: wrap;
26
+
23
27
  width: 100%;
24
28
  }
25
29
 
@@ -59,9 +63,7 @@ $base-min-width: 4px;
59
63
  $button-width: simple-var($icons-sizes, $size);
60
64
  $postfix-width: calc(var(#{$space-fields-postfix-gap}) + $button-width * 2);
61
65
  $margin-right: calc(
62
- #{simple-var($fields, $containerVariant, $size, 'padding-right')} +
63
- #{simple-var($fields, $containerVariant, $size, 'gap')} +
64
- #{$postfix-width}
66
+ #{simple-var($fields, $containerVariant, $size, 'padding-right')} + #{simple-var($fields, $containerVariant, $size, 'gap')} + #{$postfix-width}
65
67
  );
66
68
 
67
69
  width: calc(100% - $margin-right);
@@ -4,8 +4,8 @@ import { InputPrivateProps } from '@snack-uikit/input-private';
4
4
  import {
5
5
  AccordionItemProps,
6
6
  BaseItemProps,
7
+ DroplistProps,
7
8
  GroupItemProps,
8
- ListProps,
9
9
  NextListItemProps,
10
10
  SelectionMultipleState,
11
11
  SelectionSingleState,
@@ -82,7 +82,9 @@ type FiledSelectCommonProps = WithSupportProps<{
82
82
  /** Иконка-префикс для поля */
83
83
  prefixIcon?: ReactElement;
84
84
 
85
- footer?: ListProps['footer'];
85
+ footer?: DroplistProps['footer'];
86
+
87
+ widthStrategy?: DroplistProps['widthStrategy'];
86
88
 
87
89
  search?: SearchState;
88
90
 
@@ -97,7 +99,7 @@ type FiledSelectCommonProps = WithSupportProps<{
97
99
  selectedOptionFormatter?: SelectedOptionFormatter;
98
100
  }> &
99
101
  Pick<
100
- ListProps,
102
+ DroplistProps,
101
103
  'dataError' | 'noDataState' | 'noResultsState' | 'errorDataState' | 'pinTop' | 'pinBottom' | 'dataFiltered'
102
104
  >;
103
105
 
@@ -11,6 +11,8 @@ export function extractListProps({
11
11
  pinBottom,
12
12
  dataFiltered,
13
13
  loading,
14
+ footer,
15
+ widthStrategy,
14
16
  }: Partial<FieldSelectProps>): Partial<DroplistProps> {
15
17
  return {
16
18
  dataError,
@@ -21,6 +23,8 @@ export function extractListProps({
21
23
  pinBottom,
22
24
  dataFiltered,
23
25
  loading,
26
+ footer,
27
+ widthStrategy,
24
28
  trigger: 'clickAndFocusVisible',
25
29
  placement: 'bottom',
26
30
  'data-test-id': 'field-select__list',
@@ -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
+ }