@true-engineering/true-react-common-ui-kit 3.8.0 → 3.9.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.
@@ -1,157 +1,156 @@
1
- import { ReactNode, useMemo, MouseEvent } from 'react';
2
- import clsx from 'clsx';
3
- import {
4
- addDataTestId,
5
- isNotEmpty,
6
- isReactNodeNotEmpty,
7
- isStringNotEmpty,
8
- } from '@true-engineering/true-react-platform-helpers';
9
- import { ICommonProps } from '../../../../types';
10
- import { ScrollIntoViewIfNeeded } from '../../../ScrollIntoViewIfNeeded';
11
- import { ALL_OPTION_INDEX, DEFAULT_OPTION_INDEX } from '../../constants';
12
- import { IMultipleSelectValue } from '../../types';
13
- import { SelectListItem } from '../SelectListItem';
14
- import { useStyles, ISelectListStyles } from './SelectList.styles';
15
-
16
- export interface ISelectListProps<Value> extends ICommonProps<ISelectListStyles> {
17
- options: Value[];
18
- focusedIndex?: number;
19
- activeValue?: Value | Value[];
20
- noMatchesLabel?: string;
21
- isLoading?: boolean;
22
- loadingLabel?: ReactNode;
23
- defaultOptionLabel?: ReactNode;
24
- allOptionsLabel?: string;
25
- areAllOptionsSelected?: boolean;
26
- shouldScrollToList?: boolean;
27
- customListHeader?: ReactNode;
28
- onOptionSelect: (index: number, event: MouseEvent<HTMLElement>) => void;
29
- onToggleCheckbox?: (index: number, isSelected: boolean) => void;
30
- isOptionDisabled: (value: Value) => boolean;
31
- convertValueToString: (value: Value) => string | undefined;
32
- convertValueToReactNode?: (value: Value, isDisabled: boolean) => ReactNode;
33
- convertValueToId: (value: Value) => string | undefined;
34
- }
35
-
36
- export function SelectList<Value>({
37
- options,
38
- focusedIndex,
39
- activeValue,
40
- defaultOptionLabel,
41
- noMatchesLabel = 'Совпадений не найдено',
42
- isLoading,
43
- loadingLabel = 'Загрузка...',
44
- tweakStyles,
45
- testId,
46
- shouldScrollToList = true,
47
- areAllOptionsSelected,
48
- customListHeader,
49
- isOptionDisabled,
50
- allOptionsLabel,
51
- onOptionSelect,
52
- onToggleCheckbox,
53
- convertValueToString,
54
- convertValueToReactNode = convertValueToString,
55
- convertValueToId,
56
- }: ISelectListProps<Value>): JSX.Element {
57
- const classes = useStyles({ theme: tweakStyles });
58
-
59
- const isMultiSelect = isNotEmpty(onToggleCheckbox);
60
- const multiSelectValue = activeValue as IMultipleSelectValue<Value> | undefined;
61
- const selectedOptionsCount = multiSelectValue?.length ?? 0;
62
-
63
- // MultiSelect
64
- const activeOptionsIdMap = useMemo(
65
- () => (isMultiSelect ? multiSelectValue?.map(convertValueToId) ?? [] : []),
66
- [isMultiSelect, multiSelectValue, convertValueToId],
67
- );
68
-
69
- const optionsDisableMap = useMemo(
70
- () => options.map((o) => isOptionDisabled(o)),
71
- [options, isOptionDisabled],
72
- );
73
-
74
- const listOptions = useMemo(
75
- () => options.map((opt, i) => convertValueToReactNode(opt, optionsDisableMap[i])),
76
- [options, convertValueToReactNode, optionsDisableMap],
77
- );
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
-
85
- return (
86
- <ScrollIntoViewIfNeeded
87
- active={shouldScrollToList && !isMultiSelect}
88
- className={clsx(classes.root, {
89
- [classes.withListHeader]: isReactNodeNotEmpty(customListHeader),
90
- })}
91
- >
92
- {isReactNodeNotEmpty(customListHeader) && (
93
- <div className={classes.listHeader}>{customListHeader}</div>
94
- )}
95
- <div className={classes.list} {...addDataTestId(testId)}>
96
- {isLoading ? (
97
- <div className={clsx(classes.cell, classes.loading)}>{loadingLabel}</div>
98
- ) : (
99
- <>
100
- {isReactNodeNotEmpty(defaultOptionLabel) && (
101
- <ScrollIntoViewIfNeeded
102
- active={focusedIndex === DEFAULT_OPTION_INDEX}
103
- options={{ block: 'nearest' }}
104
- className={clsx(
105
- classes.cell,
106
- classes.defaultCell,
107
- focusedIndex === DEFAULT_OPTION_INDEX && classes.focused,
108
- )}
109
- onClick={(event) => onOptionSelect(DEFAULT_OPTION_INDEX, event)}
110
- >
111
- {defaultOptionLabel}
112
- </ScrollIntoViewIfNeeded>
113
- )}
114
- {isStringNotEmpty(allOptionsLabel) && (
115
- <SelectListItem
116
- classes={classes}
117
- index={ALL_OPTION_INDEX}
118
- isSemiChecked={selectedOptionsCount > 0 && !areAllOptionsSelected}
119
- isActive={areAllOptionsSelected}
120
- isFocused={focusedIndex === ALL_OPTION_INDEX}
121
- onOptionSelect={onOptionSelect}
122
- onToggleCheckbox={onToggleCheckbox}
123
- >
124
- {allOptionsLabel}
125
- </SelectListItem>
126
- )}
127
- {listOptions.map((opt, i) => {
128
- const optionValue = options[i];
129
- const isFocused = focusedIndex === i;
130
- const isActive = isActiveOption(optionValue);
131
- // проверяем, что опция задизейблена
132
- const isDisabled = optionsDisableMap[i];
133
-
134
- return (
135
- <SelectListItem
136
- key={i}
137
- classes={classes}
138
- index={i}
139
- isDisabled={isDisabled}
140
- isActive={isActive}
141
- isFocused={isFocused}
142
- onOptionSelect={onOptionSelect}
143
- onToggleCheckbox={onToggleCheckbox}
144
- >
145
- {opt}
146
- </SelectListItem>
147
- );
148
- })}
149
- {listOptions.length === 0 && (
150
- <div className={clsx(classes.cell, classes.noMatchesLabel)}>{noMatchesLabel}</div>
151
- )}
152
- </>
153
- )}
154
- </div>
155
- </ScrollIntoViewIfNeeded>
156
- );
157
- }
1
+ import { ReactNode, useMemo } from 'react';
2
+ import clsx from 'clsx';
3
+ import {
4
+ addDataTestId,
5
+ isNotEmpty,
6
+ isReactNodeNotEmpty,
7
+ } from '@true-engineering/true-react-platform-helpers';
8
+ import { ICommonProps } from '../../../../types';
9
+ import { ScrollIntoViewIfNeeded } from '../../../ScrollIntoViewIfNeeded';
10
+ import { ALL_OPTION_INDEX, DEFAULT_OPTION_INDEX } from '../../constants';
11
+ import { IMultipleSelectValue } from '../../types';
12
+ import { ISelectListItemProps, SelectListItem } from '../SelectListItem';
13
+ import { useStyles, ISelectListStyles } from './SelectList.styles';
14
+
15
+ export interface ISelectListProps<Value>
16
+ extends ICommonProps<ISelectListStyles>,
17
+ Pick<ISelectListItemProps, 'onToggleCheckbox' | 'onOptionSelect'> {
18
+ options: Value[] | Readonly<Value[]>;
19
+ focusedIndex?: number;
20
+ activeValue?: Value | Value[];
21
+ noMatchesLabel?: string;
22
+ isLoading?: boolean;
23
+ loadingLabel?: ReactNode;
24
+ defaultOptionLabel?: ReactNode;
25
+ allOptionsLabel?: ReactNode;
26
+ areAllOptionsSelected?: boolean;
27
+ shouldScrollToList?: boolean;
28
+ customListHeader?: ReactNode;
29
+ isOptionDisabled: (value: Value) => boolean;
30
+ convertValueToString: (value: Value) => string | undefined;
31
+ convertValueToReactNode?: (value: Value, isDisabled: boolean) => ReactNode;
32
+ convertValueToId: (value: Value) => string | undefined;
33
+ }
34
+
35
+ export function SelectList<Value>({
36
+ options,
37
+ focusedIndex,
38
+ activeValue,
39
+ defaultOptionLabel,
40
+ noMatchesLabel = 'Совпадений не найдено',
41
+ isLoading,
42
+ loadingLabel = 'Загрузка...',
43
+ tweakStyles,
44
+ testId,
45
+ shouldScrollToList = true,
46
+ areAllOptionsSelected,
47
+ customListHeader,
48
+ isOptionDisabled,
49
+ allOptionsLabel,
50
+ onOptionSelect,
51
+ onToggleCheckbox,
52
+ convertValueToString,
53
+ convertValueToReactNode = convertValueToString,
54
+ convertValueToId,
55
+ }: ISelectListProps<Value>): JSX.Element {
56
+ const classes = useStyles({ theme: tweakStyles });
57
+
58
+ const isMultiSelect = isNotEmpty(onToggleCheckbox);
59
+ const multiSelectValue = activeValue as IMultipleSelectValue<Value> | undefined;
60
+ const selectedOptionsCount = multiSelectValue?.length ?? 0;
61
+
62
+ // MultiSelect
63
+ const activeOptionsIdMap = useMemo(
64
+ () => (isMultiSelect ? multiSelectValue?.map(convertValueToId) ?? [] : []),
65
+ [isMultiSelect, multiSelectValue, convertValueToId],
66
+ );
67
+
68
+ const optionsDisableMap = useMemo(
69
+ () => options.map((o) => isOptionDisabled(o)),
70
+ [options, isOptionDisabled],
71
+ );
72
+
73
+ const listOptions = useMemo(
74
+ () => options.map((opt, i) => convertValueToReactNode(opt, optionsDisableMap[i])),
75
+ [options, convertValueToReactNode, optionsDisableMap],
76
+ );
77
+
78
+ const isActiveOption = (item: Value): boolean =>
79
+ isMultiSelect
80
+ ? activeOptionsIdMap.includes(convertValueToId(item))
81
+ : isNotEmpty(activeValue) &&
82
+ convertValueToId(activeValue as Value) === convertValueToId(item);
83
+
84
+ return (
85
+ <ScrollIntoViewIfNeeded
86
+ active={shouldScrollToList && !isMultiSelect}
87
+ className={clsx(classes.root, {
88
+ [classes.withListHeader]: isReactNodeNotEmpty(customListHeader),
89
+ })}
90
+ >
91
+ {isReactNodeNotEmpty(customListHeader) && (
92
+ <div className={classes.listHeader}>{customListHeader}</div>
93
+ )}
94
+ <div className={classes.list} {...addDataTestId(testId)}>
95
+ {isLoading ? (
96
+ <div className={clsx(classes.cell, classes.loading)}>{loadingLabel}</div>
97
+ ) : (
98
+ <>
99
+ {isReactNodeNotEmpty(defaultOptionLabel) && (
100
+ <ScrollIntoViewIfNeeded
101
+ active={focusedIndex === DEFAULT_OPTION_INDEX}
102
+ options={{ block: 'nearest' }}
103
+ className={clsx(
104
+ classes.cell,
105
+ classes.defaultCell,
106
+ focusedIndex === DEFAULT_OPTION_INDEX && classes.focused,
107
+ )}
108
+ onClick={(event) => onOptionSelect(DEFAULT_OPTION_INDEX, event)}
109
+ >
110
+ {defaultOptionLabel}
111
+ </ScrollIntoViewIfNeeded>
112
+ )}
113
+ {isReactNodeNotEmpty(allOptionsLabel) && (
114
+ <SelectListItem
115
+ classes={classes}
116
+ index={ALL_OPTION_INDEX}
117
+ isSemiChecked={selectedOptionsCount > 0 && !areAllOptionsSelected}
118
+ isActive={areAllOptionsSelected}
119
+ isFocused={focusedIndex === ALL_OPTION_INDEX}
120
+ onOptionSelect={onOptionSelect}
121
+ onToggleCheckbox={onToggleCheckbox}
122
+ >
123
+ {allOptionsLabel}
124
+ </SelectListItem>
125
+ )}
126
+ {listOptions.map((opt, i) => {
127
+ const optionValue = options[i];
128
+ const isFocused = focusedIndex === i;
129
+ const isActive = isActiveOption(optionValue);
130
+ // проверяем, что опция задизейблена
131
+ const isDisabled = optionsDisableMap[i];
132
+
133
+ return (
134
+ <SelectListItem
135
+ key={i}
136
+ classes={classes}
137
+ index={i}
138
+ isDisabled={isDisabled}
139
+ isActive={isActive}
140
+ isFocused={isFocused}
141
+ onOptionSelect={onOptionSelect}
142
+ onToggleCheckbox={onToggleCheckbox}
143
+ >
144
+ {opt}
145
+ </SelectListItem>
146
+ );
147
+ })}
148
+ {listOptions.length === 0 && (
149
+ <div className={clsx(classes.cell, classes.noMatchesLabel)}>{noMatchesLabel}</div>
150
+ )}
151
+ </>
152
+ )}
153
+ </div>
154
+ </ScrollIntoViewIfNeeded>
155
+ );
156
+ }
@@ -1,68 +1,72 @@
1
- import { ReactNode, MouseEvent, FC } from 'react';
2
- import clsx from 'clsx';
3
- import { Classes } from 'jss';
4
- import { isNotEmpty } from '@true-engineering/true-react-platform-helpers';
5
- import { addDataAttributes } from '../../../../helpers';
6
- import { Checkbox } from '../../../Checkbox';
7
- import { ScrollIntoViewIfNeeded } from '../../../ScrollIntoViewIfNeeded';
8
- import { checkboxStyles } from './SelectListItem.styles';
9
-
10
- export interface ISelectListItemProps {
11
- index: number;
12
- isSemiChecked?: boolean;
13
- isDisabled?: boolean;
14
- isActive?: boolean;
15
- isFocused?: boolean;
16
- children: ReactNode;
17
- classes: Classes<'cellWithCheckbox' | 'cell' | 'focused' | 'active' | 'disabled'>; // TODO: !!!
18
- onOptionSelect: (index: number, event: MouseEvent<HTMLElement>) => void;
19
- onToggleCheckbox?: (index: number, isSelected: boolean) => void;
20
- }
21
-
22
- export const SelectListItem: FC<ISelectListItemProps> = ({
23
- classes,
24
- index,
25
- isSemiChecked,
26
- isDisabled,
27
- isActive,
28
- children,
29
- isFocused,
30
- onOptionSelect,
31
- onToggleCheckbox,
32
- }) => {
33
- const isMultiSelect = isNotEmpty(onToggleCheckbox);
34
-
35
- return (
36
- <ScrollIntoViewIfNeeded
37
- active={isFocused}
38
- options={{ block: 'nearest' }}
39
- className={clsx(classes.cell, {
40
- [classes.cellWithCheckbox]: isMultiSelect,
41
- [classes.focused]: isFocused,
42
- [classes.active]: isActive && !isMultiSelect,
43
- [classes.disabled]: isDisabled,
44
- })}
45
- {...addDataAttributes({
46
- disabled: isDisabled,
47
- active: isActive,
48
- focused: isFocused,
49
- })}
50
- onClick={!isDisabled && !isMultiSelect ? (event) => onOptionSelect(index, event) : undefined}
51
- >
52
- {isMultiSelect ? (
53
- <Checkbox
54
- value={index}
55
- isChecked={isActive || isSemiChecked}
56
- isSemiChecked={isSemiChecked}
57
- isDisabled={isDisabled}
58
- tweakStyles={checkboxStyles}
59
- onSelect={(v) => onToggleCheckbox(index, v.isSelected)}
60
- >
61
- {children}
62
- </Checkbox>
63
- ) : (
64
- children
65
- )}
66
- </ScrollIntoViewIfNeeded>
67
- );
68
- };
1
+ import { ReactNode, MouseEvent, FC, KeyboardEvent, ChangeEvent } from 'react';
2
+ import clsx from 'clsx';
3
+ import { Classes } from 'jss';
4
+ import { isNotEmpty } from '@true-engineering/true-react-platform-helpers';
5
+ import { addDataAttributes } from '../../../../helpers';
6
+ import { Checkbox } from '../../../Checkbox';
7
+ import { ScrollIntoViewIfNeeded } from '../../../ScrollIntoViewIfNeeded';
8
+ import { checkboxStyles } from './SelectListItem.styles';
9
+
10
+ export interface ISelectListItemProps {
11
+ index: number;
12
+ isSemiChecked?: boolean;
13
+ isDisabled?: boolean;
14
+ isActive?: boolean;
15
+ isFocused?: boolean;
16
+ children: ReactNode;
17
+ classes: Classes<'cellWithCheckbox' | 'cell' | 'focused' | 'active' | 'disabled'>; // TODO: !!!
18
+ onOptionSelect: (index: number, event: MouseEvent<HTMLElement>) => void;
19
+ onToggleCheckbox?: (
20
+ index: number,
21
+ isSelected: boolean,
22
+ event: ChangeEvent<HTMLElement> | KeyboardEvent,
23
+ ) => void;
24
+ }
25
+
26
+ export const SelectListItem: FC<ISelectListItemProps> = ({
27
+ classes,
28
+ index,
29
+ isSemiChecked,
30
+ isDisabled,
31
+ isActive,
32
+ children,
33
+ isFocused,
34
+ onOptionSelect,
35
+ onToggleCheckbox,
36
+ }) => {
37
+ const isMultiSelect = isNotEmpty(onToggleCheckbox);
38
+
39
+ return (
40
+ <ScrollIntoViewIfNeeded
41
+ active={isFocused}
42
+ options={{ block: 'nearest' }}
43
+ className={clsx(classes.cell, {
44
+ [classes.cellWithCheckbox]: isMultiSelect,
45
+ [classes.focused]: isFocused,
46
+ [classes.active]: isActive && !isMultiSelect,
47
+ [classes.disabled]: isDisabled,
48
+ })}
49
+ {...addDataAttributes({
50
+ disabled: isDisabled,
51
+ active: isActive,
52
+ focused: isFocused,
53
+ })}
54
+ onClick={!isDisabled && !isMultiSelect ? (event) => onOptionSelect(index, event) : undefined}
55
+ >
56
+ {isMultiSelect ? (
57
+ <Checkbox
58
+ value={index}
59
+ isChecked={isActive || isSemiChecked}
60
+ isSemiChecked={isSemiChecked}
61
+ isDisabled={isDisabled}
62
+ tweakStyles={checkboxStyles}
63
+ onSelect={(v, event) => onToggleCheckbox(index, v.isSelected, event)}
64
+ >
65
+ {children}
66
+ </Checkbox>
67
+ ) : (
68
+ children
69
+ )}
70
+ </ScrollIntoViewIfNeeded>
71
+ );
72
+ };
@@ -67,6 +67,7 @@ export const Default = Template.bind({});
67
67
  Default.args = {
68
68
  isDisabled: false,
69
69
  shouldRenderInBody: true,
70
+ shouldHideOnScroll: false,
70
71
  };
