@true-engineering/true-react-common-ui-kit 1.9.0 → 1.10.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 (31) hide show
  1. package/dist/components/Input/Input.styles.d.ts +1 -0
  2. package/dist/components/Select/Select.d.ts +12 -3
  3. package/dist/components/Select/Select.styles.d.ts +10 -0
  4. package/dist/components/Select/SelectList/SelectList.d.ts +6 -4
  5. package/dist/components/Select/SelectList/SelectList.styles.d.ts +5 -0
  6. package/dist/components/Select/SelectListItem/SelectListItem.d.ts +14 -0
  7. package/dist/components/Select/SelectListItem/SelectListItem.styles.d.ts +2 -0
  8. package/dist/components/Select/constants.d.ts +2 -0
  9. package/dist/components/Select/helpers.d.ts +4 -1
  10. package/dist/components/Select/index.d.ts +1 -0
  11. package/dist/components/Select/types.d.ts +1 -0
  12. package/dist/helpers/utils.d.ts +2 -0
  13. package/dist/true-react-common-ui-kit.js +340 -163
  14. package/dist/true-react-common-ui-kit.js.map +1 -1
  15. package/dist/true-react-common-ui-kit.umd.cjs +340 -163
  16. package/dist/true-react-common-ui-kit.umd.cjs.map +1 -1
  17. package/package.json +1 -1
  18. package/src/components/Input/Input.styles.ts +2 -0
  19. package/src/components/Input/Input.tsx +4 -1
  20. package/src/components/Select/MultiSelect.stories.tsx +262 -0
  21. package/src/components/Select/Select.styles.ts +13 -0
  22. package/src/components/Select/Select.tsx +215 -114
  23. package/src/components/Select/SelectList/SelectList.styles.ts +6 -2
  24. package/src/components/Select/SelectList/SelectList.tsx +64 -39
  25. package/src/components/Select/SelectListItem/SelectListItem.styles.ts +14 -0
  26. package/src/components/Select/SelectListItem/SelectListItem.tsx +73 -0
  27. package/src/components/Select/constants.ts +2 -0
  28. package/src/components/Select/helpers.ts +16 -8
  29. package/src/components/Select/index.ts +1 -0
  30. package/src/components/Select/types.ts +1 -0
  31. package/src/helpers/utils.ts +29 -0
@@ -26,20 +26,30 @@ import {
26
26
  useTweakStyles,
27
27
  } from '../../hooks';
28
28
  import { IDropdownWithPopperOptions } from '../../types';
29
- import { getTestId, hasExactParent, isNotEmpty } from '../../helpers';
29
+ import {
30
+ createFilter,
31
+ getTestId,
32
+ hasExactParent,
33
+ isNotEmpty,
34
+ isStringNotEmpty,
35
+ } from '../../helpers';
30
36
  import {
31
37
  defaultConvertFunction,
32
38
  defaultCompareFunction,
33
- getActiveValueIndex,
34
39
  defaultIsOptionDisabled,
40
+ getDefaultConvertToIdFunction,
41
+ isMultiSelectValue,
35
42
  } from './helpers';
36
43
  import { SelectStyles, styles } from './Select.styles';
37
44
  import { ISearchInputProps, SearchInput } from '../SearchInput';
45
+ import { IMultipleSelectValue } from './types';
46
+ import { ALL_OPTION_INDEX, DEFAULT_OPTION_INDEX } from './constants';
38
47
 
39
48
  export interface ISelectProps<Value>
