@cloud-ru/ds-list 1.0.0 → 1.1.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 (34) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/cjs/components/Items/SimpleItem/components/SimpleGroupBlock/SimpleGroupBlock.js +8 -9
  3. package/dist/cjs/components/Items/SimpleItem/components/SimpleGroupBlockOverlay/SimpleGroupBlockOverlay.js +3 -1
  4. package/dist/cjs/components/Items/SimpleItem/components/SimpleItem/SimpleItem.js +10 -8
  5. package/dist/cjs/components/Items/SimpleItem/components/SimpleItemOverlay/SimpleItemOverlay.js +6 -1
  6. package/dist/cjs/components/Items/SimpleItem/styles.module.css +7 -25
  7. package/dist/cjs/components/Items/SimpleItem/utils.d.ts +2 -1
  8. package/dist/cjs/components/Items/SimpleItem/utils.js +2 -1
  9. package/dist/cjs/components/Lists/ListPrivate/ListPrivate.js +2 -2
  10. package/dist/cjs/constants.d.ts +1 -0
  11. package/dist/cjs/constants.js +1 -0
  12. package/dist/cjs/style.css +7 -25
  13. package/dist/esm/components/Items/SimpleItem/components/SimpleGroupBlock/SimpleGroupBlock.js +8 -9
  14. package/dist/esm/components/Items/SimpleItem/components/SimpleGroupBlockOverlay/SimpleGroupBlockOverlay.js +3 -1
  15. package/dist/esm/components/Items/SimpleItem/components/SimpleItem/SimpleItem.js +10 -5
  16. package/dist/esm/components/Items/SimpleItem/components/SimpleItemOverlay/SimpleItemOverlay.js +6 -1
  17. package/dist/esm/components/Items/SimpleItem/styles.module.css +7 -25
  18. package/dist/esm/components/Items/SimpleItem/utils.d.ts +2 -1
  19. package/dist/esm/components/Items/SimpleItem/utils.js +2 -1
  20. package/dist/esm/components/Lists/ListPrivate/ListPrivate.js +2 -2
  21. package/dist/esm/constants.d.ts +1 -0
  22. package/dist/esm/constants.js +1 -0
  23. package/dist/esm/style.css +7 -25
  24. package/dist/tsconfig.cjs.tsbuildinfo +1 -1
  25. package/dist/tsconfig.esm.tsbuildinfo +1 -1
  26. package/package.json +3 -2
  27. package/src/components/Items/SimpleItem/components/SimpleGroupBlock/SimpleGroupBlock.tsx +8 -16
  28. package/src/components/Items/SimpleItem/components/SimpleGroupBlockOverlay/SimpleGroupBlockOverlay.tsx +13 -8
  29. package/src/components/Items/SimpleItem/components/SimpleItem/SimpleItem.tsx +10 -5
  30. package/src/components/Items/SimpleItem/components/SimpleItemOverlay/SimpleItemOverlay.tsx +10 -3
  31. package/src/components/Items/SimpleItem/styles.module.scss +8 -45
  32. package/src/components/Items/SimpleItem/utils.ts +2 -1
  33. package/src/components/Lists/ListPrivate/ListPrivate.tsx +4 -2
  34. package/src/constants.ts +1 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloud-ru/ds-list",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Списочный UI — компонент List для плоских/вложенных списков с выбором, группами и поиском и Droplist — тот же список в поповере.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -52,10 +52,11 @@
52
52
  "@cloud-ru/ds-bottom-sheet": "^1.0.0",
53
53
  "@cloud-ru/ds-button": "^1.0.0",
54
54
  "@cloud-ru/ds-divider": "^1.0.0",
55
+ "@cloud-ru/ds-drag-and-drop": "^1.1.0",
55
56
  "@cloud-ru/ds-dropdown": "^1.0.0",
56
57
  "@cloud-ru/ds-icons": "^1.0.0",
57
- "@cloud-ru/ds-info-block": "^1.0.0",
58
58
  "@cloud-ru/ds-loader": "^1.0.0",
