@snack-uikit/list 0.11.5 → 0.13.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 +27 -0
- package/README.md +2 -0
- package/dist/components/Items/BaseItem/BaseItem.js +24 -20
- package/dist/components/Items/types.d.ts +3 -0
- package/dist/components/Items/utils.js +6 -4
- package/dist/components/Lists/Droplist/DropList.d.ts +1 -1
- package/dist/components/Lists/Droplist/DropList.js +18 -12
- package/dist/components/Lists/contexts/OpenListProvider.d.ts +7 -0
- package/dist/components/Lists/contexts/OpenListProvider.js +6 -0
- package/dist/components/Lists/contexts/index.d.ts +1 -0
- package/dist/components/Lists/contexts/index.js +1 -0
- package/dist/components/Lists/hooks.js +1 -2
- package/dist/components/Lists/types.d.ts +10 -0
- package/package.json +2 -2
- package/src/components/Items/BaseItem/BaseItem.tsx +27 -21
- package/src/components/Items/types.ts +3 -0
- package/src/components/Items/utils.ts +8 -4
- package/src/components/Lists/Droplist/DropList.tsx +63 -40
- package/src/components/Lists/contexts/OpenListProvider.tsx +13 -0
- package/src/components/Lists/contexts/index.ts +1 -0
- package/src/components/Lists/hooks.ts +1 -4
- package/src/components/Lists/types.ts +10 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,33 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# 0.13.0 (2024-05-13)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
* **FF-0000:** fix keyboard control; add listRef props ([bda03fe](https://github.com/cloud-ru-tech/snack-uikit/commit/bda03fee548908bdc07e1455cf5aa1a3c5b766f9))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Features
|
|
15
|
+
|
|
16
|
+
* **FF-0000:** add closeDroplistOnItemClick prop ([fabd3a4](https://github.com/cloud-ru-tech/snack-uikit/commit/fabd3a4fe5443a985269228f082f9cfb0f4c0182))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# 0.12.0 (2024-05-08)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
### Features
|
|
26
|
+
|
|
27
|
+
* **PDS-135:** add hidden flag for items ([489d07f](https://github.com/cloud-ru-tech/snack-uikit/commit/489d07f7b47142b6230f740e1c0c690f23a94512))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
6
33
|
## 0.11.5 (2024-05-08)
|
|
7
34
|
|
|
8
35
|
### Only dependencies have been changed
|
package/README.md
CHANGED
|
@@ -58,6 +58,8 @@
|
|
|
58
58
|
| items* | `Item[]` | - | Основные элементы списка |
|
|
59
59
|
| children* | `ReactNode \| ({onKeyDown}) => ReactNode * Рендер функция принимает аргументы `onKeyDown` - хендлер ввода, для поддержки управления с клавиатуры` | - | Триггер для дроплиста |
|
|
60
60
|
| triggerElemRef | `RefObject<HTMLElement>` | - | Ссылка на элемент-триггер для дроплиста |
|
|
61
|
+
| listRef | `RefObject<HTMLElement>` | - | Ссылка на элемент выпадающего списка |
|
|
62
|
+
| closeDroplistOnItemClick | `boolean` | - | Закрывать выпадающий список после клика на базовый айтем. Работает в режимах selection: 'none' \| 'single' |
|
|
61
63
|
| triggerClassName | `string` | - | CSS-класс триггера |
|
|
62
64
|
| open | `boolean` | - | Управляет состоянием показан/не показан. |
|
|
63
65
|
| onOpenChange | `(isOpen: boolean) => void` | - | Колбек отображения компонента. Срабатывает при изменении состояния open. |
|
|
@@ -14,7 +14,7 @@ import cn from 'classnames';
|
|
|
14
14
|
import { Checkbox, Switch } from '@snack-uikit/toggles';
|
|
15
15
|
import { extractSupportProps } from '@snack-uikit/utils';
|
|
16
16
|
import { ItemContent } from '../../../helperComponents';
|
|
17
|
-
import { useCollapseLevelContext, useFocusListContext, useNewListContext, useSelectionContext, } from '../../Lists/contexts';
|
|
17
|
+
import { useCollapseLevelContext, useFocusListContext, useNewListContext, useOpenListContext, useSelectionContext, } from '../../Lists/contexts';
|
|
18
18
|
import commonStyles from '../styles.module.css';
|
|
19
19
|
import { isContentItem } from '../utils';
|
|
20
20
|
import { CHECKBOX_SIZE_MAP } from './constants';
|
|
@@ -26,6 +26,7 @@ export function BaseItem(_a) {
|
|
|
26
26
|
const { size = 's', marker, contentRender } = useNewListContext();
|
|
27
27
|
const { level = 0 } = useCollapseLevelContext();
|
|
28
28
|
const { forceUpdateActiveItemId } = useFocusListContext();
|
|
29
|
+
const { closeDroplist, closeDroplistOnItemClick } = useOpenListContext();
|
|
29
30
|
const { value, onChange, mode, isSelectionSingle, isSelectionMultiple } = useSelectionContext();
|
|
30
31
|
const isChecked = isSelectionSingle ? value === id : checkedProp !== null && checkedProp !== void 0 ? checkedProp : value === null || value === void 0 ? void 0 : value.includes(id !== null && id !== void 0 ? id : '');
|
|
31
32
|
const handleChange = () => {
|
|
@@ -42,25 +43,6 @@ export function BaseItem(_a) {
|
|
|
42
43
|
onMouseDown === null || onMouseDown === void 0 ? void 0 : onMouseDown(e);
|
|
43
44
|
forceUpdateActiveItemId === null || forceUpdateActiveItemId === void 0 ? void 0 : forceUpdateActiveItemId('~drop-focus');
|
|
44
45
|
};
|
|
45
|
-
const handleItemKeyDown = (e) => {
|
|
46
|
-
onKeyDown === null || onKeyDown === void 0 ? void 0 : onKeyDown(e);
|
|
47
|
-
if (e.key === 'ArrowRight' && onOpenNestedList) {
|
|
48
|
-
onOpenNestedList(e);
|
|
49
|
-
e.preventDefault();
|
|
50
|
-
e.stopPropagation();
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
if (e.code === 'Space' || e.key === 'Enter') {
|
|
54
|
-
if (isSelectionMultiple && isParentNode && onSelect) {
|
|
55
|
-
onSelect();
|
|
56
|
-
}
|
|
57
|
-
!isParentNode && handleChange();
|
|
58
|
-
// TODO: should pass an event here?
|
|
59
|
-
!isParentNode && (onClick === null || onClick === void 0 ? void 0 : onClick(e));
|
|
60
|
-
e.stopPropagation();
|
|
61
|
-
e.preventDefault();
|
|
62
|
-
}
|
|
63
|
-
};
|
|
64
46
|
const handleItemFocus = (e) => {
|
|
65
47
|
onFocus === null || onFocus === void 0 ? void 0 : onFocus(e);
|
|
66
48
|
e.stopPropagation();
|
|
@@ -76,6 +58,28 @@ export function BaseItem(_a) {
|
|
|
76
58
|
const handleItemClick = (e) => {
|
|
77
59
|
if (!disabled) {
|
|
78
60
|
onClick === null || onClick === void 0 ? void 0 : onClick(e);
|
|
61
|
+
if (!isSelectionMultiple && closeDroplistOnItemClick) {
|
|
62
|
+
closeDroplist();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
const handleItemKeyDown = (e) => {
|
|
67
|
+
onKeyDown === null || onKeyDown === void 0 ? void 0 : onKeyDown(e);
|
|
68
|
+
if (e.key === 'ArrowRight' && onOpenNestedList) {
|
|
69
|
+
onOpenNestedList(e);
|
|
70
|
+
e.preventDefault();
|
|
71
|
+
e.stopPropagation();
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (e.code === 'Space' || e.key === 'Enter' || e.key === ' ') {
|
|
75
|
+
if (isSelectionMultiple && isParentNode && onSelect) {
|
|
76
|
+
onSelect();
|
|
77
|
+
}
|
|
78
|
+
!isParentNode && handleChange();
|
|
79
|
+
// TODO: should pass an event here?
|
|
80
|
+
!isParentNode && (handleItemClick === null || handleItemClick === void 0 ? void 0 : handleItemClick(e));
|
|
81
|
+
e.stopPropagation();
|
|
82
|
+
e.preventDefault();
|
|
79
83
|
}
|
|
80
84
|
};
|
|
81
85
|
const handleCheckboxClick = (e) => {
|
|
@@ -35,6 +35,8 @@ export type BaseItem = WithSupportProps<{
|
|
|
35
35
|
id?: ItemId;
|
|
36
36
|
/** Флаг неактивности элемента */
|
|
37
37
|
disabled?: boolean;
|
|
38
|
+
/** Флаг присутствия элемента */
|
|
39
|
+
hidden?: boolean;
|
|
38
40
|
itemRef?: RefObject<HTMLElement>;
|
|
39
41
|
className?: string;
|
|
40
42
|
/**
|
|
@@ -70,6 +72,7 @@ type CommonGroupItem = {
|
|
|
70
72
|
variant?: TruncateStringProps['variant'];
|
|
71
73
|
};
|
|
72
74
|
mode?: 'primary' | 'secondary';
|
|
75
|
+
hidden?: boolean;
|
|
73
76
|
divider?: boolean;
|
|
74
77
|
};
|
|
75
78
|
export type GroupItem = CommonGroupItem & {
|
|
@@ -59,9 +59,10 @@ export function kindFlattenItems({ items, prefix, parentId }) {
|
|
|
59
59
|
const autoChildIds = [];
|
|
60
60
|
const { items } = item, rest = __rest(item, ["items"]);
|
|
61
61
|
const childActiveParent = isGroupItem(item) ? parentId !== null && parentId !== void 0 ? parentId : '~main' : autoId;
|
|
62
|
-
|
|
62
|
+
const filteredItems = items.filter(item => !item.hidden);
|
|
63
|
+
for (let idx = 0; idx < filteredItems.length; idx++) {
|
|
63
64
|
const { id, children, autoId, focusChildren } = flatten({
|
|
64
|
-
item:
|
|
65
|
+
item: filteredItems[idx],
|
|
65
66
|
idx,
|
|
66
67
|
prefix: itemId,
|
|
67
68
|
parentId: childActiveParent,
|
|
@@ -90,8 +91,9 @@ export function kindFlattenItems({ items, prefix, parentId }) {
|
|
|
90
91
|
const closeChildIds = [];
|
|
91
92
|
const autoChildIds = [];
|
|
92
93
|
let allChildIds = [];
|
|
93
|
-
|
|
94
|
-
|
|
94
|
+
const filteredItems = items.filter(item => !item.hidden);
|
|
95
|
+
for (let idx = 0; idx < filteredItems.length; idx++) {
|
|
96
|
+
const { id, children, autoId } = flatten({ item: filteredItems[idx], idx, prefix, parentId });
|
|
95
97
|
autoChildIds.push(autoId);
|
|
96
98
|
closeChildIds.push(id);
|
|
97
99
|
allChildIds.push(id);
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import { DroplistProps } from '../types';
|
|
2
|
-
export declare function Droplist({ items: itemsProp, search, pinBottom: pinBottomProp, pinTop: pinTopProp, footerActiveElementsRefs, children, trigger, placement, widthStrategy, triggerElemRef: triggerElemRefProp, open: openProp, onOpenChange, collapse, triggerClassName, selection, contentRender, size, marker, className, ...props }: DroplistProps): import("react/jsx-runtime").JSX.Element;
|
|
2
|
+
export declare function Droplist({ items: itemsProp, search, pinBottom: pinBottomProp, pinTop: pinTopProp, footerActiveElementsRefs, children, trigger, placement, widthStrategy, triggerElemRef: triggerElemRefProp, open: openProp, onOpenChange, collapse, triggerClassName, selection, contentRender, size, marker, closeDroplistOnItemClick, className, listRef: listRefProp, ...props }: DroplistProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -16,13 +16,13 @@ import { cloneElement, isValidElement, useCallback, useMemo, useRef } from 'reac
|
|
|
16
16
|
import { Dropdown } from '@snack-uikit/dropdown';
|
|
17
17
|
import { useValueControl } from '@snack-uikit/utils';
|
|
18
18
|
import { extractActiveItems, kindFlattenItems, useCreateBaseItems } from '../../Items';
|
|
19
|
-
import { CollapseContext, FocusListContext, NewListContextProvider, SelectionProvider } from '../contexts';
|
|
19
|
+
import { CollapseContext, FocusListContext, NewListContextProvider, OpenListContext, SelectionProvider, } from '../contexts';
|
|
20
20
|
import { useNewKeyboardNavigation } from '../hooks';
|
|
21
21
|
import { ListPrivate } from '../ListPrivate';
|
|
22
22
|
import styles from '../styles.module.css';
|
|
23
23
|
const DEFAULT_FALLBACK_PLACEMENTS = ['top', 'right', 'bottom', 'left'];
|
|
24
24
|
export function Droplist(_a) {
|
|
25
|
-
var { items: itemsProp, search, pinBottom: pinBottomProp = [], pinTop: pinTopProp = [], footerActiveElementsRefs, children, trigger, placement, widthStrategy, triggerElemRef: triggerElemRefProp, open: openProp, onOpenChange, collapse = {}, triggerClassName, selection, contentRender, size = 's', marker = true, className } = _a, props = __rest(_a, ["items", "search", "pinBottom", "pinTop", "footerActiveElementsRefs", "children", "trigger", "placement", "widthStrategy", "triggerElemRef", "open", "onOpenChange", "collapse", "triggerClassName", "selection", "contentRender", "size", "marker", "className"]);
|
|
25
|
+
var { items: itemsProp, search, pinBottom: pinBottomProp = [], pinTop: pinTopProp = [], footerActiveElementsRefs, children, trigger, placement, widthStrategy, triggerElemRef: triggerElemRefProp, open: openProp, onOpenChange, collapse = {}, triggerClassName, selection, contentRender, size = 's', marker = true, closeDroplistOnItemClick = false, className, listRef: listRefProp } = _a, props = __rest(_a, ["items", "search", "pinBottom", "pinTop", "footerActiveElementsRefs", "children", "trigger", "placement", "widthStrategy", "triggerElemRef", "open", "onOpenChange", "collapse", "triggerClassName", "selection", "contentRender", "size", "marker", "closeDroplistOnItemClick", "className", "listRef"]);
|
|
26
26
|
const hasSearch = useMemo(() => Boolean(search), [search]);
|
|
27
27
|
const [openCollapseItems = [], setOpenCollapsedItems] = useValueControl(collapse);
|
|
28
28
|
const toggleOpenCollapseItem = useCallback((id) => setOpenCollapsedItems((items) => (items === null || items === void 0 ? void 0 : items.includes(id)) ? items.filter(item => item !== id) : (items !== null && items !== void 0 ? items : []).concat([id])), [setOpenCollapsedItems]);
|
|
@@ -75,14 +75,14 @@ export function Droplist(_a) {
|
|
|
75
75
|
const triggerElemRef = useRef(null);
|
|
76
76
|
const listRef = useRef(null);
|
|
77
77
|
const { handleListKeyDownFactory, resetActiveItemId, activeItemId, forceUpdateActiveItemId } = useNewKeyboardNavigation({
|
|
78
|
-
mainRef: triggerElemRef,
|
|
78
|
+
mainRef: triggerElemRefProp !== null && triggerElemRefProp !== void 0 ? triggerElemRefProp : triggerElemRef,
|
|
79
79
|
focusFlattenItems,
|
|
80
80
|
});
|
|
81
81
|
const handleListKeyDown = useCallback((e) => handleListKeyDownFactory(ids, expandedIds)(e), [handleListKeyDownFactory, ids, expandedIds]);
|
|
82
|
-
const handleOpenChange = (open) => {
|
|
82
|
+
const handleOpenChange = useCallback((open) => {
|
|
83
83
|
resetActiveItemId();
|
|
84
84
|
setOpen(open);
|
|
85
|
-
};
|
|
85
|
+
}, [resetActiveItemId, setOpen]);
|
|
86
86
|
const onKeyDown = useCallback((e, cb) => {
|
|
87
87
|
if (e.key === 'ArrowDown') {
|
|
88
88
|
e.preventDefault();
|
|
@@ -98,6 +98,7 @@ export function Droplist(_a) {
|
|
|
98
98
|
}
|
|
99
99
|
cb === null || cb === void 0 ? void 0 : cb(e);
|
|
100
100
|
}, [resetActiveItemId, setOpen]);
|
|
101
|
+
const isValid = useMemo(() => isValidElement(children), [children]);
|
|
101
102
|
const triggerElem = useMemo(() => {
|
|
102
103
|
if (isValidElement(children)) {
|
|
103
104
|
const props = typeof children.props === 'object' ? children.props : {};
|
|
@@ -118,11 +119,16 @@ export function Droplist(_a) {
|
|
|
118
119
|
activeItemId,
|
|
119
120
|
handleListKeyDownFactory,
|
|
120
121
|
forceUpdateActiveItemId,
|
|
121
|
-
}, children: _jsx(
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
122
|
+
}, children: _jsx(OpenListContext.Provider, { value: {
|
|
123
|
+
closeDroplistOnItemClick,
|
|
124
|
+
closeDroplist: () => {
|
|
125
|
+
var _a;
|
|
126
|
+
setOpen(false);
|
|
127
|
+
resetActiveItemId();
|
|
128
|
+
(_a = (triggerElemRefProp !== null && triggerElemRefProp !== void 0 ? triggerElemRefProp : triggerElemRef).current) === null || _a === void 0 ? void 0 : _a.focus();
|
|
129
|
+
},
|
|
130
|
+
}, children: _jsx(Dropdown, { content: _jsx("div", { className: cn(styles.wrapper, className), children: _jsx(ListPrivate, Object.assign({}, props, { items: memorizedItems.items.focusCloseChildIds, pinTop: memorizedItems.pinTop.focusCloseChildIds, pinBottom: memorizedItems.pinBottom.focusCloseChildIds, onKeyDown: handleListKeyDown, searchItem: searchItem, tabIndex: 0, ref: mergeRefs(listRef, listRefProp), search: search, onFocus: e => {
|
|
131
|
+
e.stopPropagation();
|
|
132
|
+
forceUpdateActiveItemId === null || forceUpdateActiveItemId === void 0 ? void 0 : forceUpdateActiveItemId(ids[0]);
|
|
133
|
+
}, limitedScrollHeight: true })) }), outsideClick: true, triggerClassName: triggerClassName, fallbackPlacements: DEFAULT_FALLBACK_PLACEMENTS, trigger: trigger, placement: placement, widthStrategy: widthStrategy, triggerRef: !triggerElemRefProp ? triggerElemRef : (isValid && triggerElemRefProp) || undefined, open: open, onOpenChange: handleOpenChange, children: triggerElem }) }) }) }) })) }));
|
|
128
134
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/// <reference types="react" />
|
|
2
|
+
export type OpenListContextType = {
|
|
3
|
+
closeDroplistOnItemClick?: boolean;
|
|
4
|
+
closeDroplist(): void;
|
|
5
|
+
};
|
|
6
|
+
export declare const OpenListContext: import("react").Context<OpenListContextType>;
|
|
7
|
+
export declare const useOpenListContext: () => OpenListContextType;
|
|
@@ -38,9 +38,8 @@ export function useNewKeyboardNavigation({ mainRef, btnRef, focusFlattenItems, }
|
|
|
38
38
|
setActiveItemId(undefined);
|
|
39
39
|
(_e = mainRef === null || mainRef === void 0 ? void 0 : mainRef.current) === null || _e === void 0 ? void 0 : _e.focus();
|
|
40
40
|
}
|
|
41
|
-
return;
|
|
42
41
|
}
|
|
43
|
-
if (activeItemIdRef.current !== undefined) {
|
|
42
|
+
else if (activeItemIdRef.current !== undefined) {
|
|
44
43
|
const activeIndex = ids.findIndex(id => id === activeItemIdRef.current);
|
|
45
44
|
const nextId = Math.max(activeIndex - 1, 0);
|
|
46
45
|
const itemId = ids[nextId];
|
|
@@ -44,6 +44,16 @@ export type ListProps = WithSupportProps<{
|
|
|
44
44
|
export type DroplistProps = {
|
|
45
45
|
/** Ссылка на элемент-триггер для дроплиста */
|
|
46
46
|
triggerElemRef?: RefObject<HTMLElement>;
|
|
47
|
+
/** Ссылка на элемент выпадающего списка */
|
|
48
|
+
listRef?: RefObject<HTMLElement>;
|
|
49
|
+
/**
|
|
50
|
+
* Закрывать выпадающий список после клика на базовый айтем.
|
|
51
|
+
*
|
|
52
|
+
* Работает в режимах selection: 'none' | 'single'
|
|
53
|
+
*
|
|
54
|
+
* @default false
|
|
55
|
+
*/
|
|
56
|
+
closeDroplistOnItemClick?: boolean;
|
|
47
57
|
/** Триггер для дроплиста
|
|
48
58
|
* @type ReactNode | ({onKeyDown}) => ReactNode
|
|
49
59
|
*
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
6
|
"title": "List",
|
|
7
|
-
"version": "0.
|
|
7
|
+
"version": "0.13.0",
|
|
8
8
|
"sideEffects": [
|
|
9
9
|
"*.css",
|
|
10
10
|
"*.woff",
|
|
@@ -49,5 +49,5 @@
|
|
|
49
49
|
"peerDependencies": {
|
|
50
50
|
"@snack-uikit/locale": "*"
|
|
51
51
|
},
|
|
52
|
-
"gitHead": "
|
|
52
|
+
"gitHead": "d36fe553ce72b92deafee581e70090458827f8ac"
|
|
53
53
|
}
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
useCollapseLevelContext,
|
|
10
10
|
useFocusListContext,
|
|
11
11
|
useNewListContext,
|
|
12
|
+
useOpenListContext,
|
|
12
13
|
useSelectionContext,
|
|
13
14
|
} from '../../Lists/contexts';
|
|
14
15
|
import commonStyles from '../styles.module.scss';
|
|
@@ -56,6 +57,7 @@ export function BaseItem({
|
|
|
56
57
|
const { size = 's', marker, contentRender } = useNewListContext();
|
|
57
58
|
const { level = 0 } = useCollapseLevelContext();
|
|
58
59
|
const { forceUpdateActiveItemId } = useFocusListContext();
|
|
60
|
+
const { closeDroplist, closeDroplistOnItemClick } = useOpenListContext();
|
|
59
61
|
const { value, onChange, mode, isSelectionSingle, isSelectionMultiple } = useSelectionContext();
|
|
60
62
|
|
|
61
63
|
const isChecked = isSelectionSingle ? value === id : checkedProp ?? value?.includes(id ?? '');
|
|
@@ -78,6 +80,29 @@ export function BaseItem({
|
|
|
78
80
|
forceUpdateActiveItemId?.('~drop-focus');
|
|
79
81
|
};
|
|
80
82
|
|
|
83
|
+
const handleItemFocus = (e: FocusEvent<HTMLElement>) => {
|
|
84
|
+
onFocus?.(e);
|
|
85
|
+
e.stopPropagation();
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const handleCheckboxChange = () => {
|
|
89
|
+
if (isParentNode && onSelect) {
|
|
90
|
+
onSelect();
|
|
91
|
+
} else {
|
|
92
|
+
handleChange();
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const handleItemClick = (e: MouseEvent<HTMLElement>) => {
|
|
97
|
+
if (!disabled) {
|
|
98
|
+
onClick?.(e);
|
|
99
|
+
|
|
100
|
+
if (!isSelectionMultiple && closeDroplistOnItemClick) {
|
|
101
|
+
closeDroplist();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
81
106
|
const handleItemKeyDown = (e: KeyboardEvent<HTMLElement>) => {
|
|
82
107
|
onKeyDown?.(e);
|
|
83
108
|
|
|
@@ -90,39 +115,20 @@ export function BaseItem({
|
|
|
90
115
|
return;
|
|
91
116
|
}
|
|
92
117
|
|
|
93
|
-
if (e.code === 'Space' || e.key === 'Enter') {
|
|
118
|
+
if (e.code === 'Space' || e.key === 'Enter' || e.key === ' ') {
|
|
94
119
|
if (isSelectionMultiple && isParentNode && onSelect) {
|
|
95
120
|
onSelect();
|
|
96
121
|
}
|
|
97
122
|
|
|
98
123
|
!isParentNode && handleChange();
|
|
99
124
|
// TODO: should pass an event here?
|
|
100
|
-
!isParentNode &&
|
|
125
|
+
!isParentNode && handleItemClick?.(e as unknown as MouseEvent<HTMLElement>);
|
|
101
126
|
|
|
102
127
|
e.stopPropagation();
|
|
103
128
|
e.preventDefault();
|
|
104
129
|
}
|
|
105
130
|
};
|
|
106
131
|
|
|
107
|
-
const handleItemFocus = (e: FocusEvent<HTMLElement>) => {
|
|
108
|
-
onFocus?.(e);
|
|
109
|
-
e.stopPropagation();
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
const handleCheckboxChange = () => {
|
|
113
|
-
if (isParentNode && onSelect) {
|
|
114
|
-
onSelect();
|
|
115
|
-
} else {
|
|
116
|
-
handleChange();
|
|
117
|
-
}
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
const handleItemClick = (e: MouseEvent<HTMLElement>) => {
|
|
121
|
-
if (!disabled) {
|
|
122
|
-
onClick?.(e);
|
|
123
|
-
}
|
|
124
|
-
};
|
|
125
|
-
|
|
126
132
|
const handleCheckboxClick = (e: MouseEvent) => {
|
|
127
133
|
e.stopPropagation();
|
|
128
134
|
};
|
|
@@ -42,6 +42,8 @@ export type BaseItem = WithSupportProps<{
|
|
|
42
42
|
id?: ItemId;
|
|
43
43
|
/** Флаг неактивности элемента */
|
|
44
44
|
disabled?: boolean;
|
|
45
|
+
/** Флаг присутствия элемента */
|
|
46
|
+
hidden?: boolean;
|
|
45
47
|
|
|
46
48
|
itemRef?: RefObject<HTMLElement>;
|
|
47
49
|
|
|
@@ -90,6 +92,7 @@ type CommonGroupItem = {
|
|
|
90
92
|
variant?: TruncateStringProps['variant'];
|
|
91
93
|
};
|
|
92
94
|
mode?: 'primary' | 'secondary';
|
|
95
|
+
hidden?: boolean;
|
|
93
96
|
divider?: boolean;
|
|
94
97
|
};
|
|
95
98
|
|
|
@@ -94,9 +94,11 @@ export function kindFlattenItems({ items, prefix, parentId }: KindFlattenItemsPr
|
|
|
94
94
|
const { items, ...rest } = item;
|
|
95
95
|
const childActiveParent = isGroupItem(item) ? parentId ?? '~main' : autoId;
|
|
96
96
|
|
|
97
|
-
|
|
97
|
+
const filteredItems = items.filter(item => !item.hidden);
|
|
98
|
+
|
|
99
|
+
for (let idx = 0; idx < filteredItems.length; idx++) {
|
|
98
100
|
const { id, children, autoId, focusChildren } = flatten({
|
|
99
|
-
item:
|
|
101
|
+
item: filteredItems[idx],
|
|
100
102
|
idx,
|
|
101
103
|
prefix: itemId,
|
|
102
104
|
parentId: childActiveParent,
|
|
@@ -137,8 +139,10 @@ export function kindFlattenItems({ items, prefix, parentId }: KindFlattenItemsPr
|
|
|
137
139
|
const autoChildIds: ItemId[] = [];
|
|
138
140
|
let allChildIds: ItemId[] = [];
|
|
139
141
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
+
const filteredItems = items.filter(item => !item.hidden);
|
|
143
|
+
|
|
144
|
+
for (let idx = 0; idx < filteredItems.length; idx++) {
|
|
145
|
+
const { id, children, autoId } = flatten({ item: filteredItems[idx], idx, prefix, parentId });
|
|
142
146
|
|
|
143
147
|
autoChildIds.push(autoId);
|
|
144
148
|
closeChildIds.push(id);
|
|
@@ -6,7 +6,13 @@ import { Dropdown, DropdownProps } from '@snack-uikit/dropdown';
|
|
|
6
6
|
import { useValueControl } from '@snack-uikit/utils';
|
|
7
7
|
|
|
8
8
|
import { extractActiveItems, ItemId, kindFlattenItems, useCreateBaseItems } from '../../Items';
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
CollapseContext,
|
|
11
|
+
FocusListContext,
|
|
12
|
+
NewListContextProvider,
|
|
13
|
+
OpenListContext,
|
|
14
|
+
SelectionProvider,
|
|
15
|
+
} from '../contexts';
|
|
10
16
|
import { useNewKeyboardNavigation } from '../hooks';
|
|
11
17
|
import { ListPrivate } from '../ListPrivate';
|
|
12
18
|
import styles from '../styles.module.scss';
|
|
@@ -33,7 +39,9 @@ export function Droplist({
|
|
|
33
39
|
contentRender,
|
|
34
40
|
size = 's',
|
|
35
41
|
marker = true,
|
|
42
|
+
closeDroplistOnItemClick = false,
|
|
36
43
|
className,
|
|
44
|
+
listRef: listRefProp,
|
|
37
45
|
...props
|
|
38
46
|
}: DroplistProps) {
|
|
39
47
|
const hasSearch = useMemo(() => Boolean(search), [search]);
|
|
@@ -114,7 +122,7 @@ export function Droplist({
|
|
|
114
122
|
|
|
115
123
|
const { handleListKeyDownFactory, resetActiveItemId, activeItemId, forceUpdateActiveItemId } =
|
|
116
124
|
useNewKeyboardNavigation({
|
|
117
|
-
mainRef: triggerElemRef,
|
|
125
|
+
mainRef: triggerElemRefProp ?? triggerElemRef,
|
|
118
126
|
focusFlattenItems,
|
|
119
127
|
});
|
|
120
128
|
|
|
@@ -123,11 +131,14 @@ export function Droplist({
|
|
|
123
131
|
[handleListKeyDownFactory, ids, expandedIds],
|
|
124
132
|
);
|
|
125
133
|
|
|
126
|
-
const handleOpenChange = (
|
|
127
|
-
|
|
134
|
+
const handleOpenChange = useCallback(
|
|
135
|
+
(open: boolean) => {
|
|
136
|
+
resetActiveItemId();
|
|
128
137
|
|
|
129
|
-
|
|
130
|
-
|
|
138
|
+
setOpen(open);
|
|
139
|
+
},
|
|
140
|
+
[resetActiveItemId, setOpen],
|
|
141
|
+
);
|
|
131
142
|
|
|
132
143
|
const onKeyDown = useCallback(
|
|
133
144
|
(e: KeyboardEvent<HTMLElement>, cb?: (e: KeyboardEvent<HTMLElement>) => void) => {
|
|
@@ -150,6 +161,8 @@ export function Droplist({
|
|
|
150
161
|
[resetActiveItemId, setOpen],
|
|
151
162
|
);
|
|
152
163
|
|
|
164
|
+
const isValid = useMemo(() => isValidElement(children), [children]);
|
|
165
|
+
|
|
153
166
|
const triggerElem: ReactNode = useMemo(() => {
|
|
154
167
|
if (isValidElement(children)) {
|
|
155
168
|
const props = typeof children.props === 'object' ? children.props : {};
|
|
@@ -191,41 +204,51 @@ export function Droplist({
|
|
|
191
204
|
forceUpdateActiveItemId,
|
|
192
205
|
}}
|
|
193
206
|
>
|
|
194
|
-
<
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
searchItem={searchItem}
|
|
204
|
-
tabIndex={0}
|
|
205
|
-
ref={listRef}
|
|
206
|
-
search={search}
|
|
207
|
-
onFocus={e => {
|
|
208
|
-
e.stopPropagation();
|
|
209
|
-
|
|
210
|
-
forceUpdateActiveItemId?.(ids[0]);
|
|
211
|
-
}}
|
|
212
|
-
limitedScrollHeight
|
|
213
|
-
/>
|
|
214
|
-
</div>
|
|
215
|
-
}
|
|
216
|
-
triggerClassName={triggerClassName}
|
|
217
|
-
fallbackPlacements={DEFAULT_FALLBACK_PLACEMENTS}
|
|
218
|
-
trigger={trigger}
|
|
219
|
-
placement={placement}
|
|
220
|
-
widthStrategy={widthStrategy}
|
|
221
|
-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
222
|
-
// @ts-expect-error
|
|
223
|
-
triggerRef={triggerElemRefProp ? mergeRefs(triggerElemRef, triggerElemRefProp) : triggerElemRef}
|
|
224
|
-
open={open}
|
|
225
|
-
onOpenChange={handleOpenChange}
|
|
207
|
+
<OpenListContext.Provider
|
|
208
|
+
value={{
|
|
209
|
+
closeDroplistOnItemClick,
|
|
210
|
+
closeDroplist: () => {
|
|
211
|
+
setOpen(false);
|
|
212
|
+
resetActiveItemId();
|
|
213
|
+
(triggerElemRefProp ?? triggerElemRef).current?.focus();
|
|
214
|
+
},
|
|
215
|
+
}}
|
|
226
216
|
>
|
|
227
|
-
|
|
228
|
-
|
|
217
|
+
<Dropdown
|
|
218
|
+
content={
|
|
219
|
+
<div className={cn(styles.wrapper, className)}>
|
|
220
|
+
<ListPrivate
|
|
221
|
+
{...props}
|
|
222
|
+
items={memorizedItems.items.focusCloseChildIds}
|
|
223
|
+
pinTop={memorizedItems.pinTop.focusCloseChildIds}
|
|
224
|
+
pinBottom={memorizedItems.pinBottom.focusCloseChildIds}
|
|
225
|
+
onKeyDown={handleListKeyDown}
|
|
226
|
+
searchItem={searchItem}
|
|
227
|
+
tabIndex={0}
|
|
228
|
+
ref={mergeRefs(listRef, listRefProp)}
|
|
229
|
+
search={search}
|
|
230
|
+
onFocus={e => {
|
|
231
|
+
e.stopPropagation();
|
|
232
|
+
|
|
233
|
+
forceUpdateActiveItemId?.(ids[0]);
|
|
234
|
+
}}
|
|
235
|
+
limitedScrollHeight
|
|
236
|
+
/>
|
|
237
|
+
</div>
|
|
238
|
+
}
|
|
239
|
+
outsideClick
|
|
240
|
+
triggerClassName={triggerClassName}
|
|
241
|
+
fallbackPlacements={DEFAULT_FALLBACK_PLACEMENTS}
|
|
242
|
+
trigger={trigger}
|
|
243
|
+
placement={placement}
|
|
244
|
+
widthStrategy={widthStrategy}
|
|
245
|
+
triggerRef={!triggerElemRefProp ? triggerElemRef : (isValid && triggerElemRefProp) || undefined}
|
|
246
|
+
open={open}
|
|
247
|
+
onOpenChange={handleOpenChange}
|
|
248
|
+
>
|
|
249
|
+
{triggerElem}
|
|
250
|
+
</Dropdown>
|
|
251
|
+
</OpenListContext.Provider>
|
|
229
252
|
</FocusListContext.Provider>
|
|
230
253
|
</CollapseContext.Provider>
|
|
231
254
|
</SelectionProvider>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
|
|
3
|
+
export type OpenListContextType = {
|
|
4
|
+
closeDroplistOnItemClick?: boolean;
|
|
5
|
+
closeDroplist(): void;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const OpenListContext = createContext<OpenListContextType>({
|
|
9
|
+
closeDroplist: () => {},
|
|
10
|
+
closeDroplistOnItemClick: false,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export const useOpenListContext = () => useContext(OpenListContext);
|
|
@@ -59,10 +59,7 @@ export function useNewKeyboardNavigation<T extends HTMLElement>({
|
|
|
59
59
|
setActiveItemId(undefined);
|
|
60
60
|
mainRef?.current?.focus();
|
|
61
61
|
}
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (activeItemIdRef.current !== undefined) {
|
|
62
|
+
} else if (activeItemIdRef.current !== undefined) {
|
|
66
63
|
const activeIndex = ids.findIndex(id => id === activeItemIdRef.current);
|
|
67
64
|
const nextId = Math.max(activeIndex - 1, 0);
|
|
68
65
|
const itemId = ids[nextId];
|
|
@@ -56,6 +56,16 @@ export type ListProps = WithSupportProps<
|
|
|
56
56
|
export type DroplistProps = {
|
|
57
57
|
/** Ссылка на элемент-триггер для дроплиста */
|
|
58
58
|
triggerElemRef?: RefObject<HTMLElement>;
|
|
59
|
+
/** Ссылка на элемент выпадающего списка */
|
|
60
|
+
listRef?: RefObject<HTMLElement>;
|
|
61
|
+
/**
|
|
62
|
+
* Закрывать выпадающий список после клика на базовый айтем.
|
|
63
|
+
*
|
|
64
|
+
* Работает в режимах selection: 'none' | 'single'
|
|
65
|
+
*
|
|
66
|
+
* @default false
|
|
67
|
+
*/
|
|
68
|
+
closeDroplistOnItemClick?: boolean;
|
|
59
69
|
|
|
60
70
|
/** Триггер для дроплиста
|
|
61
71
|
* @type ReactNode | ({onKeyDown}) => ReactNode
|