40
49
  extends Omit<IInputProps, 'value' | 'onChange' | 'onBlur' | 'type'> {
41
50
  tweakStyles?: SelectStyles;
42
51
  defaultOptionLabel?: string;
52
+ allOptionsLabel?: string;
43
53
  noMatchesLabel?: string;
44
54
  loadingLabel?: ReactNode;
45
55
  optionsMode?: 'search' | 'dynamic' | 'normal';
@@ -50,17 +60,18 @@ export interface ISelectProps<Value>
50
60
  options: Value[];
51
61
  value: Value | undefined;
52
62
  shouldScrollToList?: boolean;
63
+ isMultiSelect?: boolean;
53
64
  searchInput?: { shouldRenderInList: true } & Pick<
54
65
  ISearchInputProps,
55
66
  'placeholder'
56
67
  >;
57
68
  isOptionDisabled?(option: Value): boolean;
58
- onChange(value: Value | undefined): void; // подумать как возвращать индекс
69
+ onChange(value?: Value): void; // подумать как возвращать индекс
59
70
  onBlur?(event: Event | SyntheticEvent): void;
60
71
  onType?(value: string): Promise<void>;
61
72
  optionsFilter?(options: Value[], query: string): Value[];
62
73
  onOpen?(): void;
63
- compareValuesOnChange?(v1: Value | undefined, v2: Value | undefined): boolean;
74
+ compareValuesOnChange?(v1?: Value, v2?: Value): boolean;
64
75
  // Для избежания проблем юзайте useCallback на эти функции
65
76
  // или выносите их из компонента (чтобы не было сайдэфектов от перерендеринга их)
66
77
  convertValueToString?(value: Value): string | undefined;
@@ -68,45 +79,64 @@ export interface ISelectProps<Value>
68
79
  convertValueToId?(value: Value): string | undefined;
69
80
  }
70
81
 