71
72
 
72
73
  Default.parameters = {
@@ -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>;
@@ -25,6 +25,8 @@ export interface IWithPopupProps extends ICommonProps<IWithPopupStyles> {
25
25
  middlewares?: Middleware[];
26
26
  /** @default eventType === 'click' ? 'bottom-end' : 'top' */
27
27
  placement?: Placement;
28
+ /** @default false */
29
+ shouldHideOnScroll?: boolean;
28
30
  /** @default true */
29
31
  shouldRenderInBody?: boolean;
30
32
  /** @default 'click' */
@@ -44,6 +46,7 @@ export const WithPopup: FC<IWithPopupProps> = ({
44
46
  middlewares,
45
47
  eventType = 'click',
46
48
  placement = eventType === 'click' ? 'bottom-end' : 'top',
49
+ shouldHideOnScroll = false,
47
50
  shouldRenderInBody = false,
48
51
  hoverDelay = 0,
49
52
  popupOffset = DEFAULT_OFFSET,
@@ -85,7 +88,10 @@ export const WithPopup: FC<IWithPopupProps> = ({
85
88
  delay: { open: hoverDelay },
86
89
  });
87
90
  const click = useClick(context, { enabled: eventType === 'click', toggle: false });
88
- const dismiss = useDismiss(context, { enabled: eventType === 'click' });
91
+ const dismiss = useDismiss(context, {
92
+ enabled: eventType === 'click',
93
+ ancestorScroll: shouldHideOnScroll,
94
+ });
89
95
 
90
96
  const { getReferenceProps, getFloatingProps } = useInteractions([hover, click, dismiss]);
91
97
 
@@ -108,7 +114,12 @@ export const WithPopup: FC<IWithPopupProps> = ({
108
114
  <FloatingPortal
109
115
  root={!shouldRenderInBody ? (refs.reference.current as HTMLDivElement) : undefined}
110
116
  >
111
- <div style={floatingStyles} ref={refs.setFloating} {...getFloatingProps()}>
117
+ <div
118
+ style={floatingStyles}
119
+ className={classes.popup}
120
+ ref={refs.setFloating}
121
+ {...getFloatingProps()}
122
+ >
112
123
  {isFunction(Popup) ? <Popup onClose={handleClose} /> : Popup}
113
124
  </div>
114
125
  </FloatingPortal>