@cloud-ru/ds-list 2.3.3 → 2.3.4

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,12 @@
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
+ ## 2.3.4 (2026-10-09)
7
+
8
+ ### Bug Fixes
9
+
10
+ - **FF-9216:** value selection ([818fa8c](https://github.com/cloud-ru-tech/snack-v2/commit/818fa8ca8730ba81e31cd7f0ca2c7493652592e1))
11
+
6
12
  ## 2.3.3 (2026-10-09)
7
13
 
8
14
  **Note:** Version bump only for package @ds/list
@@ -1,17 +1,18 @@
1
1
  import { KeyboardEvent } from 'react';
2
+ import { Item } from '../../Items';
2
3
  /** Список айтемов: base / collapse / group / next-list, с опциональной виртуализацией. */
3
4
  export declare const List: import("react").ForwardRefExoticComponent<{
4
5
  'data-test-id'?: string;
5
6
  } & import("react").AriaAttributes & {
6
- pinTop?: import("../..").Item[];
7
- pinBottom?: import("../..").Item[];
7
+ pinTop?: Item[];
8
+ pinBottom?: Item[];
8
9
  footer?: import("react").ReactNode;
9
10
  header?: import("react").ReactNode;
10
11
  headerDivider?: boolean;
11
12
  footerDivider?: boolean;
12
13
  footerActiveElementsRefs?: import("react").RefObject<HTMLElement>[];
13
14
  keyboardNavigationRef?: import("react").RefObject<{
14
- focusItem(id: import("../..").ItemId): void;
15
+ focusItem(id: import("../../Items").ItemId): void;
15
16
  }>;
16
17
  search?: import("../../..").SearchState;
17
18
  tabIndex?: number;
@@ -24,7 +25,7 @@ export declare const List: import("react").ForwardRefExoticComponent<{
24
25
  scrollContainerClassName?: string;
25
26
  limitedScrollHeight?: boolean;
26
27
  } & import("../contexts").SelectionState & Omit<import("../contexts").PublicListContextType, "virtualized"> & import("../../..").ScrollProps & import("../types").EmptyState & {
27
- items: import("../..").Item[];
28
+ items: Item[];
28
29
  virtualized?: boolean;
29
30
  } & import("react").RefAttributes<HTMLElement>>;
30
31
  /**
@@ -35,15 +36,15 @@ export declare const List: import("react").ForwardRefExoticComponent<{
35
36
  export declare const ReorderableList: import("react").ForwardRefExoticComponent<{
36
37
  'data-test-id'?: string;
37
38
  } & import("react").AriaAttributes & {
38
- pinTop?: import("../..").Item[];
39
- pinBottom?: import("../..").Item[];
39
+ pinTop?: Item[];
40
+ pinBottom?: Item[];
40
41
  footer?: import("react").ReactNode;
41
42
  header?: import("react").ReactNode;
42
43
  headerDivider?: boolean;
43
44
  footerDivider?: boolean;
44
45
  footerActiveElementsRefs?: import("react").RefObject<HTMLElement>[];
45
46
  keyboardNavigationRef?: import("react").RefObject<{
46
- focusItem(id: import("../..").ItemId): void;
47
+ focusItem(id: import("../../Items").ItemId): void;
47
48
  }>;
48
49
  search?: import("../../..").SearchState;
49
50
  tabIndex?: number;
@@ -56,6 +57,6 @@ export declare const ReorderableList: import("react").ForwardRefExoticComponent<
56
57
  scrollContainerClassName?: string;
57
58
  limitedScrollHeight?: boolean;
58
59
  } & import("../contexts").SelectionState & Omit<import("../contexts").PublicListContextType, "virtualized"> & import("../../..").ScrollProps & import("../types").EmptyState & {
59
- items: import("../..").ReorderItem[];
60
- onItemsReorder(items: import("../..").ReorderItem[]): void;
60
+ items: import("../../Items").ReorderItem[];
61
+ onItemsReorder(items: import("../../Items").ReorderItem[]): void;
61
62
  } & import("react").RefAttributes<HTMLElement>>;
@@ -15,7 +15,8 @@ const contexts_1 = require("../contexts");
15
15
  const hooks_1 = require("../hooks");
16
16
  const ListPrivate_1 = require("../ListPrivate");
17
17
  const styles_module_scss_1 = __importDefault(require('../styles.module.css'));
18
- const ListImpl = (0, react_1.forwardRef)(({ items: itemsProp = [], search, pinBottom: pinBottomProp = [], pinTop: pinTopProp = [], footerActiveElementsRefs, onKeyDown, tabIndex = 0, className, collapse = {}, selection, contentRender, size = constants_1.DEFAULT_SIZE, marker = true, keyboardNavigationRef, hasListInFocusChain = true, onItemsReorder, ...props }, ref) => {
18
+ const EMPTY_ITEMS = [];
19
+ const ListImpl = (0, react_1.forwardRef)(({ items: itemsProp = EMPTY_ITEMS, search, pinBottom: pinBottomProp = EMPTY_ITEMS, pinTop: pinTopProp = EMPTY_ITEMS, footerActiveElementsRefs, onKeyDown, tabIndex = 0, className, collapse = {}, selection, contentRender, size = constants_1.DEFAULT_SIZE, marker = true, keyboardNavigationRef, hasListInFocusChain = true, onItemsReorder, ...props }, ref) => {
19
20
  const { openCollapseItems, toggleOpenCollapseItem, searchItem, flattenItems, focusFlattenItems, memorizedItems, ids, expandedIds, firstItemId, onDragEnd, sortableIds, } = (0, hooks_1.useListItemsModel)({
20
21
  items: itemsProp,
21
22
  pinTop: pinTopProp,
@@ -92,6 +92,7 @@ exports.ListPrivate = (0, react_1.forwardRef)(({ items, pinTop, pinBottom, onKey
92
92
  // fixed` у `DragOverlay` остаётся относительно вьюпорта (в т.ч. внутри popover-Droplist).
93
93
  const portalRoot = (0, portal_context_1.usePortalContext)();
94
94
  const innerScrollRef = (0, react_1.useRef)(null);
95
+ const lastCenteredItemRef = (0, react_1.useRef)(null);
95
96
  // `@cloud-ru/ds-scroll` (OverlayScrollbars) разрешает свой ref в viewport-элемент только после
96
97
  // инициализации инстанса. До этого `innerScrollRef.current` === null, и виртуализатор
97
98
  // не получает scroll-элемент → рендерит пустой контейнер. Флаг поднимается из
@@ -110,11 +111,11 @@ exports.ListPrivate = (0, react_1.forwardRef)(({ items, pinTop, pinBottom, onKey
110
111
  selectedItemIndex: -1,
111
112
  selectedItem: undefined,
112
113
  };
113
- if (!scrollToSelectedItem || !value) {
114
+ if (!scrollToSelectedItem || (0, utils_1.isNil)(value)) {
114
115
  return result;
115
116
  }
116
117
  const selectedItem = isSelectionSingle ? flattenItems[value] : flattenItems[value[0]];
117
- if (!selectedItem?.id) {
118
+ if (!selectedItem || (0, utils_1.isNil)(selectedItem.id)) {
118
119
  return result;
119
120
  }
120
121
  const allFocusFlattenItems = Object.values(focusFlattenItems);
@@ -223,7 +224,8 @@ exports.ListPrivate = (0, react_1.forwardRef)(({ items, pinTop, pinBottom, onKey
223
224
  // ведут эффекты выше через виртуализатор, поэтому здесь только `!virtualized`. Хелпер тот
224
225
  // же `centerItemInScrollContainer` — обычному листу отдельный виртуализатор не нужен.
225
226
  (0, react_1.useEffect)(() => {
226
- if (virtualized || !scroll || !scrollToSelectedItem || !scrollElementReady) {
227
+ if (virtualized || !scroll || !scrollToSelectedItem || !scrollElementReady || !selectedItem) {
228
+ lastCenteredItemRef.current = null;
227
229
  return;
228
230
  }
229
231
  const container = innerScrollRef.current;
@@ -231,7 +233,12 @@ exports.ListPrivate = (0, react_1.forwardRef)(({ items, pinTop, pinBottom, onKey
231
233
  if (!container || !item) {
232
234
  return;
233
235
  }
236
+ if (lastCenteredItemRef.current?.id === selectedItem.originalId &&
237
+ lastCenteredItemRef.current.container === container) {
238
+ return;
239
+ }
234
240
  centerItemInScrollContainer(container, item);
241
+ lastCenteredItemRef.current = { id: selectedItem.originalId, container };
235
242
  }, [virtualized, scroll, scrollToSelectedItem, scrollElementReady, selectedItem]);
236
243
  const loadingJSX = (0, react_1.useMemo)(() => loading && ((0, jsx_runtime_1.jsx)("div", { role: 'status', tabIndex: -1, className: styles_module_scss_2.default.loader, "data-size": size, "data-no-items": hasNoItems || undefined, "data-test-id": constants_1.TEST_IDS.loader, children: (0, jsx_runtime_1.jsx)(loader_1.Spinner, { size: constants_2.SPINNER_SIZE_MAP[size] }) })), [hasNoItems, loading, size]);
237
244
  const content = (0, react_1.useMemo)(() => {
@@ -1,10 +1,11 @@
1
1
  import { KeyboardEvent } from 'react';
2
+ import { Item } from '../../Items/index.js';
2
3
  /** Список айтемов: base / collapse / group / next-list, с опциональной виртуализацией. */
3
4
  export declare const List: import("react").ForwardRefExoticComponent<{
4
5
  'data-test-id'?: string;
5
6
  } & import("react").AriaAttributes & {
6
- pinTop?: import("../..").Item[];
7
- pinBottom?: import("../..").Item[];
7
+ pinTop?: Item[];
8
+ pinBottom?: Item[];
8
9
  footer?: import("react").ReactNode;
9
10
  header?: import("react").ReactNode;
10
11
  headerDivider?: boolean;
@@ -24,7 +25,7 @@ export declare const List: import("react").ForwardRefExoticComponent<{
24
25
  scrollContainerClassName?: string;
25
26
  limitedScrollHeight?: boolean;
26
27
  } & import("..").SelectionState & Omit<import("..").PublicListContextType, "virtualized"> & import("../../..").ScrollProps & import("..").EmptyState & {
27
- items: import("../..").Item[];
28
+ items: Item[];
28
29
  virtualized?: boolean;
29
30
  } & import("react").RefAttributes<HTMLElement>>;
30
31
  /**
@@ -35,8 +36,8 @@ export declare const List: import("react").ForwardRefExoticComponent<{
35
36
  export declare const ReorderableList: import("react").ForwardRefExoticComponent<{
36
37
  'data-test-id'?: string;
37
38
  } & import("react").AriaAttributes & {
38
- pinTop?: import("../..").Item[];
39
- pinBottom?: import("../..").Item[];
39
+ pinTop?: Item[];
40
+ pinBottom?: Item[];
40
41
  footer?: import("react").ReactNode;
41
42
  header?: import("react").ReactNode;
42
43
  headerDivider?: boolean;
@@ -9,7 +9,8 @@ import { CollapseContext, FocusListContext, NewListContextProvider, SelectionPro
9
9
  import { useListItemsModel, useNewKeyboardNavigation } from '../hooks.js';
10
10
  import { ListPrivate } from '../ListPrivate/index.js';
11
11
  import styles from '../styles.module.css';
12
- const ListImpl = forwardRef(({ items: itemsProp = [], search, pinBottom: pinBottomProp = [], pinTop: pinTopProp = [], footerActiveElementsRefs, onKeyDown, tabIndex = 0, className, collapse = {}, selection, contentRender, size = DEFAULT_SIZE, marker = true, keyboardNavigationRef, hasListInFocusChain = true, onItemsReorder, ...props }, ref) => {
12
+ const EMPTY_ITEMS = [];
13
+ const ListImpl = forwardRef(({ items: itemsProp = EMPTY_ITEMS, search, pinBottom: pinBottomProp = EMPTY_ITEMS, pinTop: pinTopProp = EMPTY_ITEMS, footerActiveElementsRefs, onKeyDown, tabIndex = 0, className, collapse = {}, selection, contentRender, size = DEFAULT_SIZE, marker = true, keyboardNavigationRef, hasListInFocusChain = true, onItemsReorder, ...props }, ref) => {
13
14
  const { openCollapseItems, toggleOpenCollapseItem, searchItem, flattenItems, focusFlattenItems, memorizedItems, ids, expandedIds, firstItemId, onDragEnd, sortableIds, } = useListItemsModel({
14
15
  items: itemsProp,
15
16
  pinTop: pinTopProp,
@@ -4,7 +4,7 @@ import { SortableContext, sortableKeyboardCoordinates, verticalListSortingStrate
4
4
  import { Spinner } from '@cloud-ru/ds-loader';
5
5
  import { usePortalContext } from '@cloud-ru/ds-portal-context';
6
6
  import { Scroll } from '@cloud-ru/ds-scroll';
7
- import { extractSupportProps, isBrowser, useLayoutEffect } from '@cloud-ru/ds-utils';
7
+ import { extractSupportProps, isBrowser, isNil, useLayoutEffect } from '@cloud-ru/ds-utils';
8
8
  import { useVirtualizer } from '@tanstack/react-virtual';
9
9
  import cn from 'classnames';
10
10
  import mergeRefs from 'merge-refs';
@@ -86,6 +86,7 @@ export const ListPrivate = forwardRef(({ items, pinTop, pinBottom, onKeyDown, on
86
86
  // fixed` у `DragOverlay` остаётся относительно вьюпорта (в т.ч. внутри popover-Droplist).
87
87
  const portalRoot = usePortalContext();
88
88
  const innerScrollRef = useRef(null);
89
+ const lastCenteredItemRef = useRef(null);
89
90
  // `@cloud-ru/ds-scroll` (OverlayScrollbars) разрешает свой ref в viewport-элемент только после
90
91
  // инициализации инстанса. До этого `innerScrollRef.current` === null, и виртуализатор
91
92
  // не получает scroll-элемент → рендерит пустой контейнер. Флаг поднимается из
@@ -104,11 +105,11 @@ export const ListPrivate = forwardRef(({ items, pinTop, pinBottom, onKeyDown, on
104
105
  selectedItemIndex: -1,
105
106
  selectedItem: undefined,
106
107
  };
107
- if (!scrollToSelectedItem || !value) {
108
+ if (!scrollToSelectedItem || isNil(value)) {
108
109
  return result;
109
110
  }
110
111
  const selectedItem = isSelectionSingle ? flattenItems[value] : flattenItems[value[0]];
111
- if (!selectedItem?.id) {
112
+ if (!selectedItem || isNil(selectedItem.id)) {
112
113
  return result;
113
114
  }
114
115
  const allFocusFlattenItems = Object.values(focusFlattenItems);
@@ -217,7 +218,8 @@ export const ListPrivate = forwardRef(({ items, pinTop, pinBottom, onKeyDown, on
217
218
  // ведут эффекты выше через виртуализатор, поэтому здесь только `!virtualized`. Хелпер тот
218
219
  // же `centerItemInScrollContainer` — обычному листу отдельный виртуализатор не нужен.
219
220
  useEffect(() => {
220
- if (virtualized || !scroll || !scrollToSelectedItem || !scrollElementReady) {
221
+ if (virtualized || !scroll || !scrollToSelectedItem || !scrollElementReady || !selectedItem) {
222
+ lastCenteredItemRef.current = null;
221
223
  return;
222
224
  }
223
225
  const container = innerScrollRef.current;
@@ -225,7 +227,12 @@ export const ListPrivate = forwardRef(({ items, pinTop, pinBottom, onKeyDown, on
225
227
  if (!container || !item) {
226
228
  return;
227
229
  }
230
+ if (lastCenteredItemRef.current?.id === selectedItem.originalId &&
231
+ lastCenteredItemRef.current.container === container) {
232
+ return;
233
+ }
228
234
  centerItemInScrollContainer(container, item);
235
+ lastCenteredItemRef.current = { id: selectedItem.originalId, container };
229
236
  }, [virtualized, scroll, scrollToSelectedItem, scrollElementReady, selectedItem]);
230
237
  const loadingJSX = useMemo(() => loading && (_jsx("div", { role: 'status', tabIndex: -1, className: styles.loader, "data-size": size, "data-no-items": hasNoItems || undefined, "data-test-id": TEST_IDS.loader, children: _jsx(Spinner, { size: SPINNER_SIZE_MAP[size] }) })), [hasNoItems, loading, size]);
231
238
  const content = useMemo(() => {