@cloud-ru/ds-list 1.1.4 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloud-ru/ds-list",
3
- "version": "1.1.4",
3
+ "version": "2.0.0",
4
4
  "description": "Списочный UI — компонент List для плоских/вложенных списков с выбором, группами и поиском и Droplist — тот же список в поповере.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -51,18 +51,18 @@
51
51
  "@cloud-ru/ds-adaptive": "^1.0.1",
52
52
  "@cloud-ru/ds-bottom-sheet": "^1.0.2",
53
53
  "@cloud-ru/ds-button": "^1.0.1",
54
+ "@cloud-ru/ds-divider": "^1.0.1",
54
55
  "@cloud-ru/ds-drag-and-drop": "^1.1.1",
55
56
  "@cloud-ru/ds-dropdown": "^1.0.2",
56
57
  "@cloud-ru/ds-icons": "^1.0.1",
57
- "@cloud-ru/ds-divider": "^1.0.1",
58
- "@cloud-ru/ds-info-block": "^1.0.1",
59
58
  "@cloud-ru/ds-loader": "^1.0.1",
59
+ "@cloud-ru/ds-info-block": "^1.0.1",
60
60
  "@cloud-ru/ds-materials": "^1.0.1",
61
61
  "@cloud-ru/ds-portal-context": "^1.0.1",
62
- "@cloud-ru/ds-scroll": "^1.0.1",
63
62
  "@cloud-ru/ds-search-private": "^1.0.2",
64
- "@cloud-ru/ds-truncate-string": "^1.0.2",
63
+ "@cloud-ru/ds-scroll": "^1.0.1",
65
64
  "@cloud-ru/ds-toggles": "^1.1.1",
65
+ "@cloud-ru/ds-truncate-string": "^1.0.2",
66
66
  "@cloud-ru/ds-utils": "^1.1.0"
67
67
  },
