@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/README.md +6 -0
- package/dist/components/FlexibleTable/types.d.ts +2 -2
- package/dist/components/Select/Select.d.ts +8 -7
- package/dist/components/Select/SelectList/SelectList.d.ts +8 -8
- package/dist/components/Select/helpers.d.ts +1 -0
- package/dist/true-react-common-ui-kit.js +193 -138
- package/dist/true-react-common-ui-kit.js.map +1 -1
- package/dist/true-react-common-ui-kit.umd.cjs +193 -138
- package/dist/true-react-common-ui-kit.umd.cjs.map +1 -1
- package/package.json +3 -2
- package/src/components/DateInput/DateInput.stories.tsx +1 -1
- package/src/components/FiltersPane/FilterWithPeriod/FilterWithPeriod.tsx +1 -1
- package/src/components/FlexibleTable/FlexibleTable.tsx +2 -0
- package/src/components/FlexibleTable/TableRow.tsx +3 -1
- package/src/components/FlexibleTable/TableValue.tsx +2 -5
- package/src/components/FlexibleTable/types.ts +2 -2
- package/src/components/IncrementInput/IncrementInput.stories.tsx +1 -1
- package/src/components/NumberInput/NumberInput.stories.tsx +1 -1
- package/src/components/Select/Select.stories.tsx +30 -18
- package/src/components/Select/Select.tsx +77 -73
- package/src/components/Select/SelectList/SelectList.tsx +35 -42
- package/src/components/Select/helpers.ts +5 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@true-engineering/true-react-common-ui-kit",
|
|
3
|
-
"version": "1.
|
|
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",
|
|
@@ -93,7 +93,7 @@ export const FilterWithPeriod: FC<IFilterWithPeriodProps> = ({
|
|
|
93
93
|
value?.periodType === 'CUSTOM',
|
|
94
94
|
);
|
|
95
95
|
|
|
96
|
-
const [period, setPeriod] = useState
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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<
|
|
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
|
|
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
|
|
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
|
|
31
|
-
v !== undefined ? `${v.name}` : undefined;
|
|
30
|
+
const convertObjectToString = (v: ObjectValue): string => v.name;
|
|
32
31
|
|
|
33
|
-
const convertObjectToId = (v
|
|
34
|
-
v !== undefined ? `${v.name}${v.age}` : undefined;
|
|
32
|
+
const convertObjectToId = (v: ObjectValue): string => `${v.name}${v.age}`;
|
|
35
33
|
|
|
36
|
-
const convertObjectToReactNode = (
|
|
37
|
-
v
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
|
44
|
-
|
|
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
|
|
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
|
|
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
|
|
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?:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
{
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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
|
|
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?:
|
|
16
|
+
loadingLabel?: ReactNode;
|
|
18
17
|
defaultOptionLabel?: string;
|
|
19
|
-
onOptionClick: (index: number) => void;
|
|
20
18
|
testId?: string;
|
|
21
19
|
shouldScrollToList?: boolean;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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,
|
|
106
|
-
const optionValue = options[
|
|
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 =
|
|
97
|
+
const isDisabled = optionsDisableMap[i];
|
|
110
98
|
|
|
111
99
|
return (
|
|
112
100
|
<ScrollIntoViewIfNeeded
|
|
113
|
-
active={
|
|
101
|
+
active={isFocused}
|
|
114
102
|
options={{ block: 'nearest' }}
|
|
115
|
-
key={
|
|
103
|
+
key={i}
|
|
116
104
|
className={clsx(classes.cell, {
|
|
117
|
-
[classes.focused]:
|
|
105
|
+
[classes.focused]: isFocused,
|
|
118
106
|
[classes.active]: isActive,
|
|
119
107
|
[classes.disabled]: isDisabled,
|
|
120
108
|
})}
|
|
121
|
-
|
|
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
|
|