@true-engineering/true-react-common-ui-kit 3.24.1 → 3.25.1

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 (100) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +21 -0
  3. package/dist/components/Select/CustomSelect.stories.d.ts +1 -1
  4. package/dist/components/Select/MultiSelect.stories.d.ts +2 -2
  5. package/dist/components/Select/Select.d.ts +14 -9
  6. package/dist/components/Select/Select.styles.d.ts +5 -5
  7. package/dist/components/Select/components/SelectList/SelectList.d.ts +7 -6
  8. package/dist/components/Select/components/SelectList/SelectList.styles.d.ts +1 -1
  9. package/dist/components/Select/components/SelectListItem/SelectListItem.d.ts +4 -3
  10. package/dist/components/Select/helpers.d.ts +0 -3
  11. package/dist/true-react-common-ui-kit.js +176 -142
  12. package/dist/true-react-common-ui-kit.js.map +1 -1
  13. package/dist/true-react-common-ui-kit.umd.cjs +175 -141
  14. package/dist/true-react-common-ui-kit.umd.cjs.map +1 -1
  15. package/package.json +1 -1
  16. package/src/components/AccountInfo/AccountInfo.stories.tsx +32 -32
  17. package/src/components/AccountInfo/AccountInfo.tsx +80 -80
  18. package/src/components/AddButton/AddButton.stories.tsx +21 -21
  19. package/src/components/AddButton/AddButton.tsx +52 -52
  20. package/src/components/Button/Button.tsx +129 -129
  21. package/src/components/Colors/Colors.stories.tsx +7 -7
  22. package/src/components/DateInput/DateInput.tsx +90 -90
  23. package/src/components/DateInput/constants.ts +2 -2
  24. package/src/components/Description/Description.stories.tsx +27 -27
  25. package/src/components/Description/Description.tsx +61 -61
  26. package/src/components/FiltersPane/components/FilterValueView/FilterValueView.tsx +166 -166
  27. package/src/components/FiltersPane/components/FilterWithDates/FilterWithDates.tsx +210 -210
  28. package/src/components/FiltersPane/components/FilterWithPeriod/FilterWithPeriod.tsx +177 -177
  29. package/src/components/Flag/Flag.stories.tsx +29 -29
  30. package/src/components/Flag/Flag.tsx +26 -26
  31. package/src/components/Flag/augment.d.ts +1 -1
  32. package/src/components/FlexibleTable/components/FlexibleTableCell/FlexibleTableCell.styles.ts +38 -38
  33. package/src/components/FlexibleTable/components/FlexibleTableRow/FlexibleTableRow.styles.ts +25 -25
  34. package/src/components/FlexibleTable/helpers.ts +13 -13
  35. package/src/components/Icon/Icon.stories.tsx +86 -86
  36. package/src/components/Icon/complexIcons/augment.d.ts +1 -1
  37. package/src/components/Icon/complexIcons/avatarGreen.svg +57 -57
  38. package/src/components/Icon/complexIcons/index.ts +1 -1
  39. package/src/components/IncrementInput/IncrementInput.tsx +105 -105
  40. package/src/components/Input/Input.tsx +297 -297
  41. package/src/components/Input/types.ts +32 -32
  42. package/src/components/List/List.stories.tsx +70 -70
  43. package/src/components/List/List.tsx +33 -33
  44. package/src/components/List/components/ListItem/ListItem.tsx +57 -57
  45. package/src/components/Modal/Modal.stories.tsx +105 -105
  46. package/src/components/MultiSelect/MultiSelect.stories.tsx +46 -46
  47. package/src/components/MultiSelect/MultiSelect.tsx +106 -106
  48. package/src/components/MultiSelect/components/MultiSelectInput/MultiSelectInput.tsx +53 -53
  49. package/src/components/Notification/Notification.stories.tsx +46 -46
  50. package/src/components/Notification/Notification.tsx +69 -69
  51. package/src/components/NumberInput/NumberInput.tsx +137 -137
  52. package/src/components/NumberInput/helpers.ts +4 -6
  53. package/src/components/NumberInput/index.ts +1 -1
  54. package/src/components/PhoneInput/PhoneInput.tsx +214 -214
  55. package/src/components/PhoneInput/components/PhoneInputCountryList/PhoneInputCountryList.tsx +155 -155
  56. package/src/components/PhoneInput/types.ts +16 -16
  57. package/src/components/RadioButton/RadioButton.stories.tsx +46 -46
  58. package/src/components/RadioButton/RadioButton.tsx +57 -57
  59. package/src/components/ScrollIntoViewIfNeeded/index.ts +1 -1
  60. package/src/components/Select/CustomSelect.stories.tsx +52 -16
  61. package/src/components/Select/MultiSelect.stories.tsx +3 -3
  62. package/src/components/Select/Select.stories.tsx +235 -235
  63. package/src/components/Select/Select.styles.ts +8 -7
  64. package/src/components/Select/Select.tsx +106 -62
  65. package/src/components/Select/components/SelectList/SelectList.styles.ts +6 -4
  66. package/src/components/Select/components/SelectList/SelectList.tsx +25 -29
  67. package/src/components/Select/components/SelectListItem/SelectListItem.tsx +23 -19
  68. package/src/components/Select/constants.ts +2 -2
  69. package/src/components/Select/helpers.ts +0 -7
  70. package/src/components/Select/types.ts +1 -1
  71. package/src/components/Selector/Selector.stories.tsx +62 -62
  72. package/src/components/Selector/Selector.tsx +115 -115
  73. package/src/components/Selector/index.ts +2 -2
  74. package/src/components/Selector/types.ts +12 -12
  75. package/src/components/Skeleton/Skeleton.stories.tsx +19 -19
  76. package/src/components/SmartInput/SmartInput.tsx +134 -134
  77. package/src/components/Status/Status.stories.tsx +73 -73
  78. package/src/components/Status/Status.styles.ts +143 -143
  79. package/src/components/Status/Status.tsx +49 -49
  80. package/src/components/Status/constants.ts +11 -11
  81. package/src/components/Status/index.ts +3 -3
  82. package/src/components/Status/types.ts +5 -5
  83. package/src/components/Switch/Switch.stories.tsx +40 -40
  84. package/src/components/Switch/Switch.tsx +75 -75
  85. package/src/components/TextWithInfo/TextWithInfo.stories.tsx +53 -53
  86. package/src/components/TextWithInfo/TextWithInfo.tsx +62 -62
  87. package/src/components/TextWithTooltip/TextWithTooltip.stories.tsx +58 -58
  88. package/src/components/TextWithTooltip/TextWithTooltip.tsx +2 -3
  89. package/src/components/ThemedPreloader/ThemedPreloader.stories.tsx +41 -41
  90. package/src/components/ThemedPreloader/ThemedPreloader.tsx +54 -54
  91. package/src/components/ThemedPreloader/components/DefaultPreloader/index.ts +1 -1
  92. package/src/components/Toaster/Toaster.stories.tsx +30 -30
  93. package/src/components/Tooltip/Tooltip.stories.tsx +19 -19
  94. package/src/components/Tooltip/Tooltip.tsx +35 -35
  95. package/src/components/Tooltip/types.ts +1 -1
  96. package/src/helpers/popper-helpers.ts +17 -17
  97. package/src/hooks/use-dropdown.ts +84 -84
  98. package/src/hooks/use-is-mounted.ts +15 -15
  99. package/src/theme/helpers.ts +76 -76
  100. package/src/vite-env.d.ts +1 -1
