@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.
- package/CHANGELOG.md +6 -0
- package/dist/cjs/components/Items/SimpleItem/components/SimpleGroupBlock/SimpleGroupBlock.js +8 -9
- package/dist/cjs/components/Items/SimpleItem/components/SimpleGroupBlockOverlay/SimpleGroupBlockOverlay.js +3 -1
- package/dist/cjs/components/Items/SimpleItem/components/SimpleItem/SimpleItem.js +10 -8
- package/dist/cjs/components/Items/SimpleItem/components/SimpleItemOverlay/SimpleItemOverlay.js +6 -1
- package/dist/cjs/components/Items/SimpleItem/styles.module.css +7 -25
- package/dist/cjs/components/Items/SimpleItem/utils.d.ts +2 -1
- package/dist/cjs/components/Items/SimpleItem/utils.js +2 -1
- package/dist/cjs/components/Lists/ListPrivate/ListPrivate.js +2 -2
- package/dist/cjs/constants.d.ts +1 -0
- package/dist/cjs/constants.js +1 -0
- package/dist/cjs/style.css +7 -25
- package/dist/esm/components/Items/SimpleItem/components/SimpleGroupBlock/SimpleGroupBlock.js +8 -9
- package/dist/esm/components/Items/SimpleItem/components/SimpleGroupBlockOverlay/SimpleGroupBlockOverlay.js +3 -1
- package/dist/esm/components/Items/SimpleItem/components/SimpleItem/SimpleItem.js +10 -5
- package/dist/esm/components/Items/SimpleItem/components/SimpleItemOverlay/SimpleItemOverlay.js +6 -1
- package/dist/esm/components/Items/SimpleItem/styles.module.css +7 -25
- package/dist/esm/components/Items/SimpleItem/utils.d.ts +2 -1
- package/dist/esm/components/Items/SimpleItem/utils.js +2 -1
- package/dist/esm/components/Lists/ListPrivate/ListPrivate.js +2 -2
- package/dist/esm/constants.d.ts +1 -0
- package/dist/esm/constants.js +1 -0
- package/dist/esm/style.css +7 -25
- package/dist/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/tsconfig.esm.tsbuildinfo +1 -1
- package/package.json +3 -2
- package/src/components/Items/SimpleItem/components/SimpleGroupBlock/SimpleGroupBlock.tsx +8 -16
- package/src/components/Items/SimpleItem/components/SimpleGroupBlockOverlay/SimpleGroupBlockOverlay.tsx +13 -8
- package/src/components/Items/SimpleItem/components/SimpleItem/SimpleItem.tsx +10 -5
- package/src/components/Items/SimpleItem/components/SimpleItemOverlay/SimpleItemOverlay.tsx +10 -3
- package/src/components/Items/SimpleItem/styles.module.scss +8 -45
- package/src/components/Items/SimpleItem/utils.ts +2 -1
- package/src/components/Lists/ListPrivate/ListPrivate.tsx +4 -2
- 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.
|
|
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
|
-
<
|
|
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
|
-
</
|
|
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
|
-
<
|
|
24
|
-
<div
|
|
25
|
-
<
|
|
26
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
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
|
-
</
|
|
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
|
-
<
|
|
23
|
-
|
|
24
|
-
</
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
//
|
|
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;
|