@cloud-ru/ds-list 2.0.4 → 2.0.5-preview-0a01eea8.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.4",
3
+ "version": "2.0.5-preview-0a01eea8.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.5",
53
- "@cloud-ru/ds-divider": "^1.0.1",
54
- "@cloud-ru/ds-button": "^1.0.1",
55
- "@cloud-ru/ds-drag-and-drop": "^1.1.1",
56
- "@cloud-ru/ds-dropdown": "^1.0.6",
57
- "@cloud-ru/ds-icons": "^1.1.0",
58
- "@cloud-ru/ds-info-block": "^1.0.1",
59
- "@cloud-ru/ds-loader": "^1.0.1",
60
- "@cloud-ru/ds-materials": "^1.0.1",
61
- "@cloud-ru/ds-portal-context": "^1.0.1",
62
- "@cloud-ru/ds-scroll": "^1.0.2",
63
- "@cloud-ru/ds-search-private": "^1.0.5",
64
- "@cloud-ru/ds-toggles": "^1.1.3",
65
- "@cloud-ru/ds-truncate-string": "^1.0.4",
66
- "@cloud-ru/ds-utils": "^1.1.0"
51
+ "@cloud-ru/ds-adaptive": "1.0.1",
52
+ "@cloud-ru/ds-divider": "1.0.1",
53
+ "@cloud-ru/ds-drag-and-drop": "1.1.1",
54
+ "@cloud-ru/ds-bottom-sheet": "1.0.6-preview-0a01eea8.0",
55
+ "@cloud-ru/ds-dropdown": "1.0.7-preview-0a01eea8.0",
56
+ "@cloud-ru/ds-loader": "1.0.1",
57
+ "@cloud-ru/ds-info-block": "1.0.1",
58
+ "@cloud-ru/ds-button": "1.0.1",
59
+ "@cloud-ru/ds-icons": "1.1.0",
60
+ "@cloud-ru/ds-materials": "1.0.1",
61
+ "@cloud-ru/ds-portal-context": "1.0.1",
62
+ "@cloud-ru/ds-scroll": "1.0.2",
63
+ "@cloud-ru/ds-search-private": "1.0.5",
64
+ "@cloud-ru/ds-utils": "1.1.0",
65
+ "@cloud-ru/ds-toggles": "1.1.3",
66
+ "@cloud-ru/ds-truncate-string": "1.0.4"
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
  }
@@ -1,4 +1,5 @@
1
1
  import { DragEndEvent } from '@dnd-kit/core';
2
+ import { BottomSheetProps } from '@cloud-ru/ds-bottom-sheet';
2
3
  import { DropdownProps } from '@cloud-ru/ds-dropdown';
3
4
  import { WithSupportProps } from '@cloud-ru/ds-utils';
4
5
  import { FocusEvent, KeyboardEvent, ReactNode, RefObject } from 'react';
@@ -182,7 +183,7 @@ type DroplistMobileSlots = {
182
183
  * (список size `l` в `BottomSheet`), иначе — `DesktopDroplist` (анкорный popover). Mobile-слоты
183
184
  * применяются только на mobile.
184
185
  */
185
- export type DroplistProps = BaseDroplistProps & DroplistMobileSlots;
186
+ export type DroplistProps = BaseDroplistProps & DroplistMobileSlots & Pick<BottomSheetProps, 'snapPoints'>;
186
187
 
187
188
  /**
188
189
  * Props адаптивного `ReorderableDroplist`: props `ReorderableList` + popover/mobile-обвязка
@@ -201,6 +202,7 @@ export type MobileDroplistProps = Omit<
201
202
  DesktopDroplistProps,
202
203
  'trigger' | 'placement' | 'widthStrategy' | 'triggerElemRef' | 'listRef' | 'triggerClassName'
203
204
  > &
205
+ Pick<BottomSheetProps, 'snapPoints'> &
204
206
  DroplistMobileSlots;
205
207
 
206
208
  export type ListPrivateProps = Omit<
@@ -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`.
@@ -35,6 +35,7 @@ export function MobileDroplist({
35
35
  container,
36
36
  closeOnPopstate,
37
37
  size,
38
+ snapPoints,
38
39
  // `footer` уводим в sticky-футер `BottomSheet` (кнопки действия прилипают к низу sheet'а, а не
39
40
  // скроллятся вместе со списком). `header` + `headerDivider` / `footerDivider` НЕ деструктурируем —
40
41
  // они уходят в `...rest` на корневой `List` (он рендерит шапку и её divider).
@@ -47,8 +48,10 @@ export function MobileDroplist({
47
48
  const scrollRef = useRef<HTMLDivElement>(null);
48
49
  const [open = false, setIsOpen] = useValueControl({ value: openProp, onChange: onOpenChange });
49
50
 
50
- // Стек заходов во вложенные next-list'ы. Пустой корневой уровень.
51
- const [path, setPath] = useState<NextListItem[]>([]);
51
+ // Стек заходов во вложенные next-list'ы: индексный путь от корня на каждый уровень.
52
+ // Пустой корневой уровень. Хранится путь, а не сам айтем: айтемы пересобираются на каждом
53
+ // рендере, и сохранённая копия оставила бы открытый sheet в устаревшем состоянии.
54
+ const [path, setPath] = useState<number[][]>([]);
52
55
 
53
56
  const handleClose = () => {
54
57
  setIsOpen(false);
@@ -135,7 +138,11 @@ export function MobileDroplist({
135
138
  // Каскад sheet'ов: корень + по одному sheet'у на каждый заход в next-list. Каждый накладывается
136
139
  // поверх предыдущего (свой backdrop), а не заменяет его содержимое. Назад — закрывает верхний.
137
140
  const sheets = [{ source: items, title: label, item: undefined as NextListItem | undefined }].concat(
138
- path.map(item => ({ source: item.items, title: nextListOption(item), item })),
141
+ path.map(indexPath => {
142
+ const item = resolveItemByPath(items as Item[], indexPath);
143
+
144
+ return { source: (item?.items ?? []) as typeof items, title: nextListOption(item), item };
145
+ }),
139
146
  );
140
147
 
141
148
  return (
@@ -151,7 +158,8 @@ export function MobileDroplist({
151
158
  ? undefined
152
159
  : buildLevelItems(
153
160
  source as Item[],
154
- next => setPath(prev => [...prev.slice(0, levelIndex), next]),
161
+ relativePath =>
162
+ setPath(prev => [...prev.slice(0, levelIndex), [...(prev[levelIndex - 1] ?? []), ...relativePath]]),
155
163
  handleClose,
156
164
  closeOnClick,
157
165
  );
@@ -220,7 +228,7 @@ export function MobileDroplist({
220
228
  slotAfterTitle={isRoot ? slotAfterTitle : undefined}
221
229
  content={content}
222
230
  footer={isRoot ? footer : undefined}
223
- snapPoints={expanded ? [1] : undefined}
231
+ snapPoints={expanded ? [1] : snapPoints}
224
232
  closeOnPopstate={closeOnPopstate ?? true}
225
233
  />
226
234
  );
@@ -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