@@ -1,49 +1,51 @@
1
1
  import {
2
- ReactNode,
3
- FocusEvent,
4
- KeyboardEvent,
5
- MouseEvent,
6
2
  useCallback,
7
3
  useEffect,
8
4
  useMemo,
9
5
  useRef,
10
6
  useState,
11
- SyntheticEvent,
12
- ChangeEvent,
13
- FormEvent,
7
+ type ChangeEvent,
8
+ type CSSProperties,
9
+ type FocusEvent,
10
+ type FormEvent,
11
+ type KeyboardEvent,
12
+ type MouseEvent,
13
+ type ReactNode,
14
+ type SyntheticEvent,
14
15
  } from 'react';
15
16
  import { Portal } from 'react-overlays';
16
17
  import clsx from 'clsx';
17
- import { Styles } from 'jss';
18
18
  import { debounce } from 'ts-debounce';
19
19
  import {
20
+ createFilter,
20
21
  getTestId,
22
+ isEmpty,
21
23
  isNotEmpty,
22
24
  isReactNodeNotEmpty,
23
25
  isStringNotEmpty,
24
- createFilter,
25
26
  } from '@true-engineering/true-react-platform-helpers';
26
27
  import { hasExactParent } from '../../helpers';
27
- import { useIsMounted, useOnClickOutsideWithRef, useDropdown, useTweakStyles } from '../../hooks';
28
- import { ICommonProps, IDropdownWithPopperOptions } from '../../types';
29
- import { renderIcon, IIcon } from '../Icon';
30
- import { IInputProps, Input } from '../Input';
31
- import { ISearchInputProps, SearchInput } from '../SearchInput';
28
+ import { useDropdown, useIsMounted, useOnClickOutsideWithRef, useTweakStyles } from '../../hooks';
29
+ import { IDropdownWithPopperOptions, type ICommonProps } from '../../types';
30
+ import { renderIcon, type IIcon } from '../Icon';
31
+ import { Input, type IInputProps } from '../Input';
32
+ import { SearchInput, type ISearchInputProps } from '../SearchInput';
32
33
  import { SelectList } from './components';
