@snack-uikit/list 0.29.6 → 0.29.7-preview-c824da44.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");
|
|
@@ -32,6 +33,7 @@ const styles_module_scss_1 = __importDefault(require('../styles.module.css'));
|
|
|
32
33
|
const constants_1 = require("./constants");
|
|
33
34
|
const styles_module_scss_2 = __importDefault(require('./styles.module.css'));
|
|
34
35
|
exports.ListPrivate = (0, react_1.forwardRef)((_a, ref) => {
|
|
36
|
+
var _b;
|
|
35
37
|
var {
|
|
36
38
|
items,
|
|
37
39
|
pinTop,
|
|
@@ -77,22 +79,110 @@ exports.ListPrivate = (0, react_1.forwardRef)((_a, ref) => {
|
|
|
77
79
|
const itemsJSX = (0, Items_1.useRenderItems)(items);
|
|
78
80
|
const itemsPinTopJSX = (0, Items_1.useRenderItems)(pinTop);
|
|
79
81
|
const itemsPinBottomJSX = (0, Items_1.useRenderItems)(pinBottom);
|
|
82
|
+
const [scrollState, setScrollState] = (0, react_1.useState)({
|
|
83
|
+
virtualizer: null,
|
|
84
|
+
browser: null,
|
|
85
|
+
measured: false
|
|
86
|
+
});
|
|
87
|
+
console.info('[DEBUG] ScrollState', scrollState);
|
|
80
88
|
const emptyStates = (0, helperComponents_1.useEmptyState)({
|
|
81
89
|
noDataState,
|
|
82
90
|
noResultsState,
|
|
83
91
|
errorDataState
|
|
84
92
|
});
|
|
85
93
|
const hasNoItems = items.length === 0;
|
|
94
|
+
const {
|
|
95
|
+
selectedItemIndex,
|
|
96
|
+
selectedItem
|
|
97
|
+
} = (0, react_1.useMemo)(() => {
|
|
98
|
+
const result = {
|
|
99
|
+
selectedItemIndex: -1,
|
|
100
|
+
selectedItem: undefined
|
|
101
|
+
};
|
|
102
|
+
if (!scrollToSelectedItem || !value) {
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
const selectedItem = isSelectionSingle ? flattenItems[value] : flattenItems[value[0]];
|
|
106
|
+
if (!(selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.id)) {
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
const allFocusFlattenItems = Object.values(focusFlattenItems);
|
|
110
|
+
const index = allFocusFlattenItems.findIndex(item => item.originalId === selectedItem.id);
|
|
111
|
+
if (index < 0) {
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
selectedItemIndex: index,
|
|
116
|
+
selectedItem: allFocusFlattenItems[index]
|
|
117
|
+
};
|
|
118
|
+
}, [flattenItems, focusFlattenItems, isSelectionSingle, scrollToSelectedItem, value]);
|
|
86
119
|
const virtualizer = (0, react_virtual_1.useVirtualizer)({
|
|
87
120
|
count: itemsJSX.length,
|
|
88
121
|
getScrollElement: () => scroll ? innerScrollRef.current : null,
|
|
89
122
|
estimateSize: () => constants_1.ALL_SIZES[size],
|
|
90
|
-
enabled: virtualized
|
|
123
|
+
enabled: virtualized,
|
|
124
|
+
overscan: 5 // Amount of elements in DOM before/after visible ones
|
|
91
125
|
});
|
|
92
126
|
const virtualItems = virtualizer.getVirtualItems();
|
|
93
127
|
(0, react_1.useEffect)(() => {
|
|
94
|
-
|
|
95
|
-
|
|
128
|
+
if (scrollState.measured) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
virtualizer.measure(); // TODO: перезамерять размер
|
|
132
|
+
setScrollState(prevState => Object.assign(Object.assign({}, prevState), {
|
|
133
|
+
measured: true
|
|
134
|
+
}));
|
|
135
|
+
}, [scrollState.measured, virtualizer]);
|
|
136
|
+
const isScrollToItemEnabled = scroll && scrollToSelectedItem && virtualized;
|
|
137
|
+
(0, react_1.useEffect)(() => {
|
|
138
|
+
var _a;
|
|
139
|
+
if (isScrollToItemEnabled) {
|
|
140
|
+
console.info('[DEBUG] selectedItemIndex', selectedItemIndex);
|
|
141
|
+
if (!scrollState.measured) {
|
|
142
|
+
console.info('[DEBUG] Not measured yet');
|
|
143
|
+
return; // Not measured yet
|
|
144
|
+
}
|
|
145
|
+
if (selectedItemIndex < 0 || !selectedItem) {
|
|
146
|
+
console.info('[DEBUG] Cannot scroll to non-existing item');
|
|
147
|
+
return; // Cannot scroll to non-existing item
|
|
148
|
+
}
|
|
149
|
+
if (scrollState.virtualizer === selectedItemIndex) {
|
|
150
|
+
console.info('[DEBUG] No need to re-scroll to the same item during re-renders');
|
|
151
|
+
return; // No need to re-scroll to the same item during re-renders
|
|
152
|
+
}
|
|
153
|
+
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))) {
|
|
154
|
+
console.info('[DEBUG] No need to scroll to manually clicked item currently present in DOM');
|
|
155
|
+
return; // No need to scroll to manually clicked item currently present in DOM
|
|
156
|
+
}
|
|
157
|
+
console.info('[DEBUG] SCROLLING BY VIRTUALIZER');
|
|
158
|
+
virtualizer.scrollToIndex(selectedItemIndex, {
|
|
159
|
+
align: 'center'
|
|
160
|
+
});
|
|
161
|
+
setScrollState(prevState => Object.assign(Object.assign({}, prevState), {
|
|
162
|
+
virtualizer: selectedItemIndex
|
|
163
|
+
}));
|
|
164
|
+
}
|
|
165
|
+
}, [isScrollToItemEnabled, scrollState, selectedItem, selectedItemIndex, virtualizer]);
|
|
166
|
+
const isTargetPresentInDom = Boolean((_b = selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.itemRef) === null || _b === void 0 ? void 0 : _b.current);
|
|
167
|
+
(0, react_1.useEffect)(() => {
|
|
168
|
+
var _a, _b, _c;
|
|
169
|
+
if (scrollState.virtualizer === null) {
|
|
170
|
+
return; // Not scrolled by virtualizer yet, no need for additional scroll
|
|
171
|
+
}
|
|
172
|
+
if (!isTargetPresentInDom) {
|
|
173
|
+
return; // Target element is not present in DOM yet, additional scroll does not work without it
|
|
174
|
+
}
|
|
175
|
+
if (scrollState.virtualizer === scrollState.browser) {
|
|
176
|
+
return; // Virtualizer scroll has not been executed => no need for additional scroll
|
|
177
|
+
}
|
|
178
|
+
console.info('[DEBUG] SCROLLING BY BROWSER', (_a = selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.itemRef) === null || _a === void 0 ? void 0 : _a.current);
|
|
179
|
+
(_c = (_b = selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.itemRef) === null || _b === void 0 ? void 0 : _b.current) === null || _c === void 0 ? void 0 : _c.scrollIntoView({
|
|
180
|
+
block: 'center'
|
|
181
|
+
});
|
|
182
|
+
setScrollState(prevState => Object.assign(Object.assign({}, prevState), {
|
|
183
|
+
browser: selectedItemIndex
|
|
184
|
+
}));
|
|
185
|
+
}, [scrollState, selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.itemRef, isTargetPresentInDom, selectedItemIndex]);
|
|
96
186
|
const loadingJSX = (0, react_1.useMemo)(() => loading && (0, jsx_runtime_1.jsx)("div", {
|
|
97
187
|
role: 'spinbutton',
|
|
98
188
|
tabIndex: -1,
|
|
@@ -104,55 +194,41 @@ exports.ListPrivate = (0, react_1.forwardRef)((_a, ref) => {
|
|
|
104
194
|
size: size === 'l' ? 's' : 'xs'
|
|
105
195
|
})
|
|
106
196
|
}), [hasNoItems, loading, size]);
|
|
107
|
-
const content = (0, react_1.useMemo)(() => {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
197
|
+
const content = (0, react_1.useMemo)(() => (0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, {
|
|
198
|
+
children: [virtualized ? (0, jsx_runtime_1.jsx)("div", {
|
|
199
|
+
className: styles_module_scss_2.default.virtualizedContainer,
|
|
200
|
+
style: {
|
|
201
|
+
height: virtualizer.getTotalSize()
|
|
202
|
+
},
|
|
203
|
+
tabIndex: -1,
|
|
204
|
+
children: virtualItems.map(virtualRow => (0, jsx_runtime_1.jsx)("div", {
|
|
205
|
+
"data-index": virtualRow.index,
|
|
206
|
+
ref: virtualizer.measureElement,
|
|
207
|
+
tabIndex: -1,
|
|
208
|
+
className: styles_module_scss_2.default.virtualizedPositionBox,
|
|
113
209
|
style: {
|
|
114
|
-
|
|
210
|
+
transform: `translateY(${virtualRow.start}px)`
|
|
115
211
|
},
|
|
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 = () => {
|
|
212
|
+
children: itemsJSX[virtualRow.index]
|
|
213
|
+
}, virtualRow.key))
|
|
214
|
+
}) : itemsJSX, loadingJSX, (0, jsx_runtime_1.jsx)(helperComponents_1.ListEmptyState, {
|
|
215
|
+
loading: loading,
|
|
216
|
+
dataError: dataError,
|
|
217
|
+
emptyStates: emptyStates,
|
|
218
|
+
hasNoItems: hasNoItems,
|
|
219
|
+
dataFiltered: dataFiltered !== null && dataFiltered !== void 0 ? dataFiltered : Boolean(search === null || search === void 0 ? void 0 : search.value),
|
|
220
|
+
size: size
|
|
221
|
+
})]
|
|
222
|
+
}), [dataError, dataFiltered, emptyStates, hasNoItems, itemsJSX, loading, loadingJSX, search === null || search === void 0 ? void 0 : search.value, size, virtualItems, virtualized, virtualizer]);
|
|
223
|
+
const onScrollInitialized = (0, react_1.useCallback)(() => {
|
|
141
224
|
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
|
-
});
|
|
225
|
+
if (!selectedItem) {
|
|
226
|
+
return;
|
|
154
227
|
}
|
|
155
|
-
|
|
228
|
+
(_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({
|
|
229
|
+
block: 'center'
|
|
230
|
+
});
|
|
231
|
+
}, [selectedItem]);
|
|
156
232
|
const listJSX = (0, jsx_runtime_1.jsxs)("ul", Object.assign({
|
|
157
233
|
className: (0, classnames_1.default)(styles_module_scss_1.default.listContainer, className),
|
|
158
234
|
ref: ref,
|
|
@@ -175,10 +251,7 @@ exports.ListPrivate = (0, react_1.forwardRef)((_a, ref) => {
|
|
|
175
251
|
}, scrollContainerClassName),
|
|
176
252
|
barHideStrategy: barHideStrategy,
|
|
177
253
|
size: size === 's' ? 's' : 'm',
|
|
178
|
-
ref:
|
|
179
|
-
innerScrollRef.current = ref;
|
|
180
|
-
if (scrollContainerRef) scrollContainerRef.current = ref;
|
|
181
|
-
},
|
|
254
|
+
ref: (0, merge_refs_1.default)(innerScrollRef, scrollContainerRef),
|
|
182
255
|
untouchableScrollbars: untouchableScrollbars,
|
|
183
256
|
onScroll: onScroll,
|
|
184
257
|
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, useState } 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';
|
|
@@ -24,6 +25,7 @@ import commonStyles from '../styles.module.css';
|
|
|
24
25
|
import { ALL_SIZES } from './constants';
|
|
25
26
|
import styles from './styles.module.css';
|
|
26
27
|
export const ListPrivate = forwardRef((_a, ref) => {
|
|
28
|
+
var _b;
|
|
27
29
|
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, barHideStrategy = 'never' } = _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", "barHideStrategy"]);
|
|
28
30
|
const { size = 's', flattenItems, focusFlattenItems } = useNewListContext();
|
|
29
31
|
const { value, isSelectionSingle } = useSelectionContext();
|
|
@@ -31,23 +33,93 @@ export const ListPrivate = forwardRef((_a, ref) => {
|
|
|
31
33
|
const itemsJSX = useRenderItems(items);
|
|
32
34
|
const itemsPinTopJSX = useRenderItems(pinTop);
|
|
33
35
|
const itemsPinBottomJSX = useRenderItems(pinBottom);
|
|
36
|
+
const [scrollState, setScrollState] = useState({ virtualizer: null, browser: null, measured: false });
|
|
37
|
+
console.info('[DEBUG] ScrollState', scrollState);
|
|
34
38
|
const emptyStates = useEmptyState({ noDataState, noResultsState, errorDataState });
|
|
35
39
|
const hasNoItems = items.length === 0;
|
|
40
|
+
const { selectedItemIndex, selectedItem } = useMemo(() => {
|
|
41
|
+
const result = {
|
|
42
|
+
selectedItemIndex: -1,
|
|
43
|
+
selectedItem: undefined,
|
|
44
|
+
};
|
|
45
|
+
if (!scrollToSelectedItem || !value) {
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
const selectedItem = isSelectionSingle ? flattenItems[value] : flattenItems[value[0]];
|
|
49
|
+
if (!(selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.id)) {
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
const allFocusFlattenItems = Object.values(focusFlattenItems);
|
|
53
|
+
const index = allFocusFlattenItems.findIndex(item => item.originalId === selectedItem.id);
|
|
54
|
+
if (index < 0) {
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
selectedItemIndex: index,
|
|
59
|
+
selectedItem: allFocusFlattenItems[index],
|
|
60
|
+
};
|
|
61
|
+
}, [flattenItems, focusFlattenItems, isSelectionSingle, scrollToSelectedItem, value]);
|
|
36
62
|
const virtualizer = useVirtualizer({
|
|
37
63
|
count: itemsJSX.length,
|
|
38
64
|
getScrollElement: () => (scroll ? innerScrollRef.current : null),
|
|
39
65
|
estimateSize: () => ALL_SIZES[size],
|
|
40
66
|
enabled: virtualized,
|
|
67
|
+
overscan: 5, // Amount of elements in DOM before/after visible ones
|
|
41
68
|
});
|
|
42
69
|
const virtualItems = virtualizer.getVirtualItems();
|
|
43
70
|
useEffect(() => {
|
|
44
|
-
|
|
45
|
-
|
|
71
|
+
if (scrollState.measured) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
virtualizer.measure(); // TODO: перезамерять размер
|
|
75
|
+
setScrollState(prevState => (Object.assign(Object.assign({}, prevState), { measured: true })));
|
|
76
|
+
}, [scrollState.measured, virtualizer]);
|
|
77
|
+
const isScrollToItemEnabled = scroll && scrollToSelectedItem && virtualized;
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
var _a;
|
|
80
|
+
if (isScrollToItemEnabled) {
|
|
81
|
+
console.info('[DEBUG] selectedItemIndex', selectedItemIndex);
|
|
82
|
+
if (!scrollState.measured) {
|
|
83
|
+
console.info('[DEBUG] Not measured yet');
|
|
84
|
+
return; // Not measured yet
|
|
85
|
+
}
|
|
86
|
+
if (selectedItemIndex < 0 || !selectedItem) {
|
|
87
|
+
console.info('[DEBUG] Cannot scroll to non-existing item');
|
|
88
|
+
return; // Cannot scroll to non-existing item
|
|
89
|
+
}
|
|
90
|
+
if (scrollState.virtualizer === selectedItemIndex) {
|
|
91
|
+
console.info('[DEBUG] No need to re-scroll to the same item during re-renders');
|
|
92
|
+
return; // No need to re-scroll to the same item during re-renders
|
|
93
|
+
}
|
|
94
|
+
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))) {
|
|
95
|
+
console.info('[DEBUG] No need to scroll to manually clicked item currently present in DOM');
|
|
96
|
+
return; // No need to scroll to manually clicked item currently present in DOM
|
|
97
|
+
}
|
|
98
|
+
console.info('[DEBUG] SCROLLING BY VIRTUALIZER');
|
|
99
|
+
virtualizer.scrollToIndex(selectedItemIndex, { align: 'center' });
|
|
100
|
+
setScrollState(prevState => (Object.assign(Object.assign({}, prevState), { virtualizer: selectedItemIndex })));
|
|
101
|
+
}
|
|
102
|
+
}, [isScrollToItemEnabled, scrollState, selectedItem, selectedItemIndex, virtualizer]);
|
|
103
|
+
const isTargetPresentInDom = Boolean((_b = selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.itemRef) === null || _b === void 0 ? void 0 : _b.current);
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
var _a, _b, _c;
|
|
106
|
+
if (scrollState.virtualizer === null) {
|
|
107
|
+
return; // Not scrolled by virtualizer yet, no need for additional scroll
|
|
108
|
+
}
|
|
109
|
+
if (!isTargetPresentInDom) {
|
|
110
|
+
return; // Target element is not present in DOM yet, additional scroll does not work without it
|
|
111
|
+
}
|
|
112
|
+
if (scrollState.virtualizer === scrollState.browser) {
|
|
113
|
+
return; // Virtualizer scroll has not been executed => no need for additional scroll
|
|
114
|
+
}
|
|
115
|
+
console.info('[DEBUG] SCROLLING BY BROWSER', (_a = selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.itemRef) === null || _a === void 0 ? void 0 : _a.current);
|
|
116
|
+
(_c = (_b = selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.itemRef) === null || _b === void 0 ? void 0 : _b.current) === null || _c === void 0 ? void 0 : _c.scrollIntoView({ block: 'center' });
|
|
117
|
+
setScrollState(prevState => (Object.assign(Object.assign({}, prevState), { browser: selectedItemIndex })));
|
|
118
|
+
}, [scrollState, selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.itemRef, isTargetPresentInDom, selectedItemIndex]);
|
|
46
119
|
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
|
-
}, [
|
|
120
|
+
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: {
|
|
121
|
+
transform: `translateY(${virtualRow.start}px)`,
|
|
122
|
+
}, 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
123
|
dataError,
|
|
52
124
|
dataFiltered,
|
|
53
125
|
emptyStates,
|
|
@@ -61,29 +133,18 @@ export const ListPrivate = forwardRef((_a, ref) => {
|
|
|
61
133
|
virtualized,
|
|
62
134
|
virtualizer,
|
|
63
135
|
]);
|
|
64
|
-
const onScrollInitialized = () => {
|
|
136
|
+
const onScrollInitialized = useCallback(() => {
|
|
65
137
|
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' });
|
|
138
|
+
if (!selectedItem) {
|
|
139
|
+
return;
|
|
76
140
|
}
|
|
77
|
-
|
|
141
|
+
(_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' });
|
|
142
|
+
}, [selectedItem]);
|
|
78
143
|
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
144
|
[commonStyles.scrollContainerS]: scroll && limitedScrollHeight && size === 's',
|
|
80
145
|
[commonStyles.scrollContainerM]: scroll && limitedScrollHeight && size === 'm',
|
|
81
146
|
[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 }))] })));
|
|
147
|
+
}, 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
148
|
if (!nested) {
|
|
88
149
|
return listJSX;
|
|
89
150
|
}
|
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-c824da44.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": "edea7e461de9f4f112e73ed1de8c1063240d3164"
|
|
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, useState } from 'react';
|
|
4
5
|
|
|
5
6
|
import { Spinner } from '@snack-uikit/loaders';
|
|
6
7
|
import { Scroll } from '@snack-uikit/scroll';
|
|
@@ -15,6 +16,12 @@ import { ListPrivateProps } from '../types';
|
|
|
15
16
|
import { ALL_SIZES } from './constants';
|
|
16
17
|
import styles from './styles.module.scss';
|
|
17
18
|
|
|
19
|
+
type ScrollState = {
|
|
20
|
+
virtualizer: number | null;
|
|
21
|
+
browser: number | null;
|
|
22
|
+
measured: boolean;
|
|
23
|
+
};
|
|
24
|
+
|
|
18
25
|
export const ListPrivate = forwardRef(
|
|
19
26
|
(
|
|
20
27
|
{
|
|
@@ -59,20 +66,117 @@ export const ListPrivate = forwardRef(
|
|
|
59
66
|
const itemsPinTopJSX = useRenderItems(pinTop);
|
|
60
67
|
const itemsPinBottomJSX = useRenderItems(pinBottom);
|
|
61
68
|
|
|
69
|
+
const [scrollState, setScrollState] = useState<ScrollState>({ virtualizer: null, browser: null, measured: false });
|
|
70
|
+
console.info('[DEBUG] ScrollState', scrollState);
|
|
71
|
+
|
|
62
72
|
const emptyStates = useEmptyState({ noDataState, noResultsState, errorDataState });
|
|
63
73
|
const hasNoItems = items.length === 0;
|
|
64
74
|
|
|
75
|
+
const { selectedItemIndex, selectedItem } = useMemo(() => {
|
|
76
|
+
const result = {
|
|
77
|
+
selectedItemIndex: -1,
|
|
78
|
+
selectedItem: undefined,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
if (!scrollToSelectedItem || !value) {
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const selectedItem = isSelectionSingle ? flattenItems[value] : flattenItems[value[0]];
|
|
86
|
+
if (!selectedItem?.id) {
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const allFocusFlattenItems = Object.values(focusFlattenItems);
|
|
91
|
+
const index = allFocusFlattenItems.findIndex(item => item.originalId === selectedItem.id);
|
|
92
|
+
if (index < 0) {
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
selectedItemIndex: index,
|
|
98
|
+
selectedItem: allFocusFlattenItems[index],
|
|
99
|
+
};
|
|
100
|
+
}, [flattenItems, focusFlattenItems, isSelectionSingle, scrollToSelectedItem, value]);
|
|
101
|
+
|
|
65
102
|
const virtualizer = useVirtualizer({
|
|
66
103
|
count: itemsJSX.length,
|
|
67
104
|
getScrollElement: () => (scroll ? innerScrollRef.current : null),
|
|
68
105
|
estimateSize: () => ALL_SIZES[size],
|
|
69
106
|
enabled: virtualized,
|
|
107
|
+
overscan: 5, // Amount of elements in DOM before/after visible ones
|
|
70
108
|
});
|
|
71
109
|
const virtualItems = virtualizer.getVirtualItems();
|
|
72
110
|
|
|
73
111
|
useEffect(() => {
|
|
74
|
-
|
|
75
|
-
|
|
112
|
+
if (scrollState.measured) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
virtualizer.measure(); // TODO: перезамерять размер
|
|
117
|
+
|
|
118
|
+
setScrollState(prevState => ({
|
|
119
|
+
...prevState,
|
|
120
|
+
measured: true,
|
|
121
|
+
}));
|
|
122
|
+
}, [scrollState.measured, virtualizer]);
|
|
123
|
+
|
|
124
|
+
const isScrollToItemEnabled = scroll && scrollToSelectedItem && virtualized;
|
|
125
|
+
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
if (isScrollToItemEnabled) {
|
|
128
|
+
console.info('[DEBUG] selectedItemIndex', selectedItemIndex);
|
|
129
|
+
if (!scrollState.measured) {
|
|
130
|
+
console.info('[DEBUG] Not measured yet');
|
|
131
|
+
return; // Not measured yet
|
|
132
|
+
}
|
|
133
|
+
if (selectedItemIndex < 0 || !selectedItem) {
|
|
134
|
+
console.info('[DEBUG] Cannot scroll to non-existing item');
|
|
135
|
+
return; // Cannot scroll to non-existing item
|
|
136
|
+
}
|
|
137
|
+
if (scrollState.virtualizer === selectedItemIndex) {
|
|
138
|
+
console.info('[DEBUG] No need to re-scroll to the same item during re-renders');
|
|
139
|
+
return; // No need to re-scroll to the same item during re-renders
|
|
140
|
+
}
|
|
141
|
+
if (selectedItem?.itemRef && innerScrollRef.current?.contains(selectedItem?.itemRef.current)) {
|
|
142
|
+
console.info('[DEBUG] No need to scroll to manually clicked item currently present in DOM');
|
|
143
|
+
return; // No need to scroll to manually clicked item currently present in DOM
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
console.info('[DEBUG] SCROLLING BY VIRTUALIZER');
|
|
147
|
+
virtualizer.scrollToIndex(selectedItemIndex, { align: 'center' });
|
|
148
|
+
|
|
149
|
+
setScrollState(prevState => ({
|
|
150
|
+
...prevState,
|
|
151
|
+
virtualizer: selectedItemIndex,
|
|
152
|
+
}));
|
|
153
|
+
}
|
|
154
|
+
}, [isScrollToItemEnabled, scrollState, selectedItem, selectedItemIndex, virtualizer]);
|
|
155
|
+
|
|
156
|
+
const isTargetPresentInDom = Boolean(selectedItem?.itemRef?.current);
|
|
157
|
+
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
if (scrollState.virtualizer === null) {
|
|
160
|
+
return; // Not scrolled by virtualizer yet, no need for additional scroll
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (!isTargetPresentInDom) {
|
|
164
|
+
return; // Target element is not present in DOM yet, additional scroll does not work without it
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (scrollState.virtualizer === scrollState.browser) {
|
|
168
|
+
return; // Virtualizer scroll has not been executed => no need for additional scroll
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
console.info('[DEBUG] SCROLLING BY BROWSER', selectedItem?.itemRef?.current);
|
|
172
|
+
|
|
173
|
+
selectedItem?.itemRef?.current?.scrollIntoView({ block: 'center' });
|
|
174
|
+
|
|
175
|
+
setScrollState(prevState => ({
|
|
176
|
+
...prevState,
|
|
177
|
+
browser: selectedItemIndex,
|
|
178
|
+
}));
|
|
179
|
+
}, [scrollState, selectedItem?.itemRef, isTargetPresentInDom, selectedItemIndex]);
|
|
76
180
|
|
|
77
181
|
const loadingJSX = useMemo(
|
|
78
182
|
() =>
|
|
@@ -93,25 +197,23 @@ export const ListPrivate = forwardRef(
|
|
|
93
197
|
|
|
94
198
|
const content = useMemo(
|
|
95
199
|
() => (
|
|
96
|
-
|
|
200
|
+
<>
|
|
97
201
|
{virtualized ? (
|
|
98
202
|
<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>
|
|
203
|
+
{virtualItems.map(virtualRow => (
|
|
204
|
+
<div
|
|
205
|
+
key={virtualRow.key}
|
|
206
|
+
data-index={virtualRow.index}
|
|
207
|
+
ref={virtualizer.measureElement}
|
|
208
|
+
tabIndex={-1}
|
|
209
|
+
className={styles.virtualizedPositionBox}
|
|
210
|
+
style={{
|
|
211
|
+
transform: `translateY(${virtualRow.start}px)`,
|
|
212
|
+
}}
|
|
213
|
+
>
|
|
214
|
+
{itemsJSX[virtualRow.index]}
|
|
215
|
+
</div>
|
|
216
|
+
))}
|
|
115
217
|
</div>
|
|
116
218
|
) : (
|
|
117
219
|
itemsJSX
|
|
@@ -126,7 +228,7 @@ export const ListPrivate = forwardRef(
|
|
|
126
228
|
dataFiltered={dataFiltered ?? Boolean(search?.value)}
|
|
127
229
|
size={size}
|
|
128
230
|
/>
|
|
129
|
-
|
|
231
|
+
</>
|
|
130
232
|
),
|
|
131
233
|
[
|
|
132
234
|
dataError,
|
|
@@ -144,22 +246,13 @@ export const ListPrivate = forwardRef(
|
|
|
144
246
|
],
|
|
145
247
|
);
|
|
146
248
|
|
|
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' });
|
|
249
|
+
const onScrollInitialized = useCallback(() => {
|
|
250
|
+
if (!selectedItem) {
|
|
251
|
+
return;
|
|
161
252
|
}
|
|
162
|
-
|
|
253
|
+
|
|
254
|
+
selectedItem?.itemRef?.current?.scrollIntoView({ block: 'center' });
|
|
255
|
+
}, [selectedItem]);
|
|
163
256
|
|
|
164
257
|
const listJSX = (
|
|
165
258
|
<ul
|
|
@@ -193,10 +286,7 @@ export const ListPrivate = forwardRef(
|
|
|
193
286
|
)}
|
|
194
287
|
barHideStrategy={barHideStrategy}
|
|
195
288
|
size={size === 's' ? 's' : 'm'}
|
|
196
|
-
ref={
|
|
197
|
-
innerScrollRef.current = ref;
|
|
198
|
-
if (scrollContainerRef) (scrollContainerRef as React.MutableRefObject<HTMLElement | null>).current = ref;
|
|
199
|
-
}}
|
|
289
|
+
ref={mergeRefs(innerScrollRef, scrollContainerRef)}
|
|
200
290
|
untouchableScrollbars={untouchableScrollbars}
|
|
201
291
|
onScroll={onScroll}
|
|
202
292
|
onInitialized={onScrollInitialized}
|