@cloud-ru/ds-list 1.1.4-preview-5ffe9ec9.0 → 1.1.4

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-preview-5ffe9ec9.0",
3
+ "version": "1.1.4",
4
4
  "description": "Списочный UI — компонент List для плоских/вложенных списков с выбором, группами и поиском и Droplist — тот же список в поповере.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -48,24 +48,24 @@
48
48
  "@tanstack/react-virtual": "^3.14.8",
49
49
  "classnames": "^2.5.1",
50
50
  "merge-refs": "^2.0.0",
51
- "@cloud-ru/ds-bottom-sheet": "1.0.1",
52
- "@cloud-ru/ds-adaptive": "1.0.1",
53
- "@cloud-ru/ds-button": "1.0.1",
54
- "@cloud-ru/ds-divider": "1.0.1",
55
- "@cloud-ru/ds-drag-and-drop": "1.1.1",
56
- "@cloud-ru/ds-dropdown": "1.0.1",
57
- "@cloud-ru/ds-icons": "1.0.1",
58
- "@cloud-ru/ds-loader": "1.0.1",
59
- "@cloud-ru/ds-info-block": "1.0.1",
60
- "@cloud-ru/ds-portal-context": "1.0.1",
61
- "@cloud-ru/ds-materials": "1.0.1",
62
- "@cloud-ru/ds-search-private": "1.0.2",
63
- "@cloud-ru/ds-toggles": "1.1.1",
64
- "@cloud-ru/ds-scroll": "1.0.1",
65
- "@cloud-ru/ds-truncate-string": "1.0.1",
66
- "@cloud-ru/ds-utils": "1.1.0"
51
+ "@cloud-ru/ds-adaptive": "^1.0.1",
52
+ "@cloud-ru/ds-bottom-sheet": "^1.0.2",
53
+ "@cloud-ru/ds-button": "^1.0.1",
54
+ "@cloud-ru/ds-drag-and-drop": "^1.1.1",
55
+ "@cloud-ru/ds-dropdown": "^1.0.2",
56
+ "@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
+ "@cloud-ru/ds-loader": "^1.0.1",
60
+ "@cloud-ru/ds-materials": "^1.0.1",
61
+ "@cloud-ru/ds-portal-context": "^1.0.1",
62
+ "@cloud-ru/ds-scroll": "^1.0.1",
63
+ "@cloud-ru/ds-search-private": "^1.0.2",
64
+ "@cloud-ru/ds-truncate-string": "^1.0.2",
65
+ "@cloud-ru/ds-toggles": "^1.1.1",
66
+ "@cloud-ru/ds-utils": "^1.1.0"
67
67
  },
68
68
  "peerDependencies": {
69
- "@cloud-ru/ds-locale": "1.0.0"
69
+ "@cloud-ru/ds-locale": "^1.0.0"
70
70
  }
71
71
  }
@@ -39,13 +39,20 @@ export function AccordionItem({ id, disabled, allChildIds, items, ...option }: A
39
39
  (isSelectionMultiple && checkedProp),
40
40
  );
41
41
 
