@true-engineering/true-react-common-ui-kit 1.9.0 → 1.11.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.
Files changed (31) hide show
  1. package/dist/components/Input/Input.styles.d.ts +1 -0
  2. package/dist/components/Select/Select.d.ts +12 -3
  3. package/dist/components/Select/Select.styles.d.ts +9 -0
  4. package/dist/components/Select/SelectList/SelectList.d.ts +7 -4
  5. package/dist/components/Select/SelectList/SelectList.styles.d.ts +5 -0
  6. package/dist/components/Select/SelectListItem/SelectListItem.d.ts +14 -0
  7. package/dist/components/Select/SelectListItem/SelectListItem.styles.d.ts +2 -0
  8. package/dist/components/Select/constants.d.ts +2 -0
  9. package/dist/components/Select/helpers.d.ts +4 -1
  10. package/dist/components/Select/index.d.ts +1 -0
  11. package/dist/components/Select/types.d.ts +1 -0
  12. package/dist/helpers/utils.d.ts +2 -0
  13. package/dist/true-react-common-ui-kit.js +365 -163
  14. package/dist/true-react-common-ui-kit.js.map +1 -1
  15. package/dist/true-react-common-ui-kit.umd.cjs +365 -163
  16. package/dist/true-react-common-ui-kit.umd.cjs.map +1 -1
  17. package/package.json +1 -1
  18. package/src/components/Input/Input.styles.ts +2 -0
  19. package/src/components/Input/Input.tsx +4 -1
  20. package/src/components/Select/MultiSelect.stories.tsx +263 -0
  21. package/src/components/Select/Select.styles.ts +11 -0
  22. package/src/components/Select/Select.tsx +234 -114
  23. package/src/components/Select/SelectList/SelectList.styles.ts +6 -2
  24. package/src/components/Select/SelectList/SelectList.tsx +65 -39
  25. package/src/components/Select/SelectListItem/SelectListItem.styles.ts +14 -0
  26. package/src/components/Select/SelectListItem/SelectListItem.tsx +73 -0
  27. package/src/components/Select/constants.ts +2 -0
  28. package/src/components/Select/helpers.ts +16 -8
  29. package/src/components/Select/index.ts +1 -0
  30. package/src/components/Select/types.ts +1 -0
  31. package/src/helpers/utils.ts +29 -0
@@ -3,30 +3,34 @@ import clsx from 'clsx';
3
3
  import { ScrollIntoViewIfNeeded } from '../../ScrollIntoViewIfNeeded';
4
4
  import { useTheme } from '../../../hooks';
5
5
  import { ICommonProps } from '../../../types';
6
- import { addDataAttributes, isNotEmpty } from '../../../helpers';
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;
23
+ areAllOptionsSelected?: boolean;
19
24
  shouldScrollToList?: boolean;
20
25
  customListHeader?: ReactNode;
21
- onOptionClick(index: number, event: MouseEvent<HTMLElement>): void;
26
+ onOptionSelect(index: number, event: MouseEvent<HTMLElement>): void;
27
+ onToggleCheckbox?(index: number, isSelected: boolean): void;
22
28
  isOptionDisabled(value: Value): boolean;
23
29
  convertValueToString(value: Value): string | undefined;
24
30
  convertValueToReactNode?(value: Value, isDisabled: boolean): ReactNode;
25
- convertValueToId?(value: Value): string | undefined;
31
+ convertValueToId(value: Value): string | undefined;
26
32
  }
27
33
 
