@snack-uikit/list 0.22.3-preview-bb2cdfb8.0 → 0.23.0

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