@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloud-ru/ds-list",
3
- "version": "2.1.5",
3
+ "version": "2.2.0",
4
4
  "description": "Списочный UI — компонент List для плоских/вложенных списков с выбором, группами и поиском и Droplist — тот же список в поповере.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -49,21 +49,21 @@
49
49
  "classnames": "^2.5.1",
50
50
  "merge-refs": "^2.0.0",
51
51
  "@cloud-ru/ds-adaptive": "^1.0.1",
52
- "@cloud-ru/ds-button": "^1.0.2",
53
52
  "@cloud-ru/ds-bottom-sheet": "^1.1.2",
53
+ "@cloud-ru/ds-button": "^1.0.2",
54
+ "@cloud-ru/ds-icons": "^1.2.0",
54
55
  "@cloud-ru/ds-divider": "^1.0.1",
55
56
  "@cloud-ru/ds-dropdown": "^1.0.9",
56
57
  "@cloud-ru/ds-drag-and-drop": "^1.1.1",
57
- "@cloud-ru/ds-icons": "^1.2.0",
58
+ "@cloud-ru/ds-info-block": "^1.0.1",
58
59
  "@cloud-ru/ds-loader": "^1.0.1",
59
60
  "@cloud-ru/ds-materials": "^1.0.1",
60
- "@cloud-ru/ds-info-block": "^1.0.1",
61
+ "@cloud-ru/ds-search-private": "^1.0.8",
61
62
  "@cloud-ru/ds-portal-context": "^1.0.1",
62
63
  "@cloud-ru/ds-scroll": "^1.0.2",
63
- "@cloud-ru/ds-search-private": "^1.0.8",
64
64
  "@cloud-ru/ds-toggles": "^1.1.4",
65
- "@cloud-ru/ds-utils": "^1.1.0",
66
- "@cloud-ru/ds-truncate-string": "^1.0.5"
65
+ "@cloud-ru/ds-truncate-string": "^1.0.5",
66
+ "@cloud-ru/ds-utils": "^1.1.0"
67
67
  },
