@ncds/ui-admin 1.8.28-alpha.3 → 1.8.28-alpha.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/dist/cjs/assets/scripts/table/rowSpanBlock.js +97 -0
- package/dist/cjs/src/components/data-display/table/Table.js +135 -57
- package/dist/cjs/src/components/data-display/table/__tests__/Table.dnd.dragCell.test.js +7 -2
- package/dist/cjs/src/components/data-display/table/__tests__/Table.dnd.test.js +13 -8
- package/dist/cjs/src/components/data-display/table/__tests__/Table.rowGroup.test.js +54 -0
- package/dist/cjs/src/components/data-display/table/__tests__/Table.rowGroupDnd.test.js +212 -0
- package/dist/cjs/src/components/data-display/table/__tests__/dnd-context.test.js +5 -2
- package/dist/cjs/src/components/data-display/table/__tests__/tableSelectableStyle.test.js +75 -0
- package/dist/cjs/src/components/data-display/table/dnd-context.js +4 -1
- package/dist/cjs/src/components/data-display/table/dnd-move.js +46 -0
- package/dist/cjs/src/components/data-display/table/dnd-preview.js +8 -3
- package/dist/cjs/src/generated/scoped-css.js +1 -1
- package/dist/esm/assets/scripts/table/rowSpanBlock.js +85 -0
- package/dist/esm/src/components/data-display/table/Table.js +135 -57
- package/dist/esm/src/components/data-display/table/__tests__/Table.dnd.dragCell.test.js +7 -2
- package/dist/esm/src/components/data-display/table/__tests__/Table.dnd.test.js +13 -8
- package/dist/esm/src/components/data-display/table/__tests__/Table.rowGroup.test.js +51 -0
- package/dist/esm/src/components/data-display/table/__tests__/Table.rowGroupDnd.test.js +210 -0
- package/dist/esm/src/components/data-display/table/__tests__/dnd-context.test.js +5 -2
- package/dist/esm/src/components/data-display/table/__tests__/tableSelectableStyle.test.js +72 -0
- package/dist/esm/src/components/data-display/table/dnd-context.js +4 -1
- package/dist/esm/src/components/data-display/table/dnd-move.js +38 -0
- package/dist/esm/src/components/data-display/table/dnd-preview.js +8 -3
- package/dist/esm/src/generated/scoped-css.js +1 -1
- package/dist/temp/assets/scripts/table/rowSpanBlock.d.ts +29 -0
- package/dist/temp/assets/scripts/table/rowSpanBlock.js +77 -0
- package/dist/temp/src/components/data-display/table/Table.d.ts +7 -3
- package/dist/temp/src/components/data-display/table/Table.js +107 -47
- package/dist/temp/src/components/data-display/table/__tests__/Table.dnd.dragCell.test.js +10 -2
- package/dist/temp/src/components/data-display/table/__tests__/Table.dnd.test.js +16 -8
- package/dist/temp/src/components/data-display/table/__tests__/Table.rowGroup.test.d.ts +1 -0
- package/dist/temp/src/components/data-display/table/__tests__/Table.rowGroup.test.js +52 -0
- package/dist/temp/src/components/data-display/table/__tests__/Table.rowGroupDnd.test.d.ts +1 -0
- package/dist/temp/src/components/data-display/table/__tests__/Table.rowGroupDnd.test.js +137 -0
- package/dist/temp/src/components/data-display/table/__tests__/dnd-context.test.js +2 -2
- package/dist/temp/src/components/data-display/table/__tests__/tableSelectableStyle.test.d.ts +1 -0
- package/dist/temp/src/components/data-display/table/__tests__/tableSelectableStyle.test.js +71 -0
- package/dist/temp/src/components/data-display/table/dnd-context.d.ts +3 -0
- package/dist/temp/src/components/data-display/table/dnd-context.js +1 -0
- package/dist/temp/src/components/data-display/table/dnd-move.d.ts +17 -0
- package/dist/temp/src/components/data-display/table/dnd-move.js +31 -0
- package/dist/temp/src/components/data-display/table/dnd-preview.d.ts +5 -1
- package/dist/temp/src/components/data-display/table/dnd-preview.js +7 -3
- package/dist/temp/src/components/data-display/table/types.d.ts +14 -5
- package/dist/temp/src/generated/scoped-css.js +1 -1
- package/dist/types/assets/scripts/table/rowSpanBlock.d.ts +29 -0
- package/dist/types/src/components/data-display/table/Table.d.ts +7 -3
- package/dist/types/src/components/data-display/table/__tests__/Table.rowGroup.test.d.ts +1 -0
- package/dist/types/src/components/data-display/table/__tests__/Table.rowGroupDnd.test.d.ts +1 -0
- package/dist/types/src/components/data-display/table/__tests__/tableSelectableStyle.test.d.ts +1 -0
- package/dist/types/src/components/data-display/table/dnd-context.d.ts +3 -0
- package/dist/types/src/components/data-display/table/dnd-move.d.ts +17 -0
- package/dist/types/src/components/data-display/table/dnd-preview.d.ts +5 -1
- package/dist/types/src/components/data-display/table/types.d.ts +14 -5
- package/dist/ui-admin/assets/styles/style.css +60 -4
- package/package.json +1 -1
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
const SINGLE_ROW = 1;
|
|
2
|
+
const collectRanges = rowSpans => rowSpans.flatMap((spans, index) => spans.filter(span => span > SINGLE_ROW).map(span => ({
|
|
3
|
+
start: index,
|
|
4
|
+
end: Math.min(index + span - SINGLE_ROW, rowSpans.length - 1)
|
|
5
|
+
})));
|
|
6
|
+
const overlaps = (a, b) => a.start <= b.end && b.start <= a.end;
|
|
7
|
+
/**
|
|
8
|
+
* 병합을 쪼개지 않고 통째로 옮길 수 있는 최소 행 구간을 구한다.
|
|
9
|
+
* 범위가 겹치면 어느 쪽도 쪼갤 수 없으므로 연결된 범위를 모두 합친다.
|
|
10
|
+
*
|
|
11
|
+
* @param rowSpans 행별 셀 rowSpan 목록. 병합에 덮여 생략된 셀은 포함하지 않는다
|
|
12
|
+
* @param index 기준 행의 위치
|
|
13
|
+
*/
|
|
14
|
+
export const computeRowSpanBlock = (rowSpans, index) => {
|
|
15
|
+
const ranges = collectRanges(rowSpans);
|
|
16
|
+
let block = {
|
|
17
|
+
start: index,
|
|
18
|
+
end: index
|
|
19
|
+
};
|
|
20
|
+
let expanded = true;
|
|
21
|
+
while (expanded) {
|
|
22
|
+
expanded = false;
|
|
23
|
+
for (const range of ranges) {
|
|
24
|
+
if (!overlaps(range, block)) continue;
|
|
25
|
+
const start = Math.min(block.start, range.start);
|
|
26
|
+
const end = Math.max(block.end, range.end);
|
|
27
|
+
if (start !== block.start || end !== block.end) {
|
|
28
|
+
block = {
|
|
29
|
+
start,
|
|
30
|
+
end
|
|
31
|
+
};
|
|
32
|
+
expanded = true;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return block;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* 드롭을 받을 수 있는 변. 블록 가운데에 떨어뜨리면 그 블록이 쪼개지므로 경계만 연다.
|
|
40
|
+
*/
|
|
41
|
+
export const computeOpenEdges = (rowSpans, index) => {
|
|
42
|
+
const block = computeRowSpanBlock(rowSpans, index);
|
|
43
|
+
const edges = [];
|
|
44
|
+
if (index === block.start) edges.push('top');
|
|
45
|
+
if (index === block.end) edges.push('bottom');
|
|
46
|
+
return edges;
|
|
47
|
+
};
|
|
48
|
+
/** 고른 행들이 끌고 오는 행 전부. 병합 블록을 쪼개면 병합이 깨지므로 블록째 더한다. */
|
|
49
|
+
export const collectBlockIndices = (rowSpans, indices) => {
|
|
50
|
+
const picked = new Set();
|
|
51
|
+
for (const index of indices) {
|
|
52
|
+
const block = computeRowSpanBlock(rowSpans, index);
|
|
53
|
+
for (let row = block.start; row <= block.end; row++) {
|
|
54
|
+
picked.add(row);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return Array.from(picked).sort((a, b) => a - b);
|
|
58
|
+
};
|
|
59
|
+
/** 행이 속한 `tbody` 와 그 안에서의 위치. 표 밖 요소면 `null`. */
|
|
60
|
+
export const locateRow = rowEl => {
|
|
61
|
+
const body = rowEl.closest('tbody');
|
|
62
|
+
if (!body) return null;
|
|
63
|
+
const index = Array.from(body.rows).indexOf(rowEl);
|
|
64
|
+
return index < 0 ? null : {
|
|
65
|
+
body,
|
|
66
|
+
index
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
/** 표 본문의 행별 rowSpan 을 읽어 `computeRowSpanBlock` 에 넘길 형태로 만든다. */
|
|
70
|
+
export const readRowSpans = body => Array.from(body.rows).map(row => Array.from(row.cells).map(cell => cell.rowSpan));
|
|
71
|
+
/** 다중 선택으로 고른 행들이 끌고 오는 행 전부. 행 순서는 표에 놓인 순서를 따른다. */
|
|
72
|
+
export const expandRowsToBlocks = rows => {
|
|
73
|
+
const perBody = new Map();
|
|
74
|
+
for (const row of rows) {
|
|
75
|
+
const located = locateRow(row);
|
|
76
|
+
if (!located) continue;
|
|
77
|
+
const picked = perBody.get(located.body) ?? [];
|
|
78
|
+
picked.push(located.index);
|
|
79
|
+
perBody.set(located.body, picked);
|
|
80
|
+
}
|
|
81
|
+
return Array.from(perBody).flatMap(_ref => {
|
|
82
|
+
let [body, indices] = _ref;
|
|
83
|
+
return collectBlockIndices(readRowSpans(body), indices).map(index => body.rows[index]);
|
|
84
|
+
});
|
|
85
|
+
};
|
|
@@ -6,8 +6,10 @@ import { attachClosestEdge, extractClosestEdge } from '@atlaskit/pragmatic-drag-
|
|
|
6
6
|
import { ChevronDown, ChevronSelectorVertical, ChevronUp, DotsGrid02 } from '@ncds/ui-admin-icon';
|
|
7
7
|
import classNames from 'classnames';
|
|
8
8
|
import { Children, forwardRef, useCallback, useContext, useEffect, useRef, useState } from 'react';
|
|
9
|
+
import { computeOpenEdges, readRowSpans } from '../../../../assets/scripts/table/rowSpanBlock';
|
|
9
10
|
import { FloatingProvider } from '../../../contexts/FloatingContext';
|
|
10
11
|
import { RowDndContext, TableDndContext } from './dnd-context';
|
|
12
|
+
import { DRAG_ID_ATTRIBUTE, findRowSpanBlock, locateRow, planMoveIds } from './dnd-move';
|
|
11
13
|
import { buildDragPreview, PREVIEW_BADGE_SIZE, TABLE_DND_ID_RADIX, TABLE_DND_ID_SLICE_END, TABLE_DND_ID_SLICE_START } from './dnd-preview';
|
|
12
14
|
import { TABLE_HEADER_HEIGHT, useTableHorizontalScrollbar, useTableVerticalScrollbar } from './useTableScrollbars';
|
|
13
15
|
// 가로 스크롤 디자인 기준 폭 — 14인치 모니터 + LNB 고려한 디자인 권장 너비
|
|
@@ -46,49 +48,51 @@ const Header = _ref => {
|
|
|
46
48
|
});
|
|
47
49
|
};
|
|
48
50
|
Header.displayName = 'Table.Header';
|
|
51
|
+
// 묶음마다 호출되므로 핸들러 없는 묶음이 있는 묶음을 덮지 않게 둔다.
|
|
52
|
+
// 거둘 때는 자기가 넣은 것만 비운다 — 조건부로 넘긴 핸들러를 끄면 드롭도 멈춰야 한다.
|
|
53
|
+
const useRowDropHandler = onRowDrop => {
|
|
54
|
+
const {
|
|
55
|
+
onRowDropRef
|
|
56
|
+
} = useContext(TableDndContext);
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
if (!onRowDrop) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
onRowDropRef.current = onRowDrop;
|
|
62
|
+
return () => {
|
|
63
|
+
if (onRowDropRef.current === onRowDrop) {
|
|
64
|
+
onRowDropRef.current = undefined;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}, [onRowDrop, onRowDropRef]);
|
|
68
|
+
};
|
|
49
69
|
const Body = _ref2 => {
|
|
50
70
|
let {
|
|
51
71
|
children,
|
|
52
72
|
className,
|
|
53
73
|
onRowDrop
|
|
54
74
|
} = _ref2;
|
|
55
|
-
|
|
56
|
-
tableId
|
|
57
|
-
} = useContext(TableDndContext);
|
|
58
|
-
// onRowDrop 참조를 ref로 유지 — 콜백이 매 렌더마다 재생성되어도 effect가 재구독하지 않도록
|
|
59
|
-
const onRowDropRef = useRef(onRowDrop);
|
|
60
|
-
onRowDropRef.current = onRowDrop;
|
|
61
|
-
useEffect(() => {
|
|
62
|
-
return monitorForElements({
|
|
63
|
-
canMonitor: _ref3 => {
|
|
64
|
-
let {
|
|
65
|
-
source
|
|
66
|
-
} = _ref3;
|
|
67
|
-
return source.data.tableId === tableId;
|
|
68
|
-
},
|
|
69
|
-
onDrop: _ref4 => {
|
|
70
|
-
let {
|
|
71
|
-
source,
|
|
72
|
-
location
|
|
73
|
-
} = _ref4;
|
|
74
|
-
if (!onRowDropRef.current) return;
|
|
75
|
-
const target = location.current.dropTargets[0];
|
|
76
|
-
if (!target) return;
|
|
77
|
-
const fromId = source.data.id;
|
|
78
|
-
const toId = target.data.id;
|
|
79
|
-
if (typeof fromId !== 'string' || typeof toId !== 'string' || fromId === toId) return;
|
|
80
|
-
const edge = extractClosestEdge(target.data);
|
|
81
|
-
onRowDropRef.current(fromId, toId, edge === 'bottom' ? 'bottom' : 'top');
|
|
82
|
-
}
|
|
83
|
-
});
|
|
84
|
-
}, [tableId]);
|
|
75
|
+
useRowDropHandler(onRowDrop);
|
|
85
76
|
return _jsx("tbody", {
|
|
86
77
|
className: classNames('ncua-table__body', className),
|
|
87
78
|
children: children
|
|
88
79
|
});
|
|
89
80
|
};
|
|
90
81
|
Body.displayName = 'Table.Body';
|
|
91
|
-
const
|
|
82
|
+
const RowGroup = _ref3 => {
|
|
83
|
+
let {
|
|
84
|
+
children,
|
|
85
|
+
className,
|
|
86
|
+
onRowDrop
|
|
87
|
+
} = _ref3;
|
|
88
|
+
useRowDropHandler(onRowDrop);
|
|
89
|
+
return _jsx("tbody", {
|
|
90
|
+
className: classNames('ncua-table__body', 'ncua-table__row-group', className),
|
|
91
|
+
children: children
|
|
92
|
+
});
|
|
93
|
+
};
|
|
94
|
+
RowGroup.displayName = 'Table.RowGroup';
|
|
95
|
+
const Row = /*#__PURE__*/forwardRef((_ref4, forwardedRef) => {
|
|
92
96
|
let {
|
|
93
97
|
children,
|
|
94
98
|
className,
|
|
@@ -96,7 +100,7 @@ const Row = /*#__PURE__*/forwardRef((_ref5, forwardedRef) => {
|
|
|
96
100
|
status,
|
|
97
101
|
dragId,
|
|
98
102
|
...rest
|
|
99
|
-
} =
|
|
103
|
+
} = _ref4;
|
|
100
104
|
const {
|
|
101
105
|
isDraggable,
|
|
102
106
|
tableId
|
|
@@ -114,8 +118,32 @@ const Row = /*#__PURE__*/forwardRef((_ref5, forwardedRef) => {
|
|
|
114
118
|
}, [forwardedRef]);
|
|
115
119
|
useEffect(() => {
|
|
116
120
|
if (!isDraggable || !dragId || !internalRef.current) return;
|
|
121
|
+
// 끌고 있는 블록 안쪽은 드롭 대상이 아니다 — 거기 놓으면 병합이 쪼개진다
|
|
122
|
+
const isInsideDraggedBlock = data => {
|
|
123
|
+
const {
|
|
124
|
+
blockBody,
|
|
125
|
+
blockStart,
|
|
126
|
+
blockEnd
|
|
127
|
+
} = data;
|
|
128
|
+
if (typeof blockStart !== 'number' || typeof blockEnd !== 'number' || blockStart < 0) return false;
|
|
129
|
+
const located = internalRef.current ? locateRow(internalRef.current) : null;
|
|
130
|
+
if (!located || located.bodyIndex !== blockBody) return false;
|
|
131
|
+
return located.index >= blockStart && located.index <= blockEnd;
|
|
132
|
+
};
|
|
133
|
+
// 병합 블록 가운데에 놓으면 그 블록이 쪼개진다 — 받을 수 있는 변만 연다
|
|
134
|
+
const openEdges = () => {
|
|
135
|
+
const located = internalRef.current ? locateRow(internalRef.current) : null;
|
|
136
|
+
if (!located) return ['top', 'bottom'];
|
|
137
|
+
return computeOpenEdges(readRowSpans(located.body), located.index);
|
|
138
|
+
};
|
|
117
139
|
return dropTargetForElements({
|
|
118
140
|
element: internalRef.current,
|
|
141
|
+
canDrop: _ref5 => {
|
|
142
|
+
let {
|
|
143
|
+
source
|
|
144
|
+
} = _ref5;
|
|
145
|
+
return !isInsideDraggedBlock(source.data) && openEdges().length > 0;
|
|
146
|
+
},
|
|
119
147
|
getData: _ref6 => {
|
|
120
148
|
let {
|
|
121
149
|
input,
|
|
@@ -127,7 +155,7 @@ const Row = /*#__PURE__*/forwardRef((_ref5, forwardedRef) => {
|
|
|
127
155
|
}, {
|
|
128
156
|
input,
|
|
129
157
|
element,
|
|
130
|
-
allowedEdges:
|
|
158
|
+
allowedEdges: openEdges()
|
|
131
159
|
});
|
|
132
160
|
},
|
|
133
161
|
onDragEnter: _ref7 => {
|
|
@@ -195,10 +223,28 @@ const DragHeaderCell = _ref9 => {
|
|
|
195
223
|
});
|
|
196
224
|
};
|
|
197
225
|
DragHeaderCell.displayName = 'Table.DragHeaderCell';
|
|
226
|
+
// 미리보기에 그릴 행과 배지에 적을 건수를 정한다 — 묶음은 행이 여러 개여도 한 건이다
|
|
227
|
+
const planDragPreview = (rowEl, input) => {
|
|
228
|
+
const rowRect = rowEl.getBoundingClientRect();
|
|
229
|
+
const badgeHalf = PREVIEW_BADGE_SIZE / 2;
|
|
230
|
+
// 묶음마다 `tbody` 가 생기므로 표 전체에서 모은다 — 자기 `tbody` 기준이면 자기 묶음만 보인다.
|
|
231
|
+
// 대상은 `Table.Body` 가 그린 `tbody` 뿐이다 — 맨 `tbody` 의 행까지 세면 TB-10 의 단일 프리뷰가 깨진다
|
|
232
|
+
const tableEl = rowEl.classList.contains('ncua-table__row--selected') ? rowEl.closest('table') : null;
|
|
233
|
+
const selectedRows = tableEl ? Array.from(tableEl.querySelectorAll('.ncua-table__body > .ncua-table__row--selected')) : [];
|
|
234
|
+
const isMultiSelect = selectedRows.length > 1;
|
|
235
|
+
return {
|
|
236
|
+
rows: isMultiSelect ? selectedRows : findRowSpanBlock(rowEl)?.rows ?? [rowEl],
|
|
237
|
+
itemCount: isMultiSelect ? selectedRows.length : 1,
|
|
238
|
+
rowRect,
|
|
239
|
+
offsetX: Math.round(input.clientX - rowRect.left) + badgeHalf,
|
|
240
|
+
offsetY: Math.round(input.clientY - rowRect.top) + badgeHalf
|
|
241
|
+
};
|
|
242
|
+
};
|
|
198
243
|
const DragCell = _ref0 => {
|
|
199
244
|
let {
|
|
200
245
|
disabled,
|
|
201
246
|
className,
|
|
247
|
+
rowSpan,
|
|
202
248
|
children
|
|
203
249
|
} = _ref0;
|
|
204
250
|
const {
|
|
@@ -217,34 +263,31 @@ const DragCell = _ref0 => {
|
|
|
217
263
|
let pendingPreview = null;
|
|
218
264
|
return makeDraggable({
|
|
219
265
|
element: handleRef.current,
|
|
220
|
-
getInitialData: () =>
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
266
|
+
getInitialData: () => {
|
|
267
|
+
const rowEl = handleRef.current?.closest('tr');
|
|
268
|
+
const block = rowEl ? findRowSpanBlock(rowEl) : null;
|
|
269
|
+
// 블록 전체가 움직이므로 소비자에게는 블록 첫 행을 출발점으로 알린다
|
|
270
|
+
const blockId = block?.rows[0]?.getAttribute(DRAG_ID_ATTRIBUTE) ?? dragId;
|
|
271
|
+
return {
|
|
272
|
+
id: blockId,
|
|
273
|
+
tableId,
|
|
274
|
+
moveIds: rowEl ? planMoveIds(rowEl) : [],
|
|
275
|
+
blockBody: block?.bodyIndex ?? -1,
|
|
276
|
+
blockStart: block?.start ?? -1,
|
|
277
|
+
blockEnd: block?.end ?? -1
|
|
278
|
+
};
|
|
279
|
+
},
|
|
224
280
|
onGenerateDragPreview: _ref1 => {
|
|
225
281
|
let {
|
|
226
282
|
nativeSetDragImage,
|
|
227
283
|
location
|
|
228
284
|
} = _ref1;
|
|
229
|
-
|
|
230
|
-
const rowEl = handleRef.current.closest('tr');
|
|
285
|
+
const rowEl = handleRef.current?.closest('tr');
|
|
231
286
|
if (!rowEl) return;
|
|
232
287
|
disableNativeDragPreview({
|
|
233
288
|
nativeSetDragImage
|
|
234
289
|
});
|
|
235
|
-
|
|
236
|
-
const badgeHalf = PREVIEW_BADGE_SIZE / 2;
|
|
237
|
-
const offsetX = Math.round(location.initial.input.clientX - rowRect.left) + badgeHalf;
|
|
238
|
-
const offsetY = Math.round(location.initial.input.clientY - rowRect.top) + badgeHalf;
|
|
239
|
-
const isSelected = rowEl.classList.contains('ncua-table__row--selected');
|
|
240
|
-
const bodyEl = rowEl.closest('.ncua-table__body');
|
|
241
|
-
const selectedRows = isSelected && bodyEl ? Array.from(bodyEl.querySelectorAll('.ncua-table__row--selected')) : [];
|
|
242
|
-
pendingPreview = {
|
|
243
|
-
rows: selectedRows.length > 1 ? selectedRows : [rowEl],
|
|
244
|
-
rowRect,
|
|
245
|
-
offsetX,
|
|
246
|
-
offsetY
|
|
247
|
-
};
|
|
290
|
+
pendingPreview = planDragPreview(rowEl, location.initial.input);
|
|
248
291
|
},
|
|
249
292
|
onDragStart: _ref10 => {
|
|
250
293
|
let {
|
|
@@ -254,11 +297,12 @@ const DragCell = _ref0 => {
|
|
|
254
297
|
if (!pendingPreview) return;
|
|
255
298
|
const {
|
|
256
299
|
rows,
|
|
300
|
+
itemCount,
|
|
257
301
|
rowRect,
|
|
258
302
|
offsetX,
|
|
259
303
|
offsetY
|
|
260
304
|
} = pendingPreview;
|
|
261
|
-
overlay = buildDragPreview(rows, rowRect);
|
|
305
|
+
overlay = buildDragPreview(rows, rowRect, itemCount);
|
|
262
306
|
overlay.style.left = `${Math.round(location.initial.input.clientX) - offsetX}px`;
|
|
263
307
|
overlay.style.top = `${Math.round(location.initial.input.clientY) - offsetY}px`;
|
|
264
308
|
document.body.appendChild(overlay);
|
|
@@ -282,6 +326,7 @@ const DragCell = _ref0 => {
|
|
|
282
326
|
}, [isDraggable, dragId, tableId, disabled, setIsDragging]);
|
|
283
327
|
return _jsx("td", {
|
|
284
328
|
className: classNames('ncua-table__drag-cell', className),
|
|
329
|
+
rowSpan: rowSpan,
|
|
285
330
|
children: _jsxs("span", {
|
|
286
331
|
className: "ncua-table__drag-cell-inner",
|
|
287
332
|
children: [isDraggable && _jsx("button", {
|
|
@@ -454,9 +499,21 @@ const sortChildren = children => {
|
|
|
454
499
|
});
|
|
455
500
|
return result;
|
|
456
501
|
};
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
502
|
+
const readRowDrop = (sourceData, target) => {
|
|
503
|
+
if (!target) return null;
|
|
504
|
+
const fromId = sourceData.id;
|
|
505
|
+
const toId = target.data.id;
|
|
506
|
+
if (typeof fromId !== 'string' || typeof toId !== 'string' || fromId === toId) return null;
|
|
507
|
+
const isStringArray = v => Array.isArray(v) && v.every(x => typeof x === 'string');
|
|
508
|
+
const moveIds = isStringArray(sourceData.moveIds) ? sourceData.moveIds : [fromId];
|
|
509
|
+
const edge = extractClosestEdge(target.data) === 'bottom' ? 'bottom' : 'top';
|
|
510
|
+
return {
|
|
511
|
+
fromId,
|
|
512
|
+
toId,
|
|
513
|
+
edge,
|
|
514
|
+
moveIds
|
|
515
|
+
};
|
|
516
|
+
};
|
|
460
517
|
const TableComponent = /*#__PURE__*/forwardRef((_ref17, ref) => {
|
|
461
518
|
let {
|
|
462
519
|
type = 'horizontal',
|
|
@@ -473,6 +530,25 @@ const TableComponent = /*#__PURE__*/forwardRef((_ref17, ref) => {
|
|
|
473
530
|
} = _ref17;
|
|
474
531
|
const tableIdRef = useRef(`ncua-table-${Math.random().toString(TABLE_DND_ID_RADIX).slice(TABLE_DND_ID_SLICE_START, TABLE_DND_ID_SLICE_END)}`);
|
|
475
532
|
const tableId = tableIdRef.current;
|
|
533
|
+
// 콜백이 매 렌더마다 재생성되어도 effect 가 재구독하지 않도록 ref 로 유지한다.
|
|
534
|
+
const onRowDropRef = useRef(undefined);
|
|
535
|
+
useEffect(() => monitorForElements({
|
|
536
|
+
canMonitor: _ref18 => {
|
|
537
|
+
let {
|
|
538
|
+
source
|
|
539
|
+
} = _ref18;
|
|
540
|
+
return source.data.tableId === tableId;
|
|
541
|
+
},
|
|
542
|
+
onDrop: _ref19 => {
|
|
543
|
+
let {
|
|
544
|
+
source,
|
|
545
|
+
location
|
|
546
|
+
} = _ref19;
|
|
547
|
+
const drop = readRowDrop(source.data, location.current.dropTargets[0]);
|
|
548
|
+
if (!drop) return;
|
|
549
|
+
onRowDropRef.current?.(drop.fromId, drop.toId, drop.edge, drop.moveIds);
|
|
550
|
+
}
|
|
551
|
+
}), [tableId]);
|
|
476
552
|
const effectiveDraggable = draggable && type === 'horizontal';
|
|
477
553
|
const tableClasses = classNames('ncua-table', className, {
|
|
478
554
|
'ncua-table--horizontal': type === 'horizontal',
|
|
@@ -561,7 +637,8 @@ const TableComponent = /*#__PURE__*/forwardRef((_ref17, ref) => {
|
|
|
561
637
|
};
|
|
562
638
|
const dndContextValue = {
|
|
563
639
|
isDraggable: effectiveDraggable,
|
|
564
|
-
tableId
|
|
640
|
+
tableId,
|
|
641
|
+
onRowDropRef
|
|
565
642
|
};
|
|
566
643
|
// horizontalScroll=true 시 외곽 wrapper + FloatingProvider 부착.
|
|
567
644
|
// 핵심 — __h-scroll-container 는 <table>(또는 scroll-area) 만 감싸고, footer/pagination 은
|
|
@@ -638,6 +715,7 @@ TableComponent.displayName = 'Table';
|
|
|
638
715
|
export const Table = Object.assign(TableComponent, {
|
|
639
716
|
Header,
|
|
640
717
|
Body,
|
|
718
|
+
RowGroup,
|
|
641
719
|
Row,
|
|
642
720
|
DragHeaderCell,
|
|
643
721
|
DragCell,
|
|
@@ -49,12 +49,17 @@ describe('Table.DragCell 핸들 등록 (TB-08)', () => {
|
|
|
49
49
|
expect(draggableArgsOf('a').element).toBe(handle);
|
|
50
50
|
expect(view.querySelectorAll('button.ncua-table__drag-handle')).toHaveLength(ROW_IDS.length);
|
|
51
51
|
});
|
|
52
|
-
it('TB-08 getInitialData 는 {id: dragId, tableId}
|
|
52
|
+
it('TB-08 getInitialData 는 {id: dragId, tableId} 와 옮길 행·블록 좌표를 돌려준다', () => {
|
|
53
53
|
render(draggableTable(vi.fn()));
|
|
54
54
|
const tableId = sourceDataOf('b').tableId;
|
|
55
|
+
// 병합이 없으면 블록은 자기 행 한 칸이고 옮길 행도 자기 자신뿐이다
|
|
55
56
|
expect(draggableArgsOf('a').getInitialData?.({})).toEqual({
|
|
56
57
|
id: 'a',
|
|
57
|
-
tableId
|
|
58
|
+
tableId,
|
|
59
|
+
moveIds: ['a'],
|
|
60
|
+
blockBody: 0,
|
|
61
|
+
blockStart: 0,
|
|
62
|
+
blockEnd: 0
|
|
58
63
|
});
|
|
59
64
|
});
|
|
60
65
|
it('TB-08 disabled 면 button 을 disabled 로 렌더하고 draggable 로 등록하지 않는다', () => {
|
|
@@ -30,14 +30,14 @@ describe('Table 행 드롭 → onRowDrop (TB-01)', () => {
|
|
|
30
30
|
const source = sourceDataOf('a');
|
|
31
31
|
dropOnMonitors(source, [targetDataOf('b', LOWER_HALF_Y, source)]);
|
|
32
32
|
expect(onRowDrop).toHaveBeenCalledTimes(1);
|
|
33
|
-
expect(onRowDrop).toHaveBeenCalledWith('a', 'b', 'bottom');
|
|
33
|
+
expect(onRowDrop).toHaveBeenCalledWith('a', 'b', 'bottom', ['a']);
|
|
34
34
|
});
|
|
35
35
|
it('TB-01 다른 행의 위쪽 절반에 놓으면 edge 는 "top" 이다', () => {
|
|
36
36
|
const onRowDrop = vi.fn();
|
|
37
37
|
render(draggableTable(onRowDrop));
|
|
38
38
|
const source = sourceDataOf('c');
|
|
39
39
|
dropOnMonitors(source, [targetDataOf('a', UPPER_HALF_Y, source)]);
|
|
40
|
-
expect(onRowDrop).toHaveBeenCalledWith('c', 'a', 'top');
|
|
40
|
+
expect(onRowDrop).toHaveBeenCalledWith('c', 'a', 'top', ['c']);
|
|
41
41
|
});
|
|
42
42
|
it('TB-01 target 에 closest edge 정보가 없으면 "top" 으로 전달한다', () => {
|
|
43
43
|
const onRowDrop = vi.fn();
|
|
@@ -47,7 +47,7 @@ describe('Table 행 드롭 → onRowDrop (TB-01)', () => {
|
|
|
47
47
|
id: 'b',
|
|
48
48
|
tableId: source.tableId
|
|
49
49
|
}]);
|
|
50
|
-
expect(onRowDrop).toHaveBeenCalledWith('a', 'b', 'top');
|
|
50
|
+
expect(onRowDrop).toHaveBeenCalledWith('a', 'b', 'top', ['a']);
|
|
51
51
|
});
|
|
52
52
|
it('TB-01 여러 drop target 이 겹치면 가장 안쪽(첫 번째) target 기준으로 전달한다', () => {
|
|
53
53
|
const onRowDrop = vi.fn();
|
|
@@ -55,15 +55,20 @@ describe('Table 행 드롭 → onRowDrop (TB-01)', () => {
|
|
|
55
55
|
const source = sourceDataOf('a');
|
|
56
56
|
dropOnMonitors(source, [targetDataOf('c', LOWER_HALF_Y, source), targetDataOf('b', UPPER_HALF_Y, source)]);
|
|
57
57
|
expect(onRowDrop).toHaveBeenCalledTimes(1);
|
|
58
|
-
expect(onRowDrop).toHaveBeenCalledWith('a', 'c', 'bottom');
|
|
58
|
+
expect(onRowDrop).toHaveBeenCalledWith('a', 'c', 'bottom', ['a']);
|
|
59
59
|
});
|
|
60
60
|
it('TB-01 행의 source·target data 에 이 테이블의 tableId 가 실린다', () => {
|
|
61
61
|
render(draggableTable(vi.fn()));
|
|
62
62
|
const source = sourceDataOf('a');
|
|
63
63
|
const target = targetDataOf('b', LOWER_HALF_Y, source);
|
|
64
|
+
// 블록 좌표는 병합 블록 드롭 판정에 쓰인다. 병합이 없으면 자기 행 한 칸이다
|
|
64
65
|
expect(source).toEqual({
|
|
65
66
|
id: 'a',
|
|
66
|
-
tableId: expect.stringMatching(/^ncua-table-/)
|
|
67
|
+
tableId: expect.stringMatching(/^ncua-table-/),
|
|
68
|
+
moveIds: ['a'],
|
|
69
|
+
blockBody: 0,
|
|
70
|
+
blockStart: 0,
|
|
71
|
+
blockEnd: 0
|
|
67
72
|
});
|
|
68
73
|
expect(target.id).toBe('b');
|
|
69
74
|
expect(target.tableId).toBe(source.tableId);
|
|
@@ -134,7 +139,7 @@ describe('Table.Body canMonitor — 테이블 격리 (TB-04)', () => {
|
|
|
134
139
|
id: 'b',
|
|
135
140
|
tableId: source.tableId
|
|
136
141
|
}]);
|
|
137
|
-
expect(first).toHaveBeenCalledWith('a', 'b', 'top');
|
|
142
|
+
expect(first).toHaveBeenCalledWith('a', 'b', 'top', ['a']);
|
|
138
143
|
expect(second).not.toHaveBeenCalled();
|
|
139
144
|
expect(sourceDataOf('a', sectionOf('second')).tableId).not.toBe(source.tableId);
|
|
140
145
|
});
|
|
@@ -164,7 +169,7 @@ describe('Table.Body canMonitor — 테이블 격리 (TB-04)', () => {
|
|
|
164
169
|
id: 'c',
|
|
165
170
|
tableId: foreignTarget.tableId
|
|
166
171
|
}]);
|
|
167
|
-
expect(first).toHaveBeenCalledWith('a', 'c', 'top');
|
|
172
|
+
expect(first).toHaveBeenCalledWith('a', 'c', 'top', ['a']);
|
|
168
173
|
expect(second).not.toHaveBeenCalled();
|
|
169
174
|
});
|
|
170
175
|
it.todo('TB-04 다른 테이블의 행 위에 놓으면 어느 테이블의 onRowDrop 도 호출하지 않는다 (canDrop 또는 target tableId 검사)');
|
|
@@ -181,7 +186,7 @@ describe('Table.Body onRowDrop ref 갱신 (TB-05)', () => {
|
|
|
181
186
|
expect(monitors()).toHaveLength(1);
|
|
182
187
|
expect(monitors()[0]).toBe(monitorBefore);
|
|
183
188
|
expect(first).not.toHaveBeenCalled();
|
|
184
|
-
expect(latest).toHaveBeenCalledWith('a', 'b', 'bottom');
|
|
189
|
+
expect(latest).toHaveBeenCalledWith('a', 'b', 'bottom', ['a']);
|
|
185
190
|
});
|
|
186
191
|
it('TB-05 onRowDrop 을 undefined 로 바꾸면 이후 drop 은 무시된다', () => {
|
|
187
192
|
const first = vi.fn();
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
/**
|
|
3
|
+
* `Table.RowGroup` 의 DOM 계약만 다룬다.
|
|
4
|
+
* hover·selected 배경은 CSS 캐스케이드 결과이므로 jsdom 으로 판정할 수 없다 (브라우저에서 확인).
|
|
5
|
+
*/
|
|
6
|
+
import { createElement } from 'react';
|
|
7
|
+
import { createRoot } from 'react-dom/client';
|
|
8
|
+
import { act } from 'react-dom/test-utils';
|
|
9
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
10
|
+
import { Table } from '../Table';
|
|
11
|
+
let container;
|
|
12
|
+
let root;
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
container = document.createElement('div');
|
|
15
|
+
document.body.appendChild(container);
|
|
16
|
+
act(() => {
|
|
17
|
+
root = createRoot(container);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
act(() => {
|
|
22
|
+
root.unmount();
|
|
23
|
+
});
|
|
24
|
+
container.remove();
|
|
25
|
+
});
|
|
26
|
+
// `children` 은 TableRowGroupProps 의 필수 prop 이라 createElement 의 props 인자에 담는다.
|
|
27
|
+
const rowGroupProps = className => ({
|
|
28
|
+
className,
|
|
29
|
+
children: /*#__PURE__*/createElement('tr', null, /*#__PURE__*/createElement('td', null, '셀'))
|
|
30
|
+
});
|
|
31
|
+
const renderRowGroup = className => {
|
|
32
|
+
act(() => {
|
|
33
|
+
root.render(/*#__PURE__*/createElement('table', null, /*#__PURE__*/createElement(Table.RowGroup, rowGroupProps(className))));
|
|
34
|
+
});
|
|
35
|
+
const tbody = container.querySelector('tbody');
|
|
36
|
+
if (!tbody) throw new Error('tbody 가 렌더되지 않았다.');
|
|
37
|
+
return tbody;
|
|
38
|
+
};
|
|
39
|
+
describe('Table.RowGroup', () => {
|
|
40
|
+
it('ncua-table__body 와 ncua-table__row-group 을 가진 tbody 를 렌더한다', () => {
|
|
41
|
+
const tbody = renderRowGroup();
|
|
42
|
+
expect(tbody.classList.contains('ncua-table__body')).toBe(true);
|
|
43
|
+
expect(tbody.classList.contains('ncua-table__row-group')).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
it('전달한 className 을 기본 클래스와 함께 붙인다', () => {
|
|
46
|
+
const tbody = renderRowGroup('custom-group');
|
|
47
|
+
expect(tbody.classList.contains('custom-group')).toBe(true);
|
|
48
|
+
expect(tbody.classList.contains('ncua-table__body')).toBe(true);
|
|
49
|
+
expect(tbody.classList.contains('ncua-table__row-group')).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
});
|