59
+ "@cloud-ru/ds-info-block": "^1.0.0",
59
60
  "@cloud-ru/ds-materials": "^1.0.0",
60
61
  "@cloud-ru/ds-portal-context": "^1.0.0",
61
62
  "@cloud-ru/ds-scroll": "^1.0.0",
@@ -1,7 +1,7 @@
1
- import { useDndContext } from '@dnd-kit/core';
2
1
  import { SortableContext, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable';
3
2
  import { CSS } from '@dnd-kit/utilities';
4
3
  import { NO_DRAG_ATTRIBUTE } from '@cloud-ru/ds-bottom-sheet';
4
+ import { DRAG_MODE, DragGhost } from '@cloud-ru/ds-drag-and-drop';
5
5
  import { CSSProperties, MouseEvent, ReactNode } from 'react';
6
6
 
7
7
  import { Separator } from '../../../../../helperComponents';
@@ -40,13 +40,6 @@ export function SimpleGroupBlock({ id, rowIds, children, ...separatorProps }: Si
40
40
  animateLayoutChanges,
41
41
  });
42
42
 
43
- // Идёт ли переупорядочивание строки этой группы (а не самой группы). Оно жёстко ограничено
44
- // своим контейнером (`container === id` в `data` строки, см. `SimpleItem`), поэтому границы
45
- // блока группы подсвечиваются — видно, что строка не выходит за пределы группы, а не «сломалась».
46
- const { active } = useDndContext();
47
- const activeContainer = (active?.data.current as { container?: ItemId } | undefined)?.container;
48
- const isReorderingWithinGroup = active != null && active.id !== id && activeContainer === id;
49
-
50
43
  const style: CSSProperties = {
51
44
  transform: CSS.Translate.toString(transform),
52
45
  transition,
@@ -59,20 +52,19 @@ export function SimpleGroupBlock({ id, rowIds, children, ...separatorProps }: Si
59
52
  onMouseDown?.(e);
60
53
  };
61
54
 
55
+ // Динамический перенос — тот же, что у строк (`SimpleItem`).
62
56
  return (
63
- <div
64
- ref={setNodeRef}
65
- style={style}
66
- className={styles.groupBlock}
67
- data-dragging={isDragging || undefined}
68
- data-reorder-boundary={isReorderingWithinGroup || undefined}
69
- >
57
+ <DragGhost innerRef={setNodeRef} style={style} dragging={isDragging} mode={DRAG_MODE.Dynamic}>
70
58
  <div className={styles.groupHeader} data-size={size}>
71
59
  <DragHandle
72
60
  // Pointer-жест не доходит до BottomSheet: иначе vertical drag одновременно закрывает sheet.
73
61
  {...{ [NO_DRAG_ATTRIBUTE]: '' }}
74
62
  {...attributes}
75
63
  {...restListeners}
64
+ // `useSortable` ставит ручке tabIndex=0, но список ведёт свою roving-навигацию и
65
+ // перехватывает Tab (см. `useNewKeyboardNavigation`) — с ручки было некуда уйти.
66
+ // Убираем её из таб-порядка: строка остаётся доступной с клавиатуры, ручка — мышью.
67
+ tabIndex={-1}
76
68
  onMouseDown={handleMouseDown}
77
69
  onClick={stopPropagation}
78
70
  />
@@ -82,6 +74,6 @@ export function SimpleGroupBlock({ id, rowIds, children, ...separatorProps }: Si
82
74
  <SortableContext items={rowIds} strategy={verticalListSortingStrategy}>
83
75
  {children}
84
76
  </SortableContext>
85
- </div>
77
+ </DragGhost>
86
78
  );
87
79
  }
@@ -1,3 +1,6 @@
1
+ import { DragPreview } from '@cloud-ru/ds-drag-and-drop';
2
+
3
+ import { TEST_IDS } from '../../../../../constants';
1
4
  import { Separator } from '../../../../../helperComponents';
2
5
  import { Size } from '../../../../../types';
3
6
  import { FlattenSimpleItem } from '../../../types';
@@ -20,14 +23,16 @@ export type SimpleGroupBlockOverlayProps = GroupHeaderContent & {
20
23
  */
21
24
  export function SimpleGroupBlockOverlay({ size, rows, ...separatorProps }: SimpleGroupBlockOverlayProps) {
22
25
  return (
23
- <div className={styles.groupBlock} data-overlay>
24
- <div className={styles.groupHeader} data-size={size}>
25
- <OverlayDragHandle />
26
- <Separator {...separatorProps} />
26
+ <DragPreview className={styles.dragPreview} data-test-id={TEST_IDS.dragOverlay}>
27
+ <div>
28
+ <div className={styles.groupHeader} data-size={size}>
29
+ <OverlayDragHandle />
30
+ <Separator {...separatorProps} />
31
+ </div>
32
+ {rows.map(row => (
33
+ <SimpleItemOverlay key={row.id} {...row} bare />
34
+ ))}
27
35
  </div>
28
- {rows.map(row => (
29
- <SimpleItemOverlay key={row.id} {...row} bare />
30
- ))}
31
- </div>
36
+ </DragPreview>
32
37
  );
33
38
  }
@@ -1,13 +1,13 @@
1
1
  import { useSortable } from '@dnd-kit/sortable';
2
2
  import { CSS } from '@dnd-kit/utilities';
3
3
  import { NO_DRAG_ATTRIBUTE } from '@cloud-ru/ds-bottom-sheet';
4
+ import { DRAG_MODE, DragGhost } from '@cloud-ru/ds-drag-and-drop';
4
5
  import { CSSProperties, MouseEvent } from 'react';
5
6
 
6
7
  import { stopPropagation } from '../../../../../utils';
7
8
  import { BaseItem } from '../../../BaseItem';
8
9
  import { FlattenSimpleItem, ItemId } from '../../../types';
9
10
  import { REORDER_TOP_LEVEL } from '../../constants';
10
- import styles from '../../styles.module.scss';
11
11
  import { animateLayoutChanges } from '../../utils';
12
12
  import { DragHandle } from '../DragHandle';
13
13
 
@@ -28,8 +28,8 @@ export function SimpleItem({ id, disabled, groupId, ...option }: SimpleItemProps
28
28
  // `disabled` на строке отключает `Switch` и клик (`BaseItem`), но не переупорядочивание:
29
29
  // ручка всегда активна.
30
30
  // `container` читает кастомный `collisionDetection` в `ListPrivate`: drop разрешён только внутри
31
- // одного контейнера. Строка без группы (`groupId` пуст) лежит в общем контейнере верхнего уровня
32
- // вместе с группами (можно менять местами строку и группу); строка группы — в контейнере группы.
31
+ // одного контейнера. Строка без группы (`groupId` пуст) лежит в контейнере верхнего уровня,
32
+ // строка группы — в контейнере своей группы.
33
33
  data: { container: groupId ?? REORDER_TOP_LEVEL },
34
34
  animateLayoutChanges,
35
35
  });
@@ -48,8 +48,9 @@ export function SimpleItem({ id, disabled, groupId, ...option }: SimpleItemProps
48
48
  onMouseDown?.(e);
49
49
  };
50
50
 
51
+ // Динамический перенос: точку вставки показывает пустой слот, поэтому линии (`DropIndicator`) нет.
51
52
  return (
52
- <div ref={setNodeRef} style={style} className={styles.item} data-dragging={isDragging || undefined}>
53
+ <DragGhost innerRef={setNodeRef} style={style} dragging={isDragging} mode={DRAG_MODE.Dynamic}>
53
54
  <BaseItem
54
55
  {...option}
55
56
  id={id}
@@ -60,11 +61,15 @@ export function SimpleItem({ id, disabled, groupId, ...option }: SimpleItemProps
60
61
  {...{ [NO_DRAG_ATTRIBUTE]: '' }}
61
62
  {...attributes}
62
63
  {...restListeners}
64
+ // `useSortable` ставит ручке tabIndex=0, но список ведёт свою roving-навигацию и
65
+ // перехватывает Tab (см. `useNewKeyboardNavigation`) — с ручки было некуда уйти.
66
+ // Убираем её из таб-порядка: строка остаётся доступной с клавиатуры, ручка — мышью.
67
+ tabIndex={-1}
63
68
  onMouseDown={handleMouseDown}
64
69
  onClick={stopPropagation}
65
70
  />
66
71
  }
67
72
  />
68
- </div>
73
+ </DragGhost>
69
74
  );
70
75
  }
