@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
- virtualizer.measure();
95
- }, [virtualizer]);
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
- var _a, _b;
109
- return (0, jsx_runtime_1.jsxs)("div", {
110
- className: styles_module_scss_2.default.content,
111
- children: [virtualized ? (0, jsx_runtime_1.jsx)("div", {
112
- className: styles_module_scss_2.default.virtualizedContainer,
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
- height: virtualizer.getTotalSize()
210
+ transform: `translateY(${virtualRow.start}px)`
115
211
  },
116
- tabIndex: -1,
117
- children: (0, jsx_runtime_1.jsx)("div", {
118
- className: styles_module_scss_2.default.virtualizedPositionBox,
119
- style: {
120
- transform: `translateY(${(_b = (_a = virtualItems[0]) === null || _a === void 0 ? void 0 : _a.start) !== null && _b !== void 0 ? _b : 0}px)`
121
- },
122
- tabIndex: -1,
123
- children: virtualItems.map(virtualRow => (0, jsx_runtime_1.jsx)("div", {
124
- "data-index": virtualRow.index,
125
- ref: virtualizer.measureElement,
126
- tabIndex: -1,
127
- children: itemsJSX[virtualRow.index]
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 (scrollToSelectedItem) {
143
- if (!value) {
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: 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, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
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 { forwardRef, useEffect, useMemo, useRef } from 'react';
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
- virtualizer.measure();
45
- }, [virtualizer]);
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
- var _a, _b;
49
- 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), size: size })] }));
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 (scrollToSelectedItem) {
67
- if (!value) {
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.6",
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": "30ce13048847439dad862be32ab65379adc107c0"
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 { ForwardedRef, forwardRef, RefObject, useEffect, useMemo, useRef } from 'react';
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
- virtualizer.measure();
75
- }, [virtualizer]);
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
- <div className={styles.content}>
200
+ <>
97
201
  {virtualized ? (
98
202
  <div className={styles.virtualizedContainer} style={{ height: virtualizer.getTotalSize() }} tabIndex={-1}>
99
- <div
100
- className={styles.virtualizedPositionBox}
101
- style={{ transform: `translateY(${virtualItems[0]?.start ?? 0}px)` }}
102
- tabIndex={-1}
103
- >
104
- {virtualItems.map(virtualRow => (
105
- <div
106
- key={virtualRow.key}
107
- data-index={virtualRow.index}
108
- ref={virtualizer.measureElement}
109
- tabIndex={-1}
110
- >
111
- {itemsJSX[virtualRow.index]}
112
- </div>
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
- </div>
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 (scrollToSelectedItem) {
149
- if (!value) {
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={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}