@cloud-ru/ds-list 2.1.5 → 2.2.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.
Files changed (35) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +85 -1
  3. package/dist/cjs/components/Items/AccordionItem/AccordionItem.js +3 -3
  4. package/dist/cjs/components/Items/BaseItem/BaseItem.d.ts +10 -4
  5. package/dist/cjs/components/Items/BaseItem/BaseItem.js +17 -5
  6. package/dist/cjs/components/Items/BaseItem/styles.module.css +4 -0
  7. package/dist/cjs/components/Lists/List/List.js +1 -1
  8. package/dist/cjs/components/Lists/contexts/CollapseProvider.d.ts +11 -0
  9. package/dist/cjs/constants.d.ts +5 -0
  10. package/dist/cjs/constants.js +6 -1
  11. package/dist/cjs/helperComponents/DesktopDroplist/DesktopDroplist.js +1 -1
  12. package/dist/cjs/style.css +4 -0
  13. package/dist/cjs/types.d.ts +3 -1
  14. package/dist/esm/components/Items/AccordionItem/AccordionItem.js +4 -4
  15. package/dist/esm/components/Items/BaseItem/BaseItem.d.ts +10 -4
  16. package/dist/esm/components/Items/BaseItem/BaseItem.js +17 -5
  17. package/dist/esm/components/Items/BaseItem/styles.module.css +4 -0
  18. package/dist/esm/components/Lists/List/List.js +1 -1
  19. package/dist/esm/components/Lists/contexts/CollapseProvider.d.ts +11 -0
  20. package/dist/esm/constants.d.ts +5 -0
  21. package/dist/esm/constants.js +5 -0
  22. package/dist/esm/helperComponents/DesktopDroplist/DesktopDroplist.js +1 -1
  23. package/dist/esm/style.css +4 -0
  24. package/dist/esm/types.d.ts +3 -1
  25. package/dist/tsconfig.cjs.tsbuildinfo +1 -1
  26. package/dist/tsconfig.esm.tsbuildinfo +1 -1
  27. package/package.json +7 -7
  28. package/src/components/Items/AccordionItem/AccordionItem.tsx +9 -3
  29. package/src/components/Items/BaseItem/BaseItem.tsx +27 -5
  30. package/src/components/Items/BaseItem/styles.module.scss +10 -2
  31. package/src/components/Lists/List/List.tsx +2 -2
  32. package/src/components/Lists/contexts/CollapseProvider.tsx +11 -0
  33. package/src/constants.ts +7 -0
  34. package/src/helperComponents/DesktopDroplist/DesktopDroplist.tsx +2 -2
  35. package/src/types.ts +4 -1
package/CHANGELOG.md CHANGED
@@ -3,6 +3,12 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # 2.2.0 (2026-09-21)
7
+
8
+ ### Features
9
+
10
+ - **DOCDEV-3699:** added expand options into List component ([1ee7af3](https://github.com/cloud-ru-tech/snack-v2/commit/1ee7af326c3b4c803db676c20a2c0ff0789705e5))
11
+
6
12
  ## 2.1.5 (2026-09-18)
7
13
 
8
14
  **Note:** Version bump only for package @ds/list
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 }` — группа, раскрывается кликом по заголовку; управление — через `collapse`.
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), шеврон `groupIndicator` при этом остаётся неинтерактивным индикатором
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
- // Раскрытие вложенного списка триггерит вся строка целиком: шеврон `groupIndicator`
55
- // только индикатор состояния, собственной интерактивности у него нет.
56
- onToggleExpand?.();
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 && ((0, jsx_runtime_1.jsx)("span", { className: styles_module_scss_2.default.groupIndicator, "data-open": open || undefined, "data-test-id": constants_1.TEST_IDS.groupIndicator, "aria-hidden": true, children: 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
  };
@@ -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";
@@ -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);
@@ -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));
@@ -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), шеврон `groupIndicator` при этом остаётся неинтерактивным индикатором
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
- // Раскрытие вложенного списка триггерит вся строка целиком: шеврон `groupIndicator`
49
- // только индикатор состояния, собственной интерактивности у него нет.
50
- onToggleExpand?.();
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 && (_jsx("span", { className: styles.groupIndicator, "data-open": open || undefined, "data-test-id": TEST_IDS.groupIndicator, "aria-hidden": true, children: 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
  };
@@ -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";
@@ -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);
@@ -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));