@snack-uikit/list 0.22.3 → 0.23.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 +11 -0
- package/README.md +1 -0
- package/dist/cjs/components/Lists/Droplist/DropList.d.ts +1 -1
- package/dist/cjs/components/Lists/Droplist/DropList.js +4 -2
- package/dist/cjs/components/Lists/ListPrivate/ListPrivate.d.ts +1 -0
- package/dist/cjs/components/Lists/ListPrivate/ListPrivate.js +51 -12
- package/dist/cjs/components/Lists/ListPrivate/constants.d.ts +4 -0
- package/dist/cjs/components/Lists/ListPrivate/constants.js +14 -0
- package/dist/cjs/components/Lists/ListPrivate/styles.module.css +12 -0
- package/dist/cjs/components/Lists/types.d.ts +5 -0
- package/dist/esm/components/Lists/Droplist/DropList.d.ts +1 -1
- package/dist/esm/components/Lists/Droplist/DropList.js +2 -2
- package/dist/esm/components/Lists/ListPrivate/ListPrivate.d.ts +1 -0
- package/dist/esm/components/Lists/ListPrivate/ListPrivate.js +36 -4
- package/dist/esm/components/Lists/ListPrivate/constants.d.ts +4 -0
- package/dist/esm/components/Lists/ListPrivate/constants.js +8 -0
- package/dist/esm/components/Lists/ListPrivate/styles.module.css +12 -0
- package/dist/esm/components/Lists/types.d.ts +5 -0
- package/package.json +3 -2
- package/src/components/Lists/Droplist/DropList.tsx +2 -0
- package/src/components/Lists/ListPrivate/ListPrivate.tsx +56 -4
- package/src/components/Lists/ListPrivate/constants.ts +9 -0
- package/src/components/Lists/ListPrivate/styles.module.scss +12 -0
- package/src/components/Lists/types.ts +6 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,17 @@
|
|
|
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.23.0 (2025-01-22)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* **CORESERV-5847:** add optional virtualization to list ([6a05691](https://github.com/cloud-ru-tech/snack-uikit/commit/6a056917c7cd8b56fa432bf4edc804c13feb0a0a))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
6
17
|
## 0.22.3 (2025-01-20)
|
|
7
18
|
|
|
8
19
|
### Only dependencies have been changed
|
package/README.md
CHANGED
|
@@ -60,6 +60,7 @@
|
|
|
60
60
|
| triggerElemRef | `RefObject<HTMLElement>` | - | Ссылка на элемент-триггер для дроплиста |
|
|
61
61
|
| listRef | `RefObject<HTMLElement>` | - | Ссылка на элемент выпадающего списка |
|
|
62
62
|
| closeDroplistOnItemClick | `boolean` | - | Закрывать выпадающий список после клика на базовый айтем. Работает в режимах selection: 'none' \| 'single' |
|
|
63
|
+
| virtualized | `boolean` | - | Включить виртуализацию на компоненты списка. Рекомендуется если у вас от 1к элементов списка |
|
|
63
64
|
| triggerClassName | `string` | - | CSS-класс триггера |
|
|
64
65
|
| open | `boolean` | - | Управляет состоянием показан/не показан. |
|
|
65
66
|
| onOpenChange | `(isOpen: boolean) => void` | - | Колбек отображения компонента. Срабатывает при изменении состояния open. |
|
|
@@ -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, closeDroplistOnItemClick, className, listRef: listRefProp, untouchableScrollbars, ...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, untouchableScrollbars, virtualized, ...props }: DroplistProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -53,9 +53,10 @@ function Droplist(_a) {
|
|
|
53
53
|
closeDroplistOnItemClick = false,
|
|
54
54
|
className,
|
|
55
55
|
listRef: listRefProp,
|
|
56
|
-
untouchableScrollbars = false
|
|
56
|
+
untouchableScrollbars = false,
|
|
57
|
+
virtualized = false
|
|
57
58
|
} = _a,
|
|
58
|
-
props = __rest(_a, ["items", "search", "pinBottom", "pinTop", "footerActiveElementsRefs", "children", "trigger", "placement", "widthStrategy", "triggerElemRef", "open", "onOpenChange", "collapse", "triggerClassName", "selection", "contentRender", "size", "marker", "closeDroplistOnItemClick", "className", "listRef", "untouchableScrollbars"]);
|
|
59
|
+
props = __rest(_a, ["items", "search", "pinBottom", "pinTop", "footerActiveElementsRefs", "children", "trigger", "placement", "widthStrategy", "triggerElemRef", "open", "onOpenChange", "collapse", "triggerClassName", "selection", "contentRender", "size", "marker", "closeDroplistOnItemClick", "className", "listRef", "untouchableScrollbars", "virtualized"]);
|
|
59
60
|
const hasSearch = (0, react_1.useMemo)(() => Boolean(search), [search]);
|
|
60
61
|
const [openCollapseItems = [], setOpenCollapsedItems] = (0, utils_1.useValueControl)(collapse);
|
|
61
62
|
const toggleOpenCollapseItem = (0, react_1.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]);
|
|
@@ -237,6 +238,7 @@ function Droplist(_a) {
|
|
|
237
238
|
items: memorizedItems.items.focusCloseChildIds,
|
|
238
239
|
pinTop: memorizedItems.pinTop.focusCloseChildIds,
|
|
239
240
|
pinBottom: memorizedItems.pinBottom.focusCloseChildIds,
|
|
241
|
+
virtualized: virtualized,
|
|
240
242
|
onKeyDown: handleListKeyDown,
|
|
241
243
|
searchItem: searchItem,
|
|
242
244
|
tabIndex: 0,
|
|
@@ -2,6 +2,7 @@ export declare const ListPrivate: import("react").ForwardRefExoticComponent<Omit
|
|
|
2
2
|
nested?: boolean;
|
|
3
3
|
active?: boolean;
|
|
4
4
|
tabIndex?: number;
|
|
5
|
+
virtualized?: boolean;
|
|
5
6
|
onFocus?(e: import("react").FocusEvent<HTMLElement>): void;
|
|
6
7
|
onBlur?(e: import("react").FocusEvent<HTMLElement>): void;
|
|
7
8
|
onKeyDown?(e: import("react").KeyboardEvent<HTMLElement>): void;
|
|
@@ -18,6 +18,7 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
18
18
|
});
|
|
19
19
|
exports.ListPrivate = void 0;
|
|
20
20
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
21
|
+
const react_virtual_1 = require("@tanstack/react-virtual");
|
|
21
22
|
const classnames_1 = __importDefault(require("classnames"));
|
|
22
23
|
const react_1 = require("react");
|
|
23
24
|
const loaders_1 = require("@snack-uikit/loaders");
|
|
@@ -27,6 +28,7 @@ const helperComponents_1 = require("../../../helperComponents");
|
|
|
27
28
|
const Items_1 = require("../../Items");
|
|
28
29
|
const contexts_1 = require("../contexts");
|
|
29
30
|
const styles_module_scss_1 = __importDefault(require('../styles.module.css'));
|
|
31
|
+
const constants_1 = require("./constants");
|
|
30
32
|
const styles_module_scss_2 = __importDefault(require('./styles.module.css'));
|
|
31
33
|
exports.ListPrivate = (0, react_1.forwardRef)((_a, ref) => {
|
|
32
34
|
var {
|
|
@@ -56,9 +58,10 @@ exports.ListPrivate = (0, react_1.forwardRef)((_a, ref) => {
|
|
|
56
58
|
dataError,
|
|
57
59
|
dataFiltered,
|
|
58
60
|
scrollToSelectedItem = false,
|
|
61
|
+
virtualized = false,
|
|
59
62
|
scrollContainerClassName
|
|
60
63
|
} = _a,
|
|
61
|
-
props = __rest(_a, ["items", "pinTop", "pinBottom", "onKeyDown", "onBlur", "onFocus", "tabIndex", "active", "scroll", "nested", "search", "searchItem", "scrollRef", "scrollContainerRef", "onScroll", "footer", "loading", "limitedScrollHeight", "untouchableScrollbars", "className", "noDataState", "noResultsState", "errorDataState", "dataError", "dataFiltered", "scrollToSelectedItem", "scrollContainerClassName"]);
|
|
64
|
+
props = __rest(_a, ["items", "pinTop", "pinBottom", "onKeyDown", "onBlur", "onFocus", "tabIndex", "active", "scroll", "nested", "search", "searchItem", "scrollRef", "scrollContainerRef", "onScroll", "footer", "loading", "limitedScrollHeight", "untouchableScrollbars", "className", "noDataState", "noResultsState", "errorDataState", "dataError", "dataFiltered", "scrollToSelectedItem", "virtualized", "scrollContainerClassName"]);
|
|
62
65
|
const {
|
|
63
66
|
size = 's',
|
|
64
67
|
flattenItems,
|
|
@@ -68,6 +71,7 @@ exports.ListPrivate = (0, react_1.forwardRef)((_a, ref) => {
|
|
|
68
71
|
value,
|
|
69
72
|
isSelectionSingle
|
|
70
73
|
} = (0, contexts_1.useSelectionContext)();
|
|
74
|
+
const innerScrollRef = (0, react_1.useRef)(null);
|
|
71
75
|
const itemsJSX = (0, Items_1.useRenderItems)(items);
|
|
72
76
|
const itemsPinTopJSX = (0, Items_1.useRenderItems)(pinTop);
|
|
73
77
|
const itemsPinBottomJSX = (0, Items_1.useRenderItems)(pinBottom);
|
|
@@ -77,6 +81,16 @@ exports.ListPrivate = (0, react_1.forwardRef)((_a, ref) => {
|
|
|
77
81
|
errorDataState
|
|
78
82
|
});
|
|
79
83
|
const hasNoItems = items.length === 0;
|
|
84
|
+
const virtualizer = (0, react_virtual_1.useVirtualizer)({
|
|
85
|
+
count: itemsJSX.length,
|
|
86
|
+
getScrollElement: () => scroll ? innerScrollRef.current : null,
|
|
87
|
+
estimateSize: () => constants_1.ALL_SIZES[size],
|
|
88
|
+
enabled: virtualized
|
|
89
|
+
});
|
|
90
|
+
const virtualItems = virtualizer.getVirtualItems();
|
|
91
|
+
(0, react_1.useEffect)(() => {
|
|
92
|
+
virtualizer.measure();
|
|
93
|
+
}, [virtualizer]);
|
|
80
94
|
const loadingJSX = (0, react_1.useMemo)(() => loading && (0, jsx_runtime_1.jsx)("div", {
|
|
81
95
|
role: 'spinbutton',
|
|
82
96
|
tabIndex: -1,
|
|
@@ -88,16 +102,38 @@ exports.ListPrivate = (0, react_1.forwardRef)((_a, ref) => {
|
|
|
88
102
|
size: size === 'l' ? 's' : 'xs'
|
|
89
103
|
})
|
|
90
104
|
}), [hasNoItems, loading, size]);
|
|
91
|
-
const content = (0, react_1.useMemo)(() =>
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
105
|
+
const content = (0, react_1.useMemo)(() => {
|
|
106
|
+
var _a, _b;
|
|
107
|
+
return (0, jsx_runtime_1.jsxs)("div", {
|
|
108
|
+
className: styles_module_scss_2.default.content,
|
|
109
|
+
children: [virtualized ? (0, jsx_runtime_1.jsx)("div", {
|
|
110
|
+
className: styles_module_scss_2.default.virtualizedContainer,
|
|
111
|
+
style: {
|
|
112
|
+
height: virtualizer.getTotalSize()
|
|
113
|
+
},
|
|
114
|
+
tabIndex: -1,
|
|
115
|
+
children: (0, jsx_runtime_1.jsx)("div", {
|
|
116
|
+
className: styles_module_scss_2.default.virtualizedPositionBox,
|
|
117
|
+
style: {
|
|
118
|
+
transform: `translateY(${(_b = (_a = virtualItems[0]) === null || _a === void 0 ? void 0 : _a.start) !== null && _b !== void 0 ? _b : 0}px)`
|
|
119
|
+
},
|
|
120
|
+
tabIndex: -1,
|
|
121
|
+
children: virtualItems.map(virtualRow => (0, jsx_runtime_1.jsx)("div", {
|
|
122
|
+
"data-index": virtualRow.index,
|
|
123
|
+
ref: virtualizer.measureElement,
|
|
124
|
+
tabIndex: -1,
|
|
125
|
+
children: itemsJSX[virtualRow.index]
|
|
126
|
+
}, virtualRow.key))
|
|
127
|
+
})
|
|
128
|
+
}) : itemsJSX, loadingJSX, (0, jsx_runtime_1.jsx)(helperComponents_1.ListEmptyState, {
|
|
129
|
+
loading: loading,
|
|
130
|
+
dataError: dataError,
|
|
131
|
+
emptyStates: emptyStates,
|
|
132
|
+
hasNoItems: hasNoItems,
|
|
133
|
+
dataFiltered: dataFiltered !== null && dataFiltered !== void 0 ? dataFiltered : Boolean(search === null || search === void 0 ? void 0 : search.value)
|
|
134
|
+
})]
|
|
135
|
+
});
|
|
136
|
+
}, [dataError, dataFiltered, emptyStates, hasNoItems, itemsJSX, loading, loadingJSX, search === null || search === void 0 ? void 0 : search.value, virtualItems, virtualized, virtualizer]);
|
|
101
137
|
const onScrollInitialized = () => {
|
|
102
138
|
var _a, _b;
|
|
103
139
|
if (scrollToSelectedItem) {
|
|
@@ -136,7 +172,10 @@ exports.ListPrivate = (0, react_1.forwardRef)((_a, ref) => {
|
|
|
136
172
|
}, scrollContainerClassName),
|
|
137
173
|
barHideStrategy: 'never',
|
|
138
174
|
size: size === 's' ? 's' : 'm',
|
|
139
|
-
ref:
|
|
175
|
+
ref: ref => {
|
|
176
|
+
innerScrollRef.current = ref;
|
|
177
|
+
if (scrollContainerRef) scrollContainerRef.current = ref;
|
|
178
|
+
},
|
|
140
179
|
untouchableScrollbars: untouchableScrollbars,
|
|
141
180
|
onScroll: onScroll,
|
|
142
181
|
onInitialized: onScrollInitialized,
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.ALL_SIZES = exports.SMALL_SIZE_HEIGHT = exports.MEDIUM_SIZE_HEIGHT = exports.LARGE_SIZE_HEIGHT = void 0;
|
|
7
|
+
exports.LARGE_SIZE_HEIGHT = 48;
|
|
8
|
+
exports.MEDIUM_SIZE_HEIGHT = 40;
|
|
9
|
+
exports.SMALL_SIZE_HEIGHT = 32;
|
|
10
|
+
exports.ALL_SIZES = {
|
|
11
|
+
m: exports.MEDIUM_SIZE_HEIGHT,
|
|
12
|
+
s: exports.SMALL_SIZE_HEIGHT,
|
|
13
|
+
l: exports.LARGE_SIZE_HEIGHT
|
|
14
|
+
};
|
|
@@ -65,6 +65,10 @@ export type DroplistProps = {
|
|
|
65
65
|
* @default false
|
|
66
66
|
*/
|
|
67
67
|
closeDroplistOnItemClick?: boolean;
|
|
68
|
+
/**
|
|
69
|
+
* Включить виртуализацию на компоненты списка. Рекомендуется если у вас от 1к элементов списка
|
|
70
|
+
*/
|
|
71
|
+
virtualized?: boolean;
|
|
68
72
|
/** Триггер для дроплиста
|
|
69
73
|
* @type ReactNode | ({onKeyDown}) => ReactNode
|
|
70
74
|
*
|
|
@@ -78,6 +82,7 @@ export type ListPrivateProps = Omit<ListProps, 'pinTop' | 'pinBottom' | 'items'
|
|
|
78
82
|
nested?: boolean;
|
|
79
83
|
active?: boolean;
|
|
80
84
|
tabIndex?: number;
|
|
85
|
+
virtualized?: boolean;
|
|
81
86
|
onFocus?(e: FocusEvent<HTMLElement>): void;
|
|
82
87
|
onBlur?(e: FocusEvent<HTMLElement>): void;
|
|
83
88
|
onKeyDown?(e: KeyboardEvent<HTMLElement>): void;
|
|
@@ -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, closeDroplistOnItemClick, className, listRef: listRefProp, untouchableScrollbars, ...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, untouchableScrollbars, virtualized, ...props }: DroplistProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -23,7 +23,7 @@ import { ListPrivate } from '../ListPrivate';
|
|
|
23
23
|
import styles from '../styles.module.css';
|
|
24
24
|
const DEFAULT_FALLBACK_PLACEMENTS = ['top', 'right', 'bottom', 'left'];
|
|
25
25
|
export function Droplist(_a) {
|
|
26
|
-
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, untouchableScrollbars = false } = _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", "untouchableScrollbars"]);
|
|
26
|
+
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, untouchableScrollbars = false, virtualized = false } = _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", "untouchableScrollbars", "virtualized"]);
|
|
27
27
|
const hasSearch = useMemo(() => Boolean(search), [search]);
|
|
28
28
|
const [openCollapseItems = [], setOpenCollapsedItems] = useValueControl(collapse);
|
|
29
29
|
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]);
|
|
@@ -143,7 +143,7 @@ export function Droplist(_a) {
|
|
|
143
143
|
resetActiveItemId();
|
|
144
144
|
(_a = (triggerElemRefProp !== null && triggerElemRefProp !== void 0 ? triggerElemRefProp : triggerElemRef).current) === null || _a === void 0 ? void 0 : _a.focus();
|
|
145
145
|
},
|
|
146
|
-
}, 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 => {
|
|
146
|
+
}, 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, virtualized: virtualized, onKeyDown: handleListKeyDown, searchItem: searchItem, tabIndex: 0, ref: mergeRefs(listRef, listRefProp), search: search, onFocus: e => {
|
|
147
147
|
e.stopPropagation();
|
|
148
148
|
forceUpdateActiveItemId === null || forceUpdateActiveItemId === void 0 ? void 0 : forceUpdateActiveItemId(ids[0]);
|
|
149
149
|
}, limitedScrollHeight: true, untouchableScrollbars: untouchableScrollbars })) }), 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 }) }) }) }) })) }));
|
|
@@ -2,6 +2,7 @@ export declare const ListPrivate: import("react").ForwardRefExoticComponent<Omit
|
|
|
2
2
|
nested?: boolean;
|
|
3
3
|
active?: boolean;
|
|
4
4
|
tabIndex?: number;
|
|
5
|
+
virtualized?: boolean;
|
|
5
6
|
onFocus?(e: import("react").FocusEvent<HTMLElement>): void;
|
|
6
7
|
onBlur?(e: import("react").FocusEvent<HTMLElement>): void;
|
|
7
8
|
onKeyDown?(e: import("react").KeyboardEvent<HTMLElement>): void;
|
|
@@ -10,8 +10,9 @@ var __rest = (this && this.__rest) || function (s, e) {
|
|
|
10
10
|
return t;
|
|
11
11
|
};
|
|
12
12
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
13
|
+
import { useVirtualizer } from '@tanstack/react-virtual';
|
|
13
14
|
import cn from 'classnames';
|
|
14
|
-
import { forwardRef, useMemo } from 'react';
|
|
15
|
+
import { forwardRef, useEffect, useMemo, useRef } from 'react';
|
|
15
16
|
import { Spinner } from '@snack-uikit/loaders';
|
|
16
17
|
import { Scroll } from '@snack-uikit/scroll';
|
|
17
18
|
import { extractSupportProps } from '@snack-uikit/utils';
|
|
@@ -19,18 +20,45 @@ import { ListEmptyState, useEmptyState } from '../../../helperComponents';
|
|
|
19
20
|
import { PinBottomGroupItem, PinTopGroupItem, SearchItem, useRenderItems } from '../../Items';
|
|
20
21
|
import { useNewListContext, useSelectionContext } from '../contexts';
|
|
21
22
|
import commonStyles from '../styles.module.css';
|
|
23
|
+
import { ALL_SIZES } from './constants';
|
|
22
24
|
import styles from './styles.module.css';
|
|
23
25
|
export const ListPrivate = forwardRef((_a, ref) => {
|
|
24
|
-
var { items, pinTop, pinBottom, onKeyDown, onBlur, onFocus, tabIndex, active, scroll, nested, search, searchItem, scrollRef, scrollContainerRef, onScroll, footer, loading, limitedScrollHeight, untouchableScrollbars, className, noDataState, noResultsState, errorDataState, dataError, dataFiltered, scrollToSelectedItem = false, scrollContainerClassName } = _a, props = __rest(_a, ["items", "pinTop", "pinBottom", "onKeyDown", "onBlur", "onFocus", "tabIndex", "active", "scroll", "nested", "search", "searchItem", "scrollRef", "scrollContainerRef", "onScroll", "footer", "loading", "limitedScrollHeight", "untouchableScrollbars", "className", "noDataState", "noResultsState", "errorDataState", "dataError", "dataFiltered", "scrollToSelectedItem", "scrollContainerClassName"]);
|
|
26
|
+
var { items, pinTop, pinBottom, onKeyDown, onBlur, onFocus, tabIndex, active, scroll, nested, search, searchItem, scrollRef, scrollContainerRef, onScroll, footer, loading, limitedScrollHeight, untouchableScrollbars, className, noDataState, noResultsState, errorDataState, dataError, dataFiltered, scrollToSelectedItem = false, virtualized = false, scrollContainerClassName } = _a, props = __rest(_a, ["items", "pinTop", "pinBottom", "onKeyDown", "onBlur", "onFocus", "tabIndex", "active", "scroll", "nested", "search", "searchItem", "scrollRef", "scrollContainerRef", "onScroll", "footer", "loading", "limitedScrollHeight", "untouchableScrollbars", "className", "noDataState", "noResultsState", "errorDataState", "dataError", "dataFiltered", "scrollToSelectedItem", "virtualized", "scrollContainerClassName"]);
|
|
25
27
|
const { size = 's', flattenItems, focusFlattenItems } = useNewListContext();
|
|
26
28
|
const { value, isSelectionSingle } = useSelectionContext();
|
|
29
|
+
const innerScrollRef = useRef(null);
|
|
27
30
|
const itemsJSX = useRenderItems(items);
|
|
28
31
|
const itemsPinTopJSX = useRenderItems(pinTop);
|
|
29
32
|
const itemsPinBottomJSX = useRenderItems(pinBottom);
|
|
30
33
|
const emptyStates = useEmptyState({ noDataState, noResultsState, errorDataState });
|
|
31
34
|
const hasNoItems = items.length === 0;
|
|
35
|
+
const virtualizer = useVirtualizer({
|
|
36
|
+
count: itemsJSX.length,
|
|
37
|
+
getScrollElement: () => (scroll ? innerScrollRef.current : null),
|
|
38
|
+
estimateSize: () => ALL_SIZES[size],
|
|
39
|
+
enabled: virtualized,
|
|
40
|
+
});
|
|
41
|
+
const virtualItems = virtualizer.getVirtualItems();
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
virtualizer.measure();
|
|
44
|
+
}, [virtualizer]);
|
|
32
45
|
const loadingJSX = useMemo(() => loading && (_jsx("div", { role: 'spinbutton', tabIndex: -1, className: styles.loader, "data-size": size, "data-no-items": hasNoItems || undefined, "data-test-id": 'list__loader', children: _jsx(Spinner, { size: size === 'l' ? 's' : 'xs' }) })), [hasNoItems, loading, size]);
|
|
33
|
-
const content = useMemo(() =>
|
|
46
|
+
const content = useMemo(() => {
|
|
47
|
+
var _a, _b;
|
|
48
|
+
return (_jsxs("div", { className: styles.content, children: [virtualized ? (_jsx("div", { className: styles.virtualizedContainer, style: { height: virtualizer.getTotalSize() }, tabIndex: -1, children: _jsx("div", { className: styles.virtualizedPositionBox, style: { transform: `translateY(${(_b = (_a = virtualItems[0]) === null || _a === void 0 ? void 0 : _a.start) !== null && _b !== void 0 ? _b : 0}px)` }, tabIndex: -1, children: virtualItems.map(virtualRow => (_jsx("div", { "data-index": virtualRow.index, ref: virtualizer.measureElement, tabIndex: -1, children: itemsJSX[virtualRow.index] }, virtualRow.key))) }) })) : (itemsJSX), loadingJSX, _jsx(ListEmptyState, { loading: loading, dataError: dataError, emptyStates: emptyStates, hasNoItems: hasNoItems, dataFiltered: dataFiltered !== null && dataFiltered !== void 0 ? dataFiltered : Boolean(search === null || search === void 0 ? void 0 : search.value) })] }));
|
|
49
|
+
}, [
|
|
50
|
+
dataError,
|
|
51
|
+
dataFiltered,
|
|
52
|
+
emptyStates,
|
|
53
|
+
hasNoItems,
|
|
54
|
+
itemsJSX,
|
|
55
|
+
loading,
|
|
56
|
+
loadingJSX,
|
|
57
|
+
search === null || search === void 0 ? void 0 : search.value,
|
|
58
|
+
virtualItems,
|
|
59
|
+
virtualized,
|
|
60
|
+
virtualizer,
|
|
61
|
+
]);
|
|
34
62
|
const onScrollInitialized = () => {
|
|
35
63
|
var _a, _b;
|
|
36
64
|
if (scrollToSelectedItem) {
|
|
@@ -49,7 +77,11 @@ export const ListPrivate = forwardRef((_a, ref) => {
|
|
|
49
77
|
[commonStyles.scrollContainerS]: scroll && limitedScrollHeight && size === 's',
|
|
50
78
|
[commonStyles.scrollContainerM]: scroll && limitedScrollHeight && size === 'm',
|
|
51
79
|
[commonStyles.scrollContainerL]: scroll && limitedScrollHeight && size === 'l',
|
|
52
|
-
}, scrollContainerClassName), barHideStrategy: 'never', size: size === 's' ? 's' : 'm', ref:
|
|
80
|
+
}, scrollContainerClassName), barHideStrategy: 'never', size: size === 's' ? 's' : 'm', ref: ref => {
|
|
81
|
+
innerScrollRef.current = ref;
|
|
82
|
+
if (scrollContainerRef)
|
|
83
|
+
scrollContainerRef.current = ref;
|
|
84
|
+
}, untouchableScrollbars: untouchableScrollbars, onScroll: onScroll, onInitialized: onScrollInitialized, children: [content, _jsx("div", { className: styles.scrollStub, ref: scrollRef })] })) : (_jsx(_Fragment, { children: content })), Number(pinBottom === null || pinBottom === void 0 ? void 0 : pinBottom.length) > 0 && _jsx(PinBottomGroupItem, { children: itemsPinBottomJSX }), footer && (_jsx("div", { className: styles.footer, onFocus: e => e.stopPropagation(), children: footer }))] })));
|
|
53
85
|
if (!nested) {
|
|
54
86
|
return listJSX;
|
|
55
87
|
}
|
|
@@ -65,6 +65,10 @@ export type DroplistProps = {
|
|
|
65
65
|
* @default false
|
|
66
66
|
*/
|
|
67
67
|
closeDroplistOnItemClick?: boolean;
|
|
68
|
+
/**
|
|
69
|
+
* Включить виртуализацию на компоненты списка. Рекомендуется если у вас от 1к элементов списка
|
|
70
|
+
*/
|
|
71
|
+
virtualized?: boolean;
|
|
68
72
|
/** Триггер для дроплиста
|
|
69
73
|
* @type ReactNode | ({onKeyDown}) => ReactNode
|
|
70
74
|
*
|
|
@@ -78,6 +82,7 @@ export type ListPrivateProps = Omit<ListProps, 'pinTop' | 'pinBottom' | 'items'
|
|
|
78
82
|
nested?: boolean;
|
|
79
83
|
active?: boolean;
|
|
80
84
|
tabIndex?: number;
|
|
85
|
+
virtualized?: boolean;
|
|
81
86
|
onFocus?(e: FocusEvent<HTMLElement>): void;
|
|
82
87
|
onBlur?(e: FocusEvent<HTMLElement>): void;
|
|
83
88
|
onKeyDown?(e: KeyboardEvent<HTMLElement>): void;
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
6
|
"title": "List",
|
|
7
|
-
"version": "0.
|
|
7
|
+
"version": "0.23.0",
|
|
8
8
|
"sideEffects": [
|
|
9
9
|
"*.css",
|
|
10
10
|
"*.woff",
|
|
@@ -47,11 +47,12 @@
|
|
|
47
47
|
"@snack-uikit/toggles": "0.13.4",
|
|
48
48
|
"@snack-uikit/truncate-string": "0.6.5",
|
|
49
49
|
"@snack-uikit/utils": "3.6.0",
|
|
50
|
+
"@tanstack/react-virtual": "3.11.2",
|
|
50
51
|
"classnames": "2.5.1",
|
|
51
52
|
"merge-refs": "1.3.0"
|
|
52
53
|
},
|
|
53
54
|
"peerDependencies": {
|
|
54
55
|
"@snack-uikit/locale": "*"
|
|
55
56
|
},
|
|
56
|
-
"gitHead": "
|
|
57
|
+
"gitHead": "546ae486452e74e78f65fd9ce0044c13b058872c"
|
|
57
58
|
}
|
|
@@ -44,6 +44,7 @@ export function Droplist({
|
|
|
44
44
|
className,
|
|
45
45
|
listRef: listRefProp,
|
|
46
46
|
untouchableScrollbars = false,
|
|
47
|
+
virtualized = false,
|
|
47
48
|
...props
|
|
48
49
|
}: DroplistProps) {
|
|
49
50
|
const hasSearch = useMemo(() => Boolean(search), [search]);
|
|
@@ -240,6 +241,7 @@ export function Droplist({
|
|
|
240
241
|
items={memorizedItems.items.focusCloseChildIds}
|
|
241
242
|
pinTop={memorizedItems.pinTop.focusCloseChildIds}
|
|
242
243
|
pinBottom={memorizedItems.pinBottom.focusCloseChildIds}
|
|
244
|
+
virtualized={virtualized}
|
|
243
245
|
onKeyDown={handleListKeyDown}
|
|
244
246
|
searchItem={searchItem}
|
|
245
247
|
tabIndex={0}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { useVirtualizer } from '@tanstack/react-virtual';
|
|
1
2
|
import cn from 'classnames';
|
|
2
|
-
import { ForwardedRef, forwardRef, RefObject, useMemo } from 'react';
|
|
3
|
+
import { ForwardedRef, forwardRef, RefObject, useEffect, useMemo, useRef } from 'react';
|
|
3
4
|
|
|
4
5
|
import { Spinner } from '@snack-uikit/loaders';
|
|
5
6
|
import { Scroll } from '@snack-uikit/scroll';
|
|
@@ -10,6 +11,7 @@ import { PinBottomGroupItem, PinTopGroupItem, SearchItem, useRenderItems } from
|
|
|
10
11
|
import { useNewListContext, useSelectionContext } from '../contexts';
|
|
11
12
|
import commonStyles from '../styles.module.scss';
|
|
12
13
|
import { ListPrivateProps } from '../types';
|
|
14
|
+
import { ALL_SIZES } from './constants';
|
|
13
15
|
import styles from './styles.module.scss';
|
|
14
16
|
|
|
15
17
|
export const ListPrivate = forwardRef(
|
|
@@ -41,6 +43,7 @@ export const ListPrivate = forwardRef(
|
|
|
41
43
|
dataError,
|
|
42
44
|
dataFiltered,
|
|
43
45
|
scrollToSelectedItem = false,
|
|
46
|
+
virtualized = false,
|
|
44
47
|
scrollContainerClassName,
|
|
45
48
|
...props
|
|
46
49
|
}: ListPrivateProps,
|
|
@@ -48,6 +51,7 @@ export const ListPrivate = forwardRef(
|
|
|
48
51
|
) => {
|
|
49
52
|
const { size = 's', flattenItems, focusFlattenItems } = useNewListContext();
|
|
50
53
|
const { value, isSelectionSingle } = useSelectionContext();
|
|
54
|
+
const innerScrollRef = useRef<HTMLElement | null>(null);
|
|
51
55
|
|
|
52
56
|
const itemsJSX = useRenderItems(items);
|
|
53
57
|
const itemsPinTopJSX = useRenderItems(pinTop);
|
|
@@ -56,6 +60,18 @@ export const ListPrivate = forwardRef(
|
|
|
56
60
|
const emptyStates = useEmptyState({ noDataState, noResultsState, errorDataState });
|
|
57
61
|
const hasNoItems = items.length === 0;
|
|
58
62
|
|
|
63
|
+
const virtualizer = useVirtualizer({
|
|
64
|
+
count: itemsJSX.length,
|
|
65
|
+
getScrollElement: () => (scroll ? innerScrollRef.current : null),
|
|
66
|
+
estimateSize: () => ALL_SIZES[size],
|
|
67
|
+
enabled: virtualized,
|
|
68
|
+
});
|
|
69
|
+
const virtualItems = virtualizer.getVirtualItems();
|
|
70
|
+
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
virtualizer.measure();
|
|
73
|
+
}, [virtualizer]);
|
|
74
|
+
|
|
59
75
|
const loadingJSX = useMemo(
|
|
60
76
|
() =>
|
|
61
77
|
loading && (
|
|
@@ -76,7 +92,28 @@ export const ListPrivate = forwardRef(
|
|
|
76
92
|
const content = useMemo(
|
|
77
93
|
() => (
|
|
78
94
|
<div className={styles.content}>
|
|
79
|
-
{
|
|
95
|
+
{virtualized ? (
|
|
96
|
+
<div className={styles.virtualizedContainer} style={{ height: virtualizer.getTotalSize() }} tabIndex={-1}>
|
|
97
|
+
<div
|
|
98
|
+
className={styles.virtualizedPositionBox}
|
|
99
|
+
style={{ transform: `translateY(${virtualItems[0]?.start ?? 0}px)` }}
|
|
100
|
+
tabIndex={-1}
|
|
101
|
+
>
|
|
102
|
+
{virtualItems.map(virtualRow => (
|
|
103
|
+
<div
|
|
104
|
+
key={virtualRow.key}
|
|
105
|
+
data-index={virtualRow.index}
|
|
106
|
+
ref={virtualizer.measureElement}
|
|
107
|
+
tabIndex={-1}
|
|
108
|
+
>
|
|
109
|
+
{itemsJSX[virtualRow.index]}
|
|
110
|
+
</div>
|
|
111
|
+
))}
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
) : (
|
|
115
|
+
itemsJSX
|
|
116
|
+
)}
|
|
80
117
|
{loadingJSX}
|
|
81
118
|
|
|
82
119
|
<ListEmptyState
|
|
@@ -88,7 +125,19 @@ export const ListPrivate = forwardRef(
|
|
|
88
125
|
/>
|
|
89
126
|
</div>
|
|
90
127
|
),
|
|
91
|
-
[
|
|
128
|
+
[
|
|
129
|
+
dataError,
|
|
130
|
+
dataFiltered,
|
|
131
|
+
emptyStates,
|
|
132
|
+
hasNoItems,
|
|
133
|
+
itemsJSX,
|
|
134
|
+
loading,
|
|
135
|
+
loadingJSX,
|
|
136
|
+
search?.value,
|
|
137
|
+
virtualItems,
|
|
138
|
+
virtualized,
|
|
139
|
+
virtualizer,
|
|
140
|
+
],
|
|
92
141
|
);
|
|
93
142
|
|
|
94
143
|
const onScrollInitialized = () => {
|
|
@@ -140,7 +189,10 @@ export const ListPrivate = forwardRef(
|
|
|
140
189
|
)}
|
|
141
190
|
barHideStrategy='never'
|
|
142
191
|
size={size === 's' ? 's' : 'm'}
|
|
143
|
-
ref={
|
|
192
|
+
ref={ref => {
|
|
193
|
+
innerScrollRef.current = ref;
|
|
194
|
+
if (scrollContainerRef) (scrollContainerRef as React.MutableRefObject<HTMLElement | null>).current = ref;
|
|
195
|
+
}}
|
|
144
196
|
untouchableScrollbars={untouchableScrollbars}
|
|
145
197
|
onScroll={onScroll}
|
|
146
198
|
onInitialized={onScrollInitialized}
|
|
@@ -77,6 +77,11 @@ export type DroplistProps = {
|
|
|
77
77
|
*/
|
|
78
78
|
closeDroplistOnItemClick?: boolean;
|
|
79
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Включить виртуализацию на компоненты списка. Рекомендуется если у вас от 1к элементов списка
|
|
82
|
+
*/
|
|
83
|
+
virtualized?: boolean;
|
|
84
|
+
|
|
80
85
|
/** Триггер для дроплиста
|
|
81
86
|
* @type ReactNode | ({onKeyDown}) => ReactNode
|
|
82
87
|
*
|
|
@@ -90,6 +95,7 @@ export type ListPrivateProps = Omit<ListProps, 'pinTop' | 'pinBottom' | 'items'
|
|
|
90
95
|
nested?: boolean;
|
|
91
96
|
active?: boolean;
|
|
92
97
|
tabIndex?: number;
|
|
98
|
+
virtualized?: boolean;
|
|
93
99
|
onFocus?(e: FocusEvent<HTMLElement>): void;
|
|
94
100
|
onBlur?(e: FocusEvent<HTMLElement>): void;
|
|
95
101
|
onKeyDown?(e: KeyboardEvent<HTMLElement>): void;
|