68
68
  "peerDependencies": {
69
69
  "@cloud-ru/ds-locale": "^1.0.0"
@@ -1,7 +1,7 @@
1
1
  import { ChevronDownSVG, ChevronUpSVG } from '@cloud-ru/ds-icons/interface/system';
2
2
  import { useCallback } from 'react';
3
3
 
4
- import { TEST_IDS } from '../../../constants';
4
+ import { COLLAPSE_TOGGLE_ON, DEFAULT_COLLAPSE_TOGGLE_ON, TEST_IDS } from '../../../constants';
5
5
  import { CollapseBlockPrivate } from '../../../helperComponents';
6
6
  import {
7
7
  CollapseLevelContext,
@@ -17,7 +17,11 @@ type AccordionItemProps = Omit<FlattenAccordionItem, 'type'> & CommonFlattenProp
17
17
 
18
18
  export function AccordionItem({ id, disabled, allChildIds, items, ...option }: AccordionItemProps) {
19
19
  const { level = 0 } = useCollapseLevelContext();
20
- const { openCollapseItems = [], toggleOpenCollapseItem } = useCollapseContext();
20
+ const {
21
+ openCollapseItems = [],
22
+ toggleOpenCollapseItem,
23
+ toggleOn = DEFAULT_COLLAPSE_TOGGLE_ON,
24
+ } = useCollapseContext();
21
25
 
22
26
  const { value, isSelectionSingle, isSelectionMultiple } = useSelectionContext();
23
27
 
@@ -39,11 +43,12 @@ export function AccordionItem({ id, disabled, allChildIds, items, ...option }: A
39
43
  (isSelectionMultiple && checkedProp),
40
44
  );
41
45
 
42
- // Раскрытие переключает вся строка целиком: клик/Enter по телу и ArrowRight с клавиатуры.
43
46
  const handleToggle = useCallback(() => {
44
47
  toggleOpenCollapseItem?.(id ?? '');
45
48
  }, [id, toggleOpenCollapseItem]);
46
49
 
50
+ const isExpandIconTrigger = toggleOn === COLLAPSE_TOGGLE_ON.ExpandIcon;
51
+
47
52
  const itemsJSX = useRenderItems(items);
48
53
 
49
54
  return (
@@ -56,6 +61,7 @@ export function AccordionItem({ id, disabled, allChildIds, items, ...option }: A
56
61
  open={isOpen}
57
62
  expandIcon={isOpen ? <ChevronUpSVG /> : <ChevronDownSVG />}
58
63
  onToggleExpand={handleToggle}
64
+ onExpandIconClick={isExpandIconTrigger ? handleToggle : undefined}
59
65
  isParentNode
60
66
  onOpenNestedList={handleToggle}
61
67
  checked={checked}
@@ -26,10 +26,16 @@ type AllBaseItemProps = FlattenBaseItem & {
26
26
  onOpenNestedList?(e?: KeyboardEvent<HTMLElement>): void;
27
27
  /**
28
28
  * Переключение раскрытия вложенного списка. Триггер — вся строка целиком (клик или
29
- * Enter/Space), шеврон `groupIndicator` при этом остаётся неинтерактивным индикатором
30
- * состояния.
29
+ * Enter/Space). Если передан `onExpandIconClick`, клик по строке раскрытие больше не
30
+ * переключает и остаётся только клавиатурным триггером.
31
31
  */
32
32
  onToggleExpand?(): void;
33
+ /**
34
+ * Переключение раскрытия кликом по шеврону `groupIndicator`. Наличие обработчика и есть
35
+ * режим: пока он не передан, шеврон — неинтерактивный индикатор состояния, а раскрытие
36
+ * переключает клик по всей строке (`onToggleExpand`).
37
+ */
38
+ onExpandIconClick?(e: MouseEvent<HTMLElement>): void;
33
39
  /**
34
40
  * Слот ручки drag&drop (Figma `centeredWrapper`) — рендерится первым в строке, перед
35
41
  * маркером/чекбоксом. Интерактивность (обработчики `@dnd-kit`) и содержимое (иконка)
@@ -58,6 +64,7 @@ export function BaseItem({
58
64
  onSelect,
59
65
  onOpenNestedList,
60
66
  onToggleExpand,
67
+ onExpandIconClick,
61
68
  isParentNode,
62
69
  className,
63
70
  inactive,
@@ -108,9 +115,12 @@ export function BaseItem({
108
115
  onClick?.(e as MouseEvent<HTMLElement>);
109
116
 
110
117
  if (isParentNode) {
111
- // Раскрытие вложенного списка триггерит вся строка целиком: шеврон `groupIndicator`
112
- // только индикатор состояния, собственной интерактивности у него нет.
113
- onToggleExpand?.();
118
+ // Раскрытие вложенного списка триггерит вся строка целиком кроме режима, где тогл
119
+ // отдан шеврону (`onExpandIconClick`): там клик по строке принадлежит потребителю
120
+ // (`onClick`, ссылка из `itemWrapRender`) и раскрытия не меняет.
121
+ if (!onExpandIconClick) {
122
+ onToggleExpand?.();
123
+ }
114
124
  } else if (interactive) {
115
125
  handleChange();
116
126
  }
@@ -152,6 +162,13 @@ export function BaseItem({
152
162
  }
153
163
  };
154
164
 
165
+ const handleExpandIconClick = (e: MouseEvent<HTMLElement>) => {
166
+ e.preventDefault();
167
+ e.stopPropagation();
168
+
169
+ onExpandIconClick?.(e);
170
+ };
171
+
155
172
  const handleCheckboxClick = (e: MouseEvent) => {
156
173
  if (isParentNode) {
157
174
  e.stopPropagation();
@@ -249,10 +266,15 @@ export function BaseItem({
249
266
  )}
250
267
 
251
268
  {!switchProp && expandIcon && (
269
+ // Интерактивный шеврон не получает роль и фокус намеренно: клавиатурный путь идёт
270
+ // через строку, у неё же живёт `aria-expanded`. Лишний таб-стоп сломал бы порядок
271
+ // фокуса списка, а озвучивать AT недостижимый с клавиатуры элемент незачем.
252
272
  <span
253
273
  className={styles.groupIndicator}
254
274
  data-open={open || undefined}
275
+ data-interactive={Boolean(onExpandIconClick) || undefined}
255
276
  data-test-id={TEST_IDS.groupIndicator}
277
+ onClick={onExpandIconClick && handleExpandIconClick}
256
278
  aria-hidden
257
279
  >
258
280
  {expandIcon}
@@ -81,8 +81,9 @@ $sizes: 's', 'm', 'l';
81
81
  }
82
82
 
83
83
  .groupIndicator {
84
- // Шеврон — индикатор состояния, а не кнопка: раскрытие переключает клик по всей строке.
85
- // Своих hover/pressed/focus-состояний у него нет.
84
+ // По умолчанию шеврон — индикатор состояния, а не кнопка: раскрытие переключает клик по
85
+ // всей строке, поэтому клики шеврон не перехватывает. Своих hover/pressed/focus-состояний
86
+ // у него нет ни в одном из режимов.
86
87
  pointer-events: none;
87
88
 
88
89
  display: flex;
@@ -100,6 +101,13 @@ $sizes: 's', 'm', 'l';
100
101
  height: 100%;
101
102
  }
102
103
 
104
+ // Режим `collapse.toggleOn: 'expandIcon'`: тогл висит на шевроне, значит он должен
105
+ // принимать клики. Атрибут ставит `BaseItem` ровно тогда, когда пришёл `onExpandIconClick`.
106
+ &[data-interactive] {
107
+ pointer-events: auto;
108
+ cursor: pointer;
109
+ }
110
+
103
111
  // Раскрытая группа держит постоянную подложку `neutral/decorTransparent`
104
112
  // (варианты мастера `groupIndicatorS/Xs`, `open=true`).
105
113
  &[data-open] {
@@ -87,8 +87,8 @@ const ListImpl = forwardRef(
87
87
  };
88
88
 
89
89
  const collapseContextValue = useMemo(
90
- () => ({ openCollapseItems, toggleOpenCollapseItem }),
91
- [openCollapseItems, toggleOpenCollapseItem],
90
+ () => ({ openCollapseItems, toggleOpenCollapseItem, toggleOn: collapse.toggleOn }),
91
+ [openCollapseItems, toggleOpenCollapseItem, collapse.toggleOn],
92
92
  );
93
93
 
94
94
  const focusListContextValue = useMemo(
@@ -1,5 +1,6 @@
1
1
  import { createContext, useContext } from 'react';
2
2
 
3
+ import { CollapseToggleOn } from '../../../types';
3
4
  import { ItemId } from '../../Items';
4
5
 
5
6
  export type CollapseLevelContextType = {
@@ -11,6 +12,7 @@ export const useCollapseLevelContext = () => useContext(CollapseLevelContext);
11
12
  export type CollapseContextType = {
12
13
  openCollapseItems?: ItemId[];
13
14
  toggleOpenCollapseItem?(id: ItemId): void;
15
+ toggleOn?: CollapseToggleOn;
14
16
  };
15
17
  export const CollapseContext = createContext<CollapseContextType>({});
16
18
  export const useCollapseContext = () => useContext(CollapseContext);
@@ -19,4 +21,13 @@ export type CollapseState = {
19
21
  value?: ItemId[];
20
22
  onChange?(value?: ItemId[]): void;
21
23
  defaultValue?: ItemId[];
24
+ /**
25
+ * Что переключает раскрытие вложенного списка:
26
+ * <br> - `item` — клик по всей строке (по умолчанию),
27
+ * <br> - `expandIcon` — только клик по шеврону; клик по строке остаётся потребителю
28
+ * (например, когда `itemWrapRender` оборачивает строку в ссылку).
29
+ *
30
+ * Клавиатура (`Enter` / `Space` / `ArrowRight` на строке) раскрывает группу в обоих режимах.
31
+ */
32
+ toggleOn?: CollapseToggleOn;
22
33
  };
package/src/constants.ts CHANGED
@@ -21,6 +21,13 @@ export const MODE = {
21
21
  Multiple: 'multiple',
22
22
  } as const;
23
23
 
24
+ export const COLLAPSE_TOGGLE_ON = {
25
+ Item: 'item',
26
+ ExpandIcon: 'expandIcon',
27
+ } as const;
28
+
29
+ export const DEFAULT_COLLAPSE_TOGGLE_ON = COLLAPSE_TOGGLE_ON.Item;
30
+
24
31
  export const ITEM_TYPE = {
25
32
  NextList: 'next-list',
26
33
  Collapse: 'collapse',
@@ -208,8 +208,8 @@ export function DesktopDroplist({
208
208
  ) : undefined;
209
209
 
210
210
  const collapseContextValue = useMemo(
211
- () => ({ openCollapseItems, toggleOpenCollapseItem }),
212
- [openCollapseItems, toggleOpenCollapseItem],
211
+ () => ({ openCollapseItems, toggleOpenCollapseItem, toggleOn: collapse.toggleOn }),
212
+ [openCollapseItems, toggleOpenCollapseItem, collapse.toggleOn],
213
213
  );
214
214
 
215
215
  const focusListContextValue = useMemo(
package/src/types.ts CHANGED
@@ -2,7 +2,7 @@ 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
4
 
5
- import { ITEM_TYPE, MODE, SIZE } from './constants';
5
+ import { COLLAPSE_TOGGLE_ON, ITEM_TYPE, MODE, SIZE } from './constants';
6
6
 
7
7
  /** Размер списка и его элементов */
8
8
  export type Size = ValueOf<typeof SIZE>;
@@ -13,6 +13,9 @@ export type Mode = ValueOf<typeof MODE>;
13
13
  /** Тип группового / составного элемента списка */
14
14
  export type ItemType = ValueOf<typeof ITEM_TYPE>;
15
15
 
16
+ /** Триггер раскрытия вложенного списка у айтема `type: 'collapse'` */
17
+ export type CollapseToggleOn = ValueOf<typeof COLLAPSE_TOGGLE_ON>;
18
+
16
19
  /** Настройки поисковой строки списка */
17
20
  export type SearchState = {
18
21
  placeholder?: string;