@true-engineering/true-react-common-ui-kit 3.8.1 → 3.9.1

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": "3.8.1",
3
+ "version": "3.9.1",
4
4
  "description": "True Engineering React UI Kit with theming support",
5
5
  "author": "True Engineering (https://trueengineering.ru)",
6
6
  "keywords": [
@@ -1,4 +1,4 @@
1
- import { useEffect, useState, ReactNode } from 'react';
1
+ import { useEffect, useState, ReactNode, ChangeEvent, KeyboardEvent } from 'react';
2
2
  import clsx from 'clsx';
3
3
  import {
4
4
  addDataTestId,
@@ -24,7 +24,10 @@ export interface ICheckboxProps<V> extends ICommonProps<ICheckboxStyles> {
24
24
  value: V;
25
25
  /** @default 'right' */
26
26
  labelPosition?: 'right' | 'left';
27
- onSelect: (value: { value: V; isSelected: boolean }) => void;
27
+ onSelect: (
28
+ value: { value: V; isSelected: boolean },
29
+ event: ChangeEvent<HTMLInputElement> | KeyboardEvent,
30
+ ) => void;
28
31
  }
29
32
 
30
33
  export function Checkbox<V>({
@@ -46,9 +49,9 @@ export function Checkbox<V>({
46
49
 
47
50
  const hasAction = !isDisabled && !isReadonly;
48
51
 
49
- const onToggle = () => {
52
+ const onToggle = (event: ChangeEvent<HTMLInputElement> | KeyboardEvent) => {
50
53
  const isSelectedNext = !isSelected;
51
- onSelect({ value, isSelected: isSelectedNext });
54
+ onSelect({ value, isSelected: isSelectedNext }, event);
52
55
  setIsSelected(isSelectedNext);
53
56
  };
54
57
 
@@ -1,9 +1,8 @@
1
1
  import { colors, ITweakStyles, createThemedStyles } from '../../theme';
2
2
  import { IListStyles } from '../List';
3
+ import { IWithPopupStyles } from '../WithPopup';
3
4
 
4
5
  export const useStyles = createThemedStyles('NewMoreMenu', {
5
- root: {},
6
-
7
6
  hasCircle: {},
8
7
 
9
8
  button: {
@@ -31,8 +30,9 @@ export const useStyles = createThemedStyles('NewMoreMenu', {
31
30
  disabled: {
32
31
  cursor: 'default',
33
32
  },
34
-
35
- menu: {},
36
33
  });
37
34
 
38
- export type INewMoreMenuStyles = ITweakStyles<typeof useStyles, { tweakList: IListStyles }>;
35
+ export type INewMoreMenuStyles = ITweakStyles<
36
+ typeof useStyles,
37
+ { tweakList: IListStyles; tweakWithPopup: IWithPopupStyles }
38
+ >;
@@ -2,6 +2,7 @@ import { FC } from 'react';
2
2
  import clsx from 'clsx';
3
3
  import { addDataTestId, isArrayNotEmpty } from '@true-engineering/true-react-platform-helpers';
4
4
  import { addDataAttributes } from '../../helpers';
5
+ import { useTweakStyles } from '../../hooks';
5
6
  import { ICommonProps } from '../../types';
6
7
  import { Icon } from '../Icon';
7
8
  import { IListItem, List } from '../List';
@@ -34,6 +35,18 @@ export const NewMoreMenu: FC<INewMoreMenuProps> = ({
34
35
  }) => {
35
36
  const classes = useStyles({ theme: tweakStyles });
36
37
 
38
+ const tweakWithPopupStyles = useTweakStyles({
39
+ tweakStyles,
40
+ className: 'tweakWithPopup',
41
+ currentComponentName: 'NewMoreMenu',
42
+ });
43
+
44
+ const tweakListStyles = useTweakStyles({
45
+ tweakStyles,
46
+ className: 'tweakList',
47
+ currentComponentName: 'NewMoreMenu',
48
+ });
49
+
37
50
  const isButtonDisabled = isDisabled || !isArrayNotEmpty(items);
38
51
 
39
52
  return (
@@ -43,6 +56,7 @@ export const NewMoreMenu: FC<INewMoreMenuProps> = ({
43
56
  shouldHideOnScroll={shouldHideOnScroll}
44
57
  shouldRenderInBody={shouldRenderInBody}
45
58
  isDisabled={isButtonDisabled}
59
+ tweakStyles={tweakWithPopupStyles}
46
60
  trigger={({ isActive }) => (
47
61
  <button
48
62
  className={clsx(classes.button, {
@@ -60,7 +74,7 @@ export const NewMoreMenu: FC<INewMoreMenuProps> = ({
60
74
  </button>
61
75
  )}
62
76
  >
63
- {({ onClose }) => <List items={items} onClick={onClose} />}
77
+ {({ onClose }) => <List items={items} tweakStyles={tweakListStyles} onClick={onClose} />}
64
78
  </WithPopup>
65
79
  );
66
80
  };
@@ -9,6 +9,8 @@ import {
9
9
  useRef,
10
10
  useState,
11
11
  SyntheticEvent,
12
+ ChangeEvent,
13
+ FormEvent,
12
14
  } from 'react';
13
15
  import { Portal } from 'react-overlays';
14
16
  import clsx from 'clsx';
@@ -55,14 +57,21 @@ export interface ISelectProps<Value>
55
57
  dropdownOptions?: IDropdownWithPopperOptions;
56
58
  /** @default 'chevron-down' */
57
59
  dropdownIcon?: IIcon;
58
- options: Value[];
60
+ options: Value[] | Readonly<Value[]>;
59
61
  value: Value | undefined;
60
62
  /** @default true */
61
63
  shouldScrollToList?: boolean;
62
64
  isMultiSelect?: boolean;
63
65
  searchInput?: { shouldRenderInList: true } & Pick<ISearchInputProps, 'placeholder'>;
64
66
  isOptionDisabled?: (option: Value) => boolean;
65
- onChange: (value?: Value) => void; // подумать как возвращать индекс
67
+ onChange: (
68
+ value: Value | undefined,
69
+ event:
70
+ | MouseEvent<HTMLElement>
71
+ | KeyboardEvent
72
+ | ChangeEvent<HTMLElement>
73
+ | FormEvent<HTMLElement>,
74
+ ) => void; // подумать как возвращать индекс
66
75
  onBlur?: (event: Event | SyntheticEvent) => void;
67
76
  onType?: (value: string) => Promise<void>;
68
77
  optionsFilter?: (options: Value[], query: string) => Value[];
@@ -79,7 +88,14 @@ export interface IMultipleSelectProps<Value>
79
88
  extends Omit<ISelectProps<Value>, 'value' | 'onChange' | 'compareValuesOnChange'> {
80
89
  isMultiSelect: true;
81
90
  value: IMultipleSelectValue<Value> | undefined;
82
- onChange: (value?: IMultipleSelectValue<Value>) => void;
91
+ onChange: (
92
+ value: IMultipleSelectValue<Value> | undefined,
93
+ event:
94
+ | MouseEvent<HTMLElement>
95
+ | KeyboardEvent
96
+ | ChangeEvent<HTMLElement>
97
+ | FormEvent<HTMLElement>,
98
+ ) => void;
83
99
  compareValuesOnChange?: (
84
100
  v1?: IMultipleSelectValue<Value>,
85
101
  v2?: IMultipleSelectValue<Value>,
@@ -123,7 +139,8 @@ export function Select<Value>(
123
139
  } = props;
124
140
  const classes = useStyles({ theme: tweakStyles });
125
141
 
126
- const shouldRenderSearchInputInList = searchInput?.shouldRenderInList === true;
142
+ const { shouldRenderInList: shouldRenderSearchInputInList = false, ...searchInputProps } =
143
+ searchInput ?? {};
127
144
  const hasSearchInputInList = optionsMode !== 'normal' && shouldRenderSearchInputInList;
128
145
  const isMultiSelect = isMultiSelectValue(props, value);
129
146
  const hasReadonlyInput = isReadonly || optionsMode === 'normal' || shouldRenderSearchInputInList;
@@ -175,7 +192,7 @@ export function Select<Value>(
175
192
  const filter =
176
193
  optionsFilter ?? createFilter<Value>((option) => [convertValueToString(option) ?? '']);
177
194
 
178
- return filter(options, searchValue);
195
+ return filter(options as Value[], searchValue);
179
196
  }, [optionsFilter, options, convertValueToString, searchValue, optionsMode]);
180
197
 
181
198
  const availableOptions = useMemo(
@@ -203,7 +220,13 @@ export function Select<Value>(
203
220
  return acc;
204
221
  }, [] as number[]),
205
222
  );
206
- }, [filteredOptions]);
223
+ }, [
224
+ filteredOptions,
225
+ hasDefaultOption,
226
+ isOptionDisabled,
227
+ shouldShowAllOption,
228
+ shouldShowDefaultOption,
229
+ ]);
207
230
 
208
231
  const stringValue = isNotEmpty(strValue) ? convertValueToString(strValue) : undefined;
209
232
  // Для мультиселекта пытаемся показать "Все опции" если выбраны все опции
@@ -267,10 +290,17 @@ export function Select<Value>(
267
290
  };
268
291
 
269
292
  const handleOnChange = useCallback(
270
- (newValue: Value | IMultipleSelectValue<Value> | undefined) => {
293
+ (
294
+ newValue: Value | IMultipleSelectValue<Value> | undefined,
295
+ event:
296
+ | MouseEvent<HTMLElement>
297
+ | KeyboardEvent
298
+ | ChangeEvent<HTMLElement>
299
+ | FormEvent<HTMLElement>,
300
+ ) => {
271
301
  // Тут беда с типами, сорри
272
302
  if (!compareValuesOnChange(value as never, newValue as never)) {
273
- onChange(newValue as (Value & IMultipleSelectValue<Value>) | undefined);
303
+ onChange(newValue as (Value & IMultipleSelectValue<Value>) | undefined, event);
274
304
  }
275
305
  },
276
306
  [value, compareValuesOnChange, onChange],
@@ -278,7 +308,7 @@ export function Select<Value>(
278
308
 
279
309
  const handleOptionSelect = useCallback(
280
310
  (index: number, event: MouseEvent<HTMLElement> | KeyboardEvent) => {
281
- handleOnChange(index === DEFAULT_OPTION_INDEX ? undefined : filteredOptions[index]);
311
+ handleOnChange(index === DEFAULT_OPTION_INDEX ? undefined : filteredOptions[index], event);
282
312
  handleListClose(event);
283
313
  input.current?.blur();
284
314
  },
@@ -287,18 +317,18 @@ export function Select<Value>(
287
317
 
288
318
  // MultiSelect
289
319
  const handleToggleOptionCheckbox = useCallback(
290
- (index: number, isSelected: boolean) => {
320
+ (index: number, isSelected: boolean, event: ChangeEvent<HTMLElement> | KeyboardEvent) => {
291
321
  if (!isMultiSelect) {
292
322
  return;
293
323
  }
294
324
 
295
325
  // Если выбрана не дефолтная опция, которая сетит андеф
296
326
  if (index === DEFAULT_OPTION_INDEX || (index === ALL_OPTION_INDEX && !isSelected)) {
297
- handleOnChange(undefined);
327
+ handleOnChange(undefined, event);
298
328
  return;
299
329
  }
300
330
  if (index === ALL_OPTION_INDEX && isSelected) {
301
- handleOnChange(availableOptions as IMultipleSelectValue<Value>);
331
+ handleOnChange(availableOptions as IMultipleSelectValue<Value>, event);
302
332
  return;
303
333
  }
304
334
  const option = filteredOptions[index];
@@ -308,9 +338,10 @@ export function Select<Value>(
308
338
  ([...(value ?? []), option] as IMultipleSelectValue<Value>)
309
339
  : // Убираем
310
340
  value?.filter((o) => convertToId(o) !== convertToId(option)),
341
+ event,
311
342
  );
312
343
  },
313
- [handleOnChange, filteredOptions, isMultiSelect, value],
344
+ [isMultiSelect, filteredOptions, handleOnChange, value, availableOptions, convertToId],
314
345
  );
315
346
 
316
347
  const handleOnType = useCallback(
@@ -329,15 +360,15 @@ export function Select<Value>(
329
360
  setShouldShowDefaultOption(v === '');
330
361
  }
331
362
  },
332
- [onType, optionsMode],
363
+ [isMounted, onType, optionsMode],
333
364
  );
334
365
 
335
- const debounceHandleOnType = useCallback(debounce(handleOnType, debounceTime), [
336
- handleOnType,
337
- debounceTime,
338
- ]);
366
+ const debounceHandleOnType = useMemo(
367
+ () => debounce(handleOnType, debounceTime),
368
+ [handleOnType, debounceTime],
369
+ );
339
370
 
340
- const handleInputChange = (v: string) => {
371
+ const handleInputChange = (v: string, event: FormEvent<HTMLElement>) => {
341
372
  if (onType !== undefined) {
342
373
  debounceHandleOnType(v);
343
374
  }
@@ -347,7 +378,7 @@ export function Select<Value>(
347
378
  }
348
379
 
349
380
  if (v === '' && !hasSearchInputInList) {
350
- handleOnChange(undefined);
381
+ handleOnChange(undefined, event);
351
382
  }
352
383
 
353
384
  setSearchValue(v);
@@ -384,7 +415,7 @@ export function Select<Value>(
384
415
  isThisValueAlreadySelected =
385
416
  value?.some((opt) => convertToId(opt) === valueIdToSelect) ?? false;
386
417
  }
387
- handleToggleOptionCheckbox(indexToSelect, !isThisValueAlreadySelected);
418
+ handleToggleOptionCheckbox(indexToSelect, !isThisValueAlreadySelected, event);
388
419
  } else {
389
420
  handleOptionSelect(indexToSelect, event);
390
421
  }
@@ -488,21 +519,19 @@ export function Select<Value>(
488
519
  {isOpen && (
489
520
  <SelectList
490
521
  options={filteredOptions}
491
- defaultOptionLabel={
492
- hasDefaultOption && shouldShowDefaultOption ? defaultOptionLabel : undefined
493
- }
494
- allOptionsLabel={shouldShowAllOption ? allOptionsLabel : undefined}
522
+ defaultOptionLabel={hasDefaultOption && shouldShowDefaultOption && defaultOptionLabel}
523
+ allOptionsLabel={shouldShowAllOption && allOptionsLabel}
495
524
  areAllOptionsSelected={areAllOptionsSelected}
496
525
  customListHeader={
497
- hasSearchInputInList ? (
526
+ hasSearchInputInList && (
498
527
  <SearchInput
499
528
  value={searchValue}
500
529
  onChange={handleInputChange}
501
530
  tweakStyles={tweakSearchInputStyles}
502
531
  placeholder="Поиск"
503
- {...searchInput}
532
+ {...searchInputProps}
504
533
  />
505
- ) : undefined
534
+ )
506
535
  }
507
536
  noMatchesLabel={noMatchesLabel}
508
537
  focusedIndex={focusedListCellIndex}
@@ -1,32 +1,31 @@
1
- import { ReactNode, useMemo, MouseEvent } from 'react';
1
+ import { ReactNode, useMemo } from 'react';
2
2
  import clsx from 'clsx';
3
3
  import {
4
4
  addDataTestId,
5
5
  isNotEmpty,
6
6
  isReactNodeNotEmpty,
7
- isStringNotEmpty,
8
7
  } from '@true-engineering/true-react-platform-helpers';
9
8
  import { ICommonProps } from '../../../../types';
10
9
  import { ScrollIntoViewIfNeeded } from '../../../ScrollIntoViewIfNeeded';
11
10
  import { ALL_OPTION_INDEX, DEFAULT_OPTION_INDEX } from '../../constants';
12
11
  import { IMultipleSelectValue } from '../../types';
13
- import { SelectListItem } from '../SelectListItem';
12
+ import { ISelectListItemProps, SelectListItem } from '../SelectListItem';
14
13
  import { useStyles, ISelectListStyles } from './SelectList.styles';
15
14
 
16
- export interface ISelectListProps<Value> extends ICommonProps<ISelectListStyles> {
17
- options: Value[];
15
+ export interface ISelectListProps<Value>
16
+ extends ICommonProps<ISelectListStyles>,
17
+ Pick<ISelectListItemProps, 'onToggleCheckbox' | 'onOptionSelect'> {
18
+ options: Value[] | Readonly<Value[]>;
18
19
  focusedIndex?: number;
19
20
  activeValue?: Value | Value[];
20
21
  noMatchesLabel?: string;
21
22
  isLoading?: boolean;
22
23
  loadingLabel?: ReactNode;
23
24
  defaultOptionLabel?: ReactNode;
24
- allOptionsLabel?: string;
25
+ allOptionsLabel?: ReactNode;
25
26
  areAllOptionsSelected?: boolean;
26
27
  shouldScrollToList?: boolean;
27
28
  customListHeader?: ReactNode;
28
- onOptionSelect: (index: number, event: MouseEvent<HTMLElement>) => void;
29
- onToggleCheckbox?: (index: number, isSelected: boolean) => void;
30
29
  isOptionDisabled: (value: Value) => boolean;
31
30
  convertValueToString: (value: Value) => string | undefined;
32
31
  convertValueToReactNode?: (value: Value, isDisabled: boolean) => ReactNode;
@@ -111,7 +110,7 @@ export function SelectList<Value>({
111
110
  {defaultOptionLabel}
112
111
  </ScrollIntoViewIfNeeded>
113
112
  )}
114
- {isStringNotEmpty(allOptionsLabel) && (
113
+ {isReactNodeNotEmpty(allOptionsLabel) && (
115
114
  <SelectListItem
116
115
  classes={classes}
117
116
  index={ALL_OPTION_INDEX}
@@ -1,4 +1,4 @@
1
- import { ReactNode, MouseEvent, FC } from 'react';
1
+ import { ReactNode, MouseEvent, FC, KeyboardEvent, ChangeEvent } from 'react';
2
2
  import clsx from 'clsx';
3
3
  import { Classes } from 'jss';
4
4
  import { isNotEmpty } from '@true-engineering/true-react-platform-helpers';
@@ -16,7 +16,11 @@ export interface ISelectListItemProps {
16
16
  children: ReactNode;
17
17
  classes: Classes<'cellWithCheckbox' | 'cell' | 'focused' | 'active' | 'disabled'>; // TODO: !!!
18
18
  onOptionSelect: (index: number, event: MouseEvent<HTMLElement>) => void;
19
- onToggleCheckbox?: (index: number, isSelected: boolean) => void;
19
+ onToggleCheckbox?: (
20
+ index: number,
21
+ isSelected: boolean,
22
+ event: ChangeEvent<HTMLElement> | KeyboardEvent,
23
+ ) => void;
20
24
  }
21
25
 
22
26
  export const SelectListItem: FC<ISelectListItemProps> = ({
@@ -56,7 +60,7 @@ export const SelectListItem: FC<ISelectListItemProps> = ({
56
60
  isSemiChecked={isSemiChecked}
57
61
  isDisabled={isDisabled}
58
62
  tweakStyles={checkboxStyles}
59
- onSelect={(v) => onToggleCheckbox(index, v.isSelected)}
63
+ onSelect={(v, event) => onToggleCheckbox(index, v.isSelected, event)}
60
64
  >
61
65
  {children}
62
66
  </Checkbox>
@@ -12,6 +12,10 @@ export const useStyles = createThemedStyles('WithPopup', {
12
12
  trigger: {
13
13
  cursor: 'pointer',
14
14
  },
15
+
16
+ popup: {
17
+ zIndex: 5,
18
+ },
15
19
  });
16
20
 
17
21
  export type IWithPopupStyles = ITweakStyles<typeof useStyles>;
@@ -106,6 +106,7 @@ export const WithPopup: FC<IWithPopupProps> = ({
106
106
  <div
107
107
  className={classes.trigger}
108
108
  onClick={eventType === 'click' ? (e) => handleToggle(!isOpen, e) : undefined}
109
+ {...addDataTestId(testId, 'trigger')}
109
110
  >
110
111
  {isFunction(Trigger) ? <Trigger isActive={isOpen} /> : Trigger}
111
112
  </div>
@@ -114,7 +115,12 @@ export const WithPopup: FC<IWithPopupProps> = ({
114
115
  <FloatingPortal
115
116
  root={!shouldRenderInBody ? (refs.reference.current as HTMLDivElement) : undefined}
116
117
  >
117
- <div style={floatingStyles} ref={refs.setFloating} {...getFloatingProps()}>
118
+ <div
119
+ style={floatingStyles}
120
+ className={classes.popup}
121
+ ref={refs.setFloating}
122
+ {...getFloatingProps()}
123
+ >
118
124
  {isFunction(Popup) ? <Popup onClose={handleClose} /> : Popup}
119
125
  </div>
120
126
  </FloatingPortal>