@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
- 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,
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
- height: virtualizer.getTotalSize()
178
+ transform: `translateY(${virtualRow.start}px)`
115
179
  },
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 = () => {
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 (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
- });
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: 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, 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 } 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
- 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
- }, [
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 (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' });
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.6",
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": "30ce13048847439dad862be32ab65379adc107c0"
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 { ForwardedRef, forwardRef, RefObject, useEffect, useMemo, useRef } from 'react';
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
- <div className={styles.content}>
155
+ <>
97
156
  {virtualized ? (
98
157
  <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>
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
- </div>
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 (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' });
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={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}