42
- // Раскрытие переключает вся строка целиком: клик/Enter по телу и ArrowRight с клавиатуры.
43
- const handleToggle = useCallback(() => {
42
+ const handleKeyDown = useCallback(() => {
44
43
  toggleOpenCollapseItem?.(id ?? '');
45
44
  }, [id, toggleOpenCollapseItem]);
46
45
 
47
46
  const itemsJSX = useRenderItems(items);
48
47
 
48
+ // Раскрытие привязано к явной кнопке-триггеру `groupIndicator` (шеврон справа) и
49
+ // ТОЛЬКО переключает collapse — `option.onClick` сюда не зовём, иначе у потребителя,
50
+ // который вешает на onClick собственный toggle, выходит двойное переключение.
51
+ // Клик по телу строки (выбор/навигация) идёт через `option.onClick` в BaseItem.
52
+ const handleExpandIconClick = () => {
53
+ toggleOpenCollapseItem?.(id ?? '');
54
+ };
55
+
49
56
  return (
50
57
  <CollapseBlockPrivate
51
58
  header={
@@ -55,9 +62,9 @@ export function AccordionItem({ id, disabled, allChildIds, items, ...option }: A
55
62
  disabled={disabled}
56
63
  open={isOpen}
57
64
  expandIcon={isOpen ? <ChevronUpSVG /> : <ChevronDownSVG />}
58
- onToggleExpand={handleToggle}
65
+ onExpandIconClick={handleExpandIconClick}
59
66
  isParentNode
60
- onOpenNestedList={handleToggle}
67
+ onOpenNestedList={handleKeyDown}
61
68
  checked={checked}
62
69
  indeterminate={indeterminate}
63
70
  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
- * Переключение раскрытия вложенного списка. Триггер — вся строка целиком (клик или
29
- * Enter/Space), шеврон `groupIndicator` при этом остаётся неинтерактивным индикатором
30
- * состояния.
28
+ * Клик по явной кнопке-триггеру `groupIndicator` (шеврон справа), открывающей/закрывающей
29
+ * вложенный список. Если передан — шеврон рендерится как отдельная интерактивная кнопка
30
+ * со своим state-layer'ом, а клик по телу строки больше не переключает раскрытие.
31
31
  */
32
- onToggleExpand?(): void;
32
+ onExpandIconClick?(e: MouseEvent<HTMLElement>): 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
- onToggleExpand,
60
+ onExpandIconClick,
61
61
  isParentNode,
62
62
  className,
63
63
  inactive,
@@ -107,11 +107,7 @@ export function BaseItem({
107
107
  // пункту меню), поэтому keyboard-событие пробрасывается под тем же типом.
108
108
  onClick?.(e as MouseEvent<HTMLElement>);
109
109
 
110
- if (isParentNode) {
111
- // Раскрытие вложенного списка триггерит вся строка целиком: шеврон `groupIndicator` —
112
- // только индикатор состояния, собственной интерактивности у него нет.
113
- onToggleExpand?.();
114
- } else if (interactive) {
110
+ if (interactive && !isParentNode) {
115
111
  handleChange();
116
112
  }
117
113
 
@@ -133,20 +129,15 @@ export function BaseItem({
133
129
  }
134
130
 
135
131
  if (e.key === 'Enter' || e.key === ' ') {
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);
132
+ if (isSelectionMultiple && isParentNode && onSelect) {
133
+ onSelect();
148
134
  }
149
135
 
136
+ // Повторяем клик по элементу: `handleItemClick` сам вызывает `handleChange` /
137
+ // `closeDroplist`. Отдельный `handleChange` здесь не нужен — иначе в multiple-режиме
138
+ // выбор переключался бы дважды (toggle + откат) и Enter не менял бы состояние.
139
+ !isParentNode && handleItemClick(e);
140
+
150
141
  e.stopPropagation();
151
142
  e.preventDefault();
152
143
  }
@@ -158,6 +149,18 @@ export function BaseItem({
158
149
  }
159
150
  };
160
151
 
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
+
161
164
  const props = extractSupportProps(rest);
162
165
 
163
166
  let contentNode: ReactNode;
@@ -198,7 +201,6 @@ export function BaseItem({
198
201
  data-non-pointer={(inactive && !onClick) || undefined}
199
202
  data-variant={mode || undefined}
200
203
  data-open={open || undefined}
201
- aria-expanded={isParentNode ? Boolean(open) : undefined}
202
204
  onKeyDown={handleItemKeyDown}
203
205
  onFocus={handleItemFocus}
204
206
  style={{ '--level': level }}
@@ -241,16 +243,26 @@ export function BaseItem({
241
243
  />
242
244
  )}
243
245
 
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
- )}
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
+ ))}
254
266
  </li>
255
267
  </div>
256
268
  );
@@ -80,35 +80,82 @@ $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
+
83
90
  .groupIndicator {
84
- // Шеврон — индикатор состояния, а не кнопка: раскрытие переключает клик по всей строке.
85
- // Своих hover/pressed/focus-состояний у него нет.
86
- pointer-events: none;
91
+ cursor: pointer;
92
+
93
+ position: relative;
87
94
 
88
95
  display: flex;
89
96
  flex-shrink: 0;
90
97
  align-items: center;
91
98
  justify-content: center;
92
99
 
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
+
93
107
  // Закрытым шеврон в макете приглушён (@0.80), сплошной `textMain` — только раскрытым.
94
108
  color: base.$sn-theme-color-available-version-textSecondary;
95
109
 
96
- fill: currentcolor;
110
+ appearance: none;
111
+ background: transparent;
112
+ outline: none;
97
113
 
98
114
  svg {
99
- width: 100%;
100
- height: 100%;
115
+ position: relative;
116
+ z-index: 1;
117
+ width: base.$sn-density-size-icon-xs;
118
+ height: base.$sn-density-size-icon-xs;
101
119
  }
102
120
 
103
- // Раскрытая группа держит постоянную подложку `neutral/decorTransparent`
104
- // (варианты мастера `groupIndicatorS/Xs`, `open=true`).
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);
124
+ }
125
+
126
+ // В раскрытом состоянии мастер меняет скругление с 8 на 2 — кнопка становится
127
+ // «квадратнее» под подложкой нажатия (варианты `groupIndicatorS/Xs`, `open=true`).
105
128
  &[data-open] {
106
129
  border-radius: base.$sn-primitive-dimension-2;
107
130
  color: base.$sn-theme-color-available-version-textMain;
108
- background: base.$sn-theme-color-neutral-decorTransparent;
109
131
  }
110
132
  }
111
133
 
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
+
112
159
  .markerContainer {
113
160
  pointer-events: none;
114
161
 
@@ -187,12 +234,17 @@ $level: var(--level, 0);
187
234
  padding-bottom: base.simple-var(list.$list, 'anatomy', 'size', $size, 'listItem', 'centeredWrapper', 'padding-bottom');
188
235
  }
189
236
 
237
+ .expandableIcon {
238
+ width: base.simple-var(icons.$iconSizeMap, $size);
239
+ height: base.simple-var(icons.$iconSizeMap, $size);
240
+ }
241
+
190
242
  .groupIndicator {
191
243
  width: base.simple-var(icons.$iconSizeMap, $size);
192
244
  height: base.simple-var(icons.$iconSizeMap, $size);
193
- // Строка выровнена по flex-start; centeredWrapper.padding-top сдвигает шеврон на центр
245
+ // Строка выровнена по flex-start; centeredWrapper.padding-top сдвигает чеврон на центр
194
246
  // первой строки текста (как у afterContent/beforeContent). margin (а не padding) —
195
- // чтобы подложка раскрытого состояния осталась icon-сеткой, а не растянулась по вертикали.
247
+ // чтобы квадрат state-layer'а остался icon-сеткой, а не растянулся по вертикали.
196
248
  margin-top: base.simple-var(list.$list, 'anatomy', 'size', $size, 'listItem', 'centeredWrapper', 'padding-top');
197
249
  }
198
250
 
@@ -228,7 +280,12 @@ $level: var(--level, 0);
228
280
 
229
281
  .innerWrapper {
230
282
  &[data-disabled] {
283
+ .expandableIcon {
284
+ color: base.$sn-theme-color-available-version-textDisabled;
285
+ }
286
+
231
287
  .groupIndicator {
288
+ cursor: not-allowed;
232
289
  color: base.$sn-theme-color-available-version-textDisabled;
233
290
  }
234
291
 
@@ -149,7 +149,7 @@ export function NextListItem({
149
149
  indeterminate={indeterminate}
150
150
  checked={checked}
151
151
  onOpenNestedList={openSublist}
152
- onToggleExpand={openSublist}
152
+ onExpandIconClick={openSublist}
153
153
  onSelect={handleOnSelect}
154
154
  />
155
155
  </Dropdown>