@snack-uikit/list 0.29.6 → 0.29.7-preview-54496a2c.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.
|
@@ -20,6 +20,7 @@ exports.ListPrivate = void 0;
|
|
|
20
20
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
21
21
|
const react_virtual_1 = require("@tanstack/react-virtual");
|
|
22
22
|
const classnames_1 = __importDefault(require("classnames"));
|
|
23
|
+
const merge_refs_1 = __importDefault(require("merge-refs"));
|
|
23
24
|
const react_1 = require("react");
|
|
24
25
|
const loaders_1 = require("@snack-uikit/loaders");
|
|
25
26
|
const scroll_1 = require("@snack-uikit/scroll");
|
|
@@ -77,22 +78,79 @@ exports.ListPrivate = (0, react_1.forwardRef)((_a, ref) => {
|
|
|
77
78
|
const itemsJSX = (0, Items_1.useRenderItems)(items);
|
|
78
79
|
const itemsPinTopJSX = (0, Items_1.useRenderItems)(pinTop);
|
|
79
80
|
const itemsPinBottomJSX = (0, Items_1.useRenderItems)(pinBottom);
|
|
81
|
+
const lastScrolledItemIndex = (0, react_1.useRef)(null);
|
|
80
82
|
const emptyStates = (0, helperComponents_1.useEmptyState)({
|
|
81
83
|
noDataState,
|
|
82
84
|
noResultsState,
|
|
83
85
|
errorDataState
|
|
84
86
|
});
|
|
85
87
|
const hasNoItems = items.length === 0;
|
|
88
|
+
const {
|
|
89
|
+
selectedItemIndex,
|
|
90
|
+
selectedItem
|
|
91
|
+
} = (0, react_1.useMemo)(() => {
|
|
92
|
+
const result = {
|
|
93
|
+
selectedItemIndex: -1,
|
|
94
|
+
selectedItem: undefined
|
|
95
|
+
};
|
|
96
|
+
if (!scrollToSelectedItem || !value) {
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
const selectedItem = isSelectionSingle ? flattenItems[value] : flattenItems[value[0]];
|
|
100
|
+
if (!(selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.id)) {
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
const allFocusFlattenItems = Object.values(focusFlattenItems);
|
|
104
|
+
const index = allFocusFlattenItems.findIndex(item => item.originalId === selectedItem.id);
|
|
105
|
+
if (index < 0) {
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
selectedItemIndex: index,
|
|
110
|
+
selectedItem: allFocusFlattenItems[index]
|
|
111
|
+
};
|
|
112
|
+
}, [flattenItems, focusFlattenItems, isSelectionSingle, scrollToSelectedItem, value]);
|
|
86
113
|
const virtualizer = (0, react_virtual_1.useVirtualizer)({
|
|
87
114
|
count: itemsJSX.length,
|
|
88
115
|
getScrollElement: () => scroll ? innerScrollRef.current : null,
|
|
89
116
|
estimateSize: () => constants_1.ALL_SIZES[size],
|
|
90
|
-
enabled: virtualized
|
|
117
|
+
enabled: virtualized,
|
|
118
|
+
overscan: 5 // Amount of elements in DOM before/after visible ones
|
|
91
119
|
});
|
|
92
120
|
const virtualItems = virtualizer.getVirtualItems();
|
|
93
121
|
(0, react_1.useEffect)(() => {
|
|
94
122
|
virtualizer.measure();
|
|
95
123
|
}, [virtualizer]);
|
|
124
|
+
const isScrollToItemEnabled = scroll && scrollToSelectedItem && virtualized;
|
|
125
|
+
(0, react_1.useEffect)(() => {
|
|
126
|
+
var _a;
|
|
127
|
+
if (isScrollToItemEnabled) {
|
|
128
|
+
if (selectedItemIndex < 0 || !selectedItem) {
|
|
129
|
+
return; // Cannot scroll to non-existing item
|
|
130
|
+
}
|
|
131
|
+
if (lastScrolledItemIndex.current === selectedItemIndex) {
|
|
132
|
+
return; // No need to re-scroll to the same item during re-renders
|
|
133
|
+
}
|
|
134
|
+
if ((selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.itemRef) && ((_a = innerScrollRef.current) === null || _a === void 0 ? void 0 : _a.contains(selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.itemRef.current))) {
|
|
135
|
+
return; // No need to scroll to manually clicked item currently present in DOM
|
|
136
|
+
}
|
|
137
|
+
setTimeout(() => {
|
|
138
|
+
// Scroll to item by virtualizer method => required item will appear in DOM, but not perfectly centered
|
|
139
|
+
virtualizer.scrollToIndex(selectedItemIndex, {
|
|
140
|
+
align: 'center'
|
|
141
|
+
});
|
|
142
|
+
setTimeout(() => {
|
|
143
|
+
var _a, _b;
|
|
144
|
+
// Scroll to item already present in DOM by browser method – fixes imperfect virtualizer centering.
|
|
145
|
+
// We need perfect centering to avoid desired item from being overlapped by pinned groups (top or bottom)
|
|
146
|
+
(_b = (_a = selectedItem.itemRef) === null || _a === void 0 ? void 0 : _a.current) === null || _b === void 0 ? void 0 : _b.scrollIntoView({
|
|
147
|
+
block: 'center'
|
|
148
|
+
});
|
|
149
|
+
lastScrolledItemIndex.current = selectedItemIndex;
|
|
150
|
+
}, 0);
|
|
151
|
+
}, 0);
|
|
152
|
+
}
|
|
153
|
+
}, [isScrollToItemEnabled, selectedItem, selectedItemIndex, virtualizer]);
|
|
96
154
|
const loadingJSX = (0, react_1.useMemo)(() => loading && (0, jsx_runtime_1.jsx)("div", {
|
|
97
155
|
role: 'spinbutton',
|
|
98
156
|
tabIndex: -1,
|
|
@@ -104,55 +162,41 @@ exports.ListPrivate = (0, react_1.forwardRef)((_a, ref) => {
|
|
|
104
162
|
size: size === 'l' ? 's' : 'xs'
|
|
105
163
|
})
|
|
106
164
|
}), [hasNoItems, loading, size]);
|
|
107
|
-
const content = (0, react_1.useMemo)(() => {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
165
|
+
const content = (0, react_1.useMemo)(() => (0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, {
|
|
166
|
+
children: [virtualized ? (0, jsx_runtime_1.jsx)("div", {
|
|
167
|
+
className: styles_module_scss_2.default.virtualizedContainer,
|
|
168
|
+
style: {
|
|
169
|
+
height: virtualizer.getTotalSize()
|
|
170
|
+
},
|
|
171
|
+
tabIndex: -1,
|
|
172
|
+
children: virtualItems.map(virtualRow => (0, jsx_runtime_1.jsx)("div", {
|
|
173
|
+
"data-index": virtualRow.index,
|
|
174
|
+
ref: virtualizer.measureElement,
|
|
175
|
+
tabIndex: -1,
|
|
176
|
+
className: styles_module_scss_2.default.virtualizedPositionBox,
|
|
113
177
|
style: {
|
|
114
|
-
|
|
178
|
+
transform: `translateY(${virtualRow.start}px)`
|
|
115
179
|
},
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}, virtualRow.key))
|
|
129
|
-
})
|
|
130
|
-
}) : itemsJSX, loadingJSX, (0, jsx_runtime_1.jsx)(helperComponents_1.ListEmptyState, {
|
|
131
|
-
loading: loading,
|
|
132
|
-
dataError: dataError,
|
|
133
|
-
emptyStates: emptyStates,
|
|
134
|
-
hasNoItems: hasNoItems,
|
|
135
|
-
dataFiltered: dataFiltered !== null && dataFiltered !== void 0 ? dataFiltered : Boolean(search === null || search === void 0 ? void 0 : search.value),
|
|
136
|
-
size: size
|
|
137
|
-
})]
|
|
138
|
-
});
|
|
139
|
-
}, [dataError, dataFiltered, emptyStates, hasNoItems, itemsJSX, loading, loadingJSX, search === null || search === void 0 ? void 0 : search.value, size, virtualItems, virtualized, virtualizer]);
|
|
140
|
-
const onScrollInitialized = () => {
|
|
180
|
+
children: itemsJSX[virtualRow.index]
|
|
181
|
+
}, virtualRow.key))
|
|
182
|
+
}) : itemsJSX, loadingJSX, (0, jsx_runtime_1.jsx)(helperComponents_1.ListEmptyState, {
|
|
183
|
+
loading: loading,
|
|
184
|
+
dataError: dataError,
|
|
185
|
+
emptyStates: emptyStates,
|
|
186
|
+
hasNoItems: hasNoItems,
|
|
187
|
+
dataFiltered: dataFiltered !== null && dataFiltered !== void 0 ? dataFiltered : Boolean(search === null || search === void 0 ? void 0 : search.value),
|
|
188
|
+
size: size
|
|
189
|
+
})]
|
|
190
|
+
}), [dataError, dataFiltered, emptyStates, hasNoItems, itemsJSX, loading, loadingJSX, search === null || search === void 0 ? void 0 : search.value, size, virtualItems, virtualized, virtualizer]);
|
|
191
|
+
const onScrollInitialized = (0, react_1.useCallback)(() => {
|
|
141
192
|
var _a, _b;
|
|
142
|
-
if (
|
|
143
|
-
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
const selectedItem = isSelectionSingle ? flattenItems[value] : flattenItems[value[0]];
|
|
147
|
-
if (!(selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.id)) {
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
const itemToScrollTo = Object.values(focusFlattenItems).find(item => item.originalId === selectedItem.id);
|
|
151
|
-
(_b = (_a = itemToScrollTo === null || itemToScrollTo === void 0 ? void 0 : itemToScrollTo.itemRef) === null || _a === void 0 ? void 0 : _a.current) === null || _b === void 0 ? void 0 : _b.scrollIntoView({
|
|
152
|
-
block: 'center'
|
|
153
|
-
});
|
|
193
|
+
if (!selectedItem) {
|
|
194
|
+
return;
|
|
154
195
|
}
|
|
155
|
-
|
|
196
|
+
(_b = (_a = selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.itemRef) === null || _a === void 0 ? void 0 : _a.current) === null || _b === void 0 ? void 0 : _b.scrollIntoView({
|
|
197
|
+
block: 'center'
|
|
198
|
+
});
|
|
199
|
+
}, [selectedItem]);
|
|
156
200
|
const listJSX = (0, jsx_runtime_1.jsxs)("ul", Object.assign({
|
|
157
201
|
className: (0, classnames_1.default)(styles_module_scss_1.default.listContainer, className),
|
|
158
202
|
ref: ref,
|
|
@@ -175,10 +219,7 @@ exports.ListPrivate = (0, react_1.forwardRef)((_a, ref) => {
|
|
|
175
219
|
}, scrollContainerClassName),
|
|
176
220
|
barHideStrategy: barHideStrategy,
|
|
177
221
|
size: size === 's' ? 's' : 'm',
|
|
178
|
-
ref:
|
|
179
|
-
innerScrollRef.current = ref;
|
|
180
|
-
if (scrollContainerRef) scrollContainerRef.current = ref;
|
|
181
|
-
},
|
|
222
|
+
ref: (0, merge_refs_1.default)(innerScrollRef, scrollContainerRef),
|
|
182
223
|
untouchableScrollbars: untouchableScrollbars,
|
|
183
224
|
onScroll: onScroll,
|
|
184
225
|
onInitialized: onScrollInitialized,
|
|
@@ -9,10 +9,11 @@ var __rest = (this && this.__rest) || function (s, e) {
|
|
|
9
9
|
}
|
|
10
10
|
return t;
|
|
11
11
|
};
|
|
12
|
-
import { jsx as _jsx,
|
|
12
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
13
13
|
import { useVirtualizer } from '@tanstack/react-virtual';
|
|
14
14
|
import cn from 'classnames';
|
|
15
|
-
import
|
|
15
|
+
import mergeRefs from 'merge-refs';
|
|
16
|
+
import { forwardRef, useCallback, useEffect, useMemo, useRef } from 'react';
|
|
16
17
|
import { Spinner } from '@snack-uikit/loaders';
|
|
17
18
|
import { Scroll } from '@snack-uikit/scroll';
|
|
18
19
|
import { extractSupportProps } from '@snack-uikit/utils';
|
|
@@ -31,23 +32,72 @@ export const ListPrivate = forwardRef((_a, ref) => {
|
|
|
31
32
|
const itemsJSX = useRenderItems(items);
|
|
32
33
|
const itemsPinTopJSX = useRenderItems(pinTop);
|
|
33
34
|
const itemsPinBottomJSX = useRenderItems(pinBottom);
|
|
35
|
+
const lastScrolledItemIndex = useRef(null);
|
|
34
36
|
const emptyStates = useEmptyState({ noDataState, noResultsState, errorDataState });
|
|
35
37
|
const hasNoItems = items.length === 0;
|
|
38
|
+
const { selectedItemIndex, selectedItem } = useMemo(() => {
|
|
39
|
+
const result = {
|
|
40
|
+
selectedItemIndex: -1,
|
|
41
|
+
selectedItem: undefined,
|
|
42
|
+
};
|
|
43
|
+
if (!scrollToSelectedItem || !value) {
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
const selectedItem = isSelectionSingle ? flattenItems[value] : flattenItems[value[0]];
|
|
47
|
+
if (!(selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.id)) {
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
const allFocusFlattenItems = Object.values(focusFlattenItems);
|
|
51
|
+
const index = allFocusFlattenItems.findIndex(item => item.originalId === selectedItem.id);
|
|
52
|
+
if (index < 0) {
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
selectedItemIndex: index,
|
|
57
|
+
selectedItem: allFocusFlattenItems[index],
|
|
58
|
+
};
|
|
59
|
+
}, [flattenItems, focusFlattenItems, isSelectionSingle, scrollToSelectedItem, value]);
|
|
36
60
|
const virtualizer = useVirtualizer({
|
|
37
61
|
count: itemsJSX.length,
|
|
38
62
|
getScrollElement: () => (scroll ? innerScrollRef.current : null),
|
|
39
63
|
estimateSize: () => ALL_SIZES[size],
|
|
40
64
|
enabled: virtualized,
|
|
65
|
+
overscan: 5, // Amount of elements in DOM before/after visible ones
|
|
41
66
|
});
|
|
42
67
|
const virtualItems = virtualizer.getVirtualItems();
|
|
43
68
|
useEffect(() => {
|
|
44
69
|
virtualizer.measure();
|
|
45
70
|
}, [virtualizer]);
|
|
71
|
+
const isScrollToItemEnabled = scroll && scrollToSelectedItem && virtualized;
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
var _a;
|
|
74
|
+
if (isScrollToItemEnabled) {
|
|
75
|
+
if (selectedItemIndex < 0 || !selectedItem) {
|
|
76
|
+
return; // Cannot scroll to non-existing item
|
|
77
|
+
}
|
|
78
|
+
if (lastScrolledItemIndex.current === selectedItemIndex) {
|
|
79
|
+
return; // No need to re-scroll to the same item during re-renders
|
|
80
|
+
}
|
|
81
|
+
if ((selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.itemRef) && ((_a = innerScrollRef.current) === null || _a === void 0 ? void 0 : _a.contains(selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.itemRef.current))) {
|
|
82
|
+
return; // No need to scroll to manually clicked item currently present in DOM
|
|
83
|
+
}
|
|
84
|
+
setTimeout(() => {
|
|
85
|
+
// Scroll to item by virtualizer method => required item will appear in DOM, but not perfectly centered
|
|
86
|
+
virtualizer.scrollToIndex(selectedItemIndex, { align: 'center' });
|
|
87
|
+
setTimeout(() => {
|
|
88
|
+
var _a, _b;
|
|
89
|
+
// Scroll to item already present in DOM by browser method – fixes imperfect virtualizer centering.
|
|
90
|
+
// We need perfect centering to avoid desired item from being overlapped by pinned groups (top or bottom)
|
|
91
|
+
(_b = (_a = selectedItem.itemRef) === null || _a === void 0 ? void 0 : _a.current) === null || _b === void 0 ? void 0 : _b.scrollIntoView({ block: 'center' });
|
|
92
|
+
lastScrolledItemIndex.current = selectedItemIndex;
|
|
93
|
+
}, 0);
|
|
94
|
+
}, 0);
|
|
95
|
+
}
|
|
96
|
+
}, [isScrollToItemEnabled, selectedItem, selectedItemIndex, virtualizer]);
|
|
46
97
|
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]);
|
|
47
|
-
const content = useMemo(() => {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}, [
|
|
98
|
+
const content = useMemo(() => (_jsxs(_Fragment, { children: [virtualized ? (_jsx("div", { className: styles.virtualizedContainer, style: { height: virtualizer.getTotalSize() }, tabIndex: -1, children: virtualItems.map(virtualRow => (_jsx("div", { "data-index": virtualRow.index, ref: virtualizer.measureElement, tabIndex: -1, className: styles.virtualizedPositionBox, style: {
|
|
99
|
+
transform: `translateY(${virtualRow.start}px)`,
|
|
100
|
+
}, 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), size: size })] })), [
|
|
51
101
|
dataError,
|
|
52
102
|
dataFiltered,
|
|
53
103
|
emptyStates,
|
|
@@ -61,29 +111,18 @@ export const ListPrivate = forwardRef((_a, ref) => {
|
|
|
61
111
|
virtualized,
|
|
62
112
|
virtualizer,
|
|
63
113
|
]);
|
|
64
|
-
const onScrollInitialized = () => {
|
|
114
|
+
const onScrollInitialized = useCallback(() => {
|
|
65
115
|
var _a, _b;
|
|
66
|
-
if (
|
|
67
|
-
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
const selectedItem = isSelectionSingle ? flattenItems[value] : flattenItems[value[0]];
|
|
71
|
-
if (!(selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.id)) {
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
const itemToScrollTo = Object.values(focusFlattenItems).find(item => item.originalId === selectedItem.id);
|
|
75
|
-
(_b = (_a = itemToScrollTo === null || itemToScrollTo === void 0 ? void 0 : itemToScrollTo.itemRef) === null || _a === void 0 ? void 0 : _a.current) === null || _b === void 0 ? void 0 : _b.scrollIntoView({ block: 'center' });
|
|
116
|
+
if (!selectedItem) {
|
|
117
|
+
return;
|
|
76
118
|
}
|
|
77
|
-
|
|
119
|
+
(_b = (_a = selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.itemRef) === null || _a === void 0 ? void 0 : _a.current) === null || _b === void 0 ? void 0 : _b.scrollIntoView({ block: 'center' });
|
|
120
|
+
}, [selectedItem]);
|
|
78
121
|
const listJSX = (_jsxs("ul", Object.assign({ className: cn(commonStyles.listContainer, className), ref: ref, onKeyDown: onKeyDown, tabIndex: tabIndex, onFocus: onFocus, onBlur: onBlur, "data-active": active || undefined, role: 'menu' }, extractSupportProps(props), { children: [(Number(pinTop === null || pinTop === void 0 ? void 0 : pinTop.length) > 0 || search) && (_jsxs(PinTopGroupItem, { children: [search && _jsx(SearchItem, Object.assign({ search: search }, searchItem)), Number(pinTop === null || pinTop === void 0 ? void 0 : pinTop.length) > 0 && itemsPinTopJSX] })), scroll ? (_jsxs(Scroll, { className: cn({
|
|
79
122
|
[commonStyles.scrollContainerS]: scroll && limitedScrollHeight && size === 's',
|
|
80
123
|
[commonStyles.scrollContainerM]: scroll && limitedScrollHeight && size === 'm',
|
|
81
124
|
[commonStyles.scrollContainerL]: scroll && limitedScrollHeight && size === 'l',
|
|
82
|
-
}, scrollContainerClassName), barHideStrategy: barHideStrategy, size: size === 's' ? 's' : 'm', ref: ref
|
|
83
|
-
innerScrollRef.current = ref;
|
|
84
|
-
if (scrollContainerRef)
|
|
85
|
-
scrollContainerRef.current = ref;
|
|
86
|
-
}, 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: stopPropagation, children: footer }))] })));
|
|
125
|
+
}, scrollContainerClassName), barHideStrategy: barHideStrategy, size: size === 's' ? 's' : 'm', ref: mergeRefs(innerScrollRef, scrollContainerRef), 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: stopPropagation, children: footer }))] })));
|
|
87
126
|
if (!nested) {
|
|
88
127
|
return listJSX;
|
|
89
128
|
}
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
6
|
"title": "List",
|
|
7
|
-
"version": "0.29.
|
|
7
|
+
"version": "0.29.7-preview-54496a2c.0",
|
|
8
8
|
"sideEffects": [
|
|
9
9
|
"*.css",
|
|
10
10
|
"*.woff",
|
|
@@ -54,5 +54,5 @@
|
|
|
54
54
|
"peerDependencies": {
|
|
55
55
|
"@snack-uikit/locale": "*"
|
|
56
56
|
},
|
|
57
|
-
"gitHead": "
|
|
57
|
+
"gitHead": "456b961bf9efadad58676e3568e48529b85c7bcf"
|
|
58
58
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useVirtualizer } from '@tanstack/react-virtual';
|
|
2
2
|
import cn from 'classnames';
|
|
3
|
-
import
|
|
3
|
+
import mergeRefs from 'merge-refs';
|
|
4
|
+
import { ForwardedRef, forwardRef, RefObject, useCallback, useEffect, useMemo, useRef } from 'react';
|
|
4
5
|
|
|
5
6
|
import { Spinner } from '@snack-uikit/loaders';
|
|
6
7
|
import { Scroll } from '@snack-uikit/scroll';
|
|
@@ -59,14 +60,44 @@ export const ListPrivate = forwardRef(
|
|
|
59
60
|
const itemsPinTopJSX = useRenderItems(pinTop);
|
|
60
61
|
const itemsPinBottomJSX = useRenderItems(pinBottom);
|
|
61
62
|
|
|
63
|
+
const lastScrolledItemIndex = useRef<number | null>(null);
|
|
64
|
+
|
|
62
65
|
const emptyStates = useEmptyState({ noDataState, noResultsState, errorDataState });
|
|
63
66
|
const hasNoItems = items.length === 0;
|
|
64
67
|
|
|
68
|
+
const { selectedItemIndex, selectedItem } = useMemo(() => {
|
|
69
|
+
const result = {
|
|
70
|
+
selectedItemIndex: -1,
|
|
71
|
+
selectedItem: undefined,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
if (!scrollToSelectedItem || !value) {
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const selectedItem = isSelectionSingle ? flattenItems[value] : flattenItems[value[0]];
|
|
79
|
+
if (!selectedItem?.id) {
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const allFocusFlattenItems = Object.values(focusFlattenItems);
|
|
84
|
+
const index = allFocusFlattenItems.findIndex(item => item.originalId === selectedItem.id);
|
|
85
|
+
if (index < 0) {
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
selectedItemIndex: index,
|
|
91
|
+
selectedItem: allFocusFlattenItems[index],
|
|
92
|
+
};
|
|
93
|
+
}, [flattenItems, focusFlattenItems, isSelectionSingle, scrollToSelectedItem, value]);
|
|
94
|
+
|
|
65
95
|
const virtualizer = useVirtualizer({
|
|
66
96
|
count: itemsJSX.length,
|
|
67
97
|
getScrollElement: () => (scroll ? innerScrollRef.current : null),
|
|
68
98
|
estimateSize: () => ALL_SIZES[size],
|
|
69
99
|
enabled: virtualized,
|
|
100
|
+
overscan: 5, // Amount of elements in DOM before/after visible ones
|
|
70
101
|
});
|
|
71
102
|
const virtualItems = virtualizer.getVirtualItems();
|
|
72
103
|
|
|
@@ -74,6 +105,34 @@ export const ListPrivate = forwardRef(
|
|
|
74
105
|
virtualizer.measure();
|
|
75
106
|
}, [virtualizer]);
|
|
76
107
|
|
|
108
|
+
const isScrollToItemEnabled = scroll && scrollToSelectedItem && virtualized;
|
|
109
|
+
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
if (isScrollToItemEnabled) {
|
|
112
|
+
if (selectedItemIndex < 0 || !selectedItem) {
|
|
113
|
+
return; // Cannot scroll to non-existing item
|
|
114
|
+
}
|
|
115
|
+
if (lastScrolledItemIndex.current === selectedItemIndex) {
|
|
116
|
+
return; // No need to re-scroll to the same item during re-renders
|
|
117
|
+
}
|
|
118
|
+
if (selectedItem?.itemRef && innerScrollRef.current?.contains(selectedItem?.itemRef.current)) {
|
|
119
|
+
return; // No need to scroll to manually clicked item currently present in DOM
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
setTimeout(() => {
|
|
123
|
+
// Scroll to item by virtualizer method => required item will appear in DOM, but not perfectly centered
|
|
124
|
+
virtualizer.scrollToIndex(selectedItemIndex, { align: 'center' });
|
|
125
|
+
|
|
126
|
+
setTimeout(() => {
|
|
127
|
+
// Scroll to item already present in DOM by browser method – fixes imperfect virtualizer centering.
|
|
128
|
+
// We need perfect centering to avoid desired item from being overlapped by pinned groups (top or bottom)
|
|
129
|
+
selectedItem.itemRef?.current?.scrollIntoView({ block: 'center' });
|
|
130
|
+
lastScrolledItemIndex.current = selectedItemIndex;
|
|
131
|
+
}, 0);
|
|
132
|
+
}, 0);
|
|
133
|
+
}
|
|
134
|
+
}, [isScrollToItemEnabled, selectedItem, selectedItemIndex, virtualizer]);
|
|
135
|
+
|
|
77
136
|
const loadingJSX = useMemo(
|
|
78
137
|
() =>
|
|
79
138
|
loading && (
|
|
@@ -93,25 +152,23 @@ export const ListPrivate = forwardRef(
|
|
|
93
152
|
|
|
94
153
|
const content = useMemo(
|
|
95
154
|
() => (
|
|
96
|
-
|
|
155
|
+
<>
|
|
97
156
|
{virtualized ? (
|
|
98
157
|
<div className={styles.virtualizedContainer} style={{ height: virtualizer.getTotalSize() }} tabIndex={-1}>
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
))}
|
|
114
|
-
</div>
|
|
158
|
+
{virtualItems.map(virtualRow => (
|
|
159
|
+
<div
|
|
160
|
+
key={virtualRow.key}
|
|
161
|
+
data-index={virtualRow.index}
|
|
162
|
+
ref={virtualizer.measureElement}
|
|
163
|
+
tabIndex={-1}
|
|
164
|
+
className={styles.virtualizedPositionBox}
|
|
165
|
+
style={{
|
|
166
|
+
transform: `translateY(${virtualRow.start}px)`,
|
|
167
|
+
}}
|
|
168
|
+
>
|
|
169
|
+
{itemsJSX[virtualRow.index]}
|
|
170
|
+
</div>
|
|
171
|
+
))}
|
|
115
172
|
</div>
|
|
116
173
|
) : (
|
|
117
174
|
itemsJSX
|
|
@@ -126,7 +183,7 @@ export const ListPrivate = forwardRef(
|
|
|
126
183
|
dataFiltered={dataFiltered ?? Boolean(search?.value)}
|
|
127
184
|
size={size}
|
|
128
185
|
/>
|
|
129
|
-
|
|
186
|
+
</>
|
|
130
187
|
),
|
|
131
188
|
[
|
|
132
189
|
dataError,
|
|
@@ -144,22 +201,13 @@ export const ListPrivate = forwardRef(
|
|
|
144
201
|
],
|
|
145
202
|
);
|
|
146
203
|
|
|
147
|
-
const onScrollInitialized = () => {
|
|
148
|
-
if (
|
|
149
|
-
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const selectedItem = isSelectionSingle ? flattenItems[value] : flattenItems[value[0]];
|
|
154
|
-
|
|
155
|
-
if (!selectedItem?.id) {
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const itemToScrollTo = Object.values(focusFlattenItems).find(item => item.originalId === selectedItem.id);
|
|
160
|
-
itemToScrollTo?.itemRef?.current?.scrollIntoView({ block: 'center' });
|
|
204
|
+
const onScrollInitialized = useCallback(() => {
|
|
205
|
+
if (!selectedItem) {
|
|
206
|
+
return;
|
|
161
207
|
}
|
|
162
|
-
|
|
208
|
+
|
|
209
|
+
selectedItem?.itemRef?.current?.scrollIntoView({ block: 'center' });
|
|
210
|
+
}, [selectedItem]);
|
|
163
211
|
|
|
164
212
|
const listJSX = (
|
|
165
213
|
<ul
|
|
@@ -193,10 +241,7 @@ export const ListPrivate = forwardRef(
|
|
|
193
241
|
)}
|
|
194
242
|
barHideStrategy={barHideStrategy}
|
|
195
243
|
size={size === 's' ? 's' : 'm'}
|
|
196
|
-
ref={
|
|
197
|
-
innerScrollRef.current = ref;
|
|
198
|
-
if (scrollContainerRef) (scrollContainerRef as React.MutableRefObject<HTMLElement | null>).current = ref;
|
|
199
|
-
}}
|
|
244
|
+
ref={mergeRefs(innerScrollRef, scrollContainerRef)}
|
|
200
245
|
untouchableScrollbars={untouchableScrollbars}
|
|
201
246
|
onScroll={onScroll}
|
|
202
247
|
onInitialized={onScrollInitialized}
|