@cloud-ru/ds-list 2.0.3 → 2.0.4-preview-9005559b.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloud-ru/ds-list",
3
- "version": "2.0.3",
3
+ "version": "2.0.4-preview-9005559b.0",
4
4
  "description": "Списочный UI — компонент List для плоских/вложенных списков с выбором, группами и поиском и Droplist — тот же список в поповере.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -48,24 +48,24 @@
48
48
  "@tanstack/react-virtual": "^3.14.8",
49
49
  "classnames": "^2.5.1",
50
50
  "merge-refs": "^2.0.0",
51
- "@cloud-ru/ds-adaptive": "^1.0.1",
52
- "@cloud-ru/ds-bottom-sheet": "^1.0.4",
53
- "@cloud-ru/ds-drag-and-drop": "^1.1.1",
54
- "@cloud-ru/ds-button": "^1.0.1",
55
- "@cloud-ru/ds-divider": "^1.0.1",
56
- "@cloud-ru/ds-icons": "^1.0.2",
57
- "@cloud-ru/ds-info-block": "^1.0.1",
58
- "@cloud-ru/ds-materials": "^1.0.1",
59
- "@cloud-ru/ds-dropdown": "^1.0.5",
60
- "@cloud-ru/ds-search-private": "^1.0.4",
61
- "@cloud-ru/ds-truncate-string": "^1.0.3",
62
- "@cloud-ru/ds-scroll": "^1.0.2",
63
- "@cloud-ru/ds-utils": "^1.1.0",
64
- "@cloud-ru/ds-toggles": "^1.1.2",
65
- "@cloud-ru/ds-portal-context": "^1.0.1",
66
- "@cloud-ru/ds-loader": "^1.0.1"
51
+ "@cloud-ru/ds-bottom-sheet": "1.0.5-preview-9005559b.0",
52
+ "@cloud-ru/ds-button": "1.0.1",
53
+ "@cloud-ru/ds-divider": "1.0.1",
54
+ "@cloud-ru/ds-icons": "1.0.2",
55
+ "@cloud-ru/ds-drag-and-drop": "1.1.1",
56
+ "@cloud-ru/ds-dropdown": "1.0.6-preview-9005559b.0",
57
+ "@cloud-ru/ds-adaptive": "1.0.1",
58
+ "@cloud-ru/ds-info-block": "1.0.1",
59
+ "@cloud-ru/ds-loader": "1.0.1",
60
+ "@cloud-ru/ds-portal-context": "1.0.1",
61
+ "@cloud-ru/ds-scroll": "1.0.2",
62
+ "@cloud-ru/ds-materials": "1.0.1",
63
+ "@cloud-ru/ds-toggles": "1.1.2",
64
+ "@cloud-ru/ds-truncate-string": "1.0.3",
65
+ "@cloud-ru/ds-search-private": "1.0.4",
66
+ "@cloud-ru/ds-utils": "1.1.0"
67
67
  },
68
68
  "peerDependencies": {
69
- "@cloud-ru/ds-locale": "^1.0.0"
69
+ "@cloud-ru/ds-locale": "1.0.0"
70
70
  }
71
71
  }
@@ -9,7 +9,7 @@ import { List, ReorderableList } from '../../components/Lists/List';
9
9
  import { BaseDroplistProps, MobileDroplistProps } from '../../components/Lists/types';
10
10
  import { TEST_IDS } from '../../constants';
11
11
  import styles from './MobileDroplist.module.scss';
12
- import { buildLevelItems, nextListOption } from './utils';
12
+ import { buildLevelItems, nextListOption, resolveItemByPath } from './utils';
13
13
 