33
34
  import { ALL_OPTION_INDEX, DEFAULT_OPTION_INDEX } from './constants';
34
35
  import {
35
- defaultConvertFunction,
36
36
  defaultCompareFunction,
37
+ defaultConvertFunction,
37
38
  defaultIsOptionDisabled,
38
39
  getDefaultConvertToIdFunction,
39
- isMultiSelectValue,
40
40
  } from './helpers';
41
41
  import { IMultipleSelectValue } from './types';
42
- import { useStyles, ISelectStyles, searchInputStyles, getInputStyles } from './Select.styles';
42
+ import { getInputStyles, searchInputStyles, useStyles, type ISelectStyles } from './Select.styles';
43
43
 
44
44
  export interface ISelectProps<Value>
45
- extends Omit<IInputProps, 'value' | 'onChange' | 'onBlur' | 'type' | 'tweakStyles'>,
45
+ extends Omit<IInputProps, 'value' | 'onChange' | 'onBlur' | 'type' | 'isActive' | 'tweakStyles'>,
46
46
  ICommonProps<ISelectStyles> {
47
+ header?: ReactNode;
48
+ footer?: ReactNode;
47
49
  defaultOptionLabel?: ReactNode;
48
50
  allOptionsLabel?: string;
49
51
  noMatchesLabel?: string;
@@ -61,7 +63,7 @@ export interface ISelectProps<Value>
61
63
  value: Value | undefined;
62
64
  /** @default true */
63
65
  shouldScrollToList?: boolean;
64
- isMultiSelect?: boolean;
66
+ isMultiSelect?: false;
65
67
  searchInput?: { shouldRenderInList: true } & Pick<ISearchInputProps, 'placeholder'>;
66
68
  isOptionDisabled?: (option: Value) => boolean;
67
69
  onChange: (
@@ -77,15 +79,19 @@ export interface ISelectProps<Value>
77
79
  optionsFilter?: (options: Value[], query: string) => Value[];
78
80
  onOpen?: () => void;
79
81
  compareValuesOnChange?: (v1?: Value, v2?: Value) => boolean;
80
- // Для избежания проблем юзайте useCallback на эти функции
81
- // или выносите их из компонента (чтобы не было сайдэфектов от перерендеринга их)
82
+ /** @description Функция должна быть мемоизирована с целью избежания ререндера */
82
83
  convertValueToString?: (value: Value) => string | undefined;
84
+ /** @description Функция должна быть мемоизирована с целью избежания ререндера */
83
85
  convertValueToReactNode?: (value: Value, isDisabled: boolean) => ReactNode;
86
+ /** @description Функция должна быть мемоизирована с целью избежания ререндера */
84
87
  convertValueToId?: (value: Value) => string | undefined;
85
88
  }
86
89
 
87
90
  export interface IMultipleSelectProps<Value>
88
- extends Omit<ISelectProps<Value>, 'value' | 'onChange' | 'compareValuesOnChange'> {
91
+ extends Omit<
92
+ ISelectProps<Value>,
93
+ 'value' | 'onChange' | 'compareValuesOnChange' | 'isMultiSelect'
94
+ > {
89
95
  isMultiSelect: true;
90
96
  value: IMultipleSelectValue<Value> | undefined;
91
97
  onChange: (
@@ -107,7 +113,10 @@ export function Select<Value>(
107
113
  ): JSX.Element {
108
114
  const {
109
115
  options,
116
+ isMultiSelect,
110
117
  value,
118
+ header,
119
+ footer,
111
120
  defaultOptionLabel,
112
121
  allOptionsLabel,
113
122
  debounceTime = 400,
@@ -142,7 +151,6 @@ export function Select<Value>(
142
151
  const { shouldRenderInList: shouldRenderSearchInputInList = false, ...searchInputProps } =
143
152
  searchInput ?? {};
144
153
  const hasSearchInputInList = optionsMode !== 'normal' && shouldRenderSearchInputInList;
145
- const isMultiSelect = isMultiSelectValue(props, value);
146
154
  const hasReadonlyInput = isReadonly || optionsMode === 'normal' || shouldRenderSearchInputInList;
147
155
 
148
156
  const tweakInputStyles = useTweakStyles({
@@ -176,6 +184,7 @@ export function Select<Value>(
176
184
  // вынесен отдельно, из-за проблем с дебаунсом при динамич. опциях
177
185
  const [shouldShowDefaultOption, setShouldShowDefaultOption] = useState(true);
178
186
 
187
+ const root = useRef<HTMLDivElement>(null);
179
188
  const inputWrapper = useRef<HTMLDivElement>(null);
180
189
  const list = useRef<HTMLDivElement>(null);
181
190
  const input = useRef<HTMLInputElement>(null); // TODO ref снаружи?
@@ -196,7 +205,7 @@ export function Select<Value>(
196
205
  }, [optionsFilter, options, convertValueToString, searchValue, optionsMode]);
197
206
 
198
207
  const availableOptions = useMemo(
199
- () => options.filter((o) => !isOptionDisabled(o)),
208
+ () => options.filter((option) => !isOptionDisabled(option)),
200
209
  [options, isOptionDisabled],
201
210
  );
202
211
 
@@ -238,19 +247,40 @@ export function Select<Value>(
238
247
  [convertValueToId, convertValueToString],
239
248
  );
240
249
 
250
+ const getDropdownOffset = () => {
251
+ if (isEmpty(input.current) || inputProps.errorPosition === 'top') {
252
+ return 0;
253
+ }
254
+
255
+ // Высота элемента inputWrapper у компонента Input
256
+ return input.current.parentElement?.offsetHeight ?? 0;
257
+ };
258
+
259
+ const closeList = useCallback(() => {
260
+ setIsListOpen(false);
261
+ setSearchValue('');
262
+ setShouldShowDefaultOption(true);
263
+
264
+ if (!dropdownOptions?.shouldUsePopper) {
265
+ root.current?.style.removeProperty('--dropdown-offset');
266
+ }
267
+ }, [dropdownOptions?.shouldUsePopper]);
268
+
241
269
  const handleListClose = useCallback(
242
270
  (event: Event | SyntheticEvent) => {
243
- setIsListOpen(false);
244
- setSearchValue('');
245
- setShouldShowDefaultOption(true);
271
+ closeList();
246
272
  onBlur?.(event);
247
273
  },
248
- [onBlur],
274
+ [closeList, onBlur],
249
275
  );
250
276
 
251
277
  const handleListOpen = () => {
252
278
  if (!isListOpen) {
253
279
  setIsListOpen(true);
280
+
281
+ if (!dropdownOptions?.shouldUsePopper) {
282
+ root.current?.style.setProperty('--dropdown-offset', `${getDropdownOffset()}px`);
283
+ }
254
284
  }
255
285
  };
256
286
 
@@ -283,13 +313,13 @@ export function Select<Value>(
283
313
  hasExactParent(event.relatedTarget, list.current) ||
284
314
  hasExactParent(event.relatedTarget, inputWrapper.current);
285
315
 
286
- // Ниче не делаем если клик был внутри селекта
316
+ // Ничего не делаем, если клик был внутри селекта
287
317
  if (!isActionInsideSelect) {
288
318
  handleListClose(event);
289
319
  }
290
320
  };
291
321
 
292
- const handleOnChange = useCallback(
322
+ const handleChange = useCallback(
293
323
  (
294
324
  newValue: Value | IMultipleSelectValue<Value> | undefined,
295
325
  event:
@@ -308,11 +338,11 @@ export function Select<Value>(
308
338
 
309
339
  const handleOptionSelect = useCallback(
310
340
  (index: number, event: MouseEvent<HTMLElement> | KeyboardEvent) => {
311
- handleOnChange(index === DEFAULT_OPTION_INDEX ? undefined : filteredOptions[index], event);
341
+ handleChange(index === DEFAULT_OPTION_INDEX ? undefined : filteredOptions[index], event);
312
342
  handleListClose(event);
313
343
  input.current?.blur();
314
344
  },
315
- [handleOnChange, handleListClose, filteredOptions],
345
+ [handleChange, handleListClose, filteredOptions],
316
346
  );
317
347
 
318
348
  // MultiSelect
@@ -324,15 +354,15 @@ export function Select<Value>(
324
354
 
325
355
  // Если выбрана не дефолтная опция, которая сетит андеф
326
356
  if (index === DEFAULT_OPTION_INDEX || (index === ALL_OPTION_INDEX && !isSelected)) {
327
- handleOnChange(undefined, event);
357
+ handleChange(undefined, event);
328
358
  return;
329
359
  }
330
360
  if (index === ALL_OPTION_INDEX && isSelected) {
331
- handleOnChange(availableOptions as IMultipleSelectValue<Value>, event);
361
+ handleChange(availableOptions as IMultipleSelectValue<Value>, event);
332
362
  return;
333
363
  }
334
364
  const option = filteredOptions[index];
335
- handleOnChange(
365
+ handleChange(
336
366
  isSelected
337
367
  ? // Добавляем
338
368
  ([...(value ?? []), option] as IMultipleSelectValue<Value>)
@@ -341,7 +371,7 @@ export function Select<Value>(
341
371
  event,
342
372
  );
343
373
  },
344
- [isMultiSelect, filteredOptions, handleOnChange, value, availableOptions, convertToId],
374
+ [isMultiSelect, filteredOptions, handleChange, value, availableOptions, convertToId],
345
375
  );
346
376
 
347
377
  const handleOnType = useCallback(
@@ -378,7 +408,7 @@ export function Select<Value>(
378
408
  }
379
409
 
380
410
  if (v === '' && !hasSearchInputInList) {
381
- handleOnChange(undefined, event);
411
+ handleChange(undefined, event);
382
412
  }
383
413
 
384
414
  setSearchValue(v);
@@ -406,12 +436,13 @@ export function Select<Value>(
406
436
  }
407
437
 
408
438
  if (isMultiSelect) {
409
- let isThisValueAlreadySelected: boolean;
439
+ let isThisValueAlreadySelected: boolean | undefined;
410
440
  if (indexToSelect === ALL_OPTION_INDEX) {
411
441
  isThisValueAlreadySelected = areAllOptionsSelected;
412
442
  } else {
413
443
  // подумать над концептом реального фокуса на опциях, а не вот эти вот focusedCell
414
- const valueIdToSelect = convertToId(filteredOptions[indexToSelect]);
444
+ const option = filteredOptions[indexToSelect];
445
+ const valueIdToSelect = convertToId(option);
415
446
  isThisValueAlreadySelected =
416
447
  value?.some((opt) => convertToId(opt) === valueIdToSelect) ?? false;
417
448
  }
@@ -446,6 +477,7 @@ export function Select<Value>(
446
477
  const onArrowClick = () => {
447
478
  if (isListOpen) {
448
479
  input.current?.blur();
480
+ closeList();
449
481
  } else {
450
482
  input.current?.focus();
451
483
  }
@@ -482,21 +514,21 @@ export function Select<Value>(
482
514
  const popperData = useDropdown({
483
515
  isOpen,
484
516
  onDropdownClose: handleListClose,
485
- referenceElement: inputWrapper.current,
517
+ referenceElement: input.current?.parentElement ?? inputWrapper.current,
486
518
  dropdownElement: list.current,
487
519
  options: dropdownOptions,
488
520
  dependenciesForPositionUpdating: [inputProps.isLoading, filteredOptions.length],
489
521
  });
490
522
 
491
523
  useEffect(() => {
492
- setFocusedListCellIndex(
493
- optionsIndexesForNavigation.find(
494
- (index) =>
495
- isNotEmpty(strValue) &&
496
- isNotEmpty(filteredOptions[index]) &&
497
- convertToId(filteredOptions[index]) === convertToId(strValue),
498
- ) ?? optionsIndexesForNavigation[0],
499
- );
524
+ const focusedCellIndex = isNotEmpty(strValue)
525
+ ? optionsIndexesForNavigation.find((index) => {
526
+ const option = filteredOptions[index];
527
+ return isNotEmpty(option) && convertToId(option) === convertToId(strValue);
528
+ })
529
+ : undefined;
530
+
531
+ setFocusedListCellIndex(focusedCellIndex ?? optionsIndexesForNavigation[0]);
500
532
  }, [strValue, filteredOptions, optionsIndexesForNavigation, convertToId]);
501
533
 
502
534
  useEffect(() => {
@@ -505,6 +537,25 @@ export function Select<Value>(
505
537
  }
506
538
  }, [isOpen]);
507
539
 
540
+ const searchInputEl = hasSearchInputInList && (
541
+ <SearchInput
542
+ value={searchValue}
543
+ onChange={handleInputChange}
544
+ tweakStyles={tweakSearchInputStyles}
545
+ placeholder="Поиск"
546
+ {...searchInputProps}
547
+ />
548
+ );
549
+
550
+ // Т.к. используется для проверки на пустой элемент `isReactNodeNotEmpty` внутри `SelectList`, то
551
+ // он пропускает React.Fragment
552
+ const customHeader = (isReactNodeNotEmpty(searchInputEl) || isReactNodeNotEmpty(header)) && (
553
+ <>
554
+ {searchInputEl}
555
+ {header}
556
+ </>
557
+ );
558
+
508
559
  const listEl = (
509
560
  <div
510
561
  className={clsx(classes.listWrapper, {
@@ -512,7 +563,8 @@ export function Select<Value>(
512
563
  [classes.listWrapperInBody]: shouldRenderInBody,
513
564
  })}
514
565
  ref={list}
515
- style={popperData?.styles.popper as Styles}
566
+ style={popperData?.styles.popper as CSSProperties}
567
+ tabIndex={0}
516
568
  onBlur={handleBlur} // обработка для Tab из списка
517
569
  {...popperData?.attributes.popper}
518
570
  >
@@ -522,17 +574,8 @@ export function Select<Value>(
522
574
  defaultOptionLabel={hasDefaultOption && shouldShowDefaultOption && defaultOptionLabel}
523
575
  allOptionsLabel={shouldShowAllOption && allOptionsLabel}
524
576
  areAllOptionsSelected={areAllOptionsSelected}
525
- customListHeader={
526
- hasSearchInputInList && (
527
- <SearchInput
528
- value={searchValue}
529
- onChange={handleInputChange}
530
- tweakStyles={tweakSearchInputStyles}
531
- placeholder="Поиск"
532
- {...searchInputProps}
533
- />
534
- )
535
- }
577
+ customListHeader={customHeader}
578
+ customListFooter={footer}
536
579
  noMatchesLabel={noMatchesLabel}
537
580
  focusedIndex={focusedListCellIndex}
538
581
  activeValue={value}
@@ -540,6 +583,7 @@ export function Select<Value>(
540
583
  loadingLabel={loadingLabel}
541
584
  tweakStyles={tweakSelectListStyles}
542
585
  testId={getTestId(testId, 'list')}
586
+ isMultiSelect={isMultiSelect}
543
587
  // скролл не работает с включеным поппером
544
588
  shouldScrollToList={shouldScrollToList && !shouldUsePopper && !shouldHideOnScroll}
545
589
  isOptionDisabled={isOptionDisabled}
@@ -547,7 +591,7 @@ export function Select<Value>(
547
591
  convertValueToReactNode={convertValueToReactNode}
548
592
  convertValueToId={convertToId}
549
593
  onOptionSelect={handleOptionSelect}
550
- onToggleCheckbox={isMultiSelect ? handleToggleOptionCheckbox : undefined}
594
+ onToggleCheckbox={handleToggleOptionCheckbox}
551
595
  />
552
596
  )}
553
597
  </div>
@@ -564,7 +608,7 @@ export function Select<Value>(
564
608
  ) : undefined;
565
609
 
566
610
  return (
567
- <div className={classes.root} onKeyDown={handleKeyDown}>
611
+ <div className={classes.root} onKeyDown={handleKeyDown} ref={root}>
568
612
  <div
569
613
  className={clsx(classes.inputWrapper, isDisabled && classes.disabled)}
570
614
  onClick={isDisabled ? undefined : handleOnClick}
@@ -18,12 +18,14 @@ export const useStyles = createThemedStyles('SelectList', {
18
18
  paddingTop: 0,
19
19
  },
20
20
 
21
- listHeader: {
22
- '& + $list': {
23
- borderTop: [1, 'solid', colors.BORDER_LIGHT],
24
- },
21
+ withListFooter: {
22
+ paddingBottom: 0,
25
23
  },
26
24
 
25
+ listHeader: {},
26
+
27
+ listFooter: {},
28
+
27
29
  list: {
28
30
  height: '100%',
29
31
  maxHeight: ROW_HEIGHT * 6,
@@ -1,20 +1,19 @@
1
- import { ReactNode, useMemo } from 'react';
1
+ import { useMemo, type ReactNode } from 'react';
2
2
  import clsx from 'clsx';
3
3
  import {
4
4
  addDataTestId,
5
- isNotEmpty,
5
+ getArray,
6
6
  isReactNodeNotEmpty,
7
7
  } from '@true-engineering/true-react-platform-helpers';
8
- import { ICommonProps } from '../../../../types';
8
+ import { type ICommonProps } from '../../../../types';
9
9
  import { ScrollIntoViewIfNeeded } from '../../../ScrollIntoViewIfNeeded';
10
10
  import { ALL_OPTION_INDEX, DEFAULT_OPTION_INDEX } from '../../constants';
11
- import { IMultipleSelectValue } from '../../types';
12
- import { ISelectListItemProps, SelectListItem } from '../SelectListItem';
13
- import { useStyles, ISelectListStyles } from './SelectList.styles';
11
+ import { SelectListItem, type ISelectListItemProps } from '../SelectListItem';
12
+ import { useStyles, type ISelectListStyles } from './SelectList.styles';
14
13
 
15
14
  export interface ISelectListProps<Value>
16
15
  extends ICommonProps<ISelectListStyles>,
17
- Pick<ISelectListItemProps, 'onToggleCheckbox' | 'onOptionSelect'> {
16
+ Pick<ISelectListItemProps, 'onToggleCheckbox' | 'onOptionSelect' | 'isMultiSelect'> {
18
17
  options: Value[] | Readonly<Value[]>;
19
18
  focusedIndex?: number;
20
19
  activeValue?: Value | Value[];
@@ -26,6 +25,7 @@ export interface ISelectListProps<Value>
26
25
  areAllOptionsSelected?: boolean;
27
26
  shouldScrollToList?: boolean;
28
27
  customListHeader?: ReactNode;
28
+ customListFooter?: ReactNode;
29
29
  isOptionDisabled: (value: Value) => boolean;
30
30
  convertValueToString: (value: Value) => string | undefined;
31
31
  convertValueToReactNode?: (value: Value, isDisabled: boolean) => ReactNode;
@@ -45,6 +45,8 @@ export function SelectList<Value>({
45
45
  shouldScrollToList = true,
46
46
  areAllOptionsSelected,
47
47
  customListHeader,
48
+ customListFooter,
49
+ isMultiSelect,
48
50
  isOptionDisabled,
49
51
  allOptionsLabel,
50
52
  onOptionSelect,
@@ -55,42 +57,33 @@ export function SelectList<Value>({
55
57
  }: ISelectListProps<Value>): JSX.Element {
56
58
  const classes = useStyles({ theme: tweakStyles });
57
59
 
58
- const isMultiSelect = isNotEmpty(onToggleCheckbox);
59
- const multiSelectValue = activeValue as IMultipleSelectValue<Value> | undefined;
60
- const selectedOptionsCount = multiSelectValue?.length ?? 0;
61
-
62
- // MultiSelect
63
- const activeOptionsIdMap = useMemo(
64
- () => (isMultiSelect ? multiSelectValue?.map(convertValueToId) ?? [] : []),
65
- [isMultiSelect, multiSelectValue, convertValueToId],
66
- );
60
+ const isHeaderNotEmpty = isReactNodeNotEmpty(customListHeader);
61
+ const isFooterNotEmpty = isReactNodeNotEmpty(customListFooter);
67
62
 
68
63
  const optionsDisableMap = useMemo(
69
- () => options.map((o) => isOptionDisabled(o)),
64
+ () => options.map(isOptionDisabled),
70
65
  [options, isOptionDisabled],
71
66
  );
72
67
 
73
68
  const listOptions = useMemo(
74
- () => options.map((opt, i) => convertValueToReactNode(opt, optionsDisableMap[i])),
69
+ () => options.map((option, index) => convertValueToReactNode(option, optionsDisableMap[index])),
75
70
  [options, convertValueToReactNode, optionsDisableMap],
76
71
  );
77
72
 
78
- const isActiveOption = (item: Value): boolean =>
79
- isMultiSelect
80
- ? activeOptionsIdMap.includes(convertValueToId(item))
81
- : isNotEmpty(activeValue) &&
82
- convertValueToId(activeValue as Value) === convertValueToId(item);
73
+ const activeOptionsIds = useMemo(
74
+ () => new Set((getArray(activeValue) as Value[]).map(convertValueToId)),
75
+ [activeValue, convertValueToId],
76
+ );
83
77
 
84
78
  return (
85
79
  <ScrollIntoViewIfNeeded
86
80
  active={shouldScrollToList && !isMultiSelect}
87
81
  className={clsx(classes.root, {
88
- [classes.withListHeader]: isReactNodeNotEmpty(customListHeader),
82
+ [classes.withListHeader]: isHeaderNotEmpty,
83
+ [classes.withListFooter]: isFooterNotEmpty,
89
84
  })}
90
85
  >
91
- {isReactNodeNotEmpty(customListHeader) && (
92
- <div className={classes.listHeader}>{customListHeader}</div>
93
- )}
86
+ {isHeaderNotEmpty && <div className={classes.listHeader}>{customListHeader}</div>}
94
87
  <div className={classes.list} {...addDataTestId(testId)}>
95
88
  {isLoading ? (
96
89
  <div className={clsx(classes.cell, classes.loading)}>{loadingLabel}</div>
@@ -114,9 +107,10 @@ export function SelectList<Value>({
114
107
  <SelectListItem
115
108
  classes={classes}
116
109
  index={ALL_OPTION_INDEX}
117
- isSemiChecked={selectedOptionsCount > 0 && !areAllOptionsSelected}
110
+ isSemiChecked={activeOptionsIds.size > 0 && !areAllOptionsSelected}
118
111
  isActive={areAllOptionsSelected}
119
112
  isFocused={focusedIndex === ALL_OPTION_INDEX}
113
+ isMultiSelect={isMultiSelect}
120
114
  onOptionSelect={onOptionSelect}
121
115
  onToggleCheckbox={onToggleCheckbox}
122
116
  >
@@ -126,7 +120,7 @@ export function SelectList<Value>({
126
120
  {listOptions.map((opt, i) => {
127
121
  const optionValue = options[i];
128
122
  const isFocused = focusedIndex === i;
129
- const isActive = isActiveOption(optionValue);
123
+ const isActive = activeOptionsIds.has(convertValueToId(optionValue));
130
124
  // проверяем, что опция задизейблена
131
125
  const isDisabled = optionsDisableMap[i];
132
126
 
@@ -138,6 +132,7 @@ export function SelectList<Value>({
138
132
  isDisabled={isDisabled}
139
133
  isActive={isActive}
140
134
  isFocused={isFocused}
135
+ isMultiSelect={isMultiSelect}
141
136
  onOptionSelect={onOptionSelect}
142
137
  onToggleCheckbox={onToggleCheckbox}
143
138
  >
@@ -151,6 +146,7 @@ export function SelectList<Value>({
151
146
  </>
152
147
  )}
153
148
  </div>
149
+ {isFooterNotEmpty && <div className={classes.listFooter}>{customListFooter}</div>}
154
150
  </ScrollIntoViewIfNeeded>
155
151
  );
156
152
  }
@@ -1,7 +1,12 @@
1
- import { ReactNode, MouseEvent, FC, KeyboardEvent, ChangeEvent } from 'react';
1
+ import {
2
+ type ChangeEvent,
3
+ type FC,
4
+ type KeyboardEvent,
5
+ type MouseEvent,
6
+ type ReactNode,
7
+ } from 'react';
2
8
  import clsx from 'clsx';
3
- import { Classes } from 'jss';
4
- import { isNotEmpty } from '@true-engineering/true-react-platform-helpers';
9
+ import { type Classes } from 'jss';
5
10
  import { addDataAttributes } from '../../../../helpers';
6
11
  import { Checkbox } from '../../../Checkbox';
7
12
  import { ScrollIntoViewIfNeeded } from '../../../ScrollIntoViewIfNeeded';
@@ -15,8 +20,9 @@ export interface ISelectListItemProps {
15
20
  isFocused?: boolean;
16
21
  children: ReactNode;
17
22
  classes: Classes<'cellWithCheckbox' | 'cell' | 'focused' | 'active' | 'disabled'>; // TODO: !!!
23
+ isMultiSelect?: boolean;
18
24
  onOptionSelect: (index: number, event: MouseEvent<HTMLElement>) => void;
19
- onToggleCheckbox?: (
25
+ onToggleCheckbox: (
20
26
  index: number,
21
27
  isSelected: boolean,
22
28
  event: ChangeEvent<HTMLElement> | KeyboardEvent,
@@ -31,10 +37,21 @@ export const SelectListItem: FC<ISelectListItemProps> = ({
31
37
  isActive,
32
38
  children,
33
39
  isFocused,
40
+ isMultiSelect,
34
41
  onOptionSelect,
35
42
  onToggleCheckbox,
36
43
  }) => {
37
- const isMultiSelect = isNotEmpty(onToggleCheckbox);
44
+ const multiSelectContent = isMultiSelect && (
45
+ <Checkbox
46
+ isChecked={isActive || isSemiChecked}
47
+ isSemiChecked={isSemiChecked}
48
+ isDisabled={isDisabled}
49
+ tweakStyles={checkboxStyles}
50
+ onSelect={({ isSelected }, event) => onToggleCheckbox(index, isSelected, event)}
51
+ >
52
+ {children}
53
+ </Checkbox>
54
+ );
38
55
 
39
56
  return (
40
57
  <ScrollIntoViewIfNeeded
@@ -53,20 +70,7 @@ export const SelectListItem: FC<ISelectListItemProps> = ({
53
70
  })}
54
71
  onClick={!isDisabled && !isMultiSelect ? (event) => onOptionSelect(index, event) : undefined}
55
72
  >
56
- {isMultiSelect ? (
57
- <Checkbox
58
- value={index}
59
- isChecked={isActive || isSemiChecked}
60
- isSemiChecked={isSemiChecked}
61
- isDisabled={isDisabled}
62
- tweakStyles={checkboxStyles}
63
- onSelect={(v, event) => onToggleCheckbox(index, v.isSelected, event)}
64
- >
65
- {children}
66
- </Checkbox>
67
- ) : (
68
- children
69
- )}
73
+ {isMultiSelect ? multiSelectContent : children}
70
74
  </ScrollIntoViewIfNeeded>
71
75
  );
72
76
  };
@@ -1,2 +1,2 @@
1
- export const DEFAULT_OPTION_INDEX = -2;
2
- export const ALL_OPTION_INDEX = -1;
1
+ export const DEFAULT_OPTION_INDEX = -2;
2
+ export const ALL_OPTION_INDEX = -1;
@@ -1,6 +1,4 @@
1
1
  import { isNotEmpty } from '@true-engineering/true-react-platform-helpers';
2
- import type { IMultipleSelectProps, ISelectProps } from './Select';
3
- import { IMultipleSelectValue } from './types';
4
2
 
5
3
  export const defaultIsOptionDisabled = <Value>(option: Value): boolean =>
6
4
  typeof option === 'object' &&
@@ -20,8 +18,3 @@ export const getDefaultConvertToIdFunction =
20
18
  isNotEmpty((value as { id: unknown })?.id)
21
19
  ? String((value as { id: unknown }).id)
22
20
  : convertValueToString(value);
23
-
24
- export const isMultiSelectValue = <Value>(
25
- props: ISelectProps<Value> | IMultipleSelectProps<Value>,
26
- _value: Value | IMultipleSelectValue<Value> | undefined,
27
- ): _value is IMultipleSelectValue<Value> | undefined => props.isMultiSelect === true;
@@ -1 +1 @@
1
- export type IMultipleSelectValue<Value> = Array<NonNullable<Value>>;
1
+ export type IMultipleSelectValue<Value> = Array<NonNullable<Value>>;