28
- const DEFAULT_OPTION_INDEX = -1;
29
-
30
34
  export function SelectList<Value>({
31
35
  options,
32
36
  focusedIndex,
@@ -38,22 +42,28 @@ export function SelectList<Value>({
38
42
  tweakStyles,
39
43
  testId,
40
44
  shouldScrollToList = true,
45
+ areAllOptionsSelected,
41
46
  customListHeader,
42
47
  isOptionDisabled,
43
- onOptionClick,
48
+ allOptionsLabel,
49
+ onOptionSelect,
50
+ onToggleCheckbox,
44
51
  convertValueToString,
45
- convertValueToReactNode,
46
- convertValueToId = convertValueToString,
52
+ convertValueToReactNode = convertValueToString,
53
+ convertValueToId,
47
54
  }: ISelectListProps<Value>): JSX.Element {
48
55
  const { classes } = useTheme('SelectList', styles, tweakStyles);
49
- const activeValueId = isNotEmpty(activeValue)
50
- ? convertValueToId(activeValue)
51
- : undefined;
52
-
53
- const isActiveOption = (item: Value): boolean =>
54
- convertValueToId(item) === activeValueId;
56
+ const isMultiSelect = isNotEmpty(onToggleCheckbox);
57
+ const multiSelectValue = activeValue as
58
+ | IMultipleSelectValue<Value>
59
+ | undefined;
60
+ const selectedOptionsCount = multiSelectValue?.length ?? 0;
55
61
 
56
- const convertFunction = convertValueToReactNode ?? convertValueToString;
62
+ // MultiSelect
63
+ const activeOptionsIdMap = useMemo(
64
+ () => (isMultiSelect ? multiSelectValue?.map(convertValueToId) ?? [] : []),
65
+ [isMultiSelect, multiSelectValue, convertValueToId],
66
+ );
57
67
 
58
68
  const optionsDisableMap = useMemo(
59
69
  () => options.map((o) => isOptionDisabled(o)),
@@ -61,13 +71,22 @@ export function SelectList<Value>({
61
71
  );
62
72
 
63
73
  const listOptions = useMemo(
64
- () => options.map((opt, i) => convertFunction(opt, optionsDisableMap[i])),
65
- [options, convertFunction, optionsDisableMap],
74
+ () =>
75
+ options.map((opt, i) =>
76
+ convertValueToReactNode(opt, optionsDisableMap[i]),
77
+ ),
78
+ [options, convertValueToReactNode, optionsDisableMap],
66
79
  );
67
80
 
81
+ const isActiveOption = (item: Value): boolean =>
82
+ isMultiSelect
83
+ ? activeOptionsIdMap.includes(convertValueToId(item))
84
+ : isNotEmpty(activeValue) &&
85
+ convertValueToId(activeValue as Value) === convertValueToId(item);
86
+
68
87
  return (
69
88
  <ScrollIntoViewIfNeeded
70
- active={shouldScrollToList}
89
+ active={shouldScrollToList && !isMultiSelect}
71
90
  className={clsx(classes.root, {
72
91
  [classes.withListHeader]: isNotEmpty(customListHeader),
73
92
  })}
@@ -82,7 +101,7 @@ export function SelectList<Value>({
82
101
  </div>
83
102
  ) : (
84
103
  <>
85
- {defaultOptionLabel !== undefined && (
104
+ {isNotEmpty(defaultOptionLabel) && (
86
105
  <ScrollIntoViewIfNeeded
87
106
  active={focusedIndex === DEFAULT_OPTION_INDEX}
88
107
  options={{ block: 'nearest' }}
@@ -91,39 +110,46 @@ export function SelectList<Value>({
91
110
  classes.defaultCell,
92
111
  focusedIndex === DEFAULT_OPTION_INDEX && classes.focused,
93
112
  )}
94
- onClick={(event) => onOptionClick(DEFAULT_OPTION_INDEX, event)}
113
+ onClick={(event) => onOptionSelect(DEFAULT_OPTION_INDEX, event)}
95
114
  >
96
115
  {defaultOptionLabel}
97
116
  </ScrollIntoViewIfNeeded>
98
117
  )}
118
+ {isNotEmpty(allOptionsLabel) && (
119
+ <SelectListItem
120
+ classes={classes}
121
+ index={ALL_OPTION_INDEX}
122
+ isSemiChecked={
123
+ selectedOptionsCount > 0 && !areAllOptionsSelected
124
+ }
125
+ isActive={areAllOptionsSelected}
126
+ isFocused={focusedIndex === ALL_OPTION_INDEX}
127
+ onOptionSelect={onOptionSelect}
128
+ onToggleCheckbox={onToggleCheckbox}
129
+ >
130
+ {allOptionsLabel}
131
+ </SelectListItem>
132
+ )}
99
133
  {listOptions.map((opt, i) => {
100
134
  const optionValue = options[i];
101
- const isFocused = i === focusedIndex;
135
+ const isFocused = focusedIndex === i;
102
136
  const isActive = isActiveOption(optionValue);
103
137
  // проверяем, что опция задизейблена
104
138
  const isDisabled = optionsDisableMap[i];
105
139
 
106
140
  return (
107
- <ScrollIntoViewIfNeeded
108
- active={isFocused}
109
- options={{ block: 'nearest' }}
141
+ <SelectListItem
110
142
  key={i}
111
- className={clsx(classes.cell, {
112
- [classes.focused]: isFocused,
113
- [classes.active]: isActive,
114
- [classes.disabled]: isDisabled,
115
- })}
116
- {...addDataAttributes({
117
- disabled: isDisabled,
118
- active: isActive,
119
- focused: isFocused,
120
- })}
121
- onClick={
122
- !isDisabled ? (event) => onOptionClick(i, event) : undefined
123
- }
143
+ classes={classes}
144
+ index={i}
145
+ isDisabled={isDisabled}
146
+ isActive={isActive}
147
+ isFocused={isFocused}
148
+ onOptionSelect={onOptionSelect}
149
+ onToggleCheckbox={onToggleCheckbox}
124
150
  >
125
151
  {opt}
126
- </ScrollIntoViewIfNeeded>
152
+ </SelectListItem>
127
153
  );
128
154
  })}
129
155
  {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
+ };
@@ -0,0 +1,2 @@
1
+ export const DEFAULT_OPTION_INDEX = -2;
2
+ export const ALL_OPTION_INDEX = -1;
@@ -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 getActiveValueIndex = <Value>(
15
- options: Value[],
16
- value: Value | undefined,
17
- convertFunc: (v: Value) => string | undefined,
18
- ): number =>
19
- isNotEmpty(value)
20
- ? options.findIndex((o) => convertFunc(o) === convertFunc(value))
21
- : -1;
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;
@@ -1,3 +1,4 @@
1
1
  export * from './Select';
2
+ export type { IMultipleSelectValue } from './types';
2
3
  export type { SelectStyles } from './Select.styles';
3
4
  export type { SelectListStyles } from './SelectList/SelectList.styles';
@@ -0,0 +1 @@
1
+ export type IMultipleSelectValue<Value> = Array<NonNullable<Value>>;
@@ -169,6 +169,13 @@ export const isNotEmpty = <T>(val: T | null | undefined): val is T =>
169
169
  ? val.trim() !== ''
170
170
  : val !== null && val !== undefined;
171
171
 
172
+ /**
173
+ * Проверяет, что передана непустая строка
174
+ */
175
+ export const isStringNotEmpty = <T extends string>(
176
+ value: T | undefined | null,
177
+ ): value is T => (value ?? '').trim() !== '';
178
+
172
179
  export const trimStringToMaxLength = (val: string, maxLength: number) =>
173
180
  val.length > maxLength ? val.slice(0, maxLength) : val;
174
181
 
@@ -219,3 +226,25 @@ export const addClickHandler = (
219
226
  : {
220
227
  tabIndex: -1,
221
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
+ });