14
14
  /**
15
15
  * Mobile-вариант `Droplist`: рендерит `List` (в переданном `size`) внутри `BottomSheet` из `@cloud-ru/ds-bottom-sheet`.
@@ -47,8 +47,10 @@ export function MobileDroplist({
47
47
  const scrollRef = useRef<HTMLDivElement>(null);
48
48
  const [open = false, setIsOpen] = useValueControl({ value: openProp, onChange: onOpenChange });
49
49
 
50
- // Стек заходов во вложенные next-list'ы. Пустой корневой уровень.
51
- const [path, setPath] = useState<NextListItem[]>([]);
50
+ // Стек заходов во вложенные next-list'ы: индексный путь от корня на каждый уровень.
51
+ // Пустой корневой уровень. Хранится путь, а не сам айтем: айтемы пересобираются на каждом
52
+ // рендере, и сохранённая копия оставила бы открытый sheet в устаревшем состоянии.
53
+ const [path, setPath] = useState<number[][]>([]);
52
54
 
53
55
  const handleClose = () => {
54
56
  setIsOpen(false);
@@ -135,7 +137,11 @@ export function MobileDroplist({
135
137
  // Каскад sheet'ов: корень + по одному sheet'у на каждый заход в next-list. Каждый накладывается
136
138
  // поверх предыдущего (свой backdrop), а не заменяет его содержимое. Назад — закрывает верхний.
137
139
  const sheets = [{ source: items, title: label, item: undefined as NextListItem | undefined }].concat(
138
- path.map(item => ({ source: item.items, title: nextListOption(item), item })),
140
+ path.map(indexPath => {
141
+ const item = resolveItemByPath(items as Item[], indexPath);
142
+
143
+ return { source: (item?.items ?? []) as typeof items, title: nextListOption(item), item };
144
+ }),
139
145
  );
140
146
 
141
147
  return (
@@ -151,7 +157,8 @@ export function MobileDroplist({
151
157
  ? undefined
152
158
  : buildLevelItems(
153
159
  source as Item[],
154
- next => setPath(prev => [...prev.slice(0, levelIndex), next]),
160
+ relativePath =>
161
+ setPath(prev => [...prev.slice(0, levelIndex), [...(prev[levelIndex - 1] ?? []), ...relativePath]]),
155
162
  handleClose,
156
163
  closeOnClick,
157
164
  );
@@ -4,9 +4,32 @@ import { MouseEvent } from 'react';
4
4
  import { BaseItemProps, Item, NextListItem } from '../../components/Items';
5
5
  import { ITEM_TYPE } from '../../constants';
6
6
 
7
+ /**
8
+ * Айтем уровня по индексному пути от корня. Путь разрешается на каждом рендере по актуальным
9
+ * `items`: иначе открытый sheet сохранял бы копию айтема, полученную при заходе во вложенный
10
+ * список, и не отражал бы её последующие изменения (например, смену выбранной строки).
11
+ */
12
+ export function resolveItemByPath(items: Item[], indexPath: number[]): NextListItem | undefined {
13
+ let source: Item[] | undefined = items;
14
+ let node: Item | undefined;
15
+
16
+ for (const index of indexPath) {
17
+ node = source?.[index];
18
+
19
+ if (!node || typeof node !== 'object' || !('items' in node)) {
20
+ source = undefined;
21
+ continue;
22
+ }
23
+
24
+ source = node.items as Item[];
25
+ }
26
+
27
+ return node as NextListItem | undefined;
28
+ }
29
+
7
30
  /** Текст `label` next-list-айтема — заголовок шапки sheet'а при заходе во вложенный список. */
8
- export function nextListOption(item: NextListItem): string | undefined {
9
- const { content } = item;
31
+ export function nextListOption(item?: NextListItem): string | undefined {
32
+ const content = item?.content;
10
33
  if (content && typeof content === 'object' && 'label' in content) {
11
34
  return String((content as { label: string | number }).label);
12
35
  }
@@ -17,16 +40,20 @@ export function nextListOption(item: NextListItem): string | undefined {
17
40
  * Готовит айтемы текущего уровня sheet'а:
18
41
  * - `next-list` превращается в базовый айтем с шевроном `>`, клик по которому уводит на уровень вложенного
19
42
  * списка (drill-down), а не открывает второй BottomSheet (десктопный nested-popover на mobile неприменим);
43
+ * позиция айтема передаётся в `onDrill` индексным путём — уровень разрешается по актуальным `items`;
20
44
  * - `group` / `group-select` / `collapse` рекурсивно обрабатываются (внутри них тоже может быть `next-list`);
21
45
  * - базовые айтемы (action-меню без `selection`) при `closeOnClick` закрывают sheet по клику.
22
46
  */
23
47
  export function buildLevelItems(
24
48
  items: Item[],
25
- onDrill: (item: NextListItem) => void,
49
+ onDrill: (indexPath: number[]) => void,
26
50
  onClose: () => void,
27
51
  closeOnClick: boolean,
52
+ basePath: number[] = [],
28
53
  ): Item[] {
29
- return items.map(item => {
54
+ return items.map((item, index) => {
55
+ const indexPath = [...basePath, index];
56
+
30
57
  if (item && typeof item === 'object' && 'type' in item) {
31
58
  if (item.type === ITEM_TYPE.NextList) {
32
59
  const next = item;
@@ -49,13 +76,13 @@ export function buildLevelItems(
49
76
  afterContent: <ChevronRightSVG />,
50
77
  onClick: (event: MouseEvent<HTMLElement>) => {
51
78
  next.onClick?.(event);
52
- onDrill(next);
79
+ onDrill(indexPath);
53
80
  },
54
81
  };
55
82
  }
56
83
 
57
84
  if (item.type === ITEM_TYPE.Group || item.type === ITEM_TYPE.GroupSelect || item.type === ITEM_TYPE.Collapse) {
58
- return { ...item, items: buildLevelItems(item.items, onDrill, onClose, closeOnClick) };
85
+ return { ...item, items: buildLevelItems(item.items, onDrill, onClose, closeOnClick, indexPath) };
59
86
  }
60
87
  }
61
88