@cloud-ru/ds-list 2.0.4 → 2.0.5-preview-9b666394.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/README.md +1 -2
- package/dist/cjs/components/Lists/types.d.ts +3 -2
- package/dist/cjs/helperComponents/MobileDroplist/MobileDroplist.d.ts +1 -1
- package/dist/cjs/helperComponents/MobileDroplist/MobileDroplist.js +10 -5
- package/dist/cjs/helperComponents/MobileDroplist/utils.d.ts +9 -2
- package/dist/cjs/helperComponents/MobileDroplist/utils.js +26 -5
- package/dist/esm/components/Lists/types.d.ts +3 -2
- package/dist/esm/helperComponents/MobileDroplist/MobileDroplist.d.ts +1 -1
- package/dist/esm/helperComponents/MobileDroplist/MobileDroplist.js +11 -6
- package/dist/esm/helperComponents/MobileDroplist/utils.d.ts +9 -2
- package/dist/esm/helperComponents/MobileDroplist/utils.js +25 -5
- package/dist/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/tsconfig.esm.tsbuildinfo +1 -1
- package/package.json +18 -18
- package/src/components/Lists/types.ts +3 -1
- package/src/helperComponents/MobileDroplist/MobileDroplist.tsx +14 -6
- package/src/helperComponents/MobileDroplist/utils.tsx +33 -6
package/README.md
CHANGED
|
@@ -900,6 +900,7 @@ export function DroplistWithHeader() {
|
|
|
900
900
|
| `selection` | `SelectionMultipleState` \| `SelectionSingleState` | — | Настройки выбора элементов. `mode: 'single'` — один выбранный элемент (`value: ItemId`), <br/> `mode: 'multiple'` — множественный выбор (`value: ItemId[]`). Без `selection` выбора нет — <br/> клик вызывает только `onClick` элемента. |
|
|
901
901
|
| `size` | `"l"` \| `"m"` \| `"s"` | — | Размер списка |
|
|
902
902
|
| `slotAfterTitle` | `ReactNode` | — | Только mobile (`BottomSheet`): slot справа от заголовка. |
|
|
903
|
+
| `snapPoints` | `SnapPoint` | — | Массив фиксированных позиций sheet'а от меньшей к большей. По дефолту `undefined` — <br/> sheet `height: auto` с одним snap'ом по высоте контента. <br/> Пример: `[0.5, 1]` — sheet открывается на половину экрана, drag вверх раскрывает <br/> до full-viewport; drag вниз ниже `0.5` ведёт к закрытию. <br/> Контракт массива (движок не сортирует и не дедуплицирует — порядок и различимость на <br/> стороне потребителя): <br/> - строго по возрастанию: индекс `0` — самая компактная позиция, последний — top / expanded; <br/> - значения должны резолвиться в различные высоты (`['50%', 0.5]` на типичном вьюпорте дадут <br/> одну высоту → дубль-индекс будет недостижим свайпом); <br/> - `'fit-content'` имеет смысл только как ЕДИНСТВЕННЫЙ snap (без `snapPoints`); внутри массива <br/> фиксированных позиций его «контентная» высота не определена. |
|
|
903
904
|
| `trigger` | `"click"` \| `"clickAndFocusVisible"` \| `"focus"` \| `"focusVisible"` \| `"hover"` \| `"hoverAndFocus"` \| `"hoverAndFocusVisible"` | — | Условие отображения поповера: <br/> - `click` - открывать по клику <br/> - `hover` - открывать по ховеру <br/> - `focusVisible` - открывать по focus-visible <br/> - `focus` - открывать по фокусу <br/> - `hoverAndFocusVisible` - открывать по ховеру и focus-visible <br/> - `hoverAndFocus` - открывать по ховеру и фокусу <br/> - `clickAndFocusVisible` - открывать по клику и focus-visible |
|
|
904
905
|
| `triggerClassName` | `string` | — | CSS-класс триггера |
|
|
905
906
|
| `triggerElemRef` | `RefObject<HTMLElement>` | — | Ссылка на элемент-триггер для дроплиста |
|
|
@@ -962,8 +963,6 @@ export function DroplistWithHeader() {
|
|
|
962
963
|
|
|
963
964
|
- `Item` = `BaseItem | GroupItem | GroupSelectItem | NextListItem | AccordionItem`
|
|
964
965
|
|
|
965
|
-
- `ItemId` = `string | number`
|
|
966
|
-
|
|
967
966
|
**ScrollProps**
|
|
968
967
|
|
|
969
968
|
| Prop | Type | Default | Description |
|
|
@@ -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';
|
|
@@ -161,7 +162,7 @@ type DroplistMobileSlots = {
|
|
|
161
162
|
* (список size `l` в `BottomSheet`), иначе — `DesktopDroplist` (анкорный popover). Mobile-слоты
|
|
162
163
|
* применяются только на mobile.
|
|
163
164
|
*/
|
|
164
|
-
export type DroplistProps = BaseDroplistProps & DroplistMobileSlots
|
|
165
|
+
export type DroplistProps = BaseDroplistProps & DroplistMobileSlots & Pick<BottomSheetProps, 'snapPoints'>;
|
|
165
166
|
/**
|
|
166
167
|
* Props адаптивного `ReorderableDroplist`: props `ReorderableList` + popover/mobile-обвязка
|
|
167
168
|
* `Droplist`. Отдельный компонент по той же причине, что и `ReorderableList`.
|
|
@@ -173,7 +174,7 @@ export type DroplistImplProps = DesktopDroplistProps & DroplistMobileSlots;
|
|
|
173
174
|
* @internal Props mobile-реализации (`BottomSheet`): реализация дроплиста без popover-пропов
|
|
174
175
|
* + mobile-слоты.
|
|
175
176
|
*/
|
|
176
|
-
export type MobileDroplistProps = Omit<DesktopDroplistProps, 'trigger' | 'placement' | 'widthStrategy' | 'triggerElemRef' | 'listRef' | 'triggerClassName'> & DroplistMobileSlots;
|
|
177
|
+
export type MobileDroplistProps = Omit<DesktopDroplistProps, 'trigger' | 'placement' | 'widthStrategy' | 'triggerElemRef' | 'listRef' | 'triggerClassName'> & Pick<BottomSheetProps, 'snapPoints'> & DroplistMobileSlots;
|
|
177
178
|
export type ListPrivateProps = Omit<ListImplProps, 'pinTop' | 'pinBottom' | 'items' | 'hasListInFocusChain' | 'onItemsReorder' | 'virtualized'> & {
|
|
178
179
|
nested?: boolean;
|
|
179
180
|
active?: boolean;
|
|
@@ -6,4 +6,4 @@ import { MobileDroplistProps } from '../../components/Lists/types';
|
|
|
6
6
|
* вложенного списка, в шапке появляется его заголовок и кнопка «назад». Internal — наружу не реэкспортится;
|
|
7
7
|
* рендерится адаптивным `Droplist` на `layoutType='mobile'`.
|
|
8
8
|
*/
|
|
9
|
-
export declare function MobileDroplist({ items, selection, open: openProp, onOpenChange, children, search, label, actionButton, slotAfterTitle, onBackButtonClick, virtualized, closeDroplistOnItemClick, scroll, container, closeOnPopstate, size, footer, onItemsReorder, ...rest }: MobileDroplistProps): import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
export declare function MobileDroplist({ items, selection, open: openProp, onOpenChange, children, search, label, actionButton, slotAfterTitle, onBackButtonClick, virtualized, closeDroplistOnItemClick, scroll, container, closeOnPopstate, size, snapPoints, footer, onItemsReorder, ...rest }: MobileDroplistProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -20,7 +20,7 @@ const utils_2 = require("./utils");
|
|
|
20
20
|
* вложенного списка, в шапке появляется его заголовок и кнопка «назад». Internal — наружу не реэкспортится;
|
|
21
21
|
* рендерится адаптивным `Droplist` на `layoutType='mobile'`.
|
|
22
22
|
*/
|
|
23
|
-
function MobileDroplist({ items, selection, open: openProp, onOpenChange, children, search, label, actionButton, slotAfterTitle, onBackButtonClick, virtualized, closeDroplistOnItemClick, scroll, container, closeOnPopstate, size,
|
|
23
|
+
function MobileDroplist({ items, selection, open: openProp, onOpenChange, children, search, label, actionButton, slotAfterTitle, onBackButtonClick, virtualized, closeDroplistOnItemClick, scroll, container, closeOnPopstate, size, snapPoints,
|
|
24
24
|
// `footer` уводим в sticky-футер `BottomSheet` (кнопки действия прилипают к низу sheet'а, а не
|
|
25
25
|
// скроллятся вместе со списком). `header` + `headerDivider` / `footerDivider` НЕ деструктурируем —
|
|
26
26
|
// они уходят в `...rest` на корневой `List` (он рендерит шапку и её divider).
|
|
@@ -29,7 +29,9 @@ footer, onItemsReorder, ...rest }) {
|
|
|
29
29
|
const portalContext = (0, portal_context_1.usePortalContext)();
|
|
30
30
|
const scrollRef = (0, react_1.useRef)(null);
|
|
31
31
|
const [open = false, setIsOpen] = (0, utils_1.useValueControl)({ value: openProp, onChange: onOpenChange });
|
|
32
|
-
// Стек заходов во вложенные next-list'
|
|
32
|
+
// Стек заходов во вложенные next-list'ы: индексный путь от корня на каждый уровень.
|
|
33
|
+
// Пустой — корневой уровень. Хранится путь, а не сам айтем: айтемы пересобираются на каждом
|
|
34
|
+
// рендере, и сохранённая копия оставила бы открытый sheet в устаревшем состоянии.
|
|
33
35
|
const [path, setPath] = (0, react_1.useState)([]);
|
|
34
36
|
const handleClose = () => {
|
|
35
37
|
setIsOpen(false);
|
|
@@ -91,7 +93,10 @@ footer, onItemsReorder, ...rest }) {
|
|
|
91
93
|
const containerRef = portalContext.current ?? undefined;
|
|
92
94
|
// Каскад sheet'ов: корень + по одному sheet'у на каждый заход в next-list. Каждый накладывается
|
|
93
95
|
// поверх предыдущего (свой backdrop), а не заменяет его содержимое. Назад — закрывает верхний.
|
|
94
|
-
const sheets = [{ source: items, title: label, item: undefined }].concat(path.map(
|
|
96
|
+
const sheets = [{ source: items, title: label, item: undefined }].concat(path.map(indexPath => {
|
|
97
|
+
const item = (0, utils_2.resolveItemByPath)(items, indexPath);
|
|
98
|
+
return { source: (item?.items ?? []), title: (0, utils_2.nextListOption)(item), item };
|
|
99
|
+
}));
|
|
95
100
|
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [trigger, sheets.map(({ source, title, item }, levelIndex) => {
|
|
96
101
|
const isRoot = levelIndex === 0;
|
|
97
102
|
// Режим переупорядочивания (`onItemsReorder`) — только корень: дерево
|
|
@@ -100,7 +105,7 @@ footer, onItemsReorder, ...rest }) {
|
|
|
100
105
|
const reorderRoot = Boolean(isRoot && onItemsReorder);
|
|
101
106
|
const levelItems = reorderRoot
|
|
102
107
|
? undefined
|
|
103
|
-
: (0, utils_2.buildLevelItems)(source,
|
|
108
|
+
: (0, utils_2.buildLevelItems)(source, relativePath => setPath(prev => [...prev.slice(0, levelIndex), [...(prev[levelIndex - 1] ?? []), ...relativePath]]), handleClose, closeOnClick);
|
|
104
109
|
// Поиск/виртуализация/footer/actions — только на корне; sublists всегда статичны по контенту.
|
|
105
110
|
const searchable = isRoot && Boolean(search);
|
|
106
111
|
const expanded = searchable || (isRoot && virtualized);
|
|
@@ -138,6 +143,6 @@ footer, onItemsReorder, ...rest }) {
|
|
|
138
143
|
container: container?.current ?? containerRef, open: isRoot ? open : true,
|
|
139
144
|
// Dismiss (backdrop / swipe / Esc): на корне — закрыть весь droplist; глубже — вернуться на уровень вверх,
|
|
140
145
|
// как ждёт пользователь на mobile (а не закрывать всю цепочку разом).
|
|
141
|
-
onClose: isRoot ? handleClose : popLevel, withDividers: true, title: title, onBackButtonClick: handleBack, actionButton: isRoot ? actionButton : undefined, slotAfterTitle: isRoot ? slotAfterTitle : undefined, content: content, footer: isRoot ? footer : undefined, snapPoints: expanded ? [1] :
|
|
146
|
+
onClose: isRoot ? handleClose : popLevel, withDividers: true, title: title, onBackButtonClick: handleBack, actionButton: isRoot ? actionButton : undefined, slotAfterTitle: isRoot ? slotAfterTitle : undefined, content: content, footer: isRoot ? footer : undefined, snapPoints: expanded ? [1] : snapPoints, closeOnPopstate: closeOnPopstate ?? true }, isRoot ? 'root' : String(item?.id ?? levelIndex)));
|
|
142
147
|
})] }));
|
|
143
148
|
}
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import { Item, NextListItem } from '../../components/Items';
|
|
2
|
+
/**
|
|
3
|
+
* Айтем уровня по индексному пути от корня. Путь разрешается на каждом рендере по актуальным
|
|
4
|
+
* `items`: иначе открытый sheet сохранял бы копию айтема, полученную при заходе во вложенный
|
|
5
|
+
* список, и не отражал бы её последующие изменения (например, смену выбранной строки).
|
|
6
|
+
*/
|
|
7
|
+
export declare function resolveItemByPath(items: Item[], indexPath: number[]): NextListItem | undefined;
|
|
2
8
|
/** Текст `label` next-list-айтема — заголовок шапки sheet'а при заходе во вложенный список. */
|
|
3
|
-
export declare function nextListOption(item
|
|
9
|
+
export declare function nextListOption(item?: NextListItem): string | undefined;
|
|
4
10
|
/**
|
|
5
11
|
* Готовит айтемы текущего уровня sheet'а:
|
|
6
12
|
* - `next-list` превращается в базовый айтем с шевроном `>`, клик по которому уводит на уровень вложенного
|
|
7
13
|
* списка (drill-down), а не открывает второй BottomSheet (десктопный nested-popover на mobile неприменим);
|
|
14
|
+
* позиция айтема передаётся в `onDrill` индексным путём — уровень разрешается по актуальным `items`;
|
|
8
15
|
* - `group` / `group-select` / `collapse` рекурсивно обрабатываются (внутри них тоже может быть `next-list`);
|
|
9
16
|
* - базовые айтемы (action-меню без `selection`) при `closeOnClick` закрывают sheet по клику.
|
|
10
17
|
*/
|
|
11
|
-
export declare function buildLevelItems(items: Item[], onDrill: (
|
|
18
|
+
export declare function buildLevelItems(items: Item[], onDrill: (indexPath: number[]) => void, onClose: () => void, closeOnClick: boolean, basePath?: number[]): Item[];
|
|
@@ -1,13 +1,32 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveItemByPath = resolveItemByPath;
|
|
3
4
|
exports.nextListOption = nextListOption;
|
|
4
5
|
exports.buildLevelItems = buildLevelItems;
|
|
5
6
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
6
7
|
const system_1 = require("@cloud-ru/ds-icons/interface/system");
|
|
7
8
|
const constants_1 = require("../../constants");
|
|
9
|
+
/**
|
|
10
|
+
* Айтем уровня по индексному пути от корня. Путь разрешается на каждом рендере по актуальным
|
|
11
|
+
* `items`: иначе открытый sheet сохранял бы копию айтема, полученную при заходе во вложенный
|
|
12
|
+
* список, и не отражал бы её последующие изменения (например, смену выбранной строки).
|
|
13
|
+
*/
|
|
14
|
+
function resolveItemByPath(items, indexPath) {
|
|
15
|
+
let source = items;
|
|
16
|
+
let node;
|
|
17
|
+
for (const index of indexPath) {
|
|
18
|
+
node = source?.[index];
|
|
19
|
+
if (!node || typeof node !== 'object' || !('items' in node)) {
|
|
20
|
+
source = undefined;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
source = node.items;
|
|
24
|
+
}
|
|
25
|
+
return node;
|
|
26
|
+
}
|
|
8
27
|
/** Текст `label` next-list-айтема — заголовок шапки sheet'а при заходе во вложенный список. */
|
|
9
28
|
function nextListOption(item) {
|
|
10
|
-
const
|
|
29
|
+
const content = item?.content;
|
|
11
30
|
if (content && typeof content === 'object' && 'label' in content) {
|
|
12
31
|
return String(content.label);
|
|
13
32
|
}
|
|
@@ -17,11 +36,13 @@ function nextListOption(item) {
|
|
|
17
36
|
* Готовит айтемы текущего уровня sheet'а:
|
|
18
37
|
* - `next-list` превращается в базовый айтем с шевроном `>`, клик по которому уводит на уровень вложенного
|
|
19
38
|
* списка (drill-down), а не открывает второй BottomSheet (десктопный nested-popover на mobile неприменим);
|
|
39
|
+
* позиция айтема передаётся в `onDrill` индексным путём — уровень разрешается по актуальным `items`;
|
|
20
40
|
* - `group` / `group-select` / `collapse` рекурсивно обрабатываются (внутри них тоже может быть `next-list`);
|
|
21
41
|
* - базовые айтемы (action-меню без `selection`) при `closeOnClick` закрывают sheet по клику.
|
|
22
42
|
*/
|
|
23
|
-
function buildLevelItems(items, onDrill, onClose, closeOnClick) {
|
|
24
|
-
return items.map(item => {
|
|
43
|
+
function buildLevelItems(items, onDrill, onClose, closeOnClick, basePath = []) {
|
|
44
|
+
return items.map((item, index) => {
|
|
45
|
+
const indexPath = [...basePath, index];
|
|
25
46
|
if (item && typeof item === 'object' && 'type' in item) {
|
|
26
47
|
if (item.type === constants_1.ITEM_TYPE.NextList) {
|
|
27
48
|
const next = item;
|
|
@@ -42,12 +63,12 @@ function buildLevelItems(items, onDrill, onClose, closeOnClick) {
|
|
|
42
63
|
afterContent: (0, jsx_runtime_1.jsx)(system_1.ChevronRightSVG, {}),
|
|
43
64
|
onClick: (event) => {
|
|
44
65
|
next.onClick?.(event);
|
|
45
|
-
onDrill(
|
|
66
|
+
onDrill(indexPath);
|
|
46
67
|
},
|
|
47
68
|
};
|
|
48
69
|
}
|
|
49
70
|
if (item.type === constants_1.ITEM_TYPE.Group || item.type === constants_1.ITEM_TYPE.GroupSelect || item.type === constants_1.ITEM_TYPE.Collapse) {
|
|
50
|
-
return { ...item, items: buildLevelItems(item.items, onDrill, onClose, closeOnClick) };
|
|
71
|
+
return { ...item, items: buildLevelItems(item.items, onDrill, onClose, closeOnClick, indexPath) };
|
|
51
72
|
}
|
|
52
73
|
}
|
|
53
74
|
if (closeOnClick && item && typeof item === 'object' && 'onClick' in item) {
|
|
@@ -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';
|
|
@@ -161,7 +162,7 @@ type DroplistMobileSlots = {
|
|
|
161
162
|
* (список size `l` в `BottomSheet`), иначе — `DesktopDroplist` (анкорный popover). Mobile-слоты
|
|
162
163
|
* применяются только на mobile.
|
|
163
164
|
*/
|
|
164
|
-
export type DroplistProps = BaseDroplistProps & DroplistMobileSlots
|
|
165
|
+
export type DroplistProps = BaseDroplistProps & DroplistMobileSlots & Pick<BottomSheetProps, 'snapPoints'>;
|
|
165
166
|
/**
|
|
166
167
|
* Props адаптивного `ReorderableDroplist`: props `ReorderableList` + popover/mobile-обвязка
|
|
167
168
|
* `Droplist`. Отдельный компонент по той же причине, что и `ReorderableList`.
|
|
@@ -173,7 +174,7 @@ export type DroplistImplProps = DesktopDroplistProps & DroplistMobileSlots;
|
|
|
173
174
|
* @internal Props mobile-реализации (`BottomSheet`): реализация дроплиста без popover-пропов
|
|
174
175
|
* + mobile-слоты.
|
|
175
176
|
*/
|
|
176
|
-
export type MobileDroplistProps = Omit<DesktopDroplistProps, 'trigger' | 'placement' | 'widthStrategy' | 'triggerElemRef' | 'listRef' | 'triggerClassName'> & DroplistMobileSlots;
|
|
177
|
+
export type MobileDroplistProps = Omit<DesktopDroplistProps, 'trigger' | 'placement' | 'widthStrategy' | 'triggerElemRef' | 'listRef' | 'triggerClassName'> & Pick<BottomSheetProps, 'snapPoints'> & DroplistMobileSlots;
|
|
177
178
|
export type ListPrivateProps = Omit<ListImplProps, 'pinTop' | 'pinBottom' | 'items' | 'hasListInFocusChain' | 'onItemsReorder' | 'virtualized'> & {
|
|
178
179
|
nested?: boolean;
|
|
179
180
|
active?: boolean;
|
|
@@ -6,4 +6,4 @@ import { MobileDroplistProps } from '../../components/Lists/types.js';
|
|
|
6
6
|
* вложенного списка, в шапке появляется его заголовок и кнопка «назад». Internal — наружу не реэкспортится;
|
|
7
7
|
* рендерится адаптивным `Droplist` на `layoutType='mobile'`.
|
|
8
8
|
*/
|
|
9
|
-
export declare function MobileDroplist({ items, selection, open: openProp, onOpenChange, children, search, label, actionButton, slotAfterTitle, onBackButtonClick, virtualized, closeDroplistOnItemClick, scroll, container, closeOnPopstate, size, footer, onItemsReorder, ...rest }: MobileDroplistProps): import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
export declare function MobileDroplist({ items, selection, open: openProp, onOpenChange, children, search, label, actionButton, slotAfterTitle, onBackButtonClick, virtualized, closeDroplistOnItemClick, scroll, container, closeOnPopstate, size, snapPoints, footer, onItemsReorder, ...rest }: MobileDroplistProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -6,7 +6,7 @@ import { cloneElement, isValidElement, useMemo, useRef, useState } from 'react';
|
|
|
6
6
|
import { List, ReorderableList } from '../../components/Lists/List/index.js';
|
|
7
7
|
import { TEST_IDS } from '../../constants.js';
|
|
8
8
|
import styles from './MobileDroplist.module.css';
|
|
9
|
-
import { buildLevelItems, nextListOption } from './utils.js';
|
|
9
|
+
import { buildLevelItems, nextListOption, resolveItemByPath } from './utils.js';
|
|
10
10
|
/**
|
|
11
11
|
* Mobile-вариант `Droplist`: рендерит `List` (в переданном `size`) внутри `BottomSheet` из `@cloud-ru/ds-bottom-sheet`.
|
|
12
12
|
* Триггер (`children`) клонируется для открытия sheet'а по клику/Enter/Space. `next-list`-айтемы на mobile
|
|
@@ -14,7 +14,7 @@ import { buildLevelItems, nextListOption } from './utils.js';
|
|
|
14
14
|
* вложенного списка, в шапке появляется его заголовок и кнопка «назад». Internal — наружу не реэкспортится;
|
|
15
15
|
* рендерится адаптивным `Droplist` на `layoutType='mobile'`.
|
|
16
16
|
*/
|
|
17
|
-
export function MobileDroplist({ items, selection, open: openProp, onOpenChange, children, search, label, actionButton, slotAfterTitle, onBackButtonClick, virtualized, closeDroplistOnItemClick, scroll, container, closeOnPopstate, size,
|
|
17
|
+
export function MobileDroplist({ items, selection, open: openProp, onOpenChange, children, search, label, actionButton, slotAfterTitle, onBackButtonClick, virtualized, closeDroplistOnItemClick, scroll, container, closeOnPopstate, size, snapPoints,
|
|
18
18
|
// `footer` уводим в sticky-футер `BottomSheet` (кнопки действия прилипают к низу sheet'а, а не
|
|
19
19
|
// скроллятся вместе со списком). `header` + `headerDivider` / `footerDivider` НЕ деструктурируем —
|
|
20
20
|
// они уходят в `...rest` на корневой `List` (он рендерит шапку и её divider).
|
|
@@ -23,7 +23,9 @@ footer, onItemsReorder, ...rest }) {
|
|
|
23
23
|
const portalContext = usePortalContext();
|
|
24
24
|
const scrollRef = useRef(null);
|
|
25
25
|
const [open = false, setIsOpen] = useValueControl({ value: openProp, onChange: onOpenChange });
|
|
26
|
-
// Стек заходов во вложенные next-list'
|
|
26
|
+
// Стек заходов во вложенные next-list'ы: индексный путь от корня на каждый уровень.
|
|
27
|
+
// Пустой — корневой уровень. Хранится путь, а не сам айтем: айтемы пересобираются на каждом
|
|
28
|
+
// рендере, и сохранённая копия оставила бы открытый sheet в устаревшем состоянии.
|
|
27
29
|
const [path, setPath] = useState([]);
|
|
28
30
|
const handleClose = () => {
|
|
29
31
|
setIsOpen(false);
|
|
@@ -85,7 +87,10 @@ footer, onItemsReorder, ...rest }) {
|
|
|
85
87
|
const containerRef = portalContext.current ?? undefined;
|
|
86
88
|
// Каскад sheet'ов: корень + по одному sheet'у на каждый заход в next-list. Каждый накладывается
|
|
87
89
|
// поверх предыдущего (свой backdrop), а не заменяет его содержимое. Назад — закрывает верхний.
|
|
88
|
-
const sheets = [{ source: items, title: label, item: undefined }].concat(path.map(
|
|
90
|
+
const sheets = [{ source: items, title: label, item: undefined }].concat(path.map(indexPath => {
|
|
91
|
+
const item = resolveItemByPath(items, indexPath);
|
|
92
|
+
return { source: (item?.items ?? []), title: nextListOption(item), item };
|
|
93
|
+
}));
|
|
89
94
|
return (_jsxs(_Fragment, { children: [trigger, sheets.map(({ source, title, item }, levelIndex) => {
|
|
90
95
|
const isRoot = levelIndex === 0;
|
|
91
96
|
// Режим переупорядочивания (`onItemsReorder`) — только корень: дерево
|
|
@@ -94,7 +99,7 @@ footer, onItemsReorder, ...rest }) {
|
|
|
94
99
|
const reorderRoot = Boolean(isRoot && onItemsReorder);
|
|
95
100
|
const levelItems = reorderRoot
|
|
96
101
|
? undefined
|
|
97
|
-
: buildLevelItems(source,
|
|
102
|
+
: buildLevelItems(source, relativePath => setPath(prev => [...prev.slice(0, levelIndex), [...(prev[levelIndex - 1] ?? []), ...relativePath]]), handleClose, closeOnClick);
|
|
98
103
|
// Поиск/виртуализация/footer/actions — только на корне; sublists всегда статичны по контенту.
|
|
99
104
|
const searchable = isRoot && Boolean(search);
|
|
100
105
|
const expanded = searchable || (isRoot && virtualized);
|
|
@@ -132,6 +137,6 @@ footer, onItemsReorder, ...rest }) {
|
|
|
132
137
|
container: container?.current ?? containerRef, open: isRoot ? open : true,
|
|
133
138
|
// Dismiss (backdrop / swipe / Esc): на корне — закрыть весь droplist; глубже — вернуться на уровень вверх,
|
|
134
139
|
// как ждёт пользователь на mobile (а не закрывать всю цепочку разом).
|
|
135
|
-
onClose: isRoot ? handleClose : popLevel, withDividers: true, title: title, onBackButtonClick: handleBack, actionButton: isRoot ? actionButton : undefined, slotAfterTitle: isRoot ? slotAfterTitle : undefined, content: content, footer: isRoot ? footer : undefined, snapPoints: expanded ? [1] :
|
|
140
|
+
onClose: isRoot ? handleClose : popLevel, withDividers: true, title: title, onBackButtonClick: handleBack, actionButton: isRoot ? actionButton : undefined, slotAfterTitle: isRoot ? slotAfterTitle : undefined, content: content, footer: isRoot ? footer : undefined, snapPoints: expanded ? [1] : snapPoints, closeOnPopstate: closeOnPopstate ?? true }, isRoot ? 'root' : String(item?.id ?? levelIndex)));
|
|
136
141
|
})] }));
|
|
137
142
|
}
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import { Item, NextListItem } from '../../components/Items/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Айтем уровня по индексному пути от корня. Путь разрешается на каждом рендере по актуальным
|
|
4
|
+
* `items`: иначе открытый sheet сохранял бы копию айтема, полученную при заходе во вложенный
|
|
5
|
+
* список, и не отражал бы её последующие изменения (например, смену выбранной строки).
|
|
6
|
+
*/
|
|
7
|
+
export declare function resolveItemByPath(items: Item[], indexPath: number[]): NextListItem | undefined;
|
|
2
8
|
/** Текст `label` next-list-айтема — заголовок шапки sheet'а при заходе во вложенный список. */
|
|
3
|
-
export declare function nextListOption(item
|
|
9
|
+
export declare function nextListOption(item?: NextListItem): string | undefined;
|
|
4
10
|
/**
|
|
5
11
|
* Готовит айтемы текущего уровня sheet'а:
|
|
6
12
|
* - `next-list` превращается в базовый айтем с шевроном `>`, клик по которому уводит на уровень вложенного
|
|
7
13
|
* списка (drill-down), а не открывает второй BottomSheet (десктопный nested-popover на mobile неприменим);
|
|
14
|
+
* позиция айтема передаётся в `onDrill` индексным путём — уровень разрешается по актуальным `items`;
|
|
8
15
|
* - `group` / `group-select` / `collapse` рекурсивно обрабатываются (внутри них тоже может быть `next-list`);
|
|
9
16
|
* - базовые айтемы (action-меню без `selection`) при `closeOnClick` закрывают sheet по клику.
|
|
10
17
|
*/
|
|
11
|
-
export declare function buildLevelItems(items: Item[], onDrill: (
|
|
18
|
+
export declare function buildLevelItems(items: Item[], onDrill: (indexPath: number[]) => void, onClose: () => void, closeOnClick: boolean, basePath?: number[]): Item[];
|
|
@@ -1,9 +1,27 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { ChevronRightSVG } from '@cloud-ru/ds-icons/interface/system';
|
|
3
3
|
import { ITEM_TYPE } from '../../constants.js';
|
|
4
|
+
/**
|
|
5
|
+
* Айтем уровня по индексному пути от корня. Путь разрешается на каждом рендере по актуальным
|
|
6
|
+
* `items`: иначе открытый sheet сохранял бы копию айтема, полученную при заходе во вложенный
|
|
7
|
+
* список, и не отражал бы её последующие изменения (например, смену выбранной строки).
|
|
8
|
+
*/
|
|
9
|
+
export function resolveItemByPath(items, indexPath) {
|
|
10
|
+
let source = items;
|
|
11
|
+
let node;
|
|
12
|
+
for (const index of indexPath) {
|
|
13
|
+
node = source?.[index];
|
|
14
|
+
if (!node || typeof node !== 'object' || !('items' in node)) {
|
|
15
|
+
source = undefined;
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
source = node.items;
|
|
19
|
+
}
|
|
20
|
+
return node;
|
|
21
|
+
}
|
|
4
22
|
/** Текст `label` next-list-айтема — заголовок шапки sheet'а при заходе во вложенный список. */
|
|
5
23
|
export function nextListOption(item) {
|
|
6
|
-
const
|
|
24
|
+
const content = item?.content;
|
|
7
25
|
if (content && typeof content === 'object' && 'label' in content) {
|
|
8
26
|
return String(content.label);
|
|
9
27
|
}
|
|
@@ -13,11 +31,13 @@ export function nextListOption(item) {
|
|
|
13
31
|
* Готовит айтемы текущего уровня sheet'а:
|
|
14
32
|
* - `next-list` превращается в базовый айтем с шевроном `>`, клик по которому уводит на уровень вложенного
|
|
15
33
|
* списка (drill-down), а не открывает второй BottomSheet (десктопный nested-popover на mobile неприменим);
|
|
34
|
+
* позиция айтема передаётся в `onDrill` индексным путём — уровень разрешается по актуальным `items`;
|
|
16
35
|
* - `group` / `group-select` / `collapse` рекурсивно обрабатываются (внутри них тоже может быть `next-list`);
|
|
17
36
|
* - базовые айтемы (action-меню без `selection`) при `closeOnClick` закрывают sheet по клику.
|
|
18
37
|
*/
|
|
19
|
-
export function buildLevelItems(items, onDrill, onClose, closeOnClick) {
|
|
20
|
-
return items.map(item => {
|
|
38
|
+
export function buildLevelItems(items, onDrill, onClose, closeOnClick, basePath = []) {
|
|
39
|
+
return items.map((item, index) => {
|
|
40
|
+
const indexPath = [...basePath, index];
|
|
21
41
|
if (item && typeof item === 'object' && 'type' in item) {
|
|
22
42
|
if (item.type === ITEM_TYPE.NextList) {
|
|
23
43
|
const next = item;
|
|
@@ -38,12 +58,12 @@ export function buildLevelItems(items, onDrill, onClose, closeOnClick) {
|
|
|
38
58
|
afterContent: _jsx(ChevronRightSVG, {}),
|
|
39
59
|
onClick: (event) => {
|
|
40
60
|
next.onClick?.(event);
|
|
41
|
-
onDrill(
|
|
61
|
+
onDrill(indexPath);
|
|
42
62
|
},
|
|
43
63
|
};
|
|
44
64
|
}
|
|
45
65
|
if (item.type === ITEM_TYPE.Group || item.type === ITEM_TYPE.GroupSelect || item.type === ITEM_TYPE.Collapse) {
|
|
46
|
-
return { ...item, items: buildLevelItems(item.items, onDrill, onClose, closeOnClick) };
|
|
66
|
+
return { ...item, items: buildLevelItems(item.items, onDrill, onClose, closeOnClick, indexPath) };
|
|
47
67
|
}
|
|
48
68
|
}
|
|
49
69
|
if (closeOnClick && item && typeof item === 'object' && 'onClick' in item) {
|