68
68
  "peerDependencies": {
@@ -39,20 +39,13 @@ export function AccordionItem({ id, disabled, allChildIds, items, ...option }: A
39
39
  (isSelectionMultiple && checkedProp),
40
40
  );
41
41
 
42
- const handleKeyDown = useCallback(() => {
42
+ // Раскрытие переключает вся строка целиком: клик/Enter по телу и ArrowRight с клавиатуры.
43
+ const handleToggle = useCallback(() => {
43
44
  toggleOpenCollapseItem?.(id ?? '');
44
45
  }, [id, toggleOpenCollapseItem]);
45
46
 
46
47
  const itemsJSX = useRenderItems(items);
47
48
 
48
- // Раскрытие привязано к явной кнопке-триггеру `groupIndicator` (шеврон справа) и
49
- // ТОЛЬКО переключает collapse — `option.onClick` сюда не зовём, иначе у потребителя,
50
- // который вешает на onClick собственный toggle, выходит двойное переключение.
51
- // Клик по телу строки (выбор/навигация) идёт через `option.onClick` в BaseItem.
52
- const handleExpandIconClick = () => {
53
- toggleOpenCollapseItem?.(id ?? '');
54
- };
55
-
56
49
  return (
57
50
  <CollapseBlockPrivate
58
51
  header={
@@ -62,9 +55,9 @@ export function AccordionItem({ id, disabled, allChildIds, items, ...option }: A
62
55
  disabled={disabled}
63
56
  open={isOpen}
64
57
  expandIcon={isOpen ? <ChevronUpSVG /> : <ChevronDownSVG />}
65
- onExpandIconClick={handleExpandIconClick}
58
+ onToggleExpand={handleToggle}
66
59
  isParentNode
67
- onOpenNestedList={handleKeyDown}
60
+ onOpenNestedList={handleToggle}
68
61
  checked={checked}
69
62
  indeterminate={indeterminate}
70
63
  onSelect={!disabled ? handleOnSelect : undefined}
@@ -25,11 +25,11 @@ type AllBaseItemProps = FlattenBaseItem & {
25
25
  isParentNode?: boolean;
26
26
  onOpenNestedList?(e?: KeyboardEvent<HTMLElement>): void;
27
27
  /**
28
- * Клик по явной кнопке-триггеру `groupIndicator` (шеврон справа), открывающей/закрывающей
29
- * вложенный список. Если передан шеврон рендерится как отдельная интерактивная кнопка
30
- * со своим state-layer'ом, а клик по телу строки больше не переключает раскрытие.
28
+ * Переключение раскрытия вложенного списка. Триггер — вся строка целиком (клик или
29
+ * Enter/Space), шеврон `groupIndicator` при этом остаётся неинтерактивным индикатором
30
+ * состояния.
31
31
  */
32
- onExpandIconClick?(e: MouseEvent<HTMLElement>): void;
32
+ onToggleExpand?(): void;
33
33
  /**
34
34
  * Слот ручки drag&drop (Figma `centeredWrapper`) — рендерится первым в строке, перед
35
35
  * маркером/чекбоксом. Интерактивность (обработчики `@dnd-kit`) и содержимое (иконка)
@@ -57,7 +57,7 @@ export function BaseItem({
57
57
  checked: checkedProp,
58
58
  onSelect,
59
59
  onOpenNestedList,
60
- onExpandIconClick,
60
+ onToggleExpand,
61
61
  isParentNode,
62
62
  className,
63
63
  inactive,
@@ -107,7 +107,11 @@ export function BaseItem({
107
107
  // пункту меню), поэтому keyboard-событие пробрасывается под тем же типом.
108
108
  onClick?.(e as MouseEvent<HTMLElement>);
109
109
 
110
- if (interactive && !isParentNode) {
110
+ if (isParentNode) {
111
+ // Раскрытие вложенного списка триггерит вся строка целиком: шеврон `groupIndicator` —
112
+ // только индикатор состояния, собственной интерактивности у него нет.
113
+ onToggleExpand?.();
114
+ } else if (interactive) {
111
115
  handleChange();
112
116
  }
113
117
 
@@ -129,15 +133,20 @@ export function BaseItem({
129
133
  }
130
134
 
131
135
  if (e.key === 'Enter' || e.key === ' ') {
132
- if (isSelectionMultiple && isParentNode && onSelect) {
133
- onSelect();
136
+ if (isParentNode) {
137
+ // В multiple-режиме Enter/Space на группе выбирает всю ветку, иначе — раскрывает её.
138
+ if (isSelectionMultiple && onSelect) {
139
+ onSelect();
140
+ } else {
141
+ onToggleExpand?.();
142
+ }
143
+ } else {
144
+ // Повторяем клик по элементу: `handleItemClick` сам вызывает `handleChange` /
145
+ // `closeDroplist`. Отдельный `handleChange` здесь не нужен — иначе в multiple-режиме
146
+ // выбор переключался бы дважды (toggle + откат) и Enter не менял бы состояние.
147
+ handleItemClick(e);
134
148
  }
135
149
 
136
- // Повторяем клик по элементу: `handleItemClick` сам вызывает `handleChange` /
137
- // `closeDroplist`. Отдельный `handleChange` здесь не нужен — иначе в multiple-режиме
138
- // выбор переключался бы дважды (toggle + откат) и Enter не менял бы состояние.
139
- !isParentNode && handleItemClick(e);
140
-
141
150
  e.stopPropagation();
142
151
  e.preventDefault();
143
152
  }
@@ -149,18 +158,6 @@ export function BaseItem({
149
158
  }
150
159
  };
151
160
 
152
- const handleExpandIconClick = (e: MouseEvent<HTMLElement>) => {
153
- if (disabled) {
154
- return;
155
- }
156
-
157
- // Триггер раскрытия изолирован от клика по строке: всплытие останавливаем,
158
- // чтобы клик по шеврону не активировал выбор/навигацию пункта.
159
- e.stopPropagation();
160
- e.preventDefault();
161
- onExpandIconClick?.(e);
162
- };
163
-
164
161
  const props = extractSupportProps(rest);
165
162
 
166
163
  let contentNode: ReactNode;
@@ -201,6 +198,7 @@ export function BaseItem({
201
198
  data-non-pointer={(inactive && !onClick) || undefined}
202
199
  data-variant={mode || undefined}
203
200
  data-open={open || undefined}
201
+ aria-expanded={isParentNode ? Boolean(open) : undefined}
204
202
  onKeyDown={handleItemKeyDown}
205
203
  onFocus={handleItemFocus}
206
204
  style={{ '--level': level }}
@@ -243,26 +241,16 @@ export function BaseItem({
243
241
  />
244
242
  )}
245
243
 
246
- {!switchProp &&
247
- expandIcon &&
248
- (onExpandIconClick ? (
249
- <button
250
- type='button'
251
- className={styles.groupIndicator}
252
- data-size={size}
253
- data-open={open || undefined}
254
- data-test-id={TEST_IDS.groupIndicator}
255
- tabIndex={-1}
256
- disabled={disabled}
257
- onClick={handleExpandIconClick}
258
- onMouseDown={e => e.stopPropagation()}
259
- >
260
- <span className={styles.groupIndicatorState} aria-hidden />
261
- {expandIcon}
262
- </button>
263
- ) : (
264
- <span className={styles.expandableIcon}>{expandIcon}</span>
265
- ))}
244
+ {!switchProp && expandIcon && (
245
+ <span
246
+ className={styles.groupIndicator}
247
+ data-open={open || undefined}
248
+ data-test-id={TEST_IDS.groupIndicator}
249
+ aria-hidden
250
+ >
251
+ {expandIcon}
252
+ </span>
253
+ )}
266
254
  </li>
267
255
  </div>
268
256
  );
@@ -80,82 +80,35 @@ $sizes: 's', 'm', 'l';
80
80
  color: base.$sn-theme-color-available-version-textSecondary;
81
81
  }
82
82
 
83
- .expandableIcon {
84
- display: flex;
85
- align-items: center;
86
- color: base.$sn-theme-color-available-version-textSecondary;
87
- fill: currentcolor;
88
- }
89
-
90
83
  .groupIndicator {
91
- cursor: pointer;
92
-
93
- position: relative;
84
+ // Шеврон — индикатор состояния, а не кнопка: раскрытие переключает клик по всей строке.
85
+ // Своих hover/pressed/focus-состояний у него нет.
86
+ pointer-events: none;
94
87
 
95
88
  display: flex;
96
89
  flex-shrink: 0;
97
90
  align-items: center;
98
91
  justify-content: center;
99
92
 
100
- padding: 0;
101
- border: none;
102
-
103
- // Token bridge: list/groupIndicator/cornerRadius (8) ещё не выгружен scss-композитом
104
- // из @cloud-ru/ds-figma-variables — берём примитив dimension-8 напрямую.
105
- border-radius: base.$sn-primitive-dimension-8;
106
-
107
93
  // Закрытым шеврон в макете приглушён (@0.80), сплошной `textMain` — только раскрытым.
108
94
  color: base.$sn-theme-color-available-version-textSecondary;
109
95
 
110
- appearance: none;
111
- background: transparent;
112
- outline: none;
96
+ fill: currentcolor;
113
97
 
114
98
  svg {
115
- position: relative;
116
- z-index: 1;
117
- width: base.$sn-density-size-icon-xs;
118
- height: base.$sn-density-size-icon-xs;
119
- }
120
-
121
- &:focus-visible {
122
- outline: base.$sn-theme-color-available-complementary solid base.$sn-primitive-strokeWeight-strokeSemiBold;
123
- outline-offset: calc(0px - base.$sn-primitive-strokeWeight-strokeSemiBold);
99
+ width: 100%;
100
+ height: 100%;
124
101
  }
125
102
 
126
- // В раскрытом состоянии мастер меняет скругление с 8 на 2 — кнопка становится
127
- // «квадратнее» под подложкой нажатия (варианты `groupIndicatorS/Xs`, `open=true`).
103
+ // Раскрытая группа держит постоянную подложку `neutral/decorTransparent`
104
+ // (варианты мастера `groupIndicatorS/Xs`, `open=true`).
128
105
  &[data-open] {
129
106
  border-radius: base.$sn-primitive-dimension-2;
130
107
  color: base.$sn-theme-color-available-version-textMain;
108
+ background: base.$sn-theme-color-neutral-decorTransparent;
131
109
  }
132
110
  }
133
111
 
134
- .groupIndicatorState {
135
- pointer-events: none;
136
-
137
- position: absolute;
138
- inset: 0;
139
-
140
- border-radius: inherit;
141
-
142
- background: transparent;
143
- }
144
-
145
- .groupIndicator:hover > .groupIndicatorState {
146
- background: base.$sn-theme-color-material-stateLayer-neutral-onBackground-hovered-filled;
147
- }
148
-
149
- .groupIndicator:active > .groupIndicatorState {
150
- background: base.$sn-theme-color-material-stateLayer-neutral-onBackground-pressed-filled;
151
- }
152
-
153
- // Раскрытая группа держит постоянную подложку `neutral/decorTransparent`,
154
- // а не pressed-слой состояния.
155
- .groupIndicator[data-open] > .groupIndicatorState {
156
- background: base.$sn-theme-color-neutral-decorTransparent;
157
- }
158
-
159
112
  .markerContainer {
160
113
  pointer-events: none;
161
114
 
@@ -234,17 +187,12 @@ $level: var(--level, 0);
234
187
  padding-bottom: base.simple-var(list.$list, 'anatomy', 'size', $size, 'listItem', 'centeredWrapper', 'padding-bottom');
235
188
  }
236
189
 
237
- .expandableIcon {
238
- width: base.simple-var(icons.$iconSizeMap, $size);
239
- height: base.simple-var(icons.$iconSizeMap, $size);
240
- }
241
-
242
190
  .groupIndicator {
243
191
  width: base.simple-var(icons.$iconSizeMap, $size);
244
192
  height: base.simple-var(icons.$iconSizeMap, $size);
245
- // Строка выровнена по flex-start; centeredWrapper.padding-top сдвигает чеврон на центр
193
+ // Строка выровнена по flex-start; centeredWrapper.padding-top сдвигает шеврон на центр
246
194
  // первой строки текста (как у afterContent/beforeContent). margin (а не padding) —
247
- // чтобы квадрат state-layer'а остался icon-сеткой, а не растянулся по вертикали.
195
+ // чтобы подложка раскрытого состояния осталась icon-сеткой, а не растянулась по вертикали.
248
196
  margin-top: base.simple-var(list.$list, 'anatomy', 'size', $size, 'listItem', 'centeredWrapper', 'padding-top');
249
197
  }
250
198
 
@@ -280,12 +228,7 @@ $level: var(--level, 0);
280
228
 
281
229
  .innerWrapper {
282
230
  &[data-disabled] {
283
- .expandableIcon {
284
- color: base.$sn-theme-color-available-version-textDisabled;
285
- }
286
-
287
231
  .groupIndicator {
288
- cursor: not-allowed;
289
232
  color: base.$sn-theme-color-available-version-textDisabled;
290
233
  }
291
234
 
@@ -149,7 +149,7 @@ export function NextListItem({
149
149
  indeterminate={indeterminate}
150
150
  checked={checked}
151
151
  onOpenNestedList={openSublist}
152
- onExpandIconClick={openSublist}
152
+ onToggleExpand={openSublist}
153
153
  onSelect={handleOnSelect}
154
154
  />
155
155
  </Dropdown>