@true-engineering/true-react-common-ui-kit 1.8.1 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/Input/Input.styles.d.ts +1 -0
- package/dist/components/Select/Select.d.ts +12 -3
- package/dist/components/Select/Select.styles.d.ts +10 -0
- package/dist/components/Select/SelectList/SelectList.d.ts +6 -4
- package/dist/components/Select/SelectList/SelectList.styles.d.ts +5 -0
- package/dist/components/Select/SelectListItem/SelectListItem.d.ts +14 -0
- package/dist/components/Select/SelectListItem/SelectListItem.styles.d.ts +2 -0
- package/dist/components/Select/constants.d.ts +2 -0
- package/dist/components/Select/helpers.d.ts +4 -1
- package/dist/components/Select/index.d.ts +1 -0
- package/dist/components/Select/types.d.ts +1 -0
- package/dist/helpers/utils.d.ts +3 -1
- package/dist/true-react-common-ui-kit.js +1133 -217
- package/dist/true-react-common-ui-kit.js.map +1 -1
- package/dist/true-react-common-ui-kit.umd.cjs +1105 -188
- package/dist/true-react-common-ui-kit.umd.cjs.map +1 -1
- package/package.json +3 -3
- package/src/components/Button/Button.tsx +1 -1
- package/src/components/FiltersPane/FilterSelect/locales.ts +1 -1
- package/src/components/FiltersPane/locales.ts +1 -1
- package/src/components/Input/Input.styles.ts +2 -0
- package/src/components/Input/Input.tsx +4 -1
- package/src/components/MultiSelectList/locales.ts +1 -1
- package/src/components/Select/MultiSelect.stories.tsx +262 -0
- package/src/components/Select/Select.styles.ts +13 -0
- package/src/components/Select/Select.tsx +218 -117
- package/src/components/Select/SelectList/SelectList.styles.ts +6 -2
- package/src/components/Select/SelectList/SelectList.tsx +64 -39
- package/src/components/Select/SelectListItem/SelectListItem.styles.ts +14 -0
- package/src/components/Select/SelectListItem/SelectListItem.tsx +73 -0
- package/src/components/Select/constants.ts +2 -0
- package/src/components/Select/helpers.ts +16 -8
- package/src/components/Select/index.ts +1 -0
- package/src/components/Select/types.ts +1 -0
- package/src/helpers/utils.ts +33 -2
- package/src/hooks/use-theme.ts +1 -1
- package/src/hooks/use-tweak-styles.ts +1 -1
|
@@ -3,30 +3,33 @@ import clsx from 'clsx';
|
|
|
3
3
|
import { ScrollIntoViewIfNeeded } from '../../ScrollIntoViewIfNeeded';
|
|
4
4
|
import { useTheme } from '../../../hooks';
|
|
5
5
|
import { ICommonProps } from '../../../types';
|
|
6
|
-
import {
|
|
6
|
+
import { isNotEmpty } from '../../../helpers';
|
|
7
7
|
import { SelectListStyles, styles } from './SelectList.styles';
|
|
8
|
+
import { IMultipleSelectValue } from '../types';
|
|
9
|
+
import { SelectListItem } from '../SelectListItem/SelectListItem';
|
|
10
|
+
import { ALL_OPTION_INDEX, DEFAULT_OPTION_INDEX } from '../constants';
|
|
8
11
|
|
|
9
12
|
export interface ISelectListProps<Value> extends ICommonProps {
|
|
10
13
|
tweakStyles?: SelectListStyles;
|
|
11
14
|
options: Value[];
|
|
12
15
|
focusedIndex?: number;
|
|
13
|
-
activeValue?: Value;
|
|
16
|
+
activeValue?: Value | Value[];
|
|
14
17
|
noMatchesLabel?: string;
|
|
15
18
|
isLoading?: boolean;
|
|
16
19
|
loadingLabel?: ReactNode;
|
|
17
20
|
defaultOptionLabel?: string;
|
|
18
21
|
testId?: string;
|
|
22
|
+
allOptionsLabel?: string;
|
|
19
23
|
shouldScrollToList?: boolean;
|
|
20
24
|
customListHeader?: ReactNode;
|
|
21
|
-
|
|
25
|
+
onOptionSelect(index: number, event: MouseEvent<HTMLElement>): void;
|
|
26
|
+
onToggleCheckbox?(index: number, isSelected: boolean): void;
|
|
22
27
|
isOptionDisabled(value: Value): boolean;
|
|
23
28
|
convertValueToString(value: Value): string | undefined;
|
|
24
29
|
convertValueToReactNode?(value: Value, isDisabled: boolean): ReactNode;
|
|
25
|
-
convertValueToId
|
|
30
|
+
convertValueToId(value: Value): string | undefined;
|
|
26
31
|
}
|
|
27
32
|
|
|
28
|
-
const DEFAULT_OPTION_INDEX = -1;
|
|
29
|
-
|
|
30
33
|
export function SelectList<Value>({
|
|
31
34
|
options,
|
|
32
35
|
focusedIndex,
|
|
@@ -40,20 +43,25 @@ export function SelectList<Value>({
|
|
|
40
43
|
shouldScrollToList = true,
|
|
41
44
|
customListHeader,
|
|
42
45
|
isOptionDisabled,
|
|
43
|
-
|
|
46
|
+
allOptionsLabel,
|
|
47
|
+
onOptionSelect,
|
|
48
|
+
onToggleCheckbox,
|
|
44
49
|
convertValueToString,
|
|
45
|
-
convertValueToReactNode,
|
|
46
|
-
convertValueToId
|
|
50
|
+
convertValueToReactNode = convertValueToString,
|
|
51
|
+
convertValueToId,
|
|
47
52
|
}: ISelectListProps<Value>): JSX.Element {
|
|
48
53
|
const { classes } = useTheme('SelectList', styles, tweakStyles);
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
convertValueToId(item) === activeValueId;
|
|
54
|
+
const isMultiSelect = isNotEmpty(onToggleCheckbox);
|
|
55
|
+
const multiSelectValue = activeValue as
|
|
56
|
+
| IMultipleSelectValue<Value>
|
|
57
|
+
| undefined;
|
|
58
|
+
const selectedOptionsCount = multiSelectValue?.length ?? 0;
|
|
55
59
|
|
|
56
|
-
|
|
60
|
+
// MultiSelect
|
|
61
|
+
const activeOptionsIdMap = useMemo(
|
|
62
|
+
() => (isMultiSelect ? multiSelectValue?.map(convertValueToId) ?? [] : []),
|
|
63
|
+
[isMultiSelect, multiSelectValue, convertValueToId],
|
|
64
|
+
);
|
|
57
65
|
|
|
58
66
|
const optionsDisableMap = useMemo(
|
|
59
67
|
() => options.map((o) => isOptionDisabled(o)),
|
|
@@ -61,13 +69,22 @@ export function SelectList<Value>({
|
|
|
61
69
|
);
|
|
62
70
|
|
|
63
71
|
const listOptions = useMemo(
|
|
64
|
-
() =>
|
|
65
|
-
|
|
72
|
+
() =>
|
|
73
|
+
options.map((opt, i) =>
|
|
74
|
+
convertValueToReactNode(opt, optionsDisableMap[i]),
|
|
75
|
+
),
|
|
76
|
+
[options, convertValueToReactNode, optionsDisableMap],
|
|
66
77
|
);
|
|
67
78
|
|
|
79
|
+
const isActiveOption = (item: Value): boolean =>
|
|
80
|
+
isMultiSelect
|
|
81
|
+
? activeOptionsIdMap.includes(convertValueToId(item))
|
|
82
|
+
: isNotEmpty(activeValue) &&
|
|
83
|
+
convertValueToId(activeValue as Value) === convertValueToId(item);
|
|
84
|
+
|
|
68
85
|
return (
|
|
69
86
|
<ScrollIntoViewIfNeeded
|
|
70
|
-
active={shouldScrollToList}
|
|
87
|
+
active={shouldScrollToList && !isMultiSelect}
|
|
71
88
|
className={clsx(classes.root, {
|
|
72
89
|
[classes.withListHeader]: isNotEmpty(customListHeader),
|
|
73
90
|
})}
|
|
@@ -82,7 +99,7 @@ export function SelectList<Value>({
|
|
|
82
99
|
</div>
|
|
83
100
|
) : (
|
|
84
101
|
<>
|
|
85
|
-
{defaultOptionLabel
|
|
102
|
+
{isNotEmpty(defaultOptionLabel) && (
|
|
86
103
|
<ScrollIntoViewIfNeeded
|
|
87
104
|
active={focusedIndex === DEFAULT_OPTION_INDEX}
|
|
88
105
|
options={{ block: 'nearest' }}
|
|
@@ -91,39 +108,47 @@ export function SelectList<Value>({
|
|
|
91
108
|
classes.defaultCell,
|
|
92
109
|
focusedIndex === DEFAULT_OPTION_INDEX && classes.focused,
|
|
93
110
|
)}
|
|
94
|
-
onClick={(event) =>
|
|
111
|
+
onClick={(event) => onOptionSelect(DEFAULT_OPTION_INDEX, event)}
|
|
95
112
|
>
|
|
96
113
|
{defaultOptionLabel}
|
|
97
114
|
</ScrollIntoViewIfNeeded>
|
|
98
115
|
)}
|
|
116
|
+
{isNotEmpty(allOptionsLabel) && (
|
|
117
|
+
<SelectListItem
|
|
118
|
+
classes={classes}
|
|
119
|
+
index={ALL_OPTION_INDEX}
|
|
120
|
+
isSemiChecked={
|
|
121
|
+
selectedOptionsCount > 0 &&
|
|
122
|
+
selectedOptionsCount < options.length
|
|
123
|
+
}
|
|
124
|
+
isActive={selectedOptionsCount === options.length}
|
|
125
|
+
isFocused={focusedIndex === ALL_OPTION_INDEX}
|
|
126
|
+
onOptionSelect={onOptionSelect}
|
|
127
|
+
onToggleCheckbox={onToggleCheckbox}
|
|
128
|
+
>
|
|
129
|
+
{allOptionsLabel}
|
|
130
|
+
</SelectListItem>
|
|
131
|
+
)}
|
|
99
132
|
{listOptions.map((opt, i) => {
|
|
100
133
|
const optionValue = options[i];
|
|
101
|
-
const isFocused =
|
|
134
|
+
const isFocused = focusedIndex === i;
|
|
102
135
|
const isActive = isActiveOption(optionValue);
|
|
103
136
|
// проверяем, что опция задизейблена
|
|
104
137
|
const isDisabled = optionsDisableMap[i];
|
|
105
138
|
|
|
106
139
|
return (
|
|
107
|
-
<
|
|
108
|
-
active={isFocused}
|
|
109
|
-
options={{ block: 'nearest' }}
|
|
140
|
+
<SelectListItem
|
|
110
141
|
key={i}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
116
|
-
{
|
|
117
|
-
|
|
118
|
-
active: isActive,
|
|
119
|
-
focused: isFocused,
|
|
120
|
-
})}
|
|
121
|
-
onClick={
|
|
122
|
-
!isDisabled ? (event) => onOptionClick(i, event) : undefined
|
|
123
|
-
}
|
|
142
|
+
classes={classes}
|
|
143
|
+
index={i}
|
|
144
|
+
isDisabled={isDisabled}
|
|
145
|
+
isActive={isActive}
|
|
146
|
+
isFocused={isFocused}
|
|
147
|
+
onOptionSelect={onOptionSelect}
|
|
148
|
+
onToggleCheckbox={onToggleCheckbox}
|
|
124
149
|
>
|
|
125
150
|
{opt}
|
|
126
|
-
</
|
|
151
|
+
</SelectListItem>
|
|
127
152
|
);
|
|
128
153
|
})}
|
|
129
154
|
{listOptions.length === 0 && (
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { CheckboxStyles } from '../../Checkbox';
|
|
2
|
+
import { CELL_PADDING } from '../SelectList/SelectList.styles';
|
|
3
|
+
|
|
4
|
+
export const checkboxStyles: CheckboxStyles = {
|
|
5
|
+
root: {
|
|
6
|
+
padding: CELL_PADDING,
|
|
7
|
+
width: '100%',
|
|
8
|
+
},
|
|
9
|
+
|
|
10
|
+
input: {
|
|
11
|
+
// иначе будет фокуситься и энтер будет вызывать изменение нескольких опций
|
|
12
|
+
display: 'none',
|
|
13
|
+
},
|
|
14
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { ReactNode, MouseEvent, FC } from 'react';
|
|
2
|
+
import clsx from 'clsx';
|
|
3
|
+
import { ScrollIntoViewIfNeeded } from '../../ScrollIntoViewIfNeeded';
|
|
4
|
+
import { addDataAttributes, isNotEmpty } from '../../../helpers';
|
|
5
|
+
import { checkboxStyles } from './SelectListItem.styles';
|
|
6
|
+
import { Checkbox } from '../../Checkbox';
|
|
7
|
+
import { Classes } from 'jss';
|
|
8
|
+
|
|
9
|
+
export interface ISelectListItemProps {
|
|
10
|
+
index: number;
|
|
11
|
+
isSemiChecked?: boolean;
|
|
12
|
+
isDisabled?: boolean;
|
|
13
|
+
isActive?: boolean;
|
|
14
|
+
isFocused?: boolean;
|
|
15
|
+
children: ReactNode;
|
|
16
|
+
classes: Classes<
|
|
17
|
+
'cellWithCheckbox' | 'cell' | 'focused' | 'active' | 'disabled'
|
|
18
|
+
>;
|
|
19
|
+
onOptionSelect(index: number, event: MouseEvent<HTMLElement>): void;
|
|
20
|
+
onToggleCheckbox?(index: number, isSelected: boolean): void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const SelectListItem: FC<ISelectListItemProps> = ({
|
|
24
|
+
classes,
|
|
25
|
+
index,
|
|
26
|
+
isSemiChecked,
|
|
27
|
+
isDisabled,
|
|
28
|
+
isActive,
|
|
29
|
+
children,
|
|
30
|
+
isFocused,
|
|
31
|
+
onOptionSelect,
|
|
32
|
+
onToggleCheckbox,
|
|
33
|
+
}) => {
|
|
34
|
+
const isMultiSelect = isNotEmpty(onToggleCheckbox);
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<ScrollIntoViewIfNeeded
|
|
38
|
+
active={isFocused}
|
|
39
|
+
options={{ block: 'nearest' }}
|
|
40
|
+
className={clsx(classes.cell, {
|
|
41
|
+
[classes.cellWithCheckbox]: isMultiSelect,
|
|
42
|
+
[classes.focused]: isFocused,
|
|
43
|
+
[classes.active]: isActive && !isMultiSelect,
|
|
44
|
+
[classes.disabled]: isDisabled,
|
|
45
|
+
})}
|
|
46
|
+
{...addDataAttributes({
|
|
47
|
+
disabled: isDisabled,
|
|
48
|
+
active: isActive,
|
|
49
|
+
focused: isFocused,
|
|
50
|
+
})}
|
|
51
|
+
onClick={
|
|
52
|
+
!isDisabled && !isMultiSelect
|
|
53
|
+
? (event) => onOptionSelect(index, event)
|
|
54
|
+
: undefined
|
|
55
|
+
}
|
|
56
|
+
>
|
|
57
|
+
{isMultiSelect ? (
|
|
58
|
+
<Checkbox
|
|
59
|
+
value={index}
|
|
60
|
+
isChecked={isActive || isSemiChecked}
|
|
61
|
+
isSemiChecked={isSemiChecked}
|
|
62
|
+
isDisabled={isDisabled}
|
|
63
|
+
tweakStyles={checkboxStyles}
|
|
64
|
+
onSelect={(v) => onToggleCheckbox(index, v.isSelected)}
|
|
65
|
+
>
|
|
66
|
+
{children}
|
|
67
|
+
</Checkbox>
|
|
68
|
+
) : (
|
|
69
|
+
children
|
|
70
|
+
)}
|
|
71
|
+
</ScrollIntoViewIfNeeded>
|
|
72
|
+
);
|
|
73
|
+
};
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { isNotEmpty } from '../../helpers';
|
|
2
|
+
import type { IMultipleSelectProps, ISelectProps } from './Select';
|
|
3
|
+
import { IMultipleSelectValue } from './types';
|
|
2
4
|
|
|
3
5
|
export const defaultIsOptionDisabled = <Value>(option: Value): boolean =>
|
|
4
6
|
typeof option === 'object' &&
|
|
@@ -11,11 +13,17 @@ export const defaultConvertFunction = (v: unknown) =>
|
|
|
11
13
|
export const defaultCompareFunction = <Value>(v1: Value, v2: Value) =>
|
|
12
14
|
v1 === v2;
|
|
13
15
|
|
|
14
|
-
export const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
export const getDefaultConvertToIdFunction =
|
|
17
|
+
<Value>(
|
|
18
|
+
convertValueToString: (value: Value) => string | undefined,
|
|
19
|
+
): ((value: Value) => string | undefined) =>
|
|
20
|
+
(value) =>
|
|
21
|
+
isNotEmpty((value as { id: unknown })?.id)
|
|
22
|
+
? String((value as { id: unknown }).id)
|
|
23
|
+
: convertValueToString(value);
|
|
24
|
+
|
|
25
|
+
export const isMultiSelectValue = <Value>(
|
|
26
|
+
props: ISelectProps<Value> | IMultipleSelectProps<Value>,
|
|
27
|
+
_value: Value | IMultipleSelectValue<Value> | undefined,
|
|
28
|
+
): _value is IMultipleSelectValue<Value> | undefined =>
|
|
29
|
+
props.isMultiSelect === true;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type IMultipleSelectValue<Value> = Array<NonNullable<Value>>;
|
package/src/helpers/utils.ts
CHANGED
|
@@ -28,8 +28,10 @@ export const hasExactParent = (element: Element, parent: Element): boolean => {
|
|
|
28
28
|
return hasExactParent(parentNode, parent);
|
|
29
29
|
};
|
|
30
30
|
|
|
31
|
-
export const getParentNode = (
|
|
32
|
-
element
|
|
31
|
+
export const getParentNode = (
|
|
32
|
+
element: Element | ShadowRoot | Document,
|
|
33
|
+
): Element =>
|
|
34
|
+
element.nodeName === 'HTML' || element === document
|
|
33
35
|
? (element as Element)
|
|
34
36
|
: (element.parentNode as Element) ?? (element as ShadowRoot).host;
|
|
35
37
|
|
|
@@ -167,6 +169,13 @@ export const isNotEmpty = <T>(val: T | null | undefined): val is T =>
|
|
|
167
169
|
? val.trim() !== ''
|
|
168
170
|
: val !== null && val !== undefined;
|
|
169
171
|
|
|
172
|
+
/**
|
|
173
|
+
* Проверяет, что передана непустая строка
|
|
174
|
+
*/
|
|
175
|
+
export const isStringNotEmpty = <T extends string>(
|
|
176
|
+
value: T | undefined | null,
|
|
177
|
+
): value is T => (value ?? '').trim() !== '';
|
|
178
|
+
|
|
170
179
|
export const trimStringToMaxLength = (val: string, maxLength: number) =>
|
|
171
180
|
val.length > maxLength ? val.slice(0, maxLength) : val;
|
|
172
181
|
|
|
@@ -217,3 +226,25 @@ export const addClickHandler = (
|
|
|
217
226
|
: {
|
|
218
227
|
tabIndex: -1,
|
|
219
228
|
};
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Позволяет создать текстовый фильтр для набора items
|
|
232
|
+
* @param getter - функция возвращающая набор строковых значений из каждого item,
|
|
233
|
+
* по которым должен осуществляться поиск
|
|
234
|
+
*/
|
|
235
|
+
export const createFilter =
|
|
236
|
+
<T>(
|
|
237
|
+
getter: (item: T) => Array<string | undefined>,
|
|
238
|
+
): ((items: T[], query: string) => T[]) =>
|
|
239
|
+
(items, query) =>
|
|
240
|
+
items.filter((item) => {
|
|
241
|
+
const possibleValues = getter(item).reduce(
|
|
242
|
+
(acc, cur) => [
|
|
243
|
+
...acc,
|
|
244
|
+
...(isStringNotEmpty(cur) ? [cur?.toLowerCase()] : []),
|
|
245
|
+
],
|
|
246
|
+
[] as string[],
|
|
247
|
+
);
|
|
248
|
+
const queryString = query.toLowerCase().trim();
|
|
249
|
+
return possibleValues.some((v) => v?.includes(queryString));
|
|
250
|
+
});
|
package/src/hooks/use-theme.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createContext, useContext, useMemo } from 'react';
|
|
2
2
|
import { createUseStyles, Styles } from 'react-jss';
|
|
3
|
-
import
|
|
3
|
+
import merge from 'lodash-es/merge';
|
|
4
4
|
|
|
5
5
|
import { ComponentName, UiKitTheme } from '../types';
|
|
6
6
|
import { commonTheme } from '../theme';
|