@snack-uikit/list 0.1.3 → 0.2.1-preview-a0815a00.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/CHANGELOG.md +16 -0
- package/README.md +12 -12
- package/dist/components/Items/BaseItem/BaseItem.d.ts +4 -3
- package/dist/components/Items/BaseItem/BaseItem.js +13 -6
- package/dist/components/Items/BaseItem/styles.module.css +3 -0
- package/dist/components/Items/NextListItem/NextListItem.js +1 -1
- package/dist/components/Items/styles.module.css +10 -10
- package/dist/components/Items/types.d.ts +33 -7
- package/dist/components/Lists/Droplist/DropList.d.ts +1 -1
- package/dist/components/Lists/Droplist/DropList.js +11 -5
- package/dist/components/Lists/List/List.d.ts +10 -2
- package/dist/components/Lists/List/List.js +25 -11
- package/dist/components/Lists/ListPrivate/ListPrivate.d.ts +10 -2
- package/dist/components/Lists/ListPrivate/ListPrivate.js +8 -8
- package/dist/components/Lists/ListPrivate/styles.module.css +4 -0
- package/dist/components/Lists/contexts/SelectionProvider.d.ts +19 -12
- package/dist/components/Lists/contexts/SelectionProvider.js +37 -16
- package/dist/components/Lists/styles.module.css +4 -0
- package/dist/components/Lists/types.d.ts +22 -6
- package/dist/helperComponents/HiddenTabButton/HiddenTabButton.d.ts +1 -0
- package/dist/helperComponents/HiddenTabButton/HiddenTabButton.js +2 -2
- package/dist/utils.js +6 -2
- package/package.json +3 -2
- package/src/components/Items/BaseItem/BaseItem.tsx +28 -13
- package/src/components/Items/BaseItem/styles.module.scss +4 -0
- package/src/components/Items/NextListItem/NextListItem.tsx +1 -0
- package/src/components/Items/styles.module.scss +17 -15
- package/src/components/Items/types.ts +34 -10
- package/src/components/Lists/Droplist/DropList.tsx +14 -5
- package/src/components/Lists/List/List.tsx +50 -18
- package/src/components/Lists/ListPrivate/ListPrivate.tsx +11 -10
- package/src/components/Lists/ListPrivate/styles.module.scss +4 -0
- package/src/components/Lists/contexts/ListProvider.tsx +0 -2
- package/src/components/Lists/contexts/SelectionProvider.tsx +43 -29
- package/src/components/Lists/styles.module.scss +5 -0
- package/src/components/Lists/types.ts +22 -9
- package/src/helperComponents/HiddenTabButton/HiddenTabButton.tsx +12 -2
- package/src/utils.ts +11 -2
|
@@ -1,18 +1,29 @@
|
|
|
1
|
+
var __rest = (this && this.__rest) || function (s, e) {
|
|
2
|
+
var t = {};
|
|
3
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
4
|
+
t[p] = s[p];
|
|
5
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
6
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
7
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
8
|
+
t[p[i]] = s[p[i]];
|
|
9
|
+
}
|
|
10
|
+
return t;
|
|
11
|
+
};
|
|
1
12
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
13
|
import { createContext, useCallback, useContext } from 'react';
|
|
3
14
|
import { useUncontrolledProp } from 'uncontrollable';
|
|
4
15
|
export const SelectionContext = createContext({
|
|
5
16
|
value: undefined,
|
|
6
17
|
onChange: undefined,
|
|
7
|
-
|
|
18
|
+
mode: undefined,
|
|
8
19
|
});
|
|
9
20
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
10
21
|
export function isSelectionMultipleProps(props) {
|
|
11
|
-
return '
|
|
22
|
+
return 'mode' in props && props['mode'] === 'multiple';
|
|
12
23
|
}
|
|
13
24
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
14
25
|
export function isSelectionSingleProps(props) {
|
|
15
|
-
return '
|
|
26
|
+
return 'mode' in props && props['mode'] === 'single';
|
|
16
27
|
}
|
|
17
28
|
function SelectionSingleProvider({ value: valueProp, defaultValue, onChange: onChangeProp, children, }) {
|
|
18
29
|
const [value, setValue] = useUncontrolledProp(valueProp, defaultValue, cb => {
|
|
@@ -24,7 +35,14 @@ function SelectionSingleProvider({ value: valueProp, defaultValue, onChange: onC
|
|
|
24
35
|
}
|
|
25
36
|
return undefined;
|
|
26
37
|
}), [setValue]);
|
|
27
|
-
return (_jsx(SelectionContext.Provider, { value: {
|
|
38
|
+
return (_jsx(SelectionContext.Provider, { value: {
|
|
39
|
+
value,
|
|
40
|
+
onChange,
|
|
41
|
+
mode: 'single',
|
|
42
|
+
isSelectionSingle: true,
|
|
43
|
+
isSelectionMultiple: false,
|
|
44
|
+
setValue,
|
|
45
|
+
}, children: children }));
|
|
28
46
|
}
|
|
29
47
|
function SelectionMultipleProvider({ value: valueProp, defaultValue, onChange: onChangeProp, children, }) {
|
|
30
48
|
const [value, setValue] = useUncontrolledProp(valueProp, defaultValue, cb => {
|
|
@@ -39,23 +57,26 @@ function SelectionMultipleProvider({ value: valueProp, defaultValue, onChange: o
|
|
|
39
57
|
}
|
|
40
58
|
return undefined;
|
|
41
59
|
}), [setValue]);
|
|
42
|
-
return (_jsx(SelectionContext.Provider, { value: {
|
|
60
|
+
return (_jsx(SelectionContext.Provider, { value: {
|
|
61
|
+
value,
|
|
62
|
+
onChange,
|
|
63
|
+
mode: 'multiple',
|
|
64
|
+
isSelectionSingle: false,
|
|
65
|
+
isSelectionMultiple: true,
|
|
66
|
+
setValue,
|
|
67
|
+
}, children: children }));
|
|
43
68
|
}
|
|
44
|
-
export function SelectionProvider(
|
|
69
|
+
export function SelectionProvider(_a) {
|
|
70
|
+
var { children } = _a, props = __rest(_a, ["children"]);
|
|
45
71
|
if (isSelectionSingleProps(props)) {
|
|
46
|
-
return _jsx(SelectionSingleProvider, Object.assign({}, props));
|
|
72
|
+
return _jsx(SelectionSingleProvider, Object.assign({}, props, { children: children }));
|
|
47
73
|
}
|
|
48
74
|
if (isSelectionMultipleProps(props)) {
|
|
49
|
-
return _jsx(SelectionMultipleProvider, Object.assign({}, props));
|
|
75
|
+
return _jsx(SelectionMultipleProvider, Object.assign({}, props, { children: children }));
|
|
50
76
|
}
|
|
51
|
-
return _jsx(SelectionContext.Provider, { value: {}, children:
|
|
77
|
+
return _jsx(SelectionContext.Provider, { value: {}, children: children });
|
|
52
78
|
}
|
|
53
79
|
export const useSelectionContext = () => useContext(SelectionContext);
|
|
54
|
-
export function extractSelectionProps({ selection
|
|
55
|
-
return {
|
|
56
|
-
selection,
|
|
57
|
-
value,
|
|
58
|
-
defaultValue,
|
|
59
|
-
onChange,
|
|
60
|
-
};
|
|
80
|
+
export function extractSelectionProps({ selection }) {
|
|
81
|
+
return Object.assign({}, (selection !== null && selection !== void 0 ? selection : {}));
|
|
61
82
|
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
.listContainer{
|
|
2
|
+
display:flex;
|
|
3
|
+
flex-direction:column;
|
|
4
|
+
height:100%;
|
|
2
5
|
margin:0;
|
|
3
6
|
padding:0;
|
|
4
7
|
}
|
|
@@ -26,6 +29,7 @@
|
|
|
26
29
|
overflow:hidden;
|
|
27
30
|
display:block;
|
|
28
31
|
box-sizing:border-box;
|
|
32
|
+
height:100%;
|
|
29
33
|
padding:var(--dimension-025m, 2px);
|
|
30
34
|
}
|
|
31
35
|
.wrapper:has(.listContainer:focus-visible){
|
|
@@ -3,7 +3,12 @@ import { DropdownProps } from '@snack-uikit/dropdown';
|
|
|
3
3
|
import { WithSupportProps } from '@snack-uikit/utils';
|
|
4
4
|
import { ScrollProps, SearchState } from '../../types';
|
|
5
5
|
import { ItemProps } from '../Items';
|
|
6
|
-
import { ListContextType,
|
|
6
|
+
import { ListContextType, SelectionState } from './contexts';
|
|
7
|
+
type CollapseState = {
|
|
8
|
+
value?: (string | number)[];
|
|
9
|
+
onChange?(value: (string | number)[]): void;
|
|
10
|
+
defaultValue?: (string | number)[];
|
|
11
|
+
};
|
|
7
12
|
export type ListProps = WithSupportProps<{
|
|
8
13
|
/** Основные элементы списка */
|
|
9
14
|
items: ItemProps[];
|
|
@@ -11,9 +16,12 @@ export type ListProps = WithSupportProps<{
|
|
|
11
16
|
pinTop?: ItemProps[];
|
|
12
17
|
/** Элементы списка, закрепленные снизу */
|
|
13
18
|
pinBottom?: ItemProps[];
|
|
14
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* Кастомизируемый элемент в конце списка
|
|
21
|
+
* @type ReactElement
|
|
22
|
+
*/
|
|
15
23
|
footer?: ReactNode;
|
|
16
|
-
/** Список ссылок на
|
|
24
|
+
/** Список ссылок на кастомные элементы, помещенные в специальную секцию внизу списка */
|
|
17
25
|
footerActiveElementsRefs?: RefObject<HTMLElement>[];
|
|
18
26
|
/** Настройки поисковой строки */
|
|
19
27
|
search?: SearchState;
|
|
@@ -23,13 +31,20 @@ export type ListProps = WithSupportProps<{
|
|
|
23
31
|
noData?: string;
|
|
24
32
|
/** Текст для состояния "Отсутсвие результата" при поиске */
|
|
25
33
|
noResults?: string;
|
|
26
|
-
|
|
34
|
+
/** Tab Index */
|
|
35
|
+
tabIndex?: number;
|
|
36
|
+
/** Настройки раскрытия элементов */
|
|
37
|
+
collapse?: CollapseState;
|
|
38
|
+
/** CSS-класс */
|
|
39
|
+
className?: string;
|
|
40
|
+
onKeyDown?(e: KeyboardEvent<HTMLElement>): void;
|
|
41
|
+
} & SelectionState & ListContextType & ScrollProps>;
|
|
27
42
|
export type DroplistProps = {
|
|
28
43
|
/** Ссылка на элемент-триггер для дроплиста */
|
|
29
44
|
triggerElemRef?: RefObject<HTMLElement>;
|
|
30
45
|
/** Триггер для дроплиста */
|
|
31
46
|
children?: ReactNode;
|
|
32
|
-
} & Pick<DropdownProps, 'trigger' | 'placement' | 'widthStrategy' | 'open' | 'onOpenChange'> & ListProps
|
|
47
|
+
} & Pick<DropdownProps, 'trigger' | 'placement' | 'widthStrategy' | 'open' | 'onOpenChange'> & Omit<ListProps, 'tabIndex' | 'onKeyDown'>;
|
|
33
48
|
export type ListPrivateProps = ListProps & {
|
|
34
49
|
nested?: boolean;
|
|
35
50
|
active?: boolean;
|
|
@@ -37,5 +52,6 @@ export type ListPrivateProps = ListProps & {
|
|
|
37
52
|
onFocus?(e: FocusEvent<HTMLElement>): void;
|
|
38
53
|
onBlur?(e: FocusEvent<HTMLElement>): void;
|
|
39
54
|
onKeyDown?(e: KeyboardEvent<HTMLElement>): void;
|
|
40
|
-
|
|
55
|
+
limitedScrollHeight?: boolean;
|
|
41
56
|
};
|
|
57
|
+
export {};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { RefObject } from 'react';
|
|
2
2
|
type HiddenTabButtonProps = {
|
|
3
3
|
listRef: RefObject<HTMLElement>;
|
|
4
|
+
tabIndex?: number;
|
|
4
5
|
};
|
|
5
6
|
export declare const HiddenTabButton: import("react").ForwardRefExoticComponent<HiddenTabButtonProps & import("react").RefAttributes<HTMLButtonElement>>;
|
|
6
7
|
export {};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { forwardRef, useCallback } from 'react';
|
|
3
3
|
import styles from './styles.module.css';
|
|
4
|
-
export const HiddenTabButton = forwardRef(({ listRef }, ref) => {
|
|
4
|
+
export const HiddenTabButton = forwardRef(({ listRef, tabIndex }, ref) => {
|
|
5
5
|
const handleFocus = useCallback((e) => {
|
|
6
6
|
var _a;
|
|
7
7
|
if (e.relatedTarget !== listRef.current) {
|
|
@@ -13,5 +13,5 @@ export const HiddenTabButton = forwardRef(({ listRef }, ref) => {
|
|
|
13
13
|
const handleKeyDown = useCallback((e) => {
|
|
14
14
|
e.stopPropagation();
|
|
15
15
|
}, []);
|
|
16
|
-
return _jsx("button", { "aria-hidden": true, ref: ref, onKeyDown: handleKeyDown, onFocus: handleFocus, className: styles.hiddenBtn });
|
|
16
|
+
return (_jsx("button", { "aria-hidden": true, ref: ref, onKeyDown: handleKeyDown, onFocus: handleFocus, className: styles.hiddenBtn, tabIndex: tabIndex }));
|
|
17
17
|
});
|
package/dist/utils.js
CHANGED
|
@@ -6,7 +6,8 @@ export function withCollapsedItems({ items, openCollapsedItems }) {
|
|
|
6
6
|
let expandedIds = [];
|
|
7
7
|
items.forEach(item => {
|
|
8
8
|
var _a;
|
|
9
|
-
if ((isBaseItemProps(item) || isNextListItemProps(item) || isAccordionItemProps(item)) &&
|
|
9
|
+
if (((isBaseItemProps(item) && !item.inactive) || isNextListItemProps(item) || isAccordionItemProps(item)) &&
|
|
10
|
+
!item.disabled) {
|
|
10
11
|
newItems = newItems.concat([item]);
|
|
11
12
|
ids = ids.concat([(_a = item.id) !== null && _a !== void 0 ? _a : '']);
|
|
12
13
|
if (item.itemRef) {
|
|
@@ -55,7 +56,10 @@ export function extractItemIds(items) {
|
|
|
55
56
|
}
|
|
56
57
|
export function extractChildIds({ items }) {
|
|
57
58
|
return items
|
|
58
|
-
.filter(item => isAccordionItemProps(item) ||
|
|
59
|
+
.filter(item => isAccordionItemProps(item) ||
|
|
60
|
+
isNextListItemProps(item) ||
|
|
61
|
+
isGroupItemProps(item) ||
|
|
62
|
+
(isBaseItemProps(item) && !item.disabled && !item.inactive))
|
|
59
63
|
.reduce((prev, item) => {
|
|
60
64
|
var _a;
|
|
61
65
|
if (isAccordionItemProps(item) || isNextListItemProps(item)) {
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
6
|
"title": "List",
|
|
7
|
-
"version": "0.1.
|
|
7
|
+
"version": "0.2.1-preview-a0815a00.0",
|
|
8
8
|
"sideEffects": [
|
|
9
9
|
"*.css",
|
|
10
10
|
"*.woff",
|
|
@@ -42,7 +42,8 @@
|
|
|
42
42
|
"@snack-uikit/truncate-string": "0.4.6",
|
|
43
43
|
"@snack-uikit/utils": "3.2.0",
|
|
44
44
|
"classnames": "2.5.1",
|
|
45
|
+
"merge-refs": "1.2.2",
|
|
45
46
|
"uncontrollable": "8.0.4"
|
|
46
47
|
},
|
|
47
|
-
"gitHead": "
|
|
48
|
+
"gitHead": "bccfe5b8df0866bff03fc00d5f5480988da8512b"
|
|
48
49
|
}
|
|
@@ -7,13 +7,12 @@ import { extractSupportProps } from '@snack-uikit/utils';
|
|
|
7
7
|
|
|
8
8
|
import { useCollapseContext, useListContext, useParentListContext, useSelectionContext } from '../../Lists/contexts';
|
|
9
9
|
import commonStyles from '../styles.module.scss';
|
|
10
|
-
import { BaseItemPrivateProps, BaseItemProps
|
|
10
|
+
import { BaseItemPrivateProps, BaseItemProps } from '../types';
|
|
11
11
|
import { CHECKBOX_SIZE_MAP } from './constants';
|
|
12
12
|
import styles from './styles.module.scss';
|
|
13
13
|
|
|
14
14
|
type AllBaseItemProps = BaseItemProps &
|
|
15
|
-
BaseItemPrivateProps &
|
|
16
|
-
SwitchProps & { indeterminate?: boolean; onSelect?(): void; isParentNode?: boolean };
|
|
15
|
+
BaseItemPrivateProps & { indeterminate?: boolean; onSelect?(): void; isParentNode?: boolean };
|
|
17
16
|
|
|
18
17
|
export function BaseItem({
|
|
19
18
|
beforeContent,
|
|
@@ -31,14 +30,18 @@ export function BaseItem({
|
|
|
31
30
|
indeterminate,
|
|
32
31
|
onSelect,
|
|
33
32
|
isParentNode,
|
|
33
|
+
className,
|
|
34
|
+
inactive,
|
|
35
|
+
itemWrapRender,
|
|
34
36
|
...rest
|
|
35
37
|
}: AllBaseItemProps) {
|
|
36
38
|
const { option, caption, description } = content || {};
|
|
39
|
+
const interactive = !inactive;
|
|
37
40
|
|
|
38
41
|
const { parentResetActiveFocusIndex } = useParentListContext();
|
|
39
42
|
const { size, marker } = useListContext();
|
|
40
43
|
const { level = 0 } = useCollapseContext();
|
|
41
|
-
const { value, onChange,
|
|
44
|
+
const { value, onChange, mode, isSelectionSingle, isSelectionMultiple } = useSelectionContext();
|
|
42
45
|
|
|
43
46
|
const isChecked = isSelectionSingle ? value === id : value?.includes(id);
|
|
44
47
|
|
|
@@ -47,10 +50,12 @@ export function BaseItem({
|
|
|
47
50
|
};
|
|
48
51
|
|
|
49
52
|
const handleItemClick = (e: MouseEvent<HTMLButtonElement>) => {
|
|
50
|
-
|
|
53
|
+
if (interactive) {
|
|
54
|
+
parentResetActiveFocusIndex?.();
|
|
51
55
|
|
|
52
|
-
|
|
53
|
-
|
|
56
|
+
if (!isParentNode) {
|
|
57
|
+
handleChange();
|
|
58
|
+
}
|
|
54
59
|
}
|
|
55
60
|
|
|
56
61
|
onClick?.(e);
|
|
@@ -92,26 +97,28 @@ export function BaseItem({
|
|
|
92
97
|
|
|
93
98
|
const props = extractSupportProps(rest);
|
|
94
99
|
|
|
95
|
-
|
|
100
|
+
const item = (
|
|
96
101
|
<li role={'menuitem'} data-test-id={props['data-test-id'] || 'list__base-item_' + id}>
|
|
97
102
|
<button
|
|
98
103
|
ref={itemRef}
|
|
99
|
-
className={cn(commonStyles.listItem, styles.droplistItem)}
|
|
104
|
+
className={cn(commonStyles.listItem, styles.droplistItem, className)}
|
|
100
105
|
data-size={size}
|
|
101
106
|
onClick={handleItemClick}
|
|
102
107
|
tabIndex={-1}
|
|
108
|
+
data-non-pointer={inactive && !onClick}
|
|
109
|
+
data-inactive={inactive || undefined}
|
|
103
110
|
data-checked={(isParentNode && (indeterminate || isChecked)) || (isChecked && !switchProp) || undefined}
|
|
104
|
-
data-variant={
|
|
111
|
+
data-variant={mode || undefined}
|
|
105
112
|
data-open={open || undefined}
|
|
106
113
|
disabled={disabled}
|
|
107
114
|
onKeyDown={handleItemKeyDown}
|
|
108
115
|
onFocus={handleItemFocus}
|
|
109
116
|
style={{ '--level': level }}
|
|
110
117
|
>
|
|
111
|
-
{!switchProp && isSelectionSingle && marker && !isParentNode && (
|
|
118
|
+
{!switchProp && isSelectionSingle && marker && !isParentNode && interactive && (
|
|
112
119
|
<div className={styles.markerContainer} data-test-id='list__base-item-marker' />
|
|
113
120
|
)}
|
|
114
|
-
{!switchProp && isSelectionMultiple && (
|
|
121
|
+
{!switchProp && isSelectionMultiple && interactive && (
|
|
115
122
|
<div className={styles.checkbox}>
|
|
116
123
|
<Checkbox
|
|
117
124
|
size={CHECKBOX_SIZE_MAP[size ?? 's']}
|
|
@@ -145,9 +152,17 @@ export function BaseItem({
|
|
|
145
152
|
|
|
146
153
|
{afterContent}
|
|
147
154
|
|
|
148
|
-
{switchProp &&
|
|
155
|
+
{switchProp && interactive && (
|
|
156
|
+
<Switch disabled={disabled} checked={isChecked} data-test-id='list__base-item-switch' />
|
|
157
|
+
)}
|
|
149
158
|
{!switchProp && expandIcon && <span className={styles.expandableIcon}>{expandIcon}</span>}
|
|
150
159
|
</button>
|
|
151
160
|
</li>
|
|
152
161
|
);
|
|
162
|
+
|
|
163
|
+
if (!itemWrapRender) {
|
|
164
|
+
return item;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return itemWrapRender(item);
|
|
153
168
|
}
|
|
@@ -46,21 +46,6 @@ $sizes: 's', 'm', 'l';
|
|
|
46
46
|
background-color: transparent;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
&[data-open],
|
|
50
|
-
&[data-focused],
|
|
51
|
-
&:hover,
|
|
52
|
-
&:focus-visible {
|
|
53
|
-
&::after {
|
|
54
|
-
opacity: $opacity-a008;
|
|
55
|
-
background-color: $sys-neutral-accent-default;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
&[data-focused],
|
|
60
|
-
&:focus-visible {
|
|
61
|
-
@include outline-inside-var($container-focused-s);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
49
|
&:disabled {
|
|
65
50
|
cursor: not-allowed;
|
|
66
51
|
background-color: transparent;
|
|
@@ -75,4 +60,21 @@ $sizes: 's', 'm', 'l';
|
|
|
75
60
|
background-color: transparent;
|
|
76
61
|
}
|
|
77
62
|
}
|
|
63
|
+
|
|
64
|
+
&:not([data-inactive]) {
|
|
65
|
+
&[data-open],
|
|
66
|
+
&[data-focused],
|
|
67
|
+
&:hover,
|
|
68
|
+
&:focus-visible {
|
|
69
|
+
&::after {
|
|
70
|
+
opacity: $opacity-a008;
|
|
71
|
+
background-color: $sys-neutral-accent-default;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
&[data-focused],
|
|
76
|
+
&:focus-visible {
|
|
77
|
+
@include outline-inside-var($container-focused-s);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
78
80
|
}
|
|
@@ -17,45 +17,69 @@ export type BaseItemPrivateProps = {
|
|
|
17
17
|
};
|
|
18
18
|
|
|
19
19
|
export type BaseItemProps = WithSupportProps<{
|
|
20
|
+
/**
|
|
21
|
+
* Слот до основного контента
|
|
22
|
+
* @type ReactElement
|
|
23
|
+
*/
|
|
20
24
|
beforeContent?: ReactNode;
|
|
25
|
+
/**
|
|
26
|
+
* Слот после основного контента
|
|
27
|
+
* @type ReactElement
|
|
28
|
+
*/
|
|
21
29
|
afterContent?: ReactNode;
|
|
22
|
-
|
|
30
|
+
/** Основной контент айтема */
|
|
23
31
|
content: ItemContentProps;
|
|
24
32
|
|
|
33
|
+
/** Колбек обработки клика */
|
|
25
34
|
onClick?(e: MouseEvent<HTMLButtonElement>): void;
|
|
35
|
+
/** Колбек обработки нажатия клавиши */
|
|
26
36
|
onKeyDown?(e: KeyboardEvent<HTMLButtonElement>): void;
|
|
37
|
+
/** Колбек обработки фокуса */
|
|
27
38
|
onFocus?(e: FocusEvent<HTMLButtonElement>): void;
|
|
39
|
+
/** Колбек обработки блюра */
|
|
28
40
|
onBlur?(e: FocusEvent<HTMLButtonElement>): void;
|
|
29
41
|
|
|
42
|
+
/** Уникальный идентификатор */
|
|
30
43
|
id?: string | number;
|
|
31
44
|
|
|
45
|
+
/** Флаг неактивности элемента */
|
|
32
46
|
disabled?: boolean;
|
|
33
47
|
|
|
34
48
|
itemRef?: RefObject<HTMLButtonElement>;
|
|
35
|
-
|
|
49
|
+
className?: string;
|
|
36
50
|
|
|
37
|
-
|
|
51
|
+
/**
|
|
52
|
+
* Флаг отображения отключения реакции на любое css состояние (hover/focus и тд)
|
|
53
|
+
* <br>
|
|
54
|
+
* Так же элемент пропадает из навигации с клавиатуры, и не может быть выбран (selection)
|
|
55
|
+
*/
|
|
56
|
+
inactive?: boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Флаг отображения состояния выбранного элемента через switch
|
|
59
|
+
*/
|
|
38
60
|
switch?: boolean;
|
|
39
|
-
|
|
61
|
+
|
|
62
|
+
itemWrapRender?(item: ReactNode): ReactNode;
|
|
63
|
+
}>;
|
|
64
|
+
|
|
65
|
+
type BaseItemsWithoutNonGroupProps = Omit<BaseItemProps, 'switch' | 'inactive'>;
|
|
40
66
|
|
|
41
67
|
// eslint-disable-next-line no-use-before-define
|
|
42
|
-
export type ItemProps =
|
|
68
|
+
export type ItemProps = BaseItemProps | AccordionItemProps | NextListItemProps | GroupItemProps;
|
|
43
69
|
|
|
44
|
-
export type AccordionItemProps =
|
|
70
|
+
export type AccordionItemProps = BaseItemsWithoutNonGroupProps & {
|
|
45
71
|
items: ItemProps[];
|
|
46
|
-
// TODO: add later
|
|
47
|
-
// mode?: 'single' | 'multiple';
|
|
48
72
|
type: 'collapse';
|
|
49
73
|
};
|
|
50
74
|
|
|
51
|
-
export type NextListItemProps =
|
|
75
|
+
export type NextListItemProps = BaseItemsWithoutNonGroupProps & {
|
|
52
76
|
items: ItemProps[];
|
|
53
77
|
type: 'next-list';
|
|
54
78
|
placement?: 'right-start' | 'left-start' | 'left' | 'right' | 'left-end' | 'right-end';
|
|
55
79
|
search?: SearchState;
|
|
56
80
|
} & ScrollProps;
|
|
57
81
|
|
|
58
|
-
export type GroupItemProps = SeparatorProps & {
|
|
82
|
+
export type GroupItemProps = Omit<SeparatorProps, 'size'> & {
|
|
59
83
|
items: ItemProps[];
|
|
60
84
|
id?: string | number;
|
|
61
85
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { cloneElement, isValidElement, KeyboardEvent, useCallback, useMemo, useRef
|
|
1
|
+
import { cloneElement, isValidElement, KeyboardEvent, useCallback, useMemo, useRef } from 'react';
|
|
2
2
|
import { useUncontrolledProp } from 'uncontrollable';
|
|
3
3
|
|
|
4
4
|
import { Dropdown } from '@snack-uikit/dropdown';
|
|
@@ -23,6 +23,7 @@ export function Droplist({
|
|
|
23
23
|
triggerElemRef: triggerElemRefProp,
|
|
24
24
|
open: openProp,
|
|
25
25
|
onOpenChange,
|
|
26
|
+
collapse = {},
|
|
26
27
|
...props
|
|
27
28
|
}: DroplistProps) {
|
|
28
29
|
const hasSearch = useMemo(() => Boolean(search), [search]);
|
|
@@ -32,8 +33,15 @@ export function Droplist({
|
|
|
32
33
|
[itemsProp, pinBottom, pinTop],
|
|
33
34
|
);
|
|
34
35
|
|
|
35
|
-
const [openCollapsedItems, setOpenCollapsedItems] =
|
|
36
|
-
|
|
36
|
+
const [openCollapsedItems, setOpenCollapsedItems] = useUncontrolledProp<Array<number | string>>(
|
|
37
|
+
collapse.value,
|
|
38
|
+
collapse.defaultValue ?? [],
|
|
39
|
+
collapse.onChange
|
|
40
|
+
? cb => {
|
|
41
|
+
collapse.onChange?.(cb(collapse.value));
|
|
42
|
+
}
|
|
43
|
+
: undefined,
|
|
44
|
+
);
|
|
37
45
|
const { search: searchItem, footerRefs } = useItemsWithIds({
|
|
38
46
|
search: hasSearch,
|
|
39
47
|
footerActiveElementsRefs,
|
|
@@ -131,8 +139,8 @@ export function Droplist({
|
|
|
131
139
|
parentResetNestedIndex: resetNestedIndex,
|
|
132
140
|
parentResetActiveFocusIndex: resetActiveFocusIndex,
|
|
133
141
|
toggleOpenCollapsedItems: id =>
|
|
134
|
-
setOpenCollapsedItems(items =>
|
|
135
|
-
items.includes(id) ? items.filter(item => item !== id) : items
|
|
142
|
+
setOpenCollapsedItems((items: Array<string | number> | undefined = []) =>
|
|
143
|
+
items.includes(id) ? items.filter(item => item !== id) : items?.concat([id]),
|
|
136
144
|
),
|
|
137
145
|
}}
|
|
138
146
|
>
|
|
@@ -143,6 +151,7 @@ export function Droplist({
|
|
|
143
151
|
tabIndex={0}
|
|
144
152
|
ref={listRef}
|
|
145
153
|
search={search}
|
|
154
|
+
limitedScrollHeight
|
|
146
155
|
{...slicedItems}
|
|
147
156
|
{...props}
|
|
148
157
|
/>
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import
|
|
1
|
+
import cn from 'classnames';
|
|
2
|
+
import mergeRefs from 'merge-refs';
|
|
3
|
+
import { FocusEvent, forwardRef, KeyboardEvent, useMemo, useRef } from 'react';
|
|
4
|
+
import { useUncontrolledProp } from 'uncontrollable';
|
|
2
5
|
|
|
3
6
|
import { HiddenTabButton } from '../../../helperComponents';
|
|
4
7
|
import { extractItemIds, extractItemRefs, withCollapsedItems } from '../../../utils';
|
|
@@ -10,7 +13,21 @@ import styles from '../styles.module.scss';
|
|
|
10
13
|
import { ListProps } from '../types';
|
|
11
14
|
|
|
12
15
|
export const List = forwardRef<HTMLElement, ListProps>(
|
|
13
|
-
(
|
|
16
|
+
(
|
|
17
|
+
{
|
|
18
|
+
items: itemsProp,
|
|
19
|
+
search,
|
|
20
|
+
pinBottom,
|
|
21
|
+
pinTop,
|
|
22
|
+
footerActiveElementsRefs,
|
|
23
|
+
onKeyDown,
|
|
24
|
+
tabIndex = 0,
|
|
25
|
+
className,
|
|
26
|
+
collapse = {},
|
|
27
|
+
...props
|
|
28
|
+
},
|
|
29
|
+
ref,
|
|
30
|
+
) => {
|
|
14
31
|
const hasSearch = useMemo(() => Boolean(search), [search]);
|
|
15
32
|
|
|
16
33
|
const memorizedItems = useMemo(
|
|
@@ -18,7 +35,15 @@ export const List = forwardRef<HTMLElement, ListProps>(
|
|
|
18
35
|
[itemsProp, pinBottom, pinTop],
|
|
19
36
|
);
|
|
20
37
|
|
|
21
|
-
const [openCollapsedItems, setOpenCollapsedItems] =
|
|
38
|
+
const [openCollapsedItems, setOpenCollapsedItems] = useUncontrolledProp<Array<number | string>>(
|
|
39
|
+
collapse.value,
|
|
40
|
+
collapse.defaultValue ?? [],
|
|
41
|
+
collapse.onChange
|
|
42
|
+
? cb => {
|
|
43
|
+
collapse.onChange?.(cb(collapse.value));
|
|
44
|
+
}
|
|
45
|
+
: undefined,
|
|
46
|
+
);
|
|
22
47
|
|
|
23
48
|
const { search: searchItem, footerRefs } = useItemsWithIds({
|
|
24
49
|
search: hasSearch,
|
|
@@ -68,6 +93,20 @@ export const List = forwardRef<HTMLElement, ListProps>(
|
|
|
68
93
|
|
|
69
94
|
const isActive = listRef.current === document.activeElement && activeFocusIndex === -1 && openNestedIndex === -1;
|
|
70
95
|
|
|
96
|
+
const mergedHandlerKeyDown = (e: KeyboardEvent<HTMLUListElement>) => {
|
|
97
|
+
onKeyDown?.(e);
|
|
98
|
+
handleListKeyDown?.(e);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const handleOnFocus = (e: FocusEvent<HTMLElement>) => {
|
|
102
|
+
if (
|
|
103
|
+
e.relatedTarget === null ||
|
|
104
|
+
(e.relatedTarget && itemRefs.every(({ current }) => current !== e.relatedTarget))
|
|
105
|
+
) {
|
|
106
|
+
resetActiveFocusIndex();
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
71
110
|
return (
|
|
72
111
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
73
112
|
// @ts-ignore
|
|
@@ -87,31 +126,24 @@ export const List = forwardRef<HTMLElement, ListProps>(
|
|
|
87
126
|
parentResetActiveFocusIndex: resetActiveFocusIndex,
|
|
88
127
|
openCollapsedItems,
|
|
89
128
|
toggleOpenCollapsedItems: id =>
|
|
90
|
-
setOpenCollapsedItems(items =>
|
|
91
|
-
items.includes(id) ? items.filter(item => item !== id) : items
|
|
129
|
+
setOpenCollapsedItems((items: Array<string | number> | undefined = []) =>
|
|
130
|
+
items.includes(id) ? items.filter(item => item !== id) : items?.concat([id]),
|
|
92
131
|
),
|
|
93
132
|
}}
|
|
94
133
|
>
|
|
95
|
-
<div className={styles.wrapper} data-active={isActive || undefined}>
|
|
134
|
+
<div className={cn(styles.wrapper, className)} data-active={isActive || undefined}>
|
|
96
135
|
<ListPrivate
|
|
97
136
|
{...props}
|
|
98
137
|
{...slicedItems}
|
|
99
|
-
ref={listRef}
|
|
100
|
-
onFocus={
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
(e.relatedTarget && itemRefs.every(({ current }) => current !== e.relatedTarget))
|
|
104
|
-
) {
|
|
105
|
-
resetActiveFocusIndex();
|
|
106
|
-
}
|
|
107
|
-
}}
|
|
108
|
-
onKeyDown={handleListKeyDown}
|
|
109
|
-
tabIndex={0}
|
|
138
|
+
ref={mergeRefs(ref, listRef)}
|
|
139
|
+
onFocus={handleOnFocus}
|
|
140
|
+
onKeyDown={mergedHandlerKeyDown}
|
|
141
|
+
tabIndex={tabIndex}
|
|
110
142
|
search={search}
|
|
111
143
|
nested={false}
|
|
112
144
|
/>
|
|
113
145
|
|
|
114
|
-
<HiddenTabButton ref={btnRef} listRef={listRef} />
|
|
146
|
+
<HiddenTabButton ref={btnRef} listRef={listRef} tabIndex={tabIndex} />
|
|
115
147
|
</div>
|
|
116
148
|
</ParentListContext.Provider>
|
|
117
149
|
</SelectionProvider>
|