@@ -1,3 +1,6 @@
1
+ import { DragPreview } from '@cloud-ru/ds-drag-and-drop';
2
+
3
+ import { TEST_IDS } from '../../../../../constants';
1
4
  import { BaseItem } from '../../../BaseItem';
2
5
  import { FlattenSimpleItem } from '../../../types';
3
6
  import styles from '../../styles.module.scss';
@@ -18,9 +21,13 @@ export type SimpleItemOverlayProps = FlattenSimpleItem & {
18
21
  * это неинтерактивный снимок: ручка и содержимое статичны, перетаскивание ведёт исходная строка.
19
22
  */
20
23
  export function SimpleItemOverlay({ bare, ...item }: SimpleItemOverlayProps) {
24
+ const row = <BaseItem {...item} dragHandle={<OverlayDragHandle />} />;
25
+
26
+ if (bare) return row;
27
+
21
28
  return (
22
- <div className={styles.item} data-overlay={bare ? undefined : true}>
23
- <BaseItem {...item} dragHandle={<OverlayDragHandle />} />
24
- </div>
29
+ <DragPreview className={styles.dragPreview} data-test-id={TEST_IDS.dragOverlay}>
30
+ {row}
31
+ </DragPreview>
25
32
  );
26
33
  }
@@ -1,5 +1,6 @@
1
1
  @use '@cloud-ru/ds-figma-variables/build/scss/styles/styles.module' as base;
2
2
  @use '@cloud-ru/ds-figma-variables/build/scss/components/list.module' as list;
3
+ @use '@cloud-ru/ds-materials' as m;
3
4
  @use '../../../iconSizes' as icons;
4
5
 
5
6
  $sizes: 's', 'm', 'l';
@@ -32,51 +33,9 @@ $listItemPaddingVertical: (
32
33
  'l': list.$sn-list-anatomy-size-l-listItem-paddingVertical,
33
34
  );
34
35
 
35
- .item {
36
- // Исходная строка во время переноса — приглушённый «призрак» на месте будущей позиции.
37
- // Саму перетаскиваемую копию рендерит `DragOverlay` (`SimpleItemOverlay`) в портале.
38
- &[data-dragging] {
39
- position: relative;
40
- z-index: 1;
41
- opacity: 0.5;
42
- }
43
-
44
- // Плавающая копия в `DragOverlay`: сплошная поверхность + elevation, чтобы строка читалась
45
- // над фоном страницы (портал вне контейнера List, своего фона у строки нет).
46
- &[data-overlay] {
47
- cursor: grabbing;
48
- opacity: 0.5;
49
- background: base.$sn-theme-color-neutral-background2Level;
50
- box-shadow: base.$sn-boxShadow-elevation-level2;
51
- }
52
- }
53
-
54
- // Сортируемый блок группы (`SimpleGroupBlock`): заголовок + строки едут вместе (`useSortable` на
55
- // блоке). Во время перетаскивания блок целиком приглушён «призраком».
56
- .groupBlock {
57
- &[data-dragging] {
58
- position: relative;
59
- z-index: 1;
60
- opacity: 0.5;
61
- }
62
-
63
- // Плавающая копия блока в `DragOverlay` (перетаскивание группы): сплошная поверхность + elevation
64
- // на весь блок (заголовок + строки), чтобы читалась над фоном страницы.
65
- &[data-overlay] {
66
- cursor: grabbing;
67
- opacity: 0.5;
68
- background: base.$sn-theme-color-neutral-background2Level;
69
- box-shadow: base.$sn-boxShadow-elevation-level2;
70
- }
71
-
72
- // Подсветка границ группы при переупорядочивании строки внутри неё (`data-reorder-boundary`):
73
- // даёт понять, что перестановка ограничена группой, иначе остановка у границы читается как баг.
74
- &[data-reorder-boundary] {
75
- background: base.$sn-theme-color-neutral-background1Level;
76
- box-shadow:
77
- inset 0 6px 12px -12px base.$sn-theme-color-blackout,
78
- inset 0 -6px 12px -12px base.$sn-theme-color-blackout;
79
- }
36
+ // Поверхность и тень копии даёт `DragPreview`, здесь остаётся только курсор переноса.
37
+ .dragPreview {
38
+ cursor: grabbing;
80
39
  }
81
40
 
82
41
  // Строка-заголовок внутри блока группы: ручка перетаскивания + `Separator`. Отступ слева и gap — как у
@@ -133,6 +92,10 @@ $listItemPaddingVertical: (
133
92
 
134
93
  appearance: none;
135
94
  background: transparent;
95
+ outline: none;
96
+
97
+ // Без своей рамки браузер рисует дефолтное синее кольцо — на Tab после переноса оно и вылезало.
98
+ @include m.with-focus-frame('regular', 'outsideOffset');
136
99
 
137
100
  svg {
138
101
  width: 100%;
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Отключает FLIP-анимацию перестановки после отпускания (общий контракт `SimpleItem` и `SimpleGroupBlock`).
3
3
  * Порядок применяется синхронно через `onItemsReorder`; без флага `@dnd-kit` доигрывает `transform 200ms`
4
- * из старого слота в новый — отпущенная строка дёргается обратно и уезжает на место.
4
+ * из старого слота в новый — отпущенная строка дёргается обратно и уезжает на место. Расступание соседей
5
+ * на время переноса флаг не трогает: его ведёт стратегия `SortableContext`.
5
6
  */
6
7
  export function animateLayoutChanges(): boolean {
7
8
  return false;
@@ -128,8 +128,8 @@ export const ListPrivate = forwardRef(
128
128
  useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
129
129
  );
130
130
  // Id перетаскиваемой строки — для рендера её копии в `DragOverlay`. Копия живёт в портале
131
- // над страницей и не режется `overflow: hidden` контейнера `List` (исходная строка остаётся
132
- // на месте «призраком»). `null`, когда перетаскивание не идёт.
131
+ // над страницей и не режется `overflow: hidden` контейнера `List` (сама строка на время
132
+ // переноса уступает место — её слот пустеет). `null`, когда перетаскивание не идёт.
133
133
  const [activeDragId, setActiveDragId] = useState<ItemId | null>(null);
134
134
  const handleDragStart = useCallback((event: DragStartEvent) => setActiveDragId(event.active.id), []);
135
135
  const handleDragEnd = useCallback(
@@ -416,6 +416,8 @@ export const ListPrivate = forwardRef(
416
416
  onDragEnd={handleDragEnd}
417
417
  onDragCancel={handleDragCancel}
418
418
  >
419
+ {/* Зону приёма (`DropTarget`) список не рисует: рамка означает перенос между зонами,
420
+ а здесь перестановка идёт строго внутри своей. */}
419
421
  <SortableContext items={sortableIds ?? []} strategy={verticalListSortingStrategy}>
420
422
  {itemsBody}
421
423
  </SortableContext>
package/src/constants.ts CHANGED
@@ -48,6 +48,7 @@ export const TEST_IDS = {
48
48
  accordionItem: 'list__accordion-item',
49
49
  groupIndicator: 'list__group-indicator',
50
50
  dragHandle: 'list__drag-handle',
51
+ dragOverlay: 'list__drag-overlay',
51
52
  mobileDroplistRoot: 'list__mobile-droplist-root',
52
53
  mobileDroplistTrigger: 'list__mobile-droplist-trigger',
53
54
  } as const;