@true-engineering/true-react-common-ui-kit 1.1.0 → 1.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@true-engineering/true-react-common-ui-kit",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "description": "True Engineering React UI Kit with theming support",
5
5
  "author": "True Engineering (https://trueengineering.ru)",
6
6
  "keywords": [
@@ -38,7 +38,8 @@
38
38
  "lint": "eslint \"**/*.{js,jsx,ts,tsx}\"",
39
39
  "format": "prettier --write \"**/*.{js,jsx,css,json,ts,tsx}\"",
40
40
  "format:check": "prettier --check \"**/*.{js,jsx,css,json,ts,tsx}\"",
41
- "full-check": "yarn format:check && yarn lint"
41
+ "full-check": "yarn format:check && yarn lint",
42
+ "types-check": "tsc --noEmit"
42
43
  },
43
44
  "dependencies": {
44
45
  "clsx": "1.2.1",
@@ -10,7 +10,7 @@ export default {
10
10
  };
11
11
 
12
12
  const Template: ComponentStory<typeof DateInput> = (args) => {
13
- const [value, setValue] = useState<string | undefined>(args.date);
13
+ const [value, setValue] = useState(args.date);
14
14
 
15
15
  useEffect(() => {
16
16
  setValue(args.date);
@@ -93,7 +93,7 @@ export const FilterWithPeriod: FC<IFilterWithPeriodProps> = ({
93
93
  value?.periodType === 'CUSTOM',
94
94
  );
95
95
 
96
- const [period, setPeriod] = useState<IPeriod | undefined>(value);
96
+ const [period, setPeriod] = useState(value);
97
97
 
98
98
  const periodGetters = useMemo(() => {
99
99
  const result: Record<string, IPeriodGetter> = { ...PERIODS_GETTERS };
@@ -21,6 +21,7 @@ import TableRow from './TableRow';
21
21
 
22
22
  import { FlexibleTableStyles, styles } from './FlexibleTable.styles';
23
23
 
24
+ // TODO: Заменить Record<string, any> на Record<string, unknown>
24
25
  export interface IFlexibleTableProps<Values extends Record<string, any>>
25
26
  extends ICommonProps {
26
27
  tweakStyles?: FlexibleTableStyles;
@@ -34,6 +35,7 @@ export interface IFlexibleTableProps<Values extends Record<string, any>>
34
35
  infinityScrollConfig?: IInfinityScrollConfig;
35
36
  uniqueField?: keyof Values;
36
37
  onHeadClick?: (column: keyof Values) => void;
38
+ // TODO: Заменить string на Generic Values[uniqueField]
37
39
  onRowClick?: (id: string) => void;
38
40
  onRowHover?: (id?: string) => void;
39
41
  rowAttributes?: Array<keyof Values>;
@@ -6,6 +6,7 @@ import { ICommonProps, IDataAttributes } from '../../types';
6
6
  import TableValue from './TableValue';
7
7
  import { addDataAttributes } from '../../helpers';
8
8
 
9
+ // TODO: Заменить Record<string, any> на Record<string, unknown>
9
10
  interface ITableRowProps<Values extends Record<string, any>>
10
11
  extends ICommonProps {
11
12
  item: Values;
@@ -20,6 +21,7 @@ interface ITableRowProps<Values extends Record<string, any>>
20
21
  isOpen: boolean,
21
22
  close: () => void,
22
23
  ) => React.ReactNode;
24
+ // TODO: Заменить string на Generic Values[uniqueField]
23
25
  onRowHover?: (id?: string) => void;
24
26
  onRowClick?: (id: string) => void;
25
27
  // чтобы не перерендеривать стили для каждой строчки / ячейки
@@ -131,7 +133,7 @@ function TableRow<Values extends Record<string, any>>({
131
133
  >
132
134
  {items.map((key, idx) => (
133
135
  <TableValue
134
- columnName={key as string}
136
+ columnName={key}
135
137
  isSticky={isFirstColumnSticky && idx === 0}
136
138
  isSecond={isFirstColumnSticky && idx === 1}
137
139
  key={key as string}
@@ -3,7 +3,7 @@ import { ReactNode } from 'react';
3
3
  import { format } from 'date-fns';
4
4
 
5
5
  import type { ICommonProps } from '../../types';
6
- import type { FlexibleTableConfigType, IValueComponent } from './types';
6
+ import type { FlexibleTableConfigType } from './types';
7
7
 
8
8
  interface ITableValueProps<Values extends Record<string, any>>
9
9
  extends ICommonProps {
@@ -42,10 +42,7 @@ function TableValue<Values extends Record<string, any>>({
42
42
  let content = null;
43
43
 
44
44
  if (itemConfig?.component) {
45
- const ValueComponent = itemConfig?.component as IValueComponent<
46
- Values,
47
- typeof columnName
48
- >;
45
+ const ValueComponent = itemConfig?.component;
49
46
  content = (
50
47
  <ValueComponent
51
48
  value={value}
@@ -17,7 +17,7 @@ export type ITitleComponent<Value> = FC<{
17
17
  }>;
18
18
 
19
19
  export type IValueComponent<Values, Value> = FC<{
20
- value?: Value;
20
+ value: Value;
21
21
  row: Values;
22
22
  isFocusedRow?: boolean;
23
23
  isNestedComponentExpanded: boolean;
@@ -28,7 +28,7 @@ export type IValueComponent<Values, Value> = FC<{
28
28
  export type FlexibleTableConfigType<Values> = {
29
29
  [Key in keyof Values]?: {
30
30
  title?: ReactNode;
31
- titleComponent?: ITitleComponent<any>;
31
+ titleComponent?: ITitleComponent<unknown>;
32
32
  component?: IValueComponent<Values, Values[Key]>;
33
33
  dateFormat?: string;
34
34
  minWidth?: string | number;
@@ -9,7 +9,7 @@ export default {
9
9
  };
10
10
 
11
11
  const Template: ComponentStory<typeof IncrementInput> = (args) => {
12
- const [value, setValue] = useState<number | undefined>(undefined);
12
+ const [value, setValue] = useState<number>();
13
13
  return (
14
14
  <IncrementInput {...args} value={value} onChange={(v) => setValue(v)} />
15
15
  );
@@ -9,7 +9,7 @@ export default {
9
9
  };
10
10
 
11
11
  const Template: ComponentStory<typeof NumberInput> = (args) => {
12
- const [value, setValue] = useState<number | undefined>(undefined);
12
+ const [value, setValue] = useState<number>();
13
13
  return <NumberInput {...args} value={value} onChange={(v) => setValue(v)} />;
14
14
  };
15
15
 
@@ -27,21 +27,23 @@ const genLetters = (qnt = 1): string =>
27
27
  .replace(/[^a-z]+/g, '')
28
28
  .substr(0, qnt);
29
29
 
30
- const convertObjectToString = (v?: ObjectValue): string | undefined =>
31
- v !== undefined ? `${v.name}` : undefined;
30
+ const convertObjectToString = (v: ObjectValue): string => v.name;
32
31
 
33
- const convertObjectToId = (v?: ObjectValue): string | undefined =>
34
- v !== undefined ? `${v.name}${v.age}` : undefined;
32
+ const convertObjectToId = (v: ObjectValue): string => `${v.name}${v.age}`;
35
33
 
36
- const convertObjectToReactNode = (v?: ObjectValue): ReactNode | undefined =>
37
- v !== undefined ? (
38
- <span>
39
- <i>{v.name}</i>, {v.age}
40
- </span>
41
- ) : undefined;
34
+ const convertObjectToReactNode = (
35
+ v: ObjectValue,
36
+ isDisabled: boolean,
37
+ ): ReactNode => (
38
+ <span style={{ color: isDisabled ? 'red' : undefined }}>
39
+ <i>{v.name}</i>, {v.age}
40
+ </span>
41
+ );
42
+
43
+ const convertStringToReactNode = (v: string): ReactNode => <i>{v}</i>;
42
44
 
43
- const convertStringToReactNode = (v?: string): ReactNode | undefined =>
44
- v !== undefined ? <i>{v}</i> : undefined;
45
+ const isOptionDisabled = (option: string) => option.startsWith('Опция');
46
+ const isObjectOptionDisabled = (option: ObjectValue) => option.age > 30;
45
47
 
46
48
  const stringOptions = [
47
49
  'Опция 1',
@@ -76,6 +78,7 @@ interface ISelectWithCustomProps<T> extends ISelectProps<T> {
76
78
  shouldUsePopper?: boolean;
77
79
  shouldRenderInBody?: boolean;
78
80
  shouldHideOnScroll?: boolean;
81
+ shouldUseCustomIsDisabledFunction?: boolean;
79
82
  canBeFlipped?: boolean;
80
83
  scrollParent?: 'document' | 'auto';
81
84
  }
@@ -87,16 +90,17 @@ function SelectWithCustomProps<T>({
87
90
  shouldUsePopper,
88
91
  shouldRenderInBody,
89
92
  shouldHideOnScroll,
93
+ shouldUseCustomIsDisabledFunction,
90
94
  canBeFlipped,
91
95
  scrollParent,
92
96
  ...rest
93
97
  }: ISelectWithCustomProps<T>) {
94
- const [stringValue, setStringValue] = useState<string | undefined>();
98
+ const [stringValue, setStringValue] = useState<string>();
95
99
  const stringHandler = (newValue?: string) => {
96
100
  setStringValue(newValue);
97
101
  };
98
102
 
99
- const [objectValue, setObjectValue] = useState<ObjectValue | undefined>();
103
+ const [objectValue, setObjectValue] = useState<ObjectValue>();
100
104
  const objectHandler = (newValue?: ObjectValue) => {
101
105
  setObjectValue(newValue);
102
106
  };
@@ -125,6 +129,10 @@ function SelectWithCustomProps<T>({
125
129
  Array<string | ObjectValue>
126
130
  >([]);
127
131
 
132
+ const handleOpen = () => {
133
+ console.log('isOpen');
134
+ };
135
+
128
136
  useEffect(() => {
129
137
  const api = async () => {
130
138
  setDynamicOptions(await getOptions());
@@ -133,10 +141,6 @@ function SelectWithCustomProps<T>({
133
141
  api();
134
142
  }, [selectValuesType]);
135
143
 
136
- const handleOpen = () => {
137
- console.log('isOpen');
138
- };
139
-
140
144
  const props =
141
145
  selectValuesType === 'strings'
142
146
  ? {
@@ -146,6 +150,9 @@ function SelectWithCustomProps<T>({
146
150
  convertValueToReactNode: shouldRenderAsReactNodes
147
151
  ? convertStringToReactNode
148
152
  : undefined,
153
+ isOptionDisabled: shouldUseCustomIsDisabledFunction
154
+ ? isOptionDisabled
155
+ : undefined,
149
156
  }
150
157
  : {
151
158
  onChange: objectHandler,
@@ -156,6 +163,9 @@ function SelectWithCustomProps<T>({
156
163
  convertValueToReactNode: shouldRenderAsReactNodes
157
164
  ? convertObjectToReactNode
158
165
  : undefined,
166
+ isOptionDisabled: shouldUseCustomIsDisabledFunction
167
+ ? isObjectOptionDisabled
168
+ : undefined,
159
169
  };
160
170
 
161
171
  return (
@@ -225,6 +235,8 @@ Default.args = {
225
235
  shouldUsePopper: false,
226
236
  shouldRenderInBody: false,
227
237
  shouldHideOnScroll: false,
238
+ shouldUseCustomIsDisabledFunction: false,
239
+ shouldScrollToList: true,
228
240
  canBeFlipped: false,
229
241
  scrollParent: 'document',
230
242
  };
@@ -1,5 +1,8 @@
1
- import React, {
1
+ import {
2
2
  ReactNode,
3
+ FocusEvent,
4
+ KeyboardEvent,
5
+ MouseEvent,
3
6
  useCallback,
4
7
  useEffect,
5
8
  useMemo,
@@ -11,8 +14,7 @@ import clsx from 'clsx';
11
14
  import { merge } from 'lodash';
12
15
  import { debounce } from 'ts-debounce';
13
16
  import { Portal } from 'react-overlays';
14
-
15
- import { SelectList, isOptionDisabled } from './SelectList';
17
+ import { SelectList } from './SelectList';
16
18
  import { IInputProps, Input } from '../Input';
17
19
  import { IIconType, Icon } from '../Icon';
18
20
  import {
@@ -27,8 +29,8 @@ import {
27
29
  defaultConvertFunction,
28
30
  defaultCompareFunction,
29
31
  getActiveValueIndex,
32
+ defaultIsOptionDisabled,
30
33
  } from './helpers';
31
-
32
34
  import { SelectStyles, styles } from './Select.styles';
33
35
 
34
36
  export interface ISelectProps<Value>
@@ -36,46 +38,34 @@ export interface ISelectProps<Value>
36
38
  tweakStyles?: SelectStyles;
37
39
  defaultOptionLabel?: string;
38
40
  noMatchesLabel?: string;
39
- loadingLabel?: React.ReactNode;
41
+ loadingLabel?: ReactNode;
40
42
  optionsMode?: 'search' | 'dynamic' | 'normal';
41
- onType?: (value: string) => Promise<void>;
42
43
  debounceTime?: number;
43
44
  minSymbolsCountToOpenList?: number;
44
45
  dropdownOptions?: IDropdownWithPopperOptions;
45
46
  dropdownIcon?: IIconType;
46
- onOpen?: () => void;
47
-
48
- optionsFilter?: (options: Value[], query: string) => Value[];
49
47
  options: Value[];
50
48
  value: Value | undefined;
51
49
  shouldScrollToList?: boolean;
50
+ isOptionDisabled?(option: Value): boolean;
52
51
  onChange(value: Value | undefined): void; // подумать как возвращать индекс
52
+ onType?(value: string): Promise<void>;
53
+ optionsFilter?(options: Value[], query: string): Value[];
54
+ onOpen?(): void;
53
55
  compareValuesOnChange?(v1: Value | undefined, v2: Value | undefined): boolean;
54
- // возможно делать какую-то индексацию опций
55
-
56
56
  // Для избежания проблем юзайте useCallback на эти функции
57
57
  // или выносите их из компонента (чтобы не было сайдэфектов от перерендеринга их)
58
58
  convertValueToString?(value: Value): string | undefined;
59
- convertValueToReactNode?(value: Value): ReactNode;
59
+ convertValueToReactNode?(value: Value, isDisabled: boolean): ReactNode;
60
60
  convertValueToId?(value: Value): string | undefined;
61
61
  }
62
62
 
63
63
  export function Select<Value>({
64
64
  options,
65
65
  value,
66
- onChange,
67
- compareValuesOnChange = defaultCompareFunction,
68
- convertValueToString = defaultConvertFunction,
69
- convertValueToId,
70
- convertValueToReactNode,
71
66
  defaultOptionLabel,
72
- onFocus,
73
- onBlur,
74
- onType,
75
- onOpen,
76
67
  debounceTime = 400,
77
68
  optionsMode = 'normal',
78
- optionsFilter,
79
69
  noMatchesLabel,
80
70
  loadingLabel,
81
71
  tweakStyles,
@@ -85,6 +75,17 @@ export function Select<Value>({
85
75
  minSymbolsCountToOpenList = 0,
86
76
  dropdownIcon = 'chevron-down',
87
77
  shouldScrollToList = true,
78
+ onChange,
79
+ onFocus,
80
+ onBlur,
81
+ onType,
82
+ onOpen,
83
+ isOptionDisabled = defaultIsOptionDisabled,
84
+ compareValuesOnChange = defaultCompareFunction,
85
+ convertValueToString = defaultConvertFunction,
86
+ convertValueToId,
87
+ convertValueToReactNode,
88
+ optionsFilter,
88
89
  ...inputProps
89
90
  }: ISelectProps<Value>): JSX.Element {
90
91
  const { classes, componentStyles } = useTheme('Select', styles, tweakStyles);
@@ -144,7 +145,7 @@ export function Select<Value>({
144
145
  setIsListOpen(true);
145
146
  };
146
147
 
147
- const handleFocus = (event: React.FocusEvent<HTMLInputElement>) => {
148
+ const handleFocus = (event: FocusEvent<HTMLInputElement>) => {
148
149
  if (onFocus !== undefined) {
149
150
  onFocus(event);
150
151
  }
@@ -155,7 +156,7 @@ export function Select<Value>({
155
156
  handleListOpen();
156
157
  };
157
158
 
158
- const handleBlur = (event: React.FocusEvent<HTMLInputElement>) => {
159
+ const handleBlur = (event: FocusEvent<HTMLInputElement>) => {
159
160
  if (onBlur !== undefined) {
160
161
  onBlur(event);
161
162
  }
@@ -222,7 +223,7 @@ export function Select<Value>({
222
223
  setSearchValue(v);
223
224
  };
224
225
 
225
- const handleKeyDown = (event: React.KeyboardEvent) => {
226
+ const handleKeyDown = (event: KeyboardEvent) => {
226
227
  if (!isListOpen) {
227
228
  return;
228
229
  }
@@ -365,6 +366,47 @@ export function Select<Value>({
365
366
  }
366
367
  }, [isOpen, onOpen]);
367
368
 
369
+ const listEl = (
370
+ <div
371
+ className={clsx(classes.listWrapper, {
372
+ [classes.withoutPopper]: !shouldUsePopper,
373
+ [classes.listWrapperInBody]: shouldRenderInBody,
374
+ })}
375
+ ref={list}
376
+ // чтобы предотвратить onBlur на инпуте
377
+ onMouseDown={(event) => event.preventDefault()}
378
+ style={popperData?.styles.popper as Styles}
379
+ {...popperData?.attributes.popper}
380
+ >
381
+ {isOpen && (
382
+ <SelectList
383
+ options={filteredOptions}
384
+ defaultOptionLabel={
385
+ hasDefaultOption && shouldShowDefaultOption
386
+ ? defaultOptionLabel
387
+ : undefined
388
+ }
389
+ noMatchesLabel={noMatchesLabel}
390
+ focusedIndex={focusedListCellIndex}
391
+ activeValue={value}
392
+ isLoading={inputProps.isLoading}
393
+ loadingLabel={loadingLabel}
394
+ tweakStyles={tweakStyles?.tweakSelectList as Styles}
395
+ testId={testId !== undefined ? `${testId}-list` : undefined}
396
+ // скролл не работает с включеным поппером
397
+ shouldScrollToList={
398
+ shouldScrollToList && !shouldUsePopper && !shouldHideOnScroll
399
+ }
400
+ isOptionDisabled={isOptionDisabled}
401
+ convertValueToString={convertValueToString}
402
+ convertValueToReactNode={convertValueToReactNode}
403
+ convertValueToId={convertValueToId}
404
+ onOptionClick={handleOptionClick}
405
+ />
406
+ )}
407
+ </div>
408
+ );
409
+
368
410
  return (
369
411
  <div className={classes.root} onKeyDown={handleKeyDown}>
370
412
  <div
@@ -387,7 +429,7 @@ export function Select<Value>({
387
429
  {...inputProps}
388
430
  />
389
431
  <div
390
- onMouseDown={(event: React.MouseEvent) => {
432
+ onMouseDown={(event: MouseEvent) => {
391
433
  event.preventDefault();
392
434
  }}
393
435
  onClick={onArrowClick}
@@ -396,53 +438,15 @@ export function Select<Value>({
396
438
  <Icon type={dropdownIcon} />
397
439
  </div>
398
440
  </div>
399
- <Portal
400
- container={shouldRenderInBody ? document.body : inputWrapper.current}
401
- >
402
- <>
403
- {(shouldUsePopper || isOpen) && (
404
- <div
405
- className={clsx(classes.listWrapper, {
406
- [classes.withoutPopper]: !shouldUsePopper,
407
- [classes.listWrapperInBody]: shouldRenderInBody,
408
- })}
409
- ref={list}
410
- // чтобы предотвратить onBlur на инпуте
411
- onMouseDown={(event) => event.preventDefault()}
412
- style={popperData?.styles.popper as Styles}
413
- {...popperData?.attributes.popper}
414
- >
415
- {isOpen && (
416
- <SelectList
417
- options={filteredOptions}
418
- convertValueToString={convertValueToString}
419
- convertValueToReactNode={convertValueToReactNode}
420
- convertValueToId={convertValueToId}
421
- onOptionClick={handleOptionClick}
422
- defaultOptionLabel={
423
- hasDefaultOption && shouldShowDefaultOption
424
- ? defaultOptionLabel
425
- : undefined
426
- }
427
- noMatchesLabel={noMatchesLabel}
428
- focusedIndex={focusedListCellIndex}
429
- activeValue={value}
430
- isLoading={inputProps.isLoading}
431
- loadingLabel={loadingLabel}
432
- tweakStyles={tweakStyles?.tweakSelectList as Styles}
433
- testId={testId !== undefined ? `${testId}-list` : undefined}
434
- // скролл не работает с включеным поппером
435
- shouldScrollToList={
436
- shouldScrollToList &&
437
- !shouldUsePopper &&
438
- !shouldHideOnScroll
439
- }
440
- />
441
- )}
442
- </div>
443
- )}
444
- </>
445
- </Portal>
441
+ {shouldUsePopper ? (
442
+ <Portal
443
+ container={shouldRenderInBody ? document.body : inputWrapper.current}
444
+ >
445
+ <>{listEl}</>
446
+ </Portal>
447
+ ) : (
448
+ <>{isOpen && listEl}</>
449
+ )}
446
450
  </div>
447
451
  );
448
452
  }
@@ -1,10 +1,9 @@
1
- import React, { ReactNode, useMemo } from 'react';
1
+ import { ReactNode, useMemo } from 'react';
2
2
  import clsx from 'clsx';
3
3
  import { ScrollIntoViewIfNeeded } from '../../ScrollIntoViewIfNeeded';
4
4
  import { useTheme } from '../../../hooks';
5
5
  import { ICommonProps } from '../../../types';
6
- import { isNotEmpty } from '../../../helpers';
7
-
6
+ import { addDataAttributes, isNotEmpty } from '../../../helpers';
8
7
  import { SelectListStyles, styles } from './SelectList.styles';
9
8
 
10
9
  export interface ISelectListProps<Value> extends ICommonProps {
@@ -14,67 +13,55 @@ export interface ISelectListProps<Value> extends ICommonProps {
14
13
  activeValue?: Value;
15
14
  noMatchesLabel?: string;
16
15
  isLoading?: boolean;
17
- loadingLabel?: React.ReactNode;
16
+ loadingLabel?: ReactNode;
18
17
  defaultOptionLabel?: string;
19
- onOptionClick: (index: number) => void;
20
18
  testId?: string;
21
19
  shouldScrollToList?: boolean;
22
- convertValueToString: (value: Value) => string | undefined;
23
- convertValueToReactNode?: (value: Value) => ReactNode;
24
- convertValueToId?: (value: Value) => string | undefined;
25
- }
26
-
27
- export function isOptionDisabled<Value>(option: Value): boolean {
28
- return (
29
- typeof option === 'object' &&
30
- option !== null &&
31
- ((option as { isDisabled?: boolean })?.isDisabled ?? false)
32
- );
20
+ onOptionClick(index: number): void;
21
+ isOptionDisabled(value: Value): boolean;
22
+ convertValueToString(value: Value): string | undefined;
23
+ convertValueToReactNode?(value: Value, isDisabled: boolean): ReactNode;
24
+ convertValueToId?(value: Value): string | undefined;
33
25
  }
34
26
 
35
27
  const DEFAULT_OPTION_INDEX = -1;
36
28
 
37
29
  export function SelectList<Value>({
38
30
  options,
39
- onOptionClick,
40
31
  focusedIndex,
41
32
  activeValue,
42
33
  defaultOptionLabel,
43
34
  noMatchesLabel = 'Совпадений не найдено',
44
35
  isLoading,
45
36
  loadingLabel = 'Загрузка...',
46
- convertValueToString,
47
- convertValueToReactNode,
48
- convertValueToId = convertValueToString,
49
37
  tweakStyles,
50
38
  testId,
51
39
  shouldScrollToList = true,
40
+ isOptionDisabled,
41
+ onOptionClick,
42
+ convertValueToString,
43
+ convertValueToReactNode,
44
+ convertValueToId = convertValueToString,
52
45
  }: ISelectListProps<Value>): JSX.Element {
53
46
  const { classes } = useTheme('SelectList', styles, tweakStyles);
54
47
  const activeValueId = isNotEmpty(activeValue)
55
48
  ? convertValueToId(activeValue)
56
49
  : undefined;
57
50
 
58
- const convertedToStringOptions = useMemo(
59
- () => options.map(convertValueToString),
60
- [options, convertValueToString],
61
- );
62
-
63
51
  const isActiveOption = (item: Value): boolean =>
64
52
  convertValueToId(item) === activeValueId;
65
53
 
66
- const convertedToReactNodesOptions = useMemo(
67
- () =>
68
- convertValueToReactNode !== undefined
69
- ? options.map(convertValueToReactNode)
70
- : [],
71
- [options, convertValueToReactNode],
54
+ const convertFunction = convertValueToReactNode ?? convertValueToString;
55
+
56
+ const optionsDisableMap = useMemo(
57
+ () => options.map((o) => isOptionDisabled(o)),
58
+ [options, isOptionDisabled],
72
59
  );
73
60
 
74
- const listOptions =
75
- convertValueToReactNode !== undefined
76
- ? convertedToReactNodesOptions
77
- : convertedToStringOptions;
61
+ const listOptions = useMemo(
62
+ () => options.map((opt, i) => convertFunction(opt, optionsDisableMap[i])),
63
+ [options, convertFunction, optionsDisableMap],
64
+ );
78
65
 
79
66
  return (
80
67
  <ScrollIntoViewIfNeeded
@@ -102,23 +89,29 @@ export function SelectList<Value>({
102
89
  {defaultOptionLabel}
103
90
  </ScrollIntoViewIfNeeded>
104
91
  )}
105
- {listOptions.map((opt, index) => {
106
- const optionValue = options[index];
92
+ {listOptions.map((opt, i) => {
93
+ const optionValue = options[i];
94
+ const isFocused = i === focusedIndex;
107
95
  const isActive = isActiveOption(optionValue);
108
96
  // проверяем, что опция задизейблена
109
- const isDisabled = isOptionDisabled(optionValue);
97
+ const isDisabled = optionsDisableMap[i];
110
98
 
111
99
  return (
112
100
  <ScrollIntoViewIfNeeded
113
- active={index === focusedIndex}
101
+ active={isFocused}
114
102
  options={{ block: 'nearest' }}
115
- key={index}
103
+ key={i}
116
104
  className={clsx(classes.cell, {
117
- [classes.focused]: index === focusedIndex,
105
+ [classes.focused]: isFocused,
118
106
  [classes.active]: isActive,
119
107
  [classes.disabled]: isDisabled,
120
108
  })}
121
- onClick={!isDisabled ? () => onOptionClick(index) : undefined}
109
+ {...addDataAttributes({
110
+ disabled: isDisabled,
111
+ active: isActive,
112
+ focused: isFocused,
113
+ })}
114
+ onClick={!isDisabled ? () => onOptionClick(i) : undefined}
122
115
  >
123
116
  {opt}
124
117
  </ScrollIntoViewIfNeeded>
@@ -1,5 +1,10 @@
1
1
  import { isNotEmpty } from '../../helpers';
2
2
 
3
+ export const defaultIsOptionDisabled = <Value>(option: Value): boolean =>
4
+ typeof option === 'object' &&
5
+ option !== null &&
6
+ ((option as { isDisabled?: boolean })?.isDisabled ?? false);
7
+
3
8
  export const defaultConvertFunction = (v: unknown) =>
4
9
  v === undefined ? undefined : String(v);
5
10