@cloud-ru/ds-list 2.1.5 → 2.1.6-preview-8b48fca0.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 +85 -1
- package/dist/cjs/components/Items/AccordionItem/AccordionItem.js +3 -3
- package/dist/cjs/components/Items/BaseItem/BaseItem.d.ts +10 -4
- package/dist/cjs/components/Items/BaseItem/BaseItem.js +17 -5
- package/dist/cjs/components/Items/BaseItem/styles.module.css +4 -0
- package/dist/cjs/components/Lists/List/List.js +1 -1
- package/dist/cjs/components/Lists/contexts/CollapseProvider.d.ts +11 -0
- package/dist/cjs/constants.d.ts +5 -0
- package/dist/cjs/constants.js +6 -1
- package/dist/cjs/helperComponents/DesktopDroplist/DesktopDroplist.js +1 -1
- package/dist/cjs/style.css +4 -0
- package/dist/cjs/types.d.ts +3 -1
- package/dist/esm/components/Items/AccordionItem/AccordionItem.js +4 -4
- package/dist/esm/components/Items/BaseItem/BaseItem.d.ts +10 -4
- package/dist/esm/components/Items/BaseItem/BaseItem.js +17 -5
- package/dist/esm/components/Items/BaseItem/styles.module.css +4 -0
- package/dist/esm/components/Lists/List/List.js +1 -1
- package/dist/esm/components/Lists/contexts/CollapseProvider.d.ts +11 -0
- package/dist/esm/constants.d.ts +5 -0
- package/dist/esm/constants.js +5 -0
- package/dist/esm/helperComponents/DesktopDroplist/DesktopDroplist.js +1 -1
- package/dist/esm/style.css +4 -0
- package/dist/esm/types.d.ts +3 -1
- package/dist/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/tsconfig.esm.tsbuildinfo +1 -1
- package/package.json +18 -18
- package/src/components/Items/AccordionItem/AccordionItem.tsx +9 -3
- package/src/components/Items/BaseItem/BaseItem.tsx +27 -5
- package/src/components/Items/BaseItem/styles.module.scss +10 -2
- package/src/components/Lists/List/List.tsx +2 -2
- package/src/components/Lists/contexts/CollapseProvider.tsx +11 -0
- package/src/constants.ts +7 -0
- package/src/helperComponents/DesktopDroplist/DesktopDroplist.tsx +2 -2
- package/src/types.ts +4 -1
package/README.md
CHANGED
|
@@ -252,6 +252,78 @@ export function GroupsCollapsible() {
|
|
|
252
252
|
}
|
|
253
253
|
```
|
|
254
254
|
|
|
255
|
+
#### Раскрытие только по шеврону
|
|
256
|
+
|
|
257
|
+
collapse.toggleOn: 'expandIcon' — строка-ссылка ведёт на роут, раскрытие переключает шеврон.
|
|
258
|
+
|
|
259
|
+
```tsx
|
|
260
|
+
import { List } from '@cloud-ru/ds-list';
|
|
261
|
+
import { ReactNode, useState } from 'react';
|
|
262
|
+
|
|
263
|
+
import styles from './styles.module.scss';
|
|
264
|
+
|
|
265
|
+
export function CollapseExpandIcon() {
|
|
266
|
+
const [route, setRoute] = useState('/guides');
|
|
267
|
+
|
|
268
|
+
// Так строку подключают к роутеру: `next/link` и аналоги отменяют свой переход, если клик уже
|
|
269
|
+
// обработали ниже по дереву (`e.defaultPrevented`). В режиме `toggleOn: 'expandIcon'` шеврон
|
|
270
|
+
// как раз гасит клик, поэтому раскрытие группы не уводит на роут.
|
|
271
|
+
const asLink = (href: string) =>
|
|
272
|
+
function wrapItem(node: ReactNode) {
|
|
273
|
+
return (
|
|
274
|
+
<a
|
|
275
|
+
href={href}
|
|
276
|
+
className={styles.link}
|
|
277
|
+
onClick={e => {
|
|
278
|
+
if (e.defaultPrevented) {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
e.preventDefault();
|
|
283
|
+
setRoute(href);
|
|
284
|
+
}}
|
|
285
|
+
>
|
|
286
|
+
{node}
|
|
287
|
+
</a>
|
|
288
|
+
);
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
return (
|
|
292
|
+
<div className={styles.wrapper}>
|
|
293
|
+
<div className={styles.box}>
|
|
294
|
+
<List
|
|
295
|
+
size='s'
|
|
296
|
+
collapse={{ defaultValue: ['guides'], toggleOn: 'expandIcon' }}
|
|
297
|
+
items={[
|
|
298
|
+
{
|
|
299
|
+
type: 'collapse',
|
|
300
|
+
id: 'guides',
|
|
301
|
+
content: { label: 'Руководства' },
|
|
302
|
+
itemWrapRender: asLink('/guides'),
|
|
303
|
+
items: [
|
|
304
|
+
{ id: 'start', content: { label: 'Быстрый старт' }, itemWrapRender: asLink('/guides/start') },
|
|
305
|
+
{ id: 'faq', content: { label: 'FAQ' }, itemWrapRender: asLink('/guides/faq') },
|
|
306
|
+
],
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
type: 'collapse',
|
|
310
|
+
id: 'components',
|
|
311
|
+
content: { label: 'Компоненты' },
|
|
312
|
+
itemWrapRender: asLink('/components'),
|
|
313
|
+
items: [
|
|
314
|
+
{ id: 'button', content: { label: 'Button' }, itemWrapRender: asLink('/components/button') },
|
|
315
|
+
{ id: 'list', content: { label: 'List' }, itemWrapRender: asLink('/components/list') },
|
|
316
|
+
],
|
|
317
|
+
},
|
|
318
|
+
]}
|
|
319
|
+
/>
|
|
320
|
+
</div>
|
|
321
|
+
<span>Текущий роут: {route}</span>
|
|
322
|
+
</div>
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
```
|
|
326
|
+
|
|
255
327
|
#### Три уровня вложенности
|
|
256
328
|
|
|
257
329
|
Группы внутри групп — подходит для каталогов, файловых деревьев.
|
|
@@ -551,7 +623,7 @@ export function ListItemWrap() {
|
|
|
551
623
|
### Item types
|
|
552
624
|
|
|
553
625
|
- `BaseItem` — обычный элемент с `content` / `beforeContent` / `afterContent`; опционально `switch: true` для тумблер-презентации выбора.
|
|
554
|
-
- `{ type: 'collapse', items }` —
|
|
626
|
+
- `{ type: 'collapse', items }` — группа с раскрытием; управление — через `collapse`, триггер раскрытия — через `collapse.toggleOn`.
|
|
555
627
|
- `{ type: 'next-list', items, placement }` — раскрытие в соседний popover (для каскадных меню).
|
|
556
628
|
- `{ type: 'group', items, groupVariant }` / `{ type: 'group-select', items, groupVariant }` — визуальная группа с label, `groupVariant` (`subtitle` / `subtitleTertiary`), опциональным `divider` и «выбрать всё» (`group-select`).
|
|
557
629
|
|
|
@@ -564,6 +636,17 @@ export function ListItemWrap() {
|
|
|
564
636
|
- **Uncontrolled** — передавайте `defaultValue`. Компонент хранит state сам, пригодно для форм/настроек, где значения потом читаются через `onChange`.
|
|
565
637
|
- **Controlled** — `value` + `onChange`. Нужен, когда state живёт в URL / query / внешнем сторе, или когда требуется программно менять выбор/раскрытие.
|
|
566
638
|
|
|
639
|
+
### Триггер раскрытия группы
|
|
640
|
+
|
|
641
|
+
`collapse.toggleOn` задаёт, что переключает раскрытие `type: 'collapse'`-группы мышью:
|
|
642
|
+
|
|
643
|
+
- `item` (по умолчанию) — клик по всей строке группы.
|
|
644
|
+
- `expandIcon` — только клик по шеврону; клик по остальной части строки остаётся потребителю.
|
|
645
|
+
|
|
646
|
+
`expandIcon` нужен, когда строка уже несёт собственное действие: `itemWrapRender` оборачивает её в ссылку роутера или на ней висит свой `onClick`. В режиме `item` такой клик делал бы две вещи разом — переходил по роуту и раскрывал группу.
|
|
647
|
+
|
|
648
|
+
Клавиатура от режима не зависит: `Enter` / `Space` и `ArrowRight` на строке группы раскрывают её в обоих случаях. Шеврон в цепочку табуляции не входит.
|
|
649
|
+
|
|
567
650
|
### Виртуализация
|
|
568
651
|
|
|
569
652
|
- Включайте `virtualized` при размере `items` от ~1000 элементов.
|
|
@@ -938,6 +1021,7 @@ export function DroplistWithHeader() {
|
|
|
938
1021
|
|------|------|---------|-------------|
|
|
939
1022
|
| `defaultValue` | `ItemId` | — | |
|
|
940
1023
|
| `onChange` | `((value?: ItemId[]) => void) \| undefined` | — | |
|
|
1024
|
+
| `toggleOn` | `"expandIcon"` \| `"item"` | — | Что переключает раскрытие вложенного списка: <br/> <br> - `item` — клик по всей строке (по умолчанию), <br/> <br> - `expandIcon` — только клик по шеврону; клик по строке остаётся потребителю <br/> (например, когда `itemWrapRender` оборачивает строку в ссылку). <br/> Клавиатура (`Enter` / `Space` / `ArrowRight` на строке) раскрывает группу в обоих режимах. |
|
|
941
1025
|
| `value` | `ItemId` | — | |
|
|
942
1026
|
|
|
943
1027
|
**CommonGroupItem**
|
|
@@ -11,7 +11,7 @@ const BaseItem_1 = require("../BaseItem");
|
|
|
11
11
|
const hooks_1 = require("../hooks");
|
|
12
12
|
function AccordionItem({ id, disabled, allChildIds, items, ...option }) {
|
|
13
13
|
const { level = 0 } = (0, contexts_1.useCollapseLevelContext)();
|
|
14
|
-
const { openCollapseItems = [], toggleOpenCollapseItem } = (0, contexts_1.useCollapseContext)();
|
|
14
|
+
const { openCollapseItems = [], toggleOpenCollapseItem, toggleOn = constants_1.DEFAULT_COLLAPSE_TOGGLE_ON, } = (0, contexts_1.useCollapseContext)();
|
|
15
15
|
const { value, isSelectionSingle, isSelectionMultiple } = (0, contexts_1.useSelectionContext)();
|
|
16
16
|
const { indeterminate, handleOnSelect, checked: checkedProp, } = (0, hooks_1.useGroupItemSelection)({
|
|
17
17
|
items,
|
|
@@ -22,10 +22,10 @@ function AccordionItem({ id, disabled, allChildIds, items, ...option }) {
|
|
|
22
22
|
const isOpen = Boolean(openCollapseItems.includes(id ?? ''));
|
|
23
23
|
const checked = Boolean((indeterminate && !isOpen && isSelectionSingle && value && allChildIds.includes(value)) ||
|
|
24
24
|
(isSelectionMultiple && checkedProp));
|
|
25
|
-
// Раскрытие переключает вся строка целиком: клик/Enter по телу и ArrowRight с клавиатуры.
|
|
26
25
|
const handleToggle = (0, react_1.useCallback)(() => {
|
|
27
26
|
toggleOpenCollapseItem?.(id ?? '');
|
|
28
27
|
}, [id, toggleOpenCollapseItem]);
|
|
28
|
+
const isExpandIconTrigger = toggleOn === constants_1.COLLAPSE_TOGGLE_ON.ExpandIcon;
|
|
29
29
|
const itemsJSX = (0, hooks_1.useRenderItems)(items);
|
|
30
|
-
return ((0, jsx_runtime_1.jsx)(helperComponents_1.CollapseBlockPrivate, { header: (0, jsx_runtime_1.jsx)(BaseItem_1.BaseItem, { ...option, id: id, disabled: disabled, open: isOpen, expandIcon: isOpen ? (0, jsx_runtime_1.jsx)(system_1.ChevronUpSVG, {}) : (0, jsx_runtime_1.jsx)(system_1.ChevronDownSVG, {}), onToggleExpand: handleToggle, isParentNode: true, onOpenNestedList: handleToggle, checked: checked, indeterminate: indeterminate, onSelect: !disabled ? handleOnSelect : undefined }), expanded: isOpen, "data-test-id": `${constants_1.TEST_IDS.accordionItem}-${id}`, children: (0, jsx_runtime_1.jsx)(contexts_1.CollapseLevelContext.Provider, { value: { level: level + 1 }, children: itemsJSX }) }));
|
|
30
|
+
return ((0, jsx_runtime_1.jsx)(helperComponents_1.CollapseBlockPrivate, { header: (0, jsx_runtime_1.jsx)(BaseItem_1.BaseItem, { ...option, id: id, disabled: disabled, open: isOpen, expandIcon: isOpen ? (0, jsx_runtime_1.jsx)(system_1.ChevronUpSVG, {}) : (0, jsx_runtime_1.jsx)(system_1.ChevronDownSVG, {}), onToggleExpand: handleToggle, onExpandIconClick: isExpandIconTrigger ? handleToggle : undefined, isParentNode: true, onOpenNestedList: handleToggle, checked: checked, indeterminate: indeterminate, onSelect: !disabled ? handleOnSelect : undefined }), expanded: isOpen, "data-test-id": `${constants_1.TEST_IDS.accordionItem}-${id}`, children: (0, jsx_runtime_1.jsx)(contexts_1.CollapseLevelContext.Provider, { value: { level: level + 1 }, children: itemsJSX }) }));
|
|
31
31
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { KeyboardEvent, ReactNode } from 'react';
|
|
1
|
+
import { KeyboardEvent, MouseEvent, ReactNode } from 'react';
|
|
2
2
|
import { FlattenBaseItem } from '../types';
|
|
3
3
|
type AllBaseItemProps = FlattenBaseItem & {
|
|
4
4
|
expandIcon?: ReactNode;
|
|
@@ -9,10 +9,16 @@ type AllBaseItemProps = FlattenBaseItem & {
|
|
|
9
9
|
onOpenNestedList?(e?: KeyboardEvent<HTMLElement>): void;
|
|
10
10
|
/**
|
|
11
11
|
* Переключение раскрытия вложенного списка. Триггер — вся строка целиком (клик или
|
|
12
|
-
* Enter/Space)
|
|
13
|
-
*
|
|
12
|
+
* Enter/Space). Если передан `onExpandIconClick`, клик по строке раскрытие больше не
|
|
13
|
+
* переключает и остаётся только клавиатурным триггером.
|
|
14
14
|
*/
|
|
15
15
|
onToggleExpand?(): void;
|
|
16
|
+
/**
|
|
17
|
+
* Переключение раскрытия кликом по шеврону `groupIndicator`. Наличие обработчика и есть
|
|
18
|
+
* режим: пока он не передан, шеврон — неинтерактивный индикатор состояния, а раскрытие
|
|
19
|
+
* переключает клик по всей строке (`onToggleExpand`).
|
|
20
|
+
*/
|
|
21
|
+
onExpandIconClick?(e: MouseEvent<HTMLElement>): void;
|
|
16
22
|
/**
|
|
17
23
|
* Слот ручки drag&drop (Figma `centeredWrapper`) — рендерится первым в строке, перед
|
|
18
24
|
* маркером/чекбоксом. Интерактивность (обработчики `@dnd-kit`) и содержимое (иконка)
|
|
@@ -20,5 +26,5 @@ type AllBaseItemProps = FlattenBaseItem & {
|
|
|
20
26
|
*/
|
|
21
27
|
dragHandle?: ReactNode;
|
|
22
28
|
};
|
|
23
|
-
export declare function BaseItem({ beforeContent, afterContent, content, onClick, onMouseDown, id, expandIcon, disabled, open, itemRef, switch: switchProp, showSwitchIcon, onKeyDown, onFocus, indeterminate, checked: checkedProp, onSelect, onOpenNestedList, onToggleExpand, isParentNode, className, inactive, itemWrapRender, dragHandle, ...rest }: AllBaseItemProps): string | number | boolean | Iterable<ReactNode> | import("react/jsx-runtime").JSX.Element | null | undefined;
|
|
29
|
+
export declare function BaseItem({ beforeContent, afterContent, content, onClick, onMouseDown, id, expandIcon, disabled, open, itemRef, switch: switchProp, showSwitchIcon, onKeyDown, onFocus, indeterminate, checked: checkedProp, onSelect, onOpenNestedList, onToggleExpand, onExpandIconClick, isParentNode, className, inactive, itemWrapRender, dragHandle, ...rest }: AllBaseItemProps): string | number | boolean | Iterable<ReactNode> | import("react/jsx-runtime").JSX.Element | null | undefined;
|
|
24
30
|
export {};
|
|
@@ -15,7 +15,7 @@ const styles_module_scss_1 = __importDefault(require('../styles.module.css'));
|
|
|
15
15
|
const utils_2 = require("../utils");
|
|
16
16
|
const constants_2 = require("./constants");
|
|
17
17
|
const styles_module_scss_2 = __importDefault(require('./styles.module.css'));
|
|
18
|
-
function BaseItem({ beforeContent, afterContent, content, onClick, onMouseDown, id, expandIcon, disabled, open, itemRef, switch: switchProp, showSwitchIcon, onKeyDown, onFocus, indeterminate, checked: checkedProp, onSelect, onOpenNestedList, onToggleExpand, isParentNode, className, inactive, itemWrapRender, dragHandle, ...rest }) {
|
|
18
|
+
function BaseItem({ beforeContent, afterContent, content, onClick, onMouseDown, id, expandIcon, disabled, open, itemRef, switch: switchProp, showSwitchIcon, onKeyDown, onFocus, indeterminate, checked: checkedProp, onSelect, onOpenNestedList, onToggleExpand, onExpandIconClick, isParentNode, className, inactive, itemWrapRender, dragHandle, ...rest }) {
|
|
19
19
|
const interactive = !inactive;
|
|
20
20
|
const { size = constants_1.DEFAULT_SIZE, marker, contentRender, firstItemId, focusFlattenItems } = (0, contexts_1.useNewListContext)();
|
|
21
21
|
const { level = 0 } = (0, contexts_1.useCollapseLevelContext)();
|
|
@@ -51,9 +51,12 @@ function BaseItem({ beforeContent, afterContent, content, onClick, onMouseDown,
|
|
|
51
51
|
// пункту меню), поэтому keyboard-событие пробрасывается под тем же типом.
|
|
52
52
|
onClick?.(e);
|
|
53
53
|
if (isParentNode) {
|
|
54
|
-
// Раскрытие вложенного списка триггерит вся строка
|
|
55
|
-
//
|
|
56
|
-
|
|
54
|
+
// Раскрытие вложенного списка триггерит вся строка целиком — кроме режима, где тогл
|
|
55
|
+
// отдан шеврону (`onExpandIconClick`): там клик по строке принадлежит потребителю
|
|
56
|
+
// (`onClick`, ссылка из `itemWrapRender`) и раскрытия не меняет.
|
|
57
|
+
if (!onExpandIconClick) {
|
|
58
|
+
onToggleExpand?.();
|
|
59
|
+
}
|
|
57
60
|
}
|
|
58
61
|
else if (interactive) {
|
|
59
62
|
handleChange();
|
|
@@ -90,6 +93,11 @@ function BaseItem({ beforeContent, afterContent, content, onClick, onMouseDown,
|
|
|
90
93
|
e.preventDefault();
|
|
91
94
|
}
|
|
92
95
|
};
|
|
96
|
+
const handleExpandIconClick = (e) => {
|
|
97
|
+
e.preventDefault();
|
|
98
|
+
e.stopPropagation();
|
|
99
|
+
onExpandIconClick?.(e);
|
|
100
|
+
};
|
|
93
101
|
const handleCheckboxClick = (e) => {
|
|
94
102
|
if (isParentNode) {
|
|
95
103
|
e.stopPropagation();
|
|
@@ -111,7 +119,11 @@ function BaseItem({ beforeContent, afterContent, content, onClick, onMouseDown,
|
|
|
111
119
|
const stateLayerState = isSelectionSingle && isChecked && !switchProp && !isParentNode
|
|
112
120
|
? 'activatedOnBackground'
|
|
113
121
|
: 'emptyNeutralOnBackground';
|
|
114
|
-
const itemJSX = ((0, jsx_runtime_1.jsxs)("div", { className: (0, classnames_1.default)(styles_module_scss_1.default.itemWrapper, styles_module_scss_2.default.innerWrapper, className), "data-size": size, "data-inactive": inactive || undefined, "data-disabled": disabled || undefined, "data-variant": mode || undefined, "data-checked": (isParentNode && isChecked) || (!isParentNode && isChecked && !switchProp) || undefined, children: [(0, jsx_runtime_1.jsx)("span", { className: styles_module_scss_1.default.stateLayer, "aria-hidden": true, "data-state": stateLayerState }), (0, jsx_runtime_1.jsxs)("li", { "data-type": 'outside', role: 'menuitem', "data-test-id": props['data-test-id'] || `${constants_1.TEST_IDS.baseItem}_${id}`, ref: itemRef, className: (0, classnames_1.default)(styles_module_scss_1.default.listItem, styles_module_scss_2.default.droplistItem), "data-size": size, onClick: handleItemClick, onMouseDown: handleItemMouseDown, tabIndex: firstItemId && id === focusFlattenItems[firstItemId]?.originalId ? 0 : -1, "data-non-pointer": (inactive && !onClick) || undefined, "data-variant": mode || undefined, "data-open": open || undefined, "aria-expanded": isParentNode ? Boolean(open) : undefined, onKeyDown: handleItemKeyDown, onFocus: handleItemFocus, style: { '--level': level }, "data-level-one": level === 1 || undefined, "data-level-more-one": level > 1 || undefined, "data-checked": (isParentNode && (indeterminate || isChecked)) || (isChecked && !switchProp) || undefined, children: [dragHandle && (0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_2.default.dragHandle, children: dragHandle }), !switchProp && isSelectionSingle && marker && !isParentNode && interactive && ((0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_2.default.markerContainer, "data-test-id": constants_1.TEST_IDS.baseItemMarker })), !switchProp && isSelectionMultiple && interactive && ((0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_2.default.checkbox, children: (0, jsx_runtime_1.jsx)(toggles_1.Checkbox, { size: constants_2.TOGGLE_SIZE_MAP[size], disabled: disabled, tabIndex: -1, onChange: isParentNode ? handleCheckboxChange : undefined, checked: isChecked, "data-test-id": constants_1.TEST_IDS.baseItemCheckbox, onClick: handleCheckboxClick, indeterminate: indeterminate }) })), (0, jsx_runtime_1.jsxs)("div", { className: styles_module_scss_2.default.contentWrapper, children: [beforeContent && (0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_2.default.beforeContent, children: beforeContent }), contentNode, afterContent && (0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_2.default.afterContent, children: afterContent })] }), switchProp && interactive && ((0, jsx_runtime_1.jsx)("span", { className: styles_module_scss_2.default.switchWrapper, children: (0, jsx_runtime_1.jsx)(toggles_1.Switch, { size: constants_2.TOGGLE_SIZE_MAP[size], disabled: disabled, checked: isChecked, "data-test-id": constants_1.TEST_IDS.baseItemSwitch, showIcon: showSwitchIcon }) })), !switchProp && expandIcon && (
|
|
122
|
+
const itemJSX = ((0, jsx_runtime_1.jsxs)("div", { className: (0, classnames_1.default)(styles_module_scss_1.default.itemWrapper, styles_module_scss_2.default.innerWrapper, className), "data-size": size, "data-inactive": inactive || undefined, "data-disabled": disabled || undefined, "data-variant": mode || undefined, "data-checked": (isParentNode && isChecked) || (!isParentNode && isChecked && !switchProp) || undefined, children: [(0, jsx_runtime_1.jsx)("span", { className: styles_module_scss_1.default.stateLayer, "aria-hidden": true, "data-state": stateLayerState }), (0, jsx_runtime_1.jsxs)("li", { "data-type": 'outside', role: 'menuitem', "data-test-id": props['data-test-id'] || `${constants_1.TEST_IDS.baseItem}_${id}`, ref: itemRef, className: (0, classnames_1.default)(styles_module_scss_1.default.listItem, styles_module_scss_2.default.droplistItem), "data-size": size, onClick: handleItemClick, onMouseDown: handleItemMouseDown, tabIndex: firstItemId && id === focusFlattenItems[firstItemId]?.originalId ? 0 : -1, "data-non-pointer": (inactive && !onClick) || undefined, "data-variant": mode || undefined, "data-open": open || undefined, "aria-expanded": isParentNode ? Boolean(open) : undefined, onKeyDown: handleItemKeyDown, onFocus: handleItemFocus, style: { '--level': level }, "data-level-one": level === 1 || undefined, "data-level-more-one": level > 1 || undefined, "data-checked": (isParentNode && (indeterminate || isChecked)) || (isChecked && !switchProp) || undefined, children: [dragHandle && (0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_2.default.dragHandle, children: dragHandle }), !switchProp && isSelectionSingle && marker && !isParentNode && interactive && ((0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_2.default.markerContainer, "data-test-id": constants_1.TEST_IDS.baseItemMarker })), !switchProp && isSelectionMultiple && interactive && ((0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_2.default.checkbox, children: (0, jsx_runtime_1.jsx)(toggles_1.Checkbox, { size: constants_2.TOGGLE_SIZE_MAP[size], disabled: disabled, tabIndex: -1, onChange: isParentNode ? handleCheckboxChange : undefined, checked: isChecked, "data-test-id": constants_1.TEST_IDS.baseItemCheckbox, onClick: handleCheckboxClick, indeterminate: indeterminate }) })), (0, jsx_runtime_1.jsxs)("div", { className: styles_module_scss_2.default.contentWrapper, children: [beforeContent && (0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_2.default.beforeContent, children: beforeContent }), contentNode, afterContent && (0, jsx_runtime_1.jsx)("div", { className: styles_module_scss_2.default.afterContent, children: afterContent })] }), switchProp && interactive && ((0, jsx_runtime_1.jsx)("span", { className: styles_module_scss_2.default.switchWrapper, children: (0, jsx_runtime_1.jsx)(toggles_1.Switch, { size: constants_2.TOGGLE_SIZE_MAP[size], disabled: disabled, checked: isChecked, "data-test-id": constants_1.TEST_IDS.baseItemSwitch, showIcon: showSwitchIcon }) })), !switchProp && expandIcon && (
|
|
123
|
+
// Интерактивный шеврон не получает роль и фокус намеренно: клавиатурный путь идёт
|
|
124
|
+
// через строку, у неё же живёт `aria-expanded`. Лишний таб-стоп сломал бы порядок
|
|
125
|
+
// фокуса списка, а озвучивать AT недостижимый с клавиатуры элемент незачем.
|
|
126
|
+
(0, jsx_runtime_1.jsx)("span", { className: styles_module_scss_2.default.groupIndicator, "data-open": open || undefined, "data-interactive": Boolean(onExpandIconClick) || undefined, "data-test-id": constants_1.TEST_IDS.groupIndicator, onClick: onExpandIconClick && handleExpandIconClick, "aria-hidden": true, children: expandIcon }))] })] }));
|
|
115
127
|
if (!itemWrapRender) {
|
|
116
128
|
return itemJSX;
|
|
117
129
|
}
|
|
@@ -69,6 +69,10 @@
|
|
|
69
69
|
width:100%;
|
|
70
70
|
height:100%;
|
|
71
71
|
}
|
|
72
|
+
.groupIndicator[data-interactive]{
|
|
73
|
+
pointer-events:auto;
|
|
74
|
+
cursor:pointer;
|
|
75
|
+
}
|
|
72
76
|
.groupIndicator[data-open]{
|
|
73
77
|
border-radius:var(--sn-primitive-dimension-2, 2px);
|
|
74
78
|
color:var(--sn-theme-color-available-version-textMain, var(--sn-brand-color-text-version-main, #41424e));
|
|
@@ -45,7 +45,7 @@ const ListImpl = (0, react_1.forwardRef)(({ items: itemsProp = [], search, pinBo
|
|
|
45
45
|
const handleOnFocus = () => {
|
|
46
46
|
resetActiveItemId();
|
|
47
47
|
};
|
|
48
|
-
const collapseContextValue = (0, react_1.useMemo)(() => ({ openCollapseItems, toggleOpenCollapseItem }), [openCollapseItems, toggleOpenCollapseItem]);
|
|
48
|
+
const collapseContextValue = (0, react_1.useMemo)(() => ({ openCollapseItems, toggleOpenCollapseItem, toggleOn: collapse.toggleOn }), [openCollapseItems, toggleOpenCollapseItem, collapse.toggleOn]);
|
|
49
49
|
const focusListContextValue = (0, react_1.useMemo)(() => ({ activeItemId, handleListKeyDownFactory, forceUpdateActiveItemId }), [activeItemId, handleListKeyDownFactory, forceUpdateActiveItemId]);
|
|
50
50
|
return ((0, jsx_runtime_1.jsx)(contexts_1.NewListContextProvider, { flattenItems: flattenItems, focusFlattenItems: focusFlattenItems, contentRender: contentRender, size: size, marker: marker, firstItemId: firstItemId, virtualized: props.virtualized, children: (0, jsx_runtime_1.jsx)(contexts_1.SelectionProvider, { ...selection, children: (0, jsx_runtime_1.jsx)(contexts_1.CollapseContext.Provider, { value: collapseContextValue, children: (0, jsx_runtime_1.jsx)(contexts_1.FocusListContext.Provider, { value: focusListContextValue, children: (0, jsx_runtime_1.jsxs)("div", { className: (0, classnames_1.default)(styles_module_scss_1.default.wrapper, className), "data-active": isActive || undefined, children: [(0, jsx_runtime_1.jsx)(ListPrivate_1.ListPrivate, { ...props, items: memorizedItems.items.focusCloseChildIds, pinTop: memorizedItems.pinTop.focusCloseChildIds, pinBottom: memorizedItems.pinBottom.focusCloseChildIds, searchItem: searchItem, ref: (0, merge_refs_1.default)(ref, listRef), onFocus: handleOnFocus, onKeyDown: mergedHandlerKeyDown, tabIndex: hasListInFocusChain ? tabIndex : undefined, search: search, nested: false, onDragEnd: onDragEnd, sortableIds: sortableIds }), hasListInFocusChain && (0, jsx_runtime_1.jsx)(helperComponents_1.HiddenTabButton, { ref: btnRef, listRef: listRef, tabIndex: tabIndex })] }) }) }) }) }));
|
|
51
51
|
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CollapseToggleOn } from '../../../types';
|
|
1
2
|
import { ItemId } from '../../Items';
|
|
2
3
|
export type CollapseLevelContextType = {
|
|
3
4
|
level?: number;
|
|
@@ -7,6 +8,7 @@ export declare const useCollapseLevelContext: () => CollapseLevelContextType;
|
|
|
7
8
|
export type CollapseContextType = {
|
|
8
9
|
openCollapseItems?: ItemId[];
|
|
9
10
|
toggleOpenCollapseItem?(id: ItemId): void;
|
|
11
|
+
toggleOn?: CollapseToggleOn;
|
|
10
12
|
};
|
|
11
13
|
export declare const CollapseContext: import("react").Context<CollapseContextType>;
|
|
12
14
|
export declare const useCollapseContext: () => CollapseContextType;
|
|
@@ -14,4 +16,13 @@ export type CollapseState = {
|
|
|
14
16
|
value?: ItemId[];
|
|
15
17
|
onChange?(value?: ItemId[]): void;
|
|
16
18
|
defaultValue?: ItemId[];
|
|
19
|
+
/**
|
|
20
|
+
* Что переключает раскрытие вложенного списка:
|
|
21
|
+
* <br> - `item` — клик по всей строке (по умолчанию),
|
|
22
|
+
* <br> - `expandIcon` — только клик по шеврону; клик по строке остаётся потребителю
|
|
23
|
+
* (например, когда `itemWrapRender` оборачивает строку в ссылку).
|
|
24
|
+
*
|
|
25
|
+
* Клавиатура (`Enter` / `Space` / `ArrowRight` на строке) раскрывает группу в обоих режимах.
|
|
26
|
+
*/
|
|
27
|
+
toggleOn?: CollapseToggleOn;
|
|
17
28
|
};
|
package/dist/cjs/constants.d.ts
CHANGED
|
@@ -17,6 +17,11 @@ export declare const MODE: {
|
|
|
17
17
|
readonly Single: "single";
|
|
18
18
|
readonly Multiple: "multiple";
|
|
19
19
|
};
|
|
20
|
+
export declare const COLLAPSE_TOGGLE_ON: {
|
|
21
|
+
readonly Item: "item";
|
|
22
|
+
readonly ExpandIcon: "expandIcon";
|
|
23
|
+
};
|
|
24
|
+
export declare const DEFAULT_COLLAPSE_TOGGLE_ON: "item";
|
|
20
25
|
export declare const ITEM_TYPE: {
|
|
21
26
|
readonly NextList: "next-list";
|
|
22
27
|
readonly Collapse: "collapse";
|
package/dist/cjs/constants.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.TEST_IDS = exports.ITEM_TYPE = exports.MODE = exports.DEFAULT_SIZE = exports.SIZE = exports.ITEM_PREFIXES = void 0;
|
|
3
|
+
exports.TEST_IDS = exports.ITEM_TYPE = exports.DEFAULT_COLLAPSE_TOGGLE_ON = exports.COLLAPSE_TOGGLE_ON = exports.MODE = exports.DEFAULT_SIZE = exports.SIZE = exports.ITEM_PREFIXES = void 0;
|
|
4
4
|
exports.ITEM_PREFIXES = {
|
|
5
5
|
default: '~main',
|
|
6
6
|
pinTop: '~pinTop',
|
|
@@ -20,6 +20,11 @@ exports.MODE = {
|
|
|
20
20
|
Single: 'single',
|
|
21
21
|
Multiple: 'multiple',
|
|
22
22
|
};
|
|
23
|
+
exports.COLLAPSE_TOGGLE_ON = {
|
|
24
|
+
Item: 'item',
|
|
25
|
+
ExpandIcon: 'expandIcon',
|
|
26
|
+
};
|
|
27
|
+
exports.DEFAULT_COLLAPSE_TOGGLE_ON = exports.COLLAPSE_TOGGLE_ON.Item;
|
|
23
28
|
exports.ITEM_TYPE = {
|
|
24
29
|
NextList: 'next-list',
|
|
25
30
|
Collapse: 'collapse',
|
|
@@ -110,7 +110,7 @@ function DesktopDroplist({ items: itemsProp, search, pinBottom: pinBottomProp =
|
|
|
110
110
|
const footerNode = footer ? (
|
|
111
111
|
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
|
|
112
112
|
(0, jsx_runtime_1.jsx)("div", { onKeyDown: handleListKeyDown, children: footer })) : undefined;
|
|
113
|
-
const collapseContextValue = (0, react_1.useMemo)(() => ({ openCollapseItems, toggleOpenCollapseItem }), [openCollapseItems, toggleOpenCollapseItem]);
|
|
113
|
+
const collapseContextValue = (0, react_1.useMemo)(() => ({ openCollapseItems, toggleOpenCollapseItem, toggleOn: collapse.toggleOn }), [openCollapseItems, toggleOpenCollapseItem, collapse.toggleOn]);
|
|
114
114
|
const focusListContextValue = (0, react_1.useMemo)(() => ({ activeItemId, handleListKeyDownFactory, forceUpdateActiveItemId }), [activeItemId, handleListKeyDownFactory, forceUpdateActiveItemId]);
|
|
115
115
|
const closeDroplist = (0, react_1.useCallback)(() => {
|
|
116
116
|
setOpen(false);
|
package/dist/cjs/style.css
CHANGED
|
@@ -69,6 +69,10 @@
|
|
|
69
69
|
width:100%;
|
|
70
70
|
height:100%;
|
|
71
71
|
}
|
|
72
|
+
.groupIndicator[data-interactive]{
|
|
73
|
+
pointer-events:auto;
|
|
74
|
+
cursor:pointer;
|
|
75
|
+
}
|
|
72
76
|
.groupIndicator[data-open]{
|
|
73
77
|
border-radius:var(--sn-primitive-dimension-2, 2px);
|
|
74
78
|
color:var(--sn-theme-color-available-version-textMain, var(--sn-brand-color-text-version-main, #41424e));
|
package/dist/cjs/types.d.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { ScrollProps as OriginalScrollProps } from '@cloud-ru/ds-scroll';
|
|
2
2
|
import { ValueOf } from '@cloud-ru/ds-utils';
|
|
3
3
|
import { ChangeEvent, KeyboardEvent, Ref } from 'react';
|
|
4
|
-
import { ITEM_TYPE, MODE, SIZE } from './constants';
|
|
4
|
+
import { COLLAPSE_TOGGLE_ON, ITEM_TYPE, MODE, SIZE } from './constants';
|
|
5
5
|
/** Размер списка и его элементов */
|
|
6
6
|
export type Size = ValueOf<typeof SIZE>;
|
|
7
7
|
/** Режим выбора элементов списка */
|
|
8
8
|
export type Mode = ValueOf<typeof MODE>;
|
|
9
9
|
/** Тип группового / составного элемента списка */
|
|
10
10
|
export type ItemType = ValueOf<typeof ITEM_TYPE>;
|
|
11
|
+
/** Триггер раскрытия вложенного списка у айтема `type: 'collapse'` */
|
|
12
|
+
export type CollapseToggleOn = ValueOf<typeof COLLAPSE_TOGGLE_ON>;
|
|
11
13
|
/** Настройки поисковой строки списка */
|
|
12
14
|
export type SearchState = {
|
|
13
15
|
placeholder?: string;
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { ChevronDownSVG, ChevronUpSVG } from '@cloud-ru/ds-icons/interface/system';
|
|
3
3
|
import { useCallback } from 'react';
|
|
4
|
-
import { TEST_IDS } from '../../../constants.js';
|
|
4
|
+
import { COLLAPSE_TOGGLE_ON, DEFAULT_COLLAPSE_TOGGLE_ON, TEST_IDS } from '../../../constants.js';
|
|
5
5
|
import { CollapseBlockPrivate } from '../../../helperComponents/index.js';
|
|
6
6
|
import { CollapseLevelContext, useCollapseContext, useCollapseLevelContext, useSelectionContext, } from '../../Lists/contexts/index.js';
|
|
7
7
|
import { BaseItem } from '../BaseItem/index.js';
|
|
8
8
|
import { useGroupItemSelection, useRenderItems } from '../hooks.js';
|
|
9
9
|
export function AccordionItem({ id, disabled, allChildIds, items, ...option }) {
|
|
10
10
|
const { level = 0 } = useCollapseLevelContext();
|
|
11
|
-
const { openCollapseItems = [], toggleOpenCollapseItem } = useCollapseContext();
|
|
11
|
+
const { openCollapseItems = [], toggleOpenCollapseItem, toggleOn = DEFAULT_COLLAPSE_TOGGLE_ON, } = useCollapseContext();
|
|
12
12
|
const { value, isSelectionSingle, isSelectionMultiple } = useSelectionContext();
|
|
13
13
|
const { indeterminate, handleOnSelect, checked: checkedProp, } = useGroupItemSelection({
|
|
14
14
|
items,
|
|
@@ -19,10 +19,10 @@ export function AccordionItem({ id, disabled, allChildIds, items, ...option }) {
|
|
|
19
19
|
const isOpen = Boolean(openCollapseItems.includes(id ?? ''));
|
|
20
20
|
const checked = Boolean((indeterminate && !isOpen && isSelectionSingle && value && allChildIds.includes(value)) ||
|
|
21
21
|
(isSelectionMultiple && checkedProp));
|
|
22
|
-
// Раскрытие переключает вся строка целиком: клик/Enter по телу и ArrowRight с клавиатуры.
|
|
23
22
|
const handleToggle = useCallback(() => {
|
|
24
23
|
toggleOpenCollapseItem?.(id ?? '');
|
|
25
24
|
}, [id, toggleOpenCollapseItem]);
|
|
25
|
+
const isExpandIconTrigger = toggleOn === COLLAPSE_TOGGLE_ON.ExpandIcon;
|
|
26
26
|
const itemsJSX = useRenderItems(items);
|
|
27
|
-
return (_jsx(CollapseBlockPrivate, { header: _jsx(BaseItem, { ...option, id: id, disabled: disabled, open: isOpen, expandIcon: isOpen ? _jsx(ChevronUpSVG, {}) : _jsx(ChevronDownSVG, {}), onToggleExpand: handleToggle, isParentNode: true, onOpenNestedList: handleToggle, checked: checked, indeterminate: indeterminate, onSelect: !disabled ? handleOnSelect : undefined }), expanded: isOpen, "data-test-id": `${TEST_IDS.accordionItem}-${id}`, children: _jsx(CollapseLevelContext.Provider, { value: { level: level + 1 }, children: itemsJSX }) }));
|
|
27
|
+
return (_jsx(CollapseBlockPrivate, { header: _jsx(BaseItem, { ...option, id: id, disabled: disabled, open: isOpen, expandIcon: isOpen ? _jsx(ChevronUpSVG, {}) : _jsx(ChevronDownSVG, {}), onToggleExpand: handleToggle, onExpandIconClick: isExpandIconTrigger ? handleToggle : undefined, isParentNode: true, onOpenNestedList: handleToggle, checked: checked, indeterminate: indeterminate, onSelect: !disabled ? handleOnSelect : undefined }), expanded: isOpen, "data-test-id": `${TEST_IDS.accordionItem}-${id}`, children: _jsx(CollapseLevelContext.Provider, { value: { level: level + 1 }, children: itemsJSX }) }));
|
|
28
28
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { KeyboardEvent, ReactNode } from 'react';
|
|
1
|
+
import { KeyboardEvent, MouseEvent, ReactNode } from 'react';
|
|
2
2
|
import { FlattenBaseItem } from '../types.js';
|
|
3
3
|
type AllBaseItemProps = FlattenBaseItem & {
|
|
4
4
|
expandIcon?: ReactNode;
|
|
@@ -9,10 +9,16 @@ type AllBaseItemProps = FlattenBaseItem & {
|
|
|
9
9
|
onOpenNestedList?(e?: KeyboardEvent<HTMLElement>): void;
|
|
10
10
|
/**
|
|
11
11
|
* Переключение раскрытия вложенного списка. Триггер — вся строка целиком (клик или
|
|
12
|
-
* Enter/Space)
|
|
13
|
-
*
|
|
12
|
+
* Enter/Space). Если передан `onExpandIconClick`, клик по строке раскрытие больше не
|
|
13
|
+
* переключает и остаётся только клавиатурным триггером.
|
|
14
14
|
*/
|
|
15
15
|
onToggleExpand?(): void;
|
|
16
|
+
/**
|
|
17
|
+
* Переключение раскрытия кликом по шеврону `groupIndicator`. Наличие обработчика и есть
|
|
18
|
+
* режим: пока он не передан, шеврон — неинтерактивный индикатор состояния, а раскрытие
|
|
19
|
+
* переключает клик по всей строке (`onToggleExpand`).
|
|
20
|
+
*/
|
|
21
|
+
onExpandIconClick?(e: MouseEvent<HTMLElement>): void;
|
|
16
22
|
/**
|
|
17
23
|
* Слот ручки drag&drop (Figma `centeredWrapper`) — рендерится первым в строке, перед
|
|
18
24
|
* маркером/чекбоксом. Интерактивность (обработчики `@dnd-kit`) и содержимое (иконка)
|
|
@@ -20,5 +26,5 @@ type AllBaseItemProps = FlattenBaseItem & {
|
|
|
20
26
|
*/
|
|
21
27
|
dragHandle?: ReactNode;
|
|
22
28
|
};
|
|
23
|
-
export declare function BaseItem({ beforeContent, afterContent, content, onClick, onMouseDown, id, expandIcon, disabled, open, itemRef, switch: switchProp, showSwitchIcon, onKeyDown, onFocus, indeterminate, checked: checkedProp, onSelect, onOpenNestedList, onToggleExpand, isParentNode, className, inactive, itemWrapRender, dragHandle, ...rest }: AllBaseItemProps): string | number | boolean | Iterable<ReactNode> | import("react/jsx-runtime").JSX.Element | null | undefined;
|
|
29
|
+
export declare function BaseItem({ beforeContent, afterContent, content, onClick, onMouseDown, id, expandIcon, disabled, open, itemRef, switch: switchProp, showSwitchIcon, onKeyDown, onFocus, indeterminate, checked: checkedProp, onSelect, onOpenNestedList, onToggleExpand, onExpandIconClick, isParentNode, className, inactive, itemWrapRender, dragHandle, ...rest }: AllBaseItemProps): string | number | boolean | Iterable<ReactNode> | import("react/jsx-runtime").JSX.Element | null | undefined;
|
|
24
30
|
export {};
|
|
@@ -9,7 +9,7 @@ import commonStyles from '../styles.module.css';
|
|
|
9
9
|
import { isContentItem, isPrimitiveContent } from '../utils.js';
|
|
10
10
|
import { TOGGLE_SIZE_MAP } from './constants.js';
|
|
11
11
|
import styles from './styles.module.css';
|
|
12
|
-
export function BaseItem({ beforeContent, afterContent, content, onClick, onMouseDown, id, expandIcon, disabled, open, itemRef, switch: switchProp, showSwitchIcon, onKeyDown, onFocus, indeterminate, checked: checkedProp, onSelect, onOpenNestedList, onToggleExpand, isParentNode, className, inactive, itemWrapRender, dragHandle, ...rest }) {
|
|
12
|
+
export function BaseItem({ beforeContent, afterContent, content, onClick, onMouseDown, id, expandIcon, disabled, open, itemRef, switch: switchProp, showSwitchIcon, onKeyDown, onFocus, indeterminate, checked: checkedProp, onSelect, onOpenNestedList, onToggleExpand, onExpandIconClick, isParentNode, className, inactive, itemWrapRender, dragHandle, ...rest }) {
|
|
13
13
|
const interactive = !inactive;
|
|
14
14
|
const { size = DEFAULT_SIZE, marker, contentRender, firstItemId, focusFlattenItems } = useNewListContext();
|
|
15
15
|
const { level = 0 } = useCollapseLevelContext();
|
|
@@ -45,9 +45,12 @@ export function BaseItem({ beforeContent, afterContent, content, onClick, onMous
|
|
|
45
45
|
// пункту меню), поэтому keyboard-событие пробрасывается под тем же типом.
|
|
46
46
|
onClick?.(e);
|
|
47
47
|
if (isParentNode) {
|
|
48
|
-
// Раскрытие вложенного списка триггерит вся строка
|
|
49
|
-
//
|
|
50
|
-
|
|
48
|
+
// Раскрытие вложенного списка триггерит вся строка целиком — кроме режима, где тогл
|
|
49
|
+
// отдан шеврону (`onExpandIconClick`): там клик по строке принадлежит потребителю
|
|
50
|
+
// (`onClick`, ссылка из `itemWrapRender`) и раскрытия не меняет.
|
|
51
|
+
if (!onExpandIconClick) {
|
|
52
|
+
onToggleExpand?.();
|
|
53
|
+
}
|
|
51
54
|
}
|
|
52
55
|
else if (interactive) {
|
|
53
56
|
handleChange();
|
|
@@ -84,6 +87,11 @@ export function BaseItem({ beforeContent, afterContent, content, onClick, onMous
|
|
|
84
87
|
e.preventDefault();
|
|
85
88
|
}
|
|
86
89
|
};
|
|
90
|
+
const handleExpandIconClick = (e) => {
|
|
91
|
+
e.preventDefault();
|
|
92
|
+
e.stopPropagation();
|
|
93
|
+
onExpandIconClick?.(e);
|
|
94
|
+
};
|
|
87
95
|
const handleCheckboxClick = (e) => {
|
|
88
96
|
if (isParentNode) {
|
|
89
97
|
e.stopPropagation();
|
|
@@ -105,7 +113,11 @@ export function BaseItem({ beforeContent, afterContent, content, onClick, onMous
|
|
|
105
113
|
const stateLayerState = isSelectionSingle && isChecked && !switchProp && !isParentNode
|
|
106
114
|
? 'activatedOnBackground'
|
|
107
115
|
: 'emptyNeutralOnBackground';
|
|
108
|
-
const itemJSX = (_jsxs("div", { className: cn(commonStyles.itemWrapper, styles.innerWrapper, className), "data-size": size, "data-inactive": inactive || undefined, "data-disabled": disabled || undefined, "data-variant": mode || undefined, "data-checked": (isParentNode && isChecked) || (!isParentNode && isChecked && !switchProp) || undefined, children: [_jsx("span", { className: commonStyles.stateLayer, "aria-hidden": true, "data-state": stateLayerState }), _jsxs("li", { "data-type": 'outside', role: 'menuitem', "data-test-id": props['data-test-id'] || `${TEST_IDS.baseItem}_${id}`, ref: itemRef, className: cn(commonStyles.listItem, styles.droplistItem), "data-size": size, onClick: handleItemClick, onMouseDown: handleItemMouseDown, tabIndex: firstItemId && id === focusFlattenItems[firstItemId]?.originalId ? 0 : -1, "data-non-pointer": (inactive && !onClick) || undefined, "data-variant": mode || undefined, "data-open": open || undefined, "aria-expanded": isParentNode ? Boolean(open) : undefined, onKeyDown: handleItemKeyDown, onFocus: handleItemFocus, style: { '--level': level }, "data-level-one": level === 1 || undefined, "data-level-more-one": level > 1 || undefined, "data-checked": (isParentNode && (indeterminate || isChecked)) || (isChecked && !switchProp) || undefined, children: [dragHandle && _jsx("div", { className: styles.dragHandle, children: dragHandle }), !switchProp && isSelectionSingle && marker && !isParentNode && interactive && (_jsx("div", { className: styles.markerContainer, "data-test-id": TEST_IDS.baseItemMarker })), !switchProp && isSelectionMultiple && interactive && (_jsx("div", { className: styles.checkbox, children: _jsx(Checkbox, { size: TOGGLE_SIZE_MAP[size], disabled: disabled, tabIndex: -1, onChange: isParentNode ? handleCheckboxChange : undefined, checked: isChecked, "data-test-id": TEST_IDS.baseItemCheckbox, onClick: handleCheckboxClick, indeterminate: indeterminate }) })), _jsxs("div", { className: styles.contentWrapper, children: [beforeContent && _jsx("div", { className: styles.beforeContent, children: beforeContent }), contentNode, afterContent && _jsx("div", { className: styles.afterContent, children: afterContent })] }), switchProp && interactive && (_jsx("span", { className: styles.switchWrapper, children: _jsx(Switch, { size: TOGGLE_SIZE_MAP[size], disabled: disabled, checked: isChecked, "data-test-id": TEST_IDS.baseItemSwitch, showIcon: showSwitchIcon }) })), !switchProp && expandIcon && (
|
|
116
|
+
const itemJSX = (_jsxs("div", { className: cn(commonStyles.itemWrapper, styles.innerWrapper, className), "data-size": size, "data-inactive": inactive || undefined, "data-disabled": disabled || undefined, "data-variant": mode || undefined, "data-checked": (isParentNode && isChecked) || (!isParentNode && isChecked && !switchProp) || undefined, children: [_jsx("span", { className: commonStyles.stateLayer, "aria-hidden": true, "data-state": stateLayerState }), _jsxs("li", { "data-type": 'outside', role: 'menuitem', "data-test-id": props['data-test-id'] || `${TEST_IDS.baseItem}_${id}`, ref: itemRef, className: cn(commonStyles.listItem, styles.droplistItem), "data-size": size, onClick: handleItemClick, onMouseDown: handleItemMouseDown, tabIndex: firstItemId && id === focusFlattenItems[firstItemId]?.originalId ? 0 : -1, "data-non-pointer": (inactive && !onClick) || undefined, "data-variant": mode || undefined, "data-open": open || undefined, "aria-expanded": isParentNode ? Boolean(open) : undefined, onKeyDown: handleItemKeyDown, onFocus: handleItemFocus, style: { '--level': level }, "data-level-one": level === 1 || undefined, "data-level-more-one": level > 1 || undefined, "data-checked": (isParentNode && (indeterminate || isChecked)) || (isChecked && !switchProp) || undefined, children: [dragHandle && _jsx("div", { className: styles.dragHandle, children: dragHandle }), !switchProp && isSelectionSingle && marker && !isParentNode && interactive && (_jsx("div", { className: styles.markerContainer, "data-test-id": TEST_IDS.baseItemMarker })), !switchProp && isSelectionMultiple && interactive && (_jsx("div", { className: styles.checkbox, children: _jsx(Checkbox, { size: TOGGLE_SIZE_MAP[size], disabled: disabled, tabIndex: -1, onChange: isParentNode ? handleCheckboxChange : undefined, checked: isChecked, "data-test-id": TEST_IDS.baseItemCheckbox, onClick: handleCheckboxClick, indeterminate: indeterminate }) })), _jsxs("div", { className: styles.contentWrapper, children: [beforeContent && _jsx("div", { className: styles.beforeContent, children: beforeContent }), contentNode, afterContent && _jsx("div", { className: styles.afterContent, children: afterContent })] }), switchProp && interactive && (_jsx("span", { className: styles.switchWrapper, children: _jsx(Switch, { size: TOGGLE_SIZE_MAP[size], disabled: disabled, checked: isChecked, "data-test-id": TEST_IDS.baseItemSwitch, showIcon: showSwitchIcon }) })), !switchProp && expandIcon && (
|
|
117
|
+
// Интерактивный шеврон не получает роль и фокус намеренно: клавиатурный путь идёт
|
|
118
|
+
// через строку, у неё же живёт `aria-expanded`. Лишний таб-стоп сломал бы порядок
|
|
119
|
+
// фокуса списка, а озвучивать AT недостижимый с клавиатуры элемент незачем.
|
|
120
|
+
_jsx("span", { className: styles.groupIndicator, "data-open": open || undefined, "data-interactive": Boolean(onExpandIconClick) || undefined, "data-test-id": TEST_IDS.groupIndicator, onClick: onExpandIconClick && handleExpandIconClick, "aria-hidden": true, children: expandIcon }))] })] }));
|
|
109
121
|
if (!itemWrapRender) {
|
|
110
122
|
return itemJSX;
|
|
111
123
|
}
|
|
@@ -69,6 +69,10 @@
|
|
|
69
69
|
width:100%;
|
|
70
70
|
height:100%;
|
|
71
71
|
}
|
|
72
|
+
.groupIndicator[data-interactive]{
|
|
73
|
+
pointer-events:auto;
|
|
74
|
+
cursor:pointer;
|
|
75
|
+
}
|
|
72
76
|
.groupIndicator[data-open]{
|
|
73
77
|
border-radius:var(--sn-primitive-dimension-2, 2px);
|
|
74
78
|
color:var(--sn-theme-color-available-version-textMain, var(--sn-brand-color-text-version-main, #41424e));
|
|
@@ -39,7 +39,7 @@ const ListImpl = forwardRef(({ items: itemsProp = [], search, pinBottom: pinBott
|
|
|
39
39
|
const handleOnFocus = () => {
|
|
40
40
|
resetActiveItemId();
|
|
41
41
|
};
|
|
42
|
-
const collapseContextValue = useMemo(() => ({ openCollapseItems, toggleOpenCollapseItem }), [openCollapseItems, toggleOpenCollapseItem]);
|
|
42
|
+
const collapseContextValue = useMemo(() => ({ openCollapseItems, toggleOpenCollapseItem, toggleOn: collapse.toggleOn }), [openCollapseItems, toggleOpenCollapseItem, collapse.toggleOn]);
|
|
43
43
|
const focusListContextValue = useMemo(() => ({ activeItemId, handleListKeyDownFactory, forceUpdateActiveItemId }), [activeItemId, handleListKeyDownFactory, forceUpdateActiveItemId]);
|
|
44
44
|
return (_jsx(NewListContextProvider, { flattenItems: flattenItems, focusFlattenItems: focusFlattenItems, contentRender: contentRender, size: size, marker: marker, firstItemId: firstItemId, virtualized: props.virtualized, children: _jsx(SelectionProvider, { ...selection, children: _jsx(CollapseContext.Provider, { value: collapseContextValue, children: _jsx(FocusListContext.Provider, { value: focusListContextValue, children: _jsxs("div", { className: cn(styles.wrapper, className), "data-active": isActive || undefined, children: [_jsx(ListPrivate, { ...props, items: memorizedItems.items.focusCloseChildIds, pinTop: memorizedItems.pinTop.focusCloseChildIds, pinBottom: memorizedItems.pinBottom.focusCloseChildIds, searchItem: searchItem, ref: mergeRefs(ref, listRef), onFocus: handleOnFocus, onKeyDown: mergedHandlerKeyDown, tabIndex: hasListInFocusChain ? tabIndex : undefined, search: search, nested: false, onDragEnd: onDragEnd, sortableIds: sortableIds }), hasListInFocusChain && _jsx(HiddenTabButton, { ref: btnRef, listRef: listRef, tabIndex: tabIndex })] }) }) }) }) }));
|
|
45
45
|
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CollapseToggleOn } from '../../../types.js';
|
|
1
2
|
import { ItemId } from '../../Items/index.js';
|
|
2
3
|
export type CollapseLevelContextType = {
|
|
3
4
|
level?: number;
|
|
@@ -7,6 +8,7 @@ export declare const useCollapseLevelContext: () => CollapseLevelContextType;
|
|
|
7
8
|
export type CollapseContextType = {
|
|
8
9
|
openCollapseItems?: ItemId[];
|
|
9
10
|
toggleOpenCollapseItem?(id: ItemId): void;
|
|
11
|
+
toggleOn?: CollapseToggleOn;
|
|
10
12
|
};
|
|
11
13
|
export declare const CollapseContext: import("react").Context<CollapseContextType>;
|
|
12
14
|
export declare const useCollapseContext: () => CollapseContextType;
|
|
@@ -14,4 +16,13 @@ export type CollapseState = {
|
|
|
14
16
|
value?: ItemId[];
|
|
15
17
|
onChange?(value?: ItemId[]): void;
|
|
16
18
|
defaultValue?: ItemId[];
|
|
19
|
+
/**
|
|
20
|
+
* Что переключает раскрытие вложенного списка:
|
|
21
|
+
* <br> - `item` — клик по всей строке (по умолчанию),
|
|
22
|
+
* <br> - `expandIcon` — только клик по шеврону; клик по строке остаётся потребителю
|
|
23
|
+
* (например, когда `itemWrapRender` оборачивает строку в ссылку).
|
|
24
|
+
*
|
|
25
|
+
* Клавиатура (`Enter` / `Space` / `ArrowRight` на строке) раскрывает группу в обоих режимах.
|
|
26
|
+
*/
|
|
27
|
+
toggleOn?: CollapseToggleOn;
|
|
17
28
|
};
|
package/dist/esm/constants.d.ts
CHANGED
|
@@ -17,6 +17,11 @@ export declare const MODE: {
|
|
|
17
17
|
readonly Single: "single";
|
|
18
18
|
readonly Multiple: "multiple";
|
|
19
19
|
};
|
|
20
|
+
export declare const COLLAPSE_TOGGLE_ON: {
|
|
21
|
+
readonly Item: "item";
|
|
22
|
+
readonly ExpandIcon: "expandIcon";
|
|
23
|
+
};
|
|
24
|
+
export declare const DEFAULT_COLLAPSE_TOGGLE_ON: "item";
|
|
20
25
|
export declare const ITEM_TYPE: {
|
|
21
26
|
readonly NextList: "next-list";
|
|
22
27
|
readonly Collapse: "collapse";
|
package/dist/esm/constants.js
CHANGED
|
@@ -17,6 +17,11 @@ export const MODE = {
|
|
|
17
17
|
Single: 'single',
|
|
18
18
|
Multiple: 'multiple',
|
|
19
19
|
};
|
|
20
|
+
export const COLLAPSE_TOGGLE_ON = {
|
|
21
|
+
Item: 'item',
|
|
22
|
+
ExpandIcon: 'expandIcon',
|
|
23
|
+
};
|
|
24
|
+
export const DEFAULT_COLLAPSE_TOGGLE_ON = COLLAPSE_TOGGLE_ON.Item;
|
|
20
25
|
export const ITEM_TYPE = {
|
|
21
26
|
NextList: 'next-list',
|
|
22
27
|
Collapse: 'collapse',
|
|
@@ -104,7 +104,7 @@ export function DesktopDroplist({ items: itemsProp, search, pinBottom: pinBottom
|
|
|
104
104
|
const footerNode = footer ? (
|
|
105
105
|
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
|
|
106
106
|
_jsx("div", { onKeyDown: handleListKeyDown, children: footer })) : undefined;
|
|
107
|
-
const collapseContextValue = useMemo(() => ({ openCollapseItems, toggleOpenCollapseItem }), [openCollapseItems, toggleOpenCollapseItem]);
|
|
107
|
+
const collapseContextValue = useMemo(() => ({ openCollapseItems, toggleOpenCollapseItem, toggleOn: collapse.toggleOn }), [openCollapseItems, toggleOpenCollapseItem, collapse.toggleOn]);
|
|
108
108
|
const focusListContextValue = useMemo(() => ({ activeItemId, handleListKeyDownFactory, forceUpdateActiveItemId }), [activeItemId, handleListKeyDownFactory, forceUpdateActiveItemId]);
|
|
109
109
|
const closeDroplist = useCallback(() => {
|
|
110
110
|
setOpen(false);
|
package/dist/esm/style.css
CHANGED
|
@@ -69,6 +69,10 @@
|
|
|
69
69
|
width:100%;
|
|
70
70
|
height:100%;
|
|
71
71
|
}
|
|
72
|
+
.groupIndicator[data-interactive]{
|
|
73
|
+
pointer-events:auto;
|
|
74
|
+
cursor:pointer;
|
|
75
|
+
}
|
|
72
76
|
.groupIndicator[data-open]{
|
|
73
77
|
border-radius:var(--sn-primitive-dimension-2, 2px);
|
|
74
78
|
color:var(--sn-theme-color-available-version-textMain, var(--sn-brand-color-text-version-main, #41424e));
|
package/dist/esm/types.d.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { ScrollProps as OriginalScrollProps } from '@cloud-ru/ds-scroll';
|
|
2
2
|
import { ValueOf } from '@cloud-ru/ds-utils';
|
|
3
3
|
import { ChangeEvent, KeyboardEvent, Ref } from 'react';
|
|
4
|
-
import { ITEM_TYPE, MODE, SIZE } from './constants.js';
|
|
4
|
+
import { COLLAPSE_TOGGLE_ON, ITEM_TYPE, MODE, SIZE } from './constants.js';
|
|
5
5
|
/** Размер списка и его элементов */
|
|
6
6
|
export type Size = ValueOf<typeof SIZE>;
|
|
7
7
|
/** Режим выбора элементов списка */
|
|
8
8
|
export type Mode = ValueOf<typeof MODE>;
|
|
9
9
|
/** Тип группового / составного элемента списка */
|
|
10
10
|
export type ItemType = ValueOf<typeof ITEM_TYPE>;
|
|
11
|
+
/** Триггер раскрытия вложенного списка у айтема `type: 'collapse'` */
|
|
12
|
+
export type CollapseToggleOn = ValueOf<typeof COLLAPSE_TOGGLE_ON>;
|
|
11
13
|
/** Настройки поисковой строки списка */
|
|
12
14
|
export type SearchState = {
|
|
13
15
|
placeholder?: string;
|