@snack-uikit/fields 0.16.1 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11,6 +11,7 @@ var __rest = (this && this.__rest) || function (s, e) {
11
11
  };
12
12
  import { ChevronDownSVG, ChevronUpSVG } from '@snack-uikit/icons';
13
13
  import { ICON_SIZE, SIZE } from '@snack-uikit/input-private';
14
+ import { flattenItems } from '@snack-uikit/list';
14
15
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
15
16
  export function isBaseOptionProps(option) {
16
17
  return !('options' in option);
@@ -27,6 +28,24 @@ export function isNextListOptionProps(option) {
27
28
  export function isGroupOptionProps(option) {
28
29
  return 'options' in option && option['type'] === undefined;
29
30
  }
31
+ export function mapOptionToAppearance(options) {
32
+ let mapOption2Appearance = {};
33
+ options.forEach(option => {
34
+ if (isAccordionOptionProps(option) || isNextListOptionProps(option)) {
35
+ const { options, value, appearance } = option;
36
+ mapOption2Appearance = Object.assign(Object.assign(Object.assign({}, mapOption2Appearance), { [value]: appearance }), mapOptionToAppearance(options));
37
+ }
38
+ if (isGroupOptionProps(option)) {
39
+ const { options } = option;
40
+ mapOption2Appearance = Object.assign(Object.assign({}, mapOption2Appearance), mapOptionToAppearance(options));
41
+ }
42
+ const { value, appearance } = option;
43
+ if (value !== undefined) {
44
+ mapOption2Appearance = Object.assign(Object.assign({}, mapOption2Appearance), { [value]: appearance });
45
+ }
46
+ });
47
+ return mapOption2Appearance;
48
+ }
30
49
  export function transformOptionsToItems(options) {
31
50
  return options.map(option => {
32
51
  if (isAccordionOptionProps(option) || isNextListOptionProps(option)) {
@@ -41,60 +60,34 @@ export function transformOptionsToItems(options) {
41
60
  return Object.assign(Object.assign({ 'data-test-id': 'field-select__list-option-' + option.value }, rest), { id: value, content: { option: contentOption, caption, description } });
42
61
  });
43
62
  }
44
- export function extractSelectedOptions(options, value) {
45
- for (let i = 0; i < options.length; i++) {
46
- const option = options[i];
47
- if (isAccordionOptionProps(option) || isNextListOptionProps(option)) {
48
- const { value: optionValue } = option;
49
- if (optionValue === value) {
50
- return option;
51
- }
52
- const selectedOptionFromNestedOptions = extractSelectedOptions(option.options, value);
53
- if (selectedOptionFromNestedOptions) {
54
- return selectedOptionFromNestedOptions;
55
- }
56
- }
57
- if (isGroupOptionProps(option)) {
58
- const selectedOptionFromNestedOptions = extractSelectedOptions(option.options, value);
59
- if (selectedOptionFromNestedOptions) {
60
- return selectedOptionFromNestedOptions;
61
- }
62
- }
63
- if (isBaseOptionProps(option)) {
64
- if (option.value === value) {
65
- return option;
66
- }
67
- }
63
+ export function findSelectedOption(items, value) {
64
+ const flatten = flattenItems(items);
65
+ if (!value) {
66
+ return [undefined, undefined];
68
67
  }
69
- return undefined;
68
+ const foundItem = flatten.find(item => String(item.id) === String(value));
69
+ const placeholderItem = { id: value, content: { option: String(value) } };
70
+ return [foundItem, !foundItem ? placeholderItem : undefined];
70
71
  }
71
- export function extractSelectedMultipleOptions(options, value) {
72
- let selectedOptions = [];
73
- for (let i = 0; i < options.length; i++) {
74
- const option = options[i];
75
- if (isAccordionOptionProps(option) || isNextListOptionProps(option)) {
76
- const { value: optionValue } = option;
77
- if (value === null || value === void 0 ? void 0 : value.includes(optionValue)) {
78
- selectedOptions.push(option);
79
- }
80
- const selectedOptionFromNestedOptions = extractSelectedMultipleOptions(option.options, value);
81
- if (selectedOptionFromNestedOptions) {
82
- selectedOptions = selectedOptions.concat(selectedOptionFromNestedOptions);
83
- }
84
- }
85
- if (isGroupOptionProps(option)) {
86
- const selectedOptionFromNestedOptions = extractSelectedMultipleOptions(option.options, value);
87
- if (selectedOptionFromNestedOptions) {
88
- selectedOptions = selectedOptions.concat(selectedOptionFromNestedOptions);
72
+ export function findSelectedOptions(items, value) {
73
+ const flatten = flattenItems(items);
74
+ if (!value || !(value === null || value === void 0 ? void 0 : value.length)) {
75
+ return [undefined, undefined];
76
+ }
77
+ let foundItems;
78
+ let placeholderItems;
79
+ value.forEach(value => {
80
+ if (flatten) {
81
+ const [found, placeholder] = findSelectedOption(flatten, value);
82
+ if (found || foundItems) {
83
+ foundItems = (foundItems !== null && foundItems !== void 0 ? foundItems : []).concat(found !== null && found !== void 0 ? found : []);
89
84
  }
90
- }
91
- if (isBaseOptionProps(option)) {
92
- if (value === null || value === void 0 ? void 0 : value.includes(option.value)) {
93
- selectedOptions.push(option);
85
+ if (placeholder || placeholderItems) {
86
+ placeholderItems = (placeholderItems !== null && placeholderItems !== void 0 ? placeholderItems : []).concat(placeholder !== null && placeholder !== void 0 ? placeholder : []);
94
87
  }
95
88
  }
96
- }
97
- return selectedOptions.length ? selectedOptions : undefined;
89
+ });
90
+ return [foundItems, placeholderItems];
98
91
  }
99
92
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
100
93
  export function isFieldSelectMultipleProps(props) {
@@ -110,3 +103,20 @@ export function getArrowIcon({ size, open }) {
110
103
  arrowIconSize: size === SIZE.S ? ICON_SIZE.Xs : ICON_SIZE.S,
111
104
  };
112
105
  }
106
+ export function extractListProps({ dataError, noDataState, noResultsState, errorDataState, pinTop, pinBottom, dataFiltered, loading, }) {
107
+ return {
108
+ dataError,
109
+ noDataState,
110
+ noResultsState,
111
+ errorDataState,
112
+ pinTop,
113
+ pinBottom,
114
+ dataFiltered,
115
+ loading,
116
+ trigger: 'clickAndFocusVisible',
117
+ placement: 'bottom',
118
+ 'data-test-id': 'field-select__list',
119
+ scroll: true,
120
+ marker: true,
121
+ };
122
+ }
@@ -1,7 +1,7 @@
1
1
  export const generateAllowedValues = (min, max, step) => {
2
2
  const values = [];
3
3
  let current = min;
4
- while (current < max) {
4
+ while (current <= max) {
5
5
  values.push(current);
6
6
  current += step;
7
7
  }
@@ -1,6 +1,6 @@
1
1
  import { useUncontrolledProp } from 'uncontrollable';
2
2
  export function useValueControl({ value, onChange, defaultValue }) {
3
- return useUncontrolledProp(value, value !== null && value !== void 0 ? value : defaultValue, (newValue) => {
3
+ return useUncontrolledProp(value, defaultValue, (newValue) => {
4
4
  const newState = typeof newValue === 'function' ? newValue(value) : newValue;
5
5
  onChange === null || onChange === void 0 ? void 0 : onChange(newState);
6
6
  });
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "access": "public"
5
5
  },
6
6
  "title": "Fields",
7
- "version": "0.16.1",
7
+ "version": "0.17.0",
8
8
  "sideEffects": [
9
9
  "*.css",
10
10
  "*.woff",
@@ -34,13 +34,13 @@
34
34
  "dependencies": {
35
35
  "@snack-uikit/button": "0.16.1",
36
36
  "@snack-uikit/calendar": "0.7.6",
37
- "@snack-uikit/droplist": "0.13.8",
37
+ "@snack-uikit/droplist": "0.13.9",
38
38
  "@snack-uikit/icons": "0.20.1",
39
39
  "@snack-uikit/input-private": "3.1.1",
40
- "@snack-uikit/list": "0.5.0",
40
+ "@snack-uikit/list": "0.6.0",
41
41
  "@snack-uikit/scroll": "0.5.2",
42
42
  "@snack-uikit/slider": "0.1.4",
43
- "@snack-uikit/tag": "0.8.0",
43
+ "@snack-uikit/tag": "0.8.1",
44
44
  "@snack-uikit/tooltip": "0.12.1",
45
45
  "@snack-uikit/truncate-string": "0.4.9",
46
46
  "@snack-uikit/utils": "3.2.0",
@@ -56,5 +56,5 @@
56
56
  "peerDependencies": {
57
57
  "@snack-uikit/locale": "*"
58
58
  },
59
- "gitHead": "bfdd4a8e3c077eab0d45e177172a62b6dcbda06b"
59
+ "gitHead": "559f1542bf57e8a3be174390e4bd1f2b8f799946"
60
60
  }
@@ -0,0 +1,31 @@
1
+ import { FieldDecoratorProps } from './FieldDecorator';
2
+
3
+ export function extractFieldDecoratorProps<T extends Partial<FieldDecoratorProps>>({
4
+ error,
5
+ required,
6
+ readonly,
7
+ label,
8
+ labelTooltip,
9
+ labelTooltipPlacement,
10
+ labelFor,
11
+ hint,
12
+ disabled,
13
+ showHintIcon,
14
+ size,
15
+ validationState,
16
+ }: T) {
17
+ return {
18
+ error,
19
+ required,
20
+ readonly,
21
+ label,
22
+ labelTooltip,
23
+ labelTooltipPlacement,
24
+ labelFor,
25
+ hint,
26
+ disabled,
27
+ showHintIcon,
28
+ size,
29
+ validationState,
30
+ };
31
+ }
@@ -1,19 +1,26 @@
1
1
  import cn from 'classnames';
2
2
  import mergeRefs from 'merge-refs';
3
- import { FocusEvent, forwardRef, KeyboardEvent, KeyboardEventHandler, useMemo, useRef, useState } from 'react';
3
+ import { FocusEvent, forwardRef, KeyboardEvent, KeyboardEventHandler, useMemo, useRef } from 'react';
4
4
 
5
5
  import { InputPrivate } from '@snack-uikit/input-private';
6
- import { Droplist, SelectionSingleValueType, useFuzzySearch } from '@snack-uikit/list';
6
+ import { BaseItemProps, Droplist, ItemProps, SelectionSingleValueType, useFuzzySearch } from '@snack-uikit/list';
7
7
  import { Tag } from '@snack-uikit/tag';
8
8
  import { extractSupportProps } from '@snack-uikit/utils';
9
9
 
10
10
  import { FieldContainerPrivate } from '../../helperComponents';
11
11
  import { useValueControl } from '../../hooks';
12
12
  import { FieldDecorator } from '../FieldDecorator';
13
+ import { extractFieldDecoratorProps } from '../FieldDecorator/utils';
13
14
  import { useButtons, useHandleDeleteItem, useHandleOnKeyDown, useSearchInput } from './hooks';
14
15
  import styles from './styles.module.scss';
15
- import { FieldSelectMultipleProps } from './types';
16
- import { extractSelectedMultipleOptions, getArrowIcon, transformOptionsToItems } from './utils';
16
+ import { FieldSelectMultipleProps, ItemWithId } from './types';
17
+ import {
18
+ extractListProps,
19
+ findSelectedOptions,
20
+ getArrowIcon,
21
+ mapOptionToAppearance,
22
+ transformOptionsToItems,
23
+ } from './utils';
17
24
 
18
25
  const BASE_MIN_WIDTH = 4;
19
26
 
@@ -28,24 +35,19 @@ export const FieldSelectMultiple = forwardRef<HTMLInputElement, FieldSelectMulti
28
35
  value: valueProp,
29
36
  defaultValue,
30
37
  onChange: onChangeProp,
31
- loading,
32
38
  disabled = false,
33
39
  readonly = false,
34
40
  searchable = true,
35
41
  showClearButton = true,
36
42
  onKeyDown: onInputKeyDownProp,
37
- label,
38
- labelTooltip,
39
- labelTooltipPlacement,
40
- required = false,
41
- hint,
42
- showHintIcon,
43
43
  validationState = 'default',
44
- footer,
45
44
  search,
46
45
  autocomplete = false,
47
46
  prefixIcon,
48
- error,
47
+ removeByBackspace = false,
48
+ addOptionByEnter = false,
49
+ open: openProp,
50
+ onOpenChange,
49
51
  ...rest
50
52
  },
51
53
  ref,
@@ -54,39 +56,56 @@ export const FieldSelectMultiple = forwardRef<HTMLInputElement, FieldSelectMulti
54
56
  const inputPlugRef = useRef<HTMLSpanElement>(null);
55
57
  const contentRef = useRef<HTMLDivElement>(null);
56
58
 
57
- const [open, setOpen] = useState<boolean>(false);
59
+ const [open = false, setOpen] = useValueControl<boolean>({ value: openProp, onChange: onOpenChange });
58
60
  const items = useMemo(() => transformOptionsToItems(options), [options]);
61
+ const mapItemsToTagAppearance = useMemo(() => mapOptionToAppearance(options), [options]);
62
+
59
63
  const [value, setValue] = useValueControl<SelectionSingleValueType[]>({
60
64
  value: valueProp,
61
65
  defaultValue,
62
66
  onChange: onChangeProp,
63
67
  });
64
68
 
65
- const selectedOption = useMemo(() => {
66
- const notSortSelectedOption = extractSelectedMultipleOptions(options, value);
69
+ const { selected, itemsWithPlaceholder, disabledSelected } = useMemo(() => {
70
+ const [notSortSelectedOption, placeholder] = findSelectedOptions(items, value);
67
71
 
68
- if (notSortSelectedOption) {
69
- return notSortSelectedOption.sort((a, b) => {
70
- if (b.disabled && !a.disabled) {
71
- return 1;
72
- }
72
+ const selectedWithPlaceholder =
73
+ notSortSelectedOption || placeholder ? (placeholder ?? []).concat(notSortSelectedOption ?? []) : undefined;
73
74
 
74
- if (a.disabled && !b.disabled) {
75
- return -1;
76
- }
75
+ const selected = selectedWithPlaceholder
76
+ ? selectedWithPlaceholder.sort((a, b) => {
77
+ if (b.disabled && !a.disabled) {
78
+ return 1;
79
+ }
77
80
 
78
- return 0;
79
- });
80
- }
81
- }, [options, value]);
81
+ if (a.disabled && !b.disabled) {
82
+ return -1;
83
+ }
84
+
85
+ return 0;
86
+ })
87
+ : undefined;
88
+
89
+ const placeholderItems: ItemProps[] = placeholder ? placeholder : [];
90
+
91
+ const disabledSelected = selected?.filter((item: ItemWithId) => item.disabled);
92
+
93
+ return {
94
+ selected,
95
+ disabledSelected,
96
+ itemsWithPlaceholder: placeholderItems.concat(items),
97
+ };
98
+ }, [items, value]);
82
99
 
83
100
  const { inputValue, onInputValueChange, prevInputValue } = useSearchInput({
84
101
  ...search,
85
- defaultValue: String(selectedOption ?? ''),
102
+ defaultValue: '',
86
103
  });
87
104
 
88
105
  const onClear = () => {
89
- setValue(undefined);
106
+ const disabledValues = disabledSelected?.map(item => item.id);
107
+
108
+ setValue(disabledValues);
90
109
  onInputValueChange('');
91
110
 
92
111
  localRef.current?.focus();
@@ -98,7 +117,8 @@ export const FieldSelectMultiple = forwardRef<HTMLInputElement, FieldSelectMulti
98
117
  const { buttons, inputKeyDownNavigationHandler, buttonsRefs } = useButtons({
99
118
  readonly,
100
119
  size,
101
- showClearButton: showClearButton && Boolean(value),
120
+ showClearButton:
121
+ showClearButton && !disabled && !readonly && (value?.length ?? 0) > (disabledSelected?.length ?? 0),
102
122
  showCopyButton: false,
103
123
  inputRef: localRef,
104
124
  onClear,
@@ -112,12 +132,23 @@ export const FieldSelectMultiple = forwardRef<HTMLInputElement, FieldSelectMulti
112
132
 
113
133
  const handleItemDelete = useHandleDeleteItem(setValue);
114
134
  const handleOnKeyDown = (onKeyDown?: KeyboardEventHandler<HTMLElement>) => (e: KeyboardEvent<HTMLInputElement>) => {
115
- if (e.code === 'Backspace' && inputValue === '') {
116
- if (selectedOption?.length && !selectedOption.slice(-1)[0].disabled) {
117
- handleItemDelete(selectedOption.pop())();
135
+ if (removeByBackspace && e.code === 'Backspace' && inputValue === '') {
136
+ if (selected?.length && !selected.slice(-1)[0].disabled) {
137
+ handleItemDelete(selected.pop() as BaseItemProps)();
118
138
  }
119
139
  }
120
140
 
141
+ if (e.code === 'Enter') {
142
+ e.stopPropagation();
143
+ e.preventDefault();
144
+ }
145
+
146
+ if (addOptionByEnter && e.code === 'Enter' && inputValue !== '') {
147
+ setValue((value: SelectionSingleValueType[]) => (value ?? []).concat(inputValue));
148
+ onInputValueChange('');
149
+ prevInputValue.current = '';
150
+ }
151
+
121
152
  if (!open && prevInputValue.current !== inputValue) {
122
153
  setOpen(true);
123
154
  }
@@ -128,11 +159,19 @@ export const FieldSelectMultiple = forwardRef<HTMLInputElement, FieldSelectMulti
128
159
  const handleOpenChange = (open: boolean) => {
129
160
  if (!readonly && !disabled && !buttonsRefs.includes(document.activeElement)) {
130
161
  setOpen(open);
162
+
131
163
  if (!open) {
132
- prevInputValue.current = inputValue;
164
+ onInputValueChange('');
165
+ prevInputValue.current = '';
166
+ if (inputPlugRef.current) {
167
+ inputPlugRef.current.style.width = BASE_MIN_WIDTH + 'px';
168
+ }
133
169
  }
170
+
134
171
  if (open) {
135
- prevInputValue.current = '';
172
+ if (inputPlugRef.current) {
173
+ inputPlugRef.current.style.width = 'unset';
174
+ }
136
175
  }
137
176
  }
138
177
  };
@@ -145,43 +184,33 @@ export const FieldSelectMultiple = forwardRef<HTMLInputElement, FieldSelectMulti
145
184
  rest?.onBlur?.(e);
146
185
  };
147
186
 
148
- const fuzzySearch = useFuzzySearch(items);
149
- const result = autocomplete ? items : fuzzySearch(prevInputValue.current !== inputValue ? inputValue : '');
187
+ const fuzzySearch = useFuzzySearch(itemsWithPlaceholder);
188
+ const result =
189
+ autocomplete || !searchable || prevInputValue.current === inputValue
190
+ ? itemsWithPlaceholder
191
+ : fuzzySearch(inputValue);
150
192
 
151
193
  return (
152
194
  <FieldDecorator
153
195
  {...extractSupportProps(rest)}
154
- error={error}
155
- required={required}
156
- readonly={readonly}
157
- label={label}
158
- labelTooltip={labelTooltip}
159
- labelTooltipPlacement={labelTooltipPlacement}
196
+ {...extractFieldDecoratorProps(rest)}
160
197
  labelFor={id}
161
- hint={hint}
162
- disabled={disabled}
163
- showHintIcon={showHintIcon}
164
198
  size={size}
165
199
  validationState={validationState}
166
200
  >
167
201
  <Droplist
168
- trigger='clickAndFocusVisible'
169
- placement='bottom'
170
- data-test-id='field-select__list'
202
+ {...extractListProps(rest)}
171
203
  items={result}
172
204
  triggerElemRef={localRef}
173
- scroll
174
- marker
175
- footer={footer}
176
205
  selection={{
177
206
  mode: 'multiple',
178
207
  value: value,
179
208
  onChange: setValue,
180
209
  }}
210
+ dataFiltered={rest.dataFiltered ?? Boolean(inputValue.length)}
181
211
  size={size}
182
212
  open={!disabled && !readonly && open}
183
213
  onOpenChange={handleOpenChange}
184
- loading={loading}
185
214
  >
186
215
  {({ onKeyDown }) => (
187
216
  <FieldContainerPrivate
@@ -197,14 +226,15 @@ export const FieldSelectMultiple = forwardRef<HTMLInputElement, FieldSelectMulti
197
226
  >
198
227
  <>
199
228
  <div className={styles.contentWrapper} ref={contentRef}>
200
- {selectedOption &&
201
- selectedOption.map(option => (
229
+ {selected &&
230
+ selected.map(option => (
202
231
  <Tag
203
232
  size={size === 'l' ? 's' : 'xs'}
204
233
  tabIndex={-1}
205
- label={String(option.option)}
206
- key={option.value}
207
- onDelete={!option.disabled && !disabled ? handleItemDelete(option) : undefined}
234
+ label={String(option.content.option)}
235
+ key={option.id}
236
+ appearance={option.id ? mapItemsToTagAppearance[option?.id] : 'neutral'}
237
+ onDelete={!option.disabled && !disabled && !readonly ? handleItemDelete(option) : undefined}
208
238
  />
209
239
  ))}
210
240
 
@@ -224,7 +254,7 @@ export const FieldSelectMultiple = forwardRef<HTMLInputElement, FieldSelectMulti
224
254
  name={name}
225
255
  type='text'
226
256
  disabled={disabled}
227
- placeholder={!selectedOption ? placeholder : undefined}
257
+ placeholder={!selected || !selected.length ? placeholder : undefined}
228
258
  ref={mergeRefs(ref, localRef)}
229
259
  onChange={searchable ? onInputValueChange : undefined}
230
260
  value={searchable ? inputValue : ''}