71
- export function Select<Value>({
72
- options,
73
- value,
74
- defaultOptionLabel,
75
- debounceTime = 400,
76
- optionsMode = 'normal',
77
- noMatchesLabel,
78
- loadingLabel,
79
- tweakStyles,
80
- testId,
81
- isReadonly,
82
- isDisabled,
83
- dropdownOptions,
84
- minSymbolsCountToOpenList = 0,
85
- dropdownIcon = 'chevron-down',
86
- shouldScrollToList = true,
87
- searchInput,
88
- onChange,
89
- onFocus,
90
- onBlur,
91
- onType,
92
- onOpen,
93
- isOptionDisabled = defaultIsOptionDisabled,
94
- compareValuesOnChange = defaultCompareFunction,
95
- convertValueToString = defaultConvertFunction,
96
- convertValueToId,
97
- convertValueToReactNode,
98
- optionsFilter,
99
- ...inputProps
100
- }: ISelectProps<Value>): JSX.Element {
82
+ export interface IMultipleSelectProps<Value>
83
+ extends Omit<
84
+ ISelectProps<Value>,
85
+ 'value' | 'onChange' | 'compareValuesOnChange'
86
+ > {
87
+ isMultiSelect: true;
88
+ value: IMultipleSelectValue<Value> | undefined;
89
+ onChange(value?: IMultipleSelectValue<Value>): void;
90
+ compareValuesOnChange?(
91
+ v1?: IMultipleSelectValue<Value>,
92
+ v2?: IMultipleSelectValue<Value>,
93
+ ): boolean;
94
+ }
95
+
96
+ export function Select<Value>(
97
+ props: ISelectProps<Value> | IMultipleSelectProps<Value>,
98
+ ): JSX.Element {
99
+ const {
100
+ options,
101
+ value,
102
+ defaultOptionLabel,
103
+ allOptionsLabel,
104
+ debounceTime = 400,
105
+ optionsMode = 'normal',
106
+ noMatchesLabel,
107
+ loadingLabel,
108
+ tweakStyles,
109
+ testId,
110
+ isReadonly,
111
+ isDisabled,
112
+ dropdownOptions,
113
+ minSymbolsCountToOpenList = 0,
114
+ dropdownIcon = 'chevron-down',
115
+ shouldScrollToList = true,
116
+ searchInput,
117
+ units,
118
+ onChange,
119
+ onFocus,
120
+ onBlur,
121
+ onType,
122
+ onOpen,
123
+ isOptionDisabled = defaultIsOptionDisabled,
124
+ compareValuesOnChange = defaultCompareFunction,
125
+ convertValueToString = defaultConvertFunction,
126
+ convertValueToId,
127
+ convertValueToReactNode,
128
+ optionsFilter,
129
+ ...inputProps
130
+ } = props;
101
131
  const { classes, componentStyles } = useTheme('Select', styles, tweakStyles);
102
132
  const isMounted = useIsMounted();
103
133
  const [isListOpen, setIsListOpen] = useState(false);
104
134
  const [areOptionsLoading, setAreOptionsLoading] = useState(false);
105
- const hasDefaultOption = defaultOptionLabel !== undefined;
135
+ const hasDefaultOption = isStringNotEmpty(defaultOptionLabel);
106
136
 
107
- const [focusedListCellIndex, setFocusedListCellIndex] = useState(-1);
137
+ const [focusedListCellIndex, setFocusedListCellIndex] =
138
+ useState(DEFAULT_OPTION_INDEX);
108
139
  const [searchValue, setSearchValue] = useState('');
109
-
110
140
  // если мы ввели что то в строку поиска - то этот булеан будет отключаться
111
141
  // вынесен отдельно, из-за проблем с дебаунсом при динамич. опциях
112
142
  const [shouldShowDefaultOption, setShouldShowDefaultOption] = useState(true);
@@ -117,36 +147,62 @@ export function Select<Value>({
117
147
 
118
148
  const shouldRenderSearchInputInList =
119
149
  searchInput?.shouldRenderInList === true;
120
-
121
150
  const hasSearchInputInList =
122
151
  optionsMode !== 'normal' && shouldRenderSearchInputInList;
123
152
 
124
- const stringValue = isNotEmpty(value)
125
- ? convertValueToString(value)
126
- : undefined;
153
+ const isMultiSelect = isMultiSelectValue(props, value);
154
+ const strValue = isMultiSelect ? value?.[0] : value;
155
+ const shouldShowAllOption =
156
+ isMultiSelect && isNotEmpty(allOptionsLabel) && searchValue === '';
127
157
 
128
158
  const filteredOptions = useMemo(() => {
129
159
  if (optionsMode !== 'search') {
130
160
  return options;
131
161
  }
132
- if (isNotEmpty(optionsFilter)) {
133
- return optionsFilter(options, searchValue);
134
- }
135
- const lowerCaseValue = searchValue?.toLowerCase();
136
- return options.filter((option) => {
137
- const convertedOption = convertValueToString(option);
138
- return (
139
- convertedOption !== undefined &&
140
- convertedOption.toLowerCase().includes(lowerCaseValue)
141
- );
142
- });
143
- }, [optionsMode, optionsFilter, options, convertValueToString, searchValue]);
144
162
 
145
- useEffect(() => {
146
- setFocusedListCellIndex(
147
- getActiveValueIndex(filteredOptions, value, convertValueToString),
163
+ const filter =
164
+ optionsFilter ??
165
+ createFilter<Value>((option) => [convertValueToString(option) ?? '']);
166
+
167
+ return filter(options, searchValue);
168
+ }, [optionsFilter, options, convertValueToString, searchValue, optionsMode]);
169
+
170
+ const optionsIndexesForNavigation = useMemo(() => {
171
+ const result: number[] = [];
172
+ if (shouldShowDefaultOption && hasDefaultOption) {
173
+ result.push(DEFAULT_OPTION_INDEX);
174
+ }
175
+ if (shouldShowAllOption) {
176
+ result.push(ALL_OPTION_INDEX);
177
+ }
178
+ return result.concat(
179
+ filteredOptions.reduce((acc, cur, i) => {
180
+ if (!isOptionDisabled(cur)) {
181
+ acc.push(i);
182
+ }
183
+ return acc;
184
+ }, [] as number[]),
148
185
  );
149
- }, [filteredOptions, value, convertValueToString]);
186
+ }, [filteredOptions]);
187
+
188
+ const stringValue = isNotEmpty(strValue)
189
+ ? convertValueToString(strValue)
190
+ : undefined;
191
+ // Для мультиселекта пытаемся показать "Все опции" если выбраны все опции
192
+ const showedStringValue =
193
+ isMultiSelect &&
194
+ value?.length === filteredOptions.length &&
195
+ isNotEmpty(allOptionsLabel)
196
+ ? allOptionsLabel
197
+ : stringValue;
198
+
199
+ const convertToId = useCallback(
200
+ (v: Value) =>
201
+ (convertValueToId ?? getDefaultConvertToIdFunction(convertValueToString))(
202
+ v,
203
+ ),
204
+ [convertValueToId, convertValueToString],
205
+ );
150
206
 
151
207
  const handleListClose = useCallback(
152
208
  (event: Event | SyntheticEvent) => {
@@ -200,25 +256,57 @@ export function Select<Value>({
200
256
  };
201
257
 
202
258
  const handleOnChange = useCallback(
203
- (newValue: Value | undefined) => {
204
- const areValuesEqual = compareValuesOnChange(value, newValue);
205
- if (areValuesEqual) {
206
- return;
259
+ (newValue: Value | IMultipleSelectValue<Value> | undefined) => {
260
+ // Тут беда с типами, сорри
261
+ if (!compareValuesOnChange(value as never, newValue as never)) {
262
+ onChange(newValue as (Value & IMultipleSelectValue<Value>) | undefined);
207
263
  }
208
- onChange(newValue);
209
264
  },
210
265
  [value, compareValuesOnChange, onChange],
211
266
  );
212
267
 
213
268
  const handleOptionSelect = useCallback(
214
269
  (index: number, event: MouseEvent<HTMLElement> | KeyboardEvent) => {
215
- handleOnChange(index === -1 ? undefined : filteredOptions[index]);
270
+ handleOnChange(
271
+ index === DEFAULT_OPTION_INDEX ? undefined : filteredOptions[index],
272
+ );
216
273
  handleListClose(event);
217
274
  input.current?.blur();
218
275
  },
219
276
  [handleOnChange, handleListClose, filteredOptions],
220
277
  );
221
278
 
279
+ // MultiSelect
280
+ const handleToggleOptionCheckbox = useCallback(
281
+ (index: number, isSelected: boolean) => {
282
+ if (!isMultiSelect) {
283
+ return;
284
+ }
285
+
286
+ // Если выбрана не дефолтная опция, которая сетит андеф
287
+ if (
288
+ index === DEFAULT_OPTION_INDEX ||
289
+ (index === ALL_OPTION_INDEX && !isSelected)
290
+ ) {
291
+ handleOnChange(undefined);
292
+ return;
293
+ }
294
+ if (index === ALL_OPTION_INDEX && isSelected) {
295
+ handleOnChange(options as IMultipleSelectValue<Value>);
296
+ return;
297
+ }
298
+ const option = filteredOptions[index];
299
+ handleOnChange(
300
+ isSelected
301
+ ? // Добавляем
302
+ ([...(value ?? []), option] as IMultipleSelectValue<Value>)
303
+ : // Убираем
304
+ value?.filter((o) => convertToId(o) !== convertToId(option)),
305
+ );
306
+ },
307
+ [handleOnChange, filteredOptions, isMultiSelect, value],
308
+ );
309
+
222
310
  const handleOnType = useCallback(
223
311
  async (v: string) => {
224
312
  if (onType === undefined) {
@@ -265,71 +353,65 @@ export function Select<Value>({
265
353
  }
266
354
 
267
355
  event.stopPropagation();
356
+ const curIndexInNavigation = optionsIndexesForNavigation.findIndex(
357
+ (index) => index === focusedListCellIndex,
358
+ );
268
359
 
269
360
  switch (event.code) {
270
361
  case 'Enter':
271
362
  case 'NumpadEnter': {
272
- let indexToClick = focusedListCellIndex;
363
+ let indexToSelect = focusedListCellIndex;
273
364
 
274
- // если осталось одна опция в списке,
365
+ // если осталась одна опция в списке,
275
366
  // то выбираем ее нажатием на enter
276
- if (indexToClick === -1 && filteredOptions.length === 1) {
277
- indexToClick = 0;
367
+ if (
368
+ indexToSelect === DEFAULT_OPTION_INDEX &&
369
+ filteredOptions.length === 1
370
+ ) {
371
+ indexToSelect = 0;
278
372
  }
279
373
 
280
- handleOptionSelect(indexToClick, event);
374
+ if (isMultiSelect) {
375
+ let isThisValueAlreadySelected: boolean;
376
+ if (indexToSelect === ALL_OPTION_INDEX) {
377
+ isThisValueAlreadySelected = value?.length === options.length;
378
+ } else {
379
+ // подумать над концептом реального фокуса на опциях, а не вот эти вот focusedCell
380
+ const valueIdToSelect = convertToId(filteredOptions[indexToSelect]);
381
+ isThisValueAlreadySelected =
382
+ value?.some((opt) => convertToId(opt) === valueIdToSelect) ??
383
+ false;
384
+ }
385
+ handleToggleOptionCheckbox(
386
+ indexToSelect,
387
+ !isThisValueAlreadySelected,
388
+ );
389
+ } else {
390
+ handleOptionSelect(indexToSelect, event);
391
+ }
281
392
  break;
282
393
  }
283
394
 
284
395
  case 'ArrowDown': {
285
396
  // чтобы убрать перемещение курсора в инпуте
286
397
  event.preventDefault();
287
- let newIndex: number = focusedListCellIndex ?? -1;
288
- // ищем первый незадизейбленный вариант, либо дефолтную опцию
289
- for (let i = newIndex; i < newIndex + filteredOptions.length; i++) {
290
- const targetIndex = (i + 1) % filteredOptions.length;
291
- if (
292
- shouldShowDefaultOption &&
293
- hasDefaultOption &&
294
- newIndex > -1 &&
295
- targetIndex === 0
296
- ) {
297
- newIndex = -1;
298
- break;
299
- }
300
- if (!isOptionDisabled(filteredOptions[targetIndex])) {
301
- newIndex = targetIndex;
302
- break;
303
- }
304
- }
305
- setFocusedListCellIndex(newIndex);
398
+ const targetIndexInNavigation =
399
+ (curIndexInNavigation + 1) % optionsIndexesForNavigation.length;
400
+ setFocusedListCellIndex(
401
+ optionsIndexesForNavigation[targetIndexInNavigation],
402
+ );
306
403
  break;
307
404
  }
308
405
 
309
406
  case 'ArrowUp': {
310
407
  // чтобы убрать перемещение курсора в инпуте
311
408
  event.preventDefault();
312
- const newIndex: number =
313
- (focusedListCellIndex ?? -1) === -1 ? 0 : focusedListCellIndex;
314
- // ищем первый незадизейбленный вариант, либо дефолтную опцию
315
- for (let i = newIndex; i > newIndex - filteredOptions.length; i--) {
316
- const targetIndex = (i > 0 ? i : filteredOptions.length + i) - 1;
317
- if (
318
- shouldShowDefaultOption &&
319
- hasDefaultOption &&
320
- focusedListCellIndex !== -1 &&
321
- targetIndex === filteredOptions.length - 1
322
- ) {
323
- // не выносить сет наружу (приведет к багу)
324
- setFocusedListCellIndex(-1);
325
- break;
326
- }
327
- if (!isOptionDisabled(filteredOptions[targetIndex])) {
328
- // не выносить сет наружу (приведет к багу)
329
- setFocusedListCellIndex(targetIndex);
330
- break;
331
- }
332
- }
409
+ const targetIndexInNavigation =
410
+ (curIndexInNavigation - 1 + optionsIndexesForNavigation.length) %
411
+ optionsIndexesForNavigation.length;
412
+ setFocusedListCellIndex(
413
+ optionsIndexesForNavigation[targetIndexInNavigation],
414
+ );
333
415
  break;
334
416
  }
335
417
  }
@@ -405,10 +487,17 @@ export function Select<Value>({
405
487
  });
406
488
 
407
489
  useEffect(() => {
408
- if (isOpen && onOpen !== undefined) {
409
- onOpen();
490
+ const val = isMultiSelect ? value?.[0] : value;
491
+ setFocusedListCellIndex(
492
+ optionsIndexesForNavigation.find(
493
+ (index) => filteredOptions[index] === val,
494
+ ) ?? optionsIndexesForNavigation[0],
495
+ );
496
+
497
+ if (isOpen) {
498
+ onOpen?.();
410
499
  }
411
- }, [isOpen, onOpen]);
500
+ }, [isOpen]);
412
501
 
413
502
  const listEl = (
414
503
  <div
@@ -429,6 +518,7 @@ export function Select<Value>({
429
518
  ? defaultOptionLabel
430
519
  : undefined
431
520
  }
521
+ allOptionsLabel={shouldShowAllOption ? allOptionsLabel : undefined}
432
522
  customListHeader={
433
523
  hasSearchInputInList ? (
434
524
  <SearchInput
@@ -454,8 +544,11 @@ export function Select<Value>({
454
544
  isOptionDisabled={isOptionDisabled}
455
545
  convertValueToString={convertValueToString}
456
546
  convertValueToReactNode={convertValueToReactNode}
457
- convertValueToId={convertValueToId}
458
- onOptionClick={handleOptionSelect}
547
+ convertValueToId={convertToId}
548
+ onOptionSelect={handleOptionSelect}
549
+ onToggleCheckbox={
550
+ isMultiSelect ? handleToggleOptionCheckbox : undefined
551
+ }
459
552
  />
460
553
  )}
461
554
  </div>
@@ -472,7 +565,7 @@ export function Select<Value>({
472
565
  value={
473
566
  searchValue !== '' && !shouldRenderSearchInputInList
474
567
  ? searchValue
475
- : stringValue
568
+ : showedStringValue
476
569
  }
477
570
  onChange={handleInputChange}
478
571
  isActive={isListOpen}
@@ -484,6 +577,14 @@ export function Select<Value>({
484
577
  isLoading={areOptionsLoading}
485
578
  tweakStyles={tweakInputStyles}
486
579
  testId={testId}
580
+ units={
581
+ isMultiSelect &&
582
+ isNotEmpty(value) &&
583
+ value.length > 1 &&
584
+ value.length !== options.length
585
+ ? `(+${value.length - 1})`
586
+ : units
587
+ }
487
588
  {...inputProps}
488
589
  />
489
590
  <div
@@ -2,8 +2,8 @@ import { colors, dimensions, helpers } from '../../../theme';
2
2
  import { ComponentStyles } from '../../../types';
3
3
 
4
4
  export const ROW_HEIGHT = 40;
5
- const CONTAINER_PADDING = 10;
6
- const CELL_PADDING = [10, 20];
5
+ export const CONTAINER_PADDING = 10;
6
+ export const CELL_PADDING = [10, 20];
7
7
 
8
8
  export const styles = {
9
9
  root: {
@@ -45,6 +45,10 @@ export const styles = {
45
45
  fontSize: 14,
46
46
  },
47
47
 
48
+ cellWithCheckbox: {
49
+ padding: 0,
50
+ },
51
+
48
52
  noMatchesLabel: {
49
53
  pointerEvents: 'none',
50
54
  },
@@ -3,30 +3,33 @@ import clsx from 'clsx';
3
3
  import { ScrollIntoViewIfNeeded } from '../../ScrollIntoViewIfNeeded';
4
4
  import { useTheme } from '../../../hooks';
5
5
  import { ICommonProps } from '../../../types';
6
- import { addDataAttributes, isNotEmpty } from '../../../helpers';
6
+ import { isNotEmpty } from '../../../helpers';
7
7
  import { SelectListStyles, styles } from './SelectList.styles';
8
+ import { IMultipleSelectValue } from '../types';
9
+ import { SelectListItem } from '../SelectListItem/SelectListItem';
10
+ import { ALL_OPTION_INDEX, DEFAULT_OPTION_INDEX } from '../constants';
8
11
 
9
12
  export interface ISelectListProps<Value> extends ICommonProps {
10
13
  tweakStyles?: SelectListStyles;
11
14
  options: Value[];
12
15
  focusedIndex?: number;
13
- activeValue?: Value;
16
+ activeValue?: Value | Value[];
14
17
  noMatchesLabel?: string;
15
18
  isLoading?: boolean;
16
19
  loadingLabel?: ReactNode;
17
20
  defaultOptionLabel?: string;
18
21
  testId?: string;
22
+ allOptionsLabel?: string;
19
23
  shouldScrollToList?: boolean;
20
24
  customListHeader?: ReactNode;
21
- onOptionClick(index: number, event: MouseEvent<HTMLElement>): void;
25
+ onOptionSelect(index: number, event: MouseEvent<HTMLElement>): void;
26
+ onToggleCheckbox?(index: number, isSelected: boolean): void;
22
27
  isOptionDisabled(value: Value): boolean;
23
28
  convertValueToString(value: Value): string | undefined;
24
29
  convertValueToReactNode?(value: Value, isDisabled: boolean): ReactNode;
25
- convertValueToId?(value: Value): string | undefined;
30
+ convertValueToId(value: Value): string | undefined;
26
31
  }
27
32
 
28
- const DEFAULT_OPTION_INDEX = -1;
29
-
30
33
  export function SelectList<Value>({
31
34
  options,
32
35
  focusedIndex,
@@ -40,20 +43,25 @@ export function SelectList<Value>({
40
43
  shouldScrollToList = true,
41
44
  customListHeader,
42
45
  isOptionDisabled,
43
- onOptionClick,
46
+ allOptionsLabel,
47
+ onOptionSelect,
48
+ onToggleCheckbox,
44
49
  convertValueToString,
45
- convertValueToReactNode,
46
- convertValueToId = convertValueToString,
50
+ convertValueToReactNode = convertValueToString,
51
+ convertValueToId,
47
52
  }: ISelectListProps<Value>): JSX.Element {
48
53
  const { classes } = useTheme('SelectList', styles, tweakStyles);
49
- const activeValueId = isNotEmpty(activeValue)
50
- ? convertValueToId(activeValue)
51
- : undefined;
52
-
53
- const isActiveOption = (item: Value): boolean =>
54
- convertValueToId(item) === activeValueId;
54
+ const isMultiSelect = isNotEmpty(onToggleCheckbox);
55
+ const multiSelectValue = activeValue as
56
+ | IMultipleSelectValue<Value>
57
+ | undefined;
58
+ const selectedOptionsCount = multiSelectValue?.length ?? 0;
55
59
 
56
- const convertFunction = convertValueToReactNode ?? convertValueToString;
60
+ // MultiSelect
61
+ const activeOptionsIdMap = useMemo(
62
+ () => (isMultiSelect ? multiSelectValue?.map(convertValueToId) ?? [] : []),
63
+ [isMultiSelect, multiSelectValue, convertValueToId],
64
+ );
57
65
 
58
66
  const optionsDisableMap = useMemo(
59
67
  () => options.map((o) => isOptionDisabled(o)),
@@ -61,13 +69,22 @@ export function SelectList<Value>({
61
69
  );
62
70
 
63
71
  const listOptions = useMemo(
64
- () => options.map((opt, i) => convertFunction(opt, optionsDisableMap[i])),
65
- [options, convertFunction, optionsDisableMap],
72
+ () =>
73
+ options.map((opt, i) =>
74
+ convertValueToReactNode(opt, optionsDisableMap[i]),
75
+ ),
76
+ [options, convertValueToReactNode, optionsDisableMap],
66
77
  );
67
78
 
79
+ const isActiveOption = (item: Value): boolean =>
80
+ isMultiSelect
81
+ ? activeOptionsIdMap.includes(convertValueToId(item))
82
+ : isNotEmpty(activeValue) &&
83
+ convertValueToId(activeValue as Value) === convertValueToId(item);
84
+
68
85
  return (
69
86
  <ScrollIntoViewIfNeeded
70
- active={shouldScrollToList}
87
+ active={shouldScrollToList && !isMultiSelect}
71
88
  className={clsx(classes.root, {
72
89
  [classes.withListHeader]: isNotEmpty(customListHeader),
73
90
  })}
@@ -82,7 +99,7 @@ export function SelectList<Value>({
82
99
  </div>
83
100
  ) : (
84
101
  <>
85
- {defaultOptionLabel !== undefined && (
102
+ {isNotEmpty(defaultOptionLabel) && (
86
103
  <ScrollIntoViewIfNeeded
87
104
  active={focusedIndex === DEFAULT_OPTION_INDEX}
88
105
  options={{ block: 'nearest' }}
@@ -91,39 +108,47 @@ export function SelectList<Value>({
91
108
  classes.defaultCell,
92
109
  focusedIndex === DEFAULT_OPTION_INDEX && classes.focused,
93
110
  )}
94
- onClick={(event) => onOptionClick(DEFAULT_OPTION_INDEX, event)}
111
+ onClick={(event) => onOptionSelect(DEFAULT_OPTION_INDEX, event)}
95
112
  >
96
113
  {defaultOptionLabel}
97
114
  </ScrollIntoViewIfNeeded>
98
115
  )}
116
+ {isNotEmpty(allOptionsLabel) && (
117
+ <SelectListItem
118
+ classes={classes}
119
+ index={ALL_OPTION_INDEX}
120
+ isSemiChecked={
121
+ selectedOptionsCount > 0 &&
122
+ selectedOptionsCount < options.length
123
+ }
124
+ isActive={selectedOptionsCount === options.length}
125
+ isFocused={focusedIndex === ALL_OPTION_INDEX}
126
+ onOptionSelect={onOptionSelect}
127
+ onToggleCheckbox={onToggleCheckbox}
128
+ >
129
+ {allOptionsLabel}
130
+ </SelectListItem>
131
+ )}
99
132
  {listOptions.map((opt, i) => {
100
133
  const optionValue = options[i];
101
- const isFocused = i === focusedIndex;
134
+ const isFocused = focusedIndex === i;
102
135
  const isActive = isActiveOption(optionValue);
103
136
  // проверяем, что опция задизейблена
104
137
  const isDisabled = optionsDisableMap[i];
105
138
 
106
139
  return (
107
- <ScrollIntoViewIfNeeded
108
- active={isFocused}
109
- options={{ block: 'nearest' }}
140
+ <SelectListItem
110
141
  key={i}
111
- className={clsx(classes.cell, {
112
- [classes.focused]: isFocused,
113
- [classes.active]: isActive,
114
- [classes.disabled]: isDisabled,
115
- })}
116
- {...addDataAttributes({
117
- disabled: isDisabled,
118
- active: isActive,
119
- focused: isFocused,
120
- })}
121
- onClick={
122
- !isDisabled ? (event) => onOptionClick(i, event) : undefined
123
- }
142
+ classes={classes}
143
+ index={i}
144
+ isDisabled={isDisabled}
145
+ isActive={isActive}
146
+ isFocused={isFocused}
147
+ onOptionSelect={onOptionSelect}
148
+ onToggleCheckbox={onToggleCheckbox}
124
149
  >
125
150
  {opt}
126
- </ScrollIntoViewIfNeeded>
151
+ </SelectListItem>
127
152
  );
128
153
  })}
129
154
  {listOptions.length === 0 && (