@true-engineering/true-react-common-ui-kit 3.24.0 → 3.25.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/README.md +21 -0
- package/dist/components/Select/CustomSelect.stories.d.ts +1 -1
- package/dist/components/Select/MultiSelect.stories.d.ts +2 -2
- package/dist/components/Select/Select.d.ts +14 -9
- package/dist/components/Select/Select.styles.d.ts +5 -5
- package/dist/components/Select/components/SelectList/SelectList.d.ts +7 -6
- package/dist/components/Select/components/SelectList/SelectList.styles.d.ts +1 -1
- package/dist/components/Select/components/SelectListItem/SelectListItem.d.ts +4 -3
- package/dist/components/Select/helpers.d.ts +0 -3
- package/dist/true-react-common-ui-kit.js +140 -114
- package/dist/true-react-common-ui-kit.js.map +1 -1
- package/dist/true-react-common-ui-kit.umd.cjs +139 -113
- package/dist/true-react-common-ui-kit.umd.cjs.map +1 -1
- package/package.json +2 -2
- package/src/components/FlexibleTable/components/FlexibleTableRow/FlexibleTableRow.tsx +21 -38
- package/src/components/Select/CustomSelect.stories.tsx +52 -16
- package/src/components/Select/MultiSelect.stories.tsx +3 -3
- package/src/components/Select/Select.styles.ts +8 -7
- package/src/components/Select/Select.tsx +106 -62
- package/src/components/Select/components/SelectList/SelectList.styles.ts +6 -4
- package/src/components/Select/components/SelectList/SelectList.tsx +25 -29
- package/src/components/Select/components/SelectListItem/SelectListItem.tsx +23 -19
- package/src/components/Select/helpers.ts +0 -7
- package/src/components/TextWithTooltip/TextWithTooltip.tsx +2 -3
|
@@ -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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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,
|
|
28
|
-
import {
|
|
29
|
-
import { renderIcon, IIcon } from '../Icon';
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
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 {
|
|
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?:
|
|
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
|
-
|
|
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<
|
|
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((
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
341
|
+
handleChange(index === DEFAULT_OPTION_INDEX ? undefined : filteredOptions[index], event);
|
|
312
342
|
handleListClose(event);
|
|
313
343
|
input.current?.blur();
|
|
314
344
|
},
|
|
315
|
-
[
|
|
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
|
-
|
|
357
|
+
handleChange(undefined, event);
|
|
328
358
|
return;
|
|
329
359
|
}
|
|
330
360
|
if (index === ALL_OPTION_INDEX && isSelected) {
|
|
331
|
-
|
|
361
|
+
handleChange(availableOptions as IMultipleSelectValue<Value>, event);
|
|
332
362
|
return;
|
|
333
363
|
}
|
|
334
364
|
const option = filteredOptions[index];
|
|
335
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
493
|
-
optionsIndexesForNavigation.find(
|
|
494
|
-
|
|
495
|
-
isNotEmpty(
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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
|
|
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
|
-
|
|
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={
|
|
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
|
-
|
|
22
|
-
|
|
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 {
|
|
1
|
+
import { useMemo, type ReactNode } from 'react';
|
|
2
2
|
import clsx from 'clsx';
|
|
3
3
|
import {
|
|
4
4
|
addDataTestId,
|
|
5
|
-
|
|
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 {
|
|
12
|
-
import {
|
|
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
|
|
59
|
-
const
|
|
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(
|
|
64
|
+
() => options.map(isOptionDisabled),
|
|
70
65
|
[options, isOptionDisabled],
|
|
71
66
|
);
|
|
72
67
|
|
|
73
68
|
const listOptions = useMemo(
|
|
74
|
-
() => options.map((
|
|
69
|
+
() => options.map((option, index) => convertValueToReactNode(option, optionsDisableMap[index])),
|
|
75
70
|
[options, convertValueToReactNode, optionsDisableMap],
|
|
76
71
|
);
|
|
77
72
|
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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]:
|
|
82
|
+
[classes.withListHeader]: isHeaderNotEmpty,
|
|
83
|
+
[classes.withListFooter]: isFooterNotEmpty,
|
|
89
84
|
})}
|
|
90
85
|
>
|
|
91
|
-
{
|
|
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={
|
|
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 =
|
|
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 {
|
|
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
|
|
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,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,5 +1,4 @@
|
|
|
1
|
-
import { FC, ReactNode, useEffect, useRef, useState } from 'react';
|
|
2
|
-
import { Styles } from 'react-jss';
|
|
1
|
+
import { CSSProperties, FC, ReactNode, useEffect, useRef, useState } from 'react';
|
|
3
2
|
import { Portal } from 'react-overlays';
|
|
4
3
|
import usePopper, { Modifier, Placement } from 'react-overlays/usePopper';
|
|
5
4
|
import clsx from 'clsx';
|
|
@@ -137,7 +136,7 @@ export const TextWithTooltip: FC<ITextWithTooltipProps> = ({
|
|
|
137
136
|
<Portal container={shouldRenderInBody ? document.body : root.current}>
|
|
138
137
|
<div
|
|
139
138
|
className={classes.tooltip}
|
|
140
|
-
style={popperStyles.popper as
|
|
139
|
+
style={popperStyles.popper as CSSProperties}
|
|
141
140
|
{...attributes.popper}
|
|
142
141
|
ref={tooltip}
|
|
143
142
|
>
|