@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.
Files changed (56) hide show
  1. package/dist/cjs/assets/scripts/table/rowSpanBlock.js +97 -0
  2. package/dist/cjs/src/components/data-display/table/Table.js +135 -57
  3. package/dist/cjs/src/components/data-display/table/__tests__/Table.dnd.dragCell.test.js +7 -2
  4. package/dist/cjs/src/components/data-display/table/__tests__/Table.dnd.test.js +13 -8
  5. package/dist/cjs/src/components/data-display/table/__tests__/Table.rowGroup.test.js +54 -0
  6. package/dist/cjs/src/components/data-display/table/__tests__/Table.rowGroupDnd.test.js +212 -0
  7. package/dist/cjs/src/components/data-display/table/__tests__/dnd-context.test.js +5 -2
  8. package/dist/cjs/src/components/data-display/table/__tests__/tableSelectableStyle.test.js +75 -0
  9. package/dist/cjs/src/components/data-display/table/dnd-context.js +4 -1
  10. package/dist/cjs/src/components/data-display/table/dnd-move.js +46 -0
  11. package/dist/cjs/src/components/data-display/table/dnd-preview.js +8 -3
  12. package/dist/cjs/src/generated/scoped-css.js +1 -1
  13. package/dist/esm/assets/scripts/table/rowSpanBlock.js +85 -0
  14. package/dist/esm/src/components/data-display/table/Table.js +135 -57
  15. package/dist/esm/src/components/data-display/table/__tests__/Table.dnd.dragCell.test.js +7 -2
  16. package/dist/esm/src/components/data-display/table/__tests__/Table.dnd.test.js +13 -8
  17. package/dist/esm/src/components/data-display/table/__tests__/Table.rowGroup.test.js +51 -0
  18. package/dist/esm/src/components/data-display/table/__tests__/Table.rowGroupDnd.test.js +210 -0
  19. package/dist/esm/src/components/data-display/table/__tests__/dnd-context.test.js +5 -2
  20. package/dist/esm/src/components/data-display/table/__tests__/tableSelectableStyle.test.js +72 -0
  21. package/dist/esm/src/components/data-display/table/dnd-context.js +4 -1
  22. package/dist/esm/src/components/data-display/table/dnd-move.js +38 -0
  23. package/dist/esm/src/components/data-display/table/dnd-preview.js +8 -3
  24. package/dist/esm/src/generated/scoped-css.js +1 -1
  25. package/dist/temp/assets/scripts/table/rowSpanBlock.d.ts +29 -0
  26. package/dist/temp/assets/scripts/table/rowSpanBlock.js +77 -0
  27. package/dist/temp/src/components/data-display/table/Table.d.ts +7 -3
  28. package/dist/temp/src/components/data-display/table/Table.js +107 -47
  29. package/dist/temp/src/components/data-display/table/__tests__/Table.dnd.dragCell.test.js +10 -2
  30. package/dist/temp/src/components/data-display/table/__tests__/Table.dnd.test.js +16 -8
  31. package/dist/temp/src/components/data-display/table/__tests__/Table.rowGroup.test.d.ts +1 -0
  32. package/dist/temp/src/components/data-display/table/__tests__/Table.rowGroup.test.js +52 -0
  33. package/dist/temp/src/components/data-display/table/__tests__/Table.rowGroupDnd.test.d.ts +1 -0
  34. package/dist/temp/src/components/data-display/table/__tests__/Table.rowGroupDnd.test.js +137 -0
  35. package/dist/temp/src/components/data-display/table/__tests__/dnd-context.test.js +2 -2
  36. package/dist/temp/src/components/data-display/table/__tests__/tableSelectableStyle.test.d.ts +1 -0
  37. package/dist/temp/src/components/data-display/table/__tests__/tableSelectableStyle.test.js +71 -0
  38. package/dist/temp/src/components/data-display/table/dnd-context.d.ts +3 -0
  39. package/dist/temp/src/components/data-display/table/dnd-context.js +1 -0
  40. package/dist/temp/src/components/data-display/table/dnd-move.d.ts +17 -0
  41. package/dist/temp/src/components/data-display/table/dnd-move.js +31 -0
  42. package/dist/temp/src/components/data-display/table/dnd-preview.d.ts +5 -1
  43. package/dist/temp/src/components/data-display/table/dnd-preview.js +7 -3
  44. package/dist/temp/src/components/data-display/table/types.d.ts +14 -5
  45. package/dist/temp/src/generated/scoped-css.js +1 -1
  46. package/dist/types/assets/scripts/table/rowSpanBlock.d.ts +29 -0
  47. package/dist/types/src/components/data-display/table/Table.d.ts +7 -3
  48. package/dist/types/src/components/data-display/table/__tests__/Table.rowGroup.test.d.ts +1 -0
  49. package/dist/types/src/components/data-display/table/__tests__/Table.rowGroupDnd.test.d.ts +1 -0
  50. package/dist/types/src/components/data-display/table/__tests__/tableSelectableStyle.test.d.ts +1 -0
  51. package/dist/types/src/components/data-display/table/dnd-context.d.ts +3 -0
  52. package/dist/types/src/components/data-display/table/dnd-move.d.ts +17 -0
  53. package/dist/types/src/components/data-display/table/dnd-preview.d.ts +5 -1
  54. package/dist/types/src/components/data-display/table/types.d.ts +14 -5
  55. package/dist/ui-admin/assets/styles/style.css +60 -4
  56. package/package.json +1 -1
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.readRowSpans = exports.locateRow = exports.expandRowsToBlocks = exports.computeRowSpanBlock = exports.computeOpenEdges = exports.collectBlockIndices = void 0;
7
+ const SINGLE_ROW = 1;
8
+ const collectRanges = rowSpans => rowSpans.flatMap((spans, index) => spans.filter(span => span > SINGLE_ROW).map(span => ({
9
+ start: index,
10
+ end: Math.min(index + span - SINGLE_ROW, rowSpans.length - 1)
11
+ })));
12
+ const overlaps = (a, b) => a.start <= b.end && b.start <= a.end;
13
+ /**
14
+ * 병합을 쪼개지 않고 통째로 옮길 수 있는 최소 행 구간을 구한다.
15
+ * 범위가 겹치면 어느 쪽도 쪼갤 수 없으므로 연결된 범위를 모두 합친다.
16
+ *
17
+ * @param rowSpans 행별 셀 rowSpan 목록. 병합에 덮여 생략된 셀은 포함하지 않는다
18
+ * @param index 기준 행의 위치
19
+ */
20
+ const computeRowSpanBlock = (rowSpans, index) => {
21
+ const ranges = collectRanges(rowSpans);
22
+ let block = {
23
+ start: index,
24
+ end: index
25
+ };
26
+ let expanded = true;
27
+ while (expanded) {
28
+ expanded = false;
29
+ for (const range of ranges) {
30
+ if (!overlaps(range, block)) continue;
31
+ const start = Math.min(block.start, range.start);
32
+ const end = Math.max(block.end, range.end);
33
+ if (start !== block.start || end !== block.end) {
34
+ block = {
35
+ start,
36
+ end
37
+ };
38
+ expanded = true;
39
+ }
40
+ }
41
+ }
42
+ return block;
43
+ };
44
+ /**
45
+ * 드롭을 받을 수 있는 변. 블록 가운데에 떨어뜨리면 그 블록이 쪼개지므로 경계만 연다.
46
+ */
47
+ exports.computeRowSpanBlock = computeRowSpanBlock;
48
+ const computeOpenEdges = (rowSpans, index) => {
49
+ const block = computeRowSpanBlock(rowSpans, index);
50
+ const edges = [];
51
+ if (index === block.start) edges.push('top');
52
+ if (index === block.end) edges.push('bottom');
53
+ return edges;
54
+ };
55
+ /** 고른 행들이 끌고 오는 행 전부. 병합 블록을 쪼개면 병합이 깨지므로 블록째 더한다. */
56
+ exports.computeOpenEdges = computeOpenEdges;
57
+ const collectBlockIndices = (rowSpans, indices) => {
58
+ const picked = new Set();
59
+ for (const index of indices) {
60
+ const block = computeRowSpanBlock(rowSpans, index);
61
+ for (let row = block.start; row <= block.end; row++) {
62
+ picked.add(row);
63
+ }
64
+ }
65
+ return Array.from(picked).sort((a, b) => a - b);
66
+ };
67
+ /** 행이 속한 `tbody` 와 그 안에서의 위치. 표 밖 요소면 `null`. */
68
+ exports.collectBlockIndices = collectBlockIndices;
69
+ const locateRow = rowEl => {
70
+ const body = rowEl.closest('tbody');
71
+ if (!body) return null;
72
+ const index = Array.from(body.rows).indexOf(rowEl);
73
+ return index < 0 ? null : {
74
+ body,
75
+ index
76
+ };
77
+ };
78
+ /** 표 본문의 행별 rowSpan 을 읽어 `computeRowSpanBlock` 에 넘길 형태로 만든다. */
79
+ exports.locateRow = locateRow;
80
+ const readRowSpans = body => Array.from(body.rows).map(row => Array.from(row.cells).map(cell => cell.rowSpan));
81
+ /** 다중 선택으로 고른 행들이 끌고 오는 행 전부. 행 순서는 표에 놓인 순서를 따른다. */
82
+ exports.readRowSpans = readRowSpans;
83
+ const expandRowsToBlocks = rows => {
84
+ const perBody = new Map();
85
+ for (const row of rows) {
86
+ const located = locateRow(row);
87
+ if (!located) continue;
88
+ const picked = perBody.get(located.body) ?? [];
89
+ picked.push(located.index);
90
+ perBody.set(located.body, picked);
91
+ }
92
+ return Array.from(perBody).flatMap(_ref => {
93
+ let [body, indices] = _ref;
94
+ return collectBlockIndices(readRowSpans(body), indices).map(index => body.rows[index]);
95
+ });
96
+ };
97
+ exports.expandRowsToBlocks = expandRowsToBlocks;
@@ -12,8 +12,10 @@ var _closestEdge = require("@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edg
12
12
  var _uiAdminIcon = require("@ncds/ui-admin-icon");
13
13
  var _classnames = _interopRequireDefault(require("classnames"));
14
14
  var _react = require("react");
15
+ var _rowSpanBlock = require("../../../../assets/scripts/table/rowSpanBlock");
15
16
  var _FloatingContext = require("../../../contexts/FloatingContext");
16
17
  var _dndContext = require("./dnd-context");
18
+ var _dndMove = require("./dnd-move");
17
19
  var _dndPreview = require("./dnd-preview");
18
20
  var _useTableScrollbars = require("./useTableScrollbars");
19
21
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
@@ -53,49 +55,51 @@ const Header = _ref => {
53
55
  });
54
56
  };
55
57
  Header.displayName = 'Table.Header';
58
+ // 묶음마다 호출되므로 핸들러 없는 묶음이 있는 묶음을 덮지 않게 둔다.
59
+ // 거둘 때는 자기가 넣은 것만 비운다 — 조건부로 넘긴 핸들러를 끄면 드롭도 멈춰야 한다.
60
+ const useRowDropHandler = onRowDrop => {
61
+ const {
62
+ onRowDropRef
63
+ } = (0, _react.useContext)(_dndContext.TableDndContext);
64
+ (0, _react.useEffect)(() => {
65
+ if (!onRowDrop) {
66
+ return;
67
+ }
68
+ onRowDropRef.current = onRowDrop;
69
+ return () => {
70
+ if (onRowDropRef.current === onRowDrop) {
71
+ onRowDropRef.current = undefined;
72
+ }
73
+ };
74
+ }, [onRowDrop, onRowDropRef]);
75
+ };
56
76
  const Body = _ref2 => {
57
77
  let {
58
78
  children,
59
79
  className,
60
80
  onRowDrop
61
81
  } = _ref2;
62
- const {
63
- tableId
64
- } = (0, _react.useContext)(_dndContext.TableDndContext);
65
- // onRowDrop 참조를 ref로 유지 — 콜백이 매 렌더마다 재생성되어도 effect가 재구독하지 않도록
66
- const onRowDropRef = (0, _react.useRef)(onRowDrop);
67
- onRowDropRef.current = onRowDrop;
68
- (0, _react.useEffect)(() => {
69
- return (0, _adapter.monitorForElements)({
70
- canMonitor: _ref3 => {
71
- let {
72
- source
73
- } = _ref3;
74
- return source.data.tableId === tableId;
75
- },
76
- onDrop: _ref4 => {
77
- let {
78
- source,
79
- location
80
- } = _ref4;
81
- if (!onRowDropRef.current) return;
82
- const target = location.current.dropTargets[0];
83
- if (!target) return;
84
- const fromId = source.data.id;
85
- const toId = target.data.id;
86
- if (typeof fromId !== 'string' || typeof toId !== 'string' || fromId === toId) return;
87
- const edge = (0, _closestEdge.extractClosestEdge)(target.data);
88
- onRowDropRef.current(fromId, toId, edge === 'bottom' ? 'bottom' : 'top');
89
- }
90
- });
91
- }, [tableId]);
82
+ useRowDropHandler(onRowDrop);
92
83
  return (0, _jsxRuntime.jsx)("tbody", {
93
84
  className: (0, _classnames.default)('ncua-table__body', className),
94
85
  children: children
95
86
  });
96
87
  };
97
88
  Body.displayName = 'Table.Body';
98
- const Row = /*#__PURE__*/(0, _react.forwardRef)((_ref5, forwardedRef) => {
89
+ const RowGroup = _ref3 => {
90
+ let {
91
+ children,
92
+ className,
93
+ onRowDrop
94
+ } = _ref3;
95
+ useRowDropHandler(onRowDrop);
96
+ return (0, _jsxRuntime.jsx)("tbody", {
97
+ className: (0, _classnames.default)('ncua-table__body', 'ncua-table__row-group', className),
98
+ children: children
99
+ });
100
+ };
101
+ RowGroup.displayName = 'Table.RowGroup';
102
+ const Row = /*#__PURE__*/(0, _react.forwardRef)((_ref4, forwardedRef) => {
99
103
  let {
100
104
  children,
101
105
  className,
@@ -103,7 +107,7 @@ const Row = /*#__PURE__*/(0, _react.forwardRef)((_ref5, forwardedRef) => {
103
107
  status,
104
108
  dragId,
105
109
  ...rest
106
- } = _ref5;
110
+ } = _ref4;
107
111
  const {
108
112
  isDraggable,
109
113
  tableId
@@ -121,8 +125,32 @@ const Row = /*#__PURE__*/(0, _react.forwardRef)((_ref5, forwardedRef) => {
121
125
  }, [forwardedRef]);
122
126
  (0, _react.useEffect)(() => {
123
127
  if (!isDraggable || !dragId || !internalRef.current) return;
128
+ // 끌고 있는 블록 안쪽은 드롭 대상이 아니다 — 거기 놓으면 병합이 쪼개진다
129
+ const isInsideDraggedBlock = data => {
130
+ const {
131
+ blockBody,
132
+ blockStart,
133
+ blockEnd
134
+ } = data;
135
+ if (typeof blockStart !== 'number' || typeof blockEnd !== 'number' || blockStart < 0) return false;
136
+ const located = internalRef.current ? (0, _dndMove.locateRow)(internalRef.current) : null;
137
+ if (!located || located.bodyIndex !== blockBody) return false;
138
+ return located.index >= blockStart && located.index <= blockEnd;
139
+ };
140
+ // 병합 블록 가운데에 놓으면 그 블록이 쪼개진다 — 받을 수 있는 변만 연다
141
+ const openEdges = () => {
142
+ const located = internalRef.current ? (0, _dndMove.locateRow)(internalRef.current) : null;
143
+ if (!located) return ['top', 'bottom'];
144
+ return (0, _rowSpanBlock.computeOpenEdges)((0, _rowSpanBlock.readRowSpans)(located.body), located.index);
145
+ };
124
146
  return (0, _adapter.dropTargetForElements)({
125
147
  element: internalRef.current,
148
+ canDrop: _ref5 => {
149
+ let {
150
+ source
151
+ } = _ref5;
152
+ return !isInsideDraggedBlock(source.data) && openEdges().length > 0;
153
+ },
126
154
  getData: _ref6 => {
127
155
  let {
128
156
  input,
@@ -134,7 +162,7 @@ const Row = /*#__PURE__*/(0, _react.forwardRef)((_ref5, forwardedRef) => {
134
162
  }, {
135
163
  input,
136
164
  element,
137
- allowedEdges: ['top', 'bottom']
165
+ allowedEdges: openEdges()
138
166
  });
139
167
  },
140
168
  onDragEnter: _ref7 => {
@@ -202,10 +230,28 @@ const DragHeaderCell = _ref9 => {
202
230
  });
203
231
  };
204
232
  DragHeaderCell.displayName = 'Table.DragHeaderCell';
233
+ // 미리보기에 그릴 행과 배지에 적을 건수를 정한다 — 묶음은 행이 여러 개여도 한 건이다
234
+ const planDragPreview = (rowEl, input) => {
235
+ const rowRect = rowEl.getBoundingClientRect();
236
+ const badgeHalf = _dndPreview.PREVIEW_BADGE_SIZE / 2;
237
+ // 묶음마다 `tbody` 가 생기므로 표 전체에서 모은다 — 자기 `tbody` 기준이면 자기 묶음만 보인다.
238
+ // 대상은 `Table.Body` 가 그린 `tbody` 뿐이다 — 맨 `tbody` 의 행까지 세면 TB-10 의 단일 프리뷰가 깨진다
239
+ const tableEl = rowEl.classList.contains('ncua-table__row--selected') ? rowEl.closest('table') : null;
240
+ const selectedRows = tableEl ? Array.from(tableEl.querySelectorAll('.ncua-table__body > .ncua-table__row--selected')) : [];
241
+ const isMultiSelect = selectedRows.length > 1;
242
+ return {
243
+ rows: isMultiSelect ? selectedRows : (0, _dndMove.findRowSpanBlock)(rowEl)?.rows ?? [rowEl],
244
+ itemCount: isMultiSelect ? selectedRows.length : 1,
245
+ rowRect,
246
+ offsetX: Math.round(input.clientX - rowRect.left) + badgeHalf,
247
+ offsetY: Math.round(input.clientY - rowRect.top) + badgeHalf
248
+ };
249
+ };
205
250
  const DragCell = _ref0 => {
206
251
  let {
207
252
  disabled,
208
253
  className,
254
+ rowSpan,
209
255
  children
210
256
  } = _ref0;
211
257
  const {
@@ -224,34 +270,31 @@ const DragCell = _ref0 => {
224
270
  let pendingPreview = null;
225
271
  return (0, _adapter.draggable)({
226
272
  element: handleRef.current,
227
- getInitialData: () => ({
228
- id: dragId,
229
- tableId
230
- }),
273
+ getInitialData: () => {
274
+ const rowEl = handleRef.current?.closest('tr');
275
+ const block = rowEl ? (0, _dndMove.findRowSpanBlock)(rowEl) : null;
276
+ // 블록 전체가 움직이므로 소비자에게는 블록 첫 행을 출발점으로 알린다
277
+ const blockId = block?.rows[0]?.getAttribute(_dndMove.DRAG_ID_ATTRIBUTE) ?? dragId;
278
+ return {
279
+ id: blockId,
280
+ tableId,
281
+ moveIds: rowEl ? (0, _dndMove.planMoveIds)(rowEl) : [],
282
+ blockBody: block?.bodyIndex ?? -1,
283
+ blockStart: block?.start ?? -1,
284
+ blockEnd: block?.end ?? -1
285
+ };
286
+ },
231
287
  onGenerateDragPreview: _ref1 => {
232
288
  let {
233
289
  nativeSetDragImage,
234
290
  location
235
291
  } = _ref1;
236
- if (!handleRef.current) return;
237
- const rowEl = handleRef.current.closest('tr');
292
+ const rowEl = handleRef.current?.closest('tr');
238
293
  if (!rowEl) return;
239
294
  (0, _disableNativeDragPreview.disableNativeDragPreview)({
240
295
  nativeSetDragImage
241
296
  });
242
- const rowRect = rowEl.getBoundingClientRect();
243
- const badgeHalf = _dndPreview.PREVIEW_BADGE_SIZE / 2;
244
- const offsetX = Math.round(location.initial.input.clientX - rowRect.left) + badgeHalf;
245
- const offsetY = Math.round(location.initial.input.clientY - rowRect.top) + badgeHalf;
246
- const isSelected = rowEl.classList.contains('ncua-table__row--selected');
247
- const bodyEl = rowEl.closest('.ncua-table__body');
248
- const selectedRows = isSelected && bodyEl ? Array.from(bodyEl.querySelectorAll('.ncua-table__row--selected')) : [];
249
- pendingPreview = {
250
- rows: selectedRows.length > 1 ? selectedRows : [rowEl],
251
- rowRect,
252
- offsetX,
253
- offsetY
254
- };
297
+ pendingPreview = planDragPreview(rowEl, location.initial.input);
255
298
  },
256
299
  onDragStart: _ref10 => {
257
300
  let {
@@ -261,11 +304,12 @@ const DragCell = _ref0 => {
261
304
  if (!pendingPreview) return;
262
305
  const {
263
306
  rows,
307
+ itemCount,
264
308
  rowRect,
265
309
  offsetX,
266
310
  offsetY
267
311
  } = pendingPreview;
268
- overlay = (0, _dndPreview.buildDragPreview)(rows, rowRect);
312
+ overlay = (0, _dndPreview.buildDragPreview)(rows, rowRect, itemCount);
269
313
  overlay.style.left = `${Math.round(location.initial.input.clientX) - offsetX}px`;
270
314
  overlay.style.top = `${Math.round(location.initial.input.clientY) - offsetY}px`;
271
315
  document.body.appendChild(overlay);
@@ -289,6 +333,7 @@ const DragCell = _ref0 => {
289
333
  }, [isDraggable, dragId, tableId, disabled, setIsDragging]);
290
334
  return (0, _jsxRuntime.jsx)("td", {
291
335
  className: (0, _classnames.default)('ncua-table__drag-cell', className),
336
+ rowSpan: rowSpan,
292
337
  children: (0, _jsxRuntime.jsxs)("span", {
293
338
  className: "ncua-table__drag-cell-inner",
294
339
  children: [isDraggable && (0, _jsxRuntime.jsx)("button", {
@@ -461,9 +506,21 @@ const sortChildren = children => {
461
506
  });
462
507
  return result;
463
508
  };
464
- // ──────────────────────────────────────────────
465
- // Main Table component
466
- // ──────────────────────────────────────────────
509
+ const readRowDrop = (sourceData, target) => {
510
+ if (!target) return null;
511
+ const fromId = sourceData.id;
512
+ const toId = target.data.id;
513
+ if (typeof fromId !== 'string' || typeof toId !== 'string' || fromId === toId) return null;
514
+ const isStringArray = v => Array.isArray(v) && v.every(x => typeof x === 'string');
515
+ const moveIds = isStringArray(sourceData.moveIds) ? sourceData.moveIds : [fromId];
516
+ const edge = (0, _closestEdge.extractClosestEdge)(target.data) === 'bottom' ? 'bottom' : 'top';
517
+ return {
518
+ fromId,
519
+ toId,
520
+ edge,
521
+ moveIds
522
+ };
523
+ };
467
524
  const TableComponent = /*#__PURE__*/(0, _react.forwardRef)((_ref17, ref) => {
468
525
  let {
469
526
  type = 'horizontal',
@@ -480,6 +537,25 @@ const TableComponent = /*#__PURE__*/(0, _react.forwardRef)((_ref17, ref) => {
480
537
  } = _ref17;
481
538
  const tableIdRef = (0, _react.useRef)(`ncua-table-${Math.random().toString(_dndPreview.TABLE_DND_ID_RADIX).slice(_dndPreview.TABLE_DND_ID_SLICE_START, _dndPreview.TABLE_DND_ID_SLICE_END)}`);
482
539
  const tableId = tableIdRef.current;
540
+ // 콜백이 매 렌더마다 재생성되어도 effect 가 재구독하지 않도록 ref 로 유지한다.
541
+ const onRowDropRef = (0, _react.useRef)(undefined);
542
+ (0, _react.useEffect)(() => (0, _adapter.monitorForElements)({
543
+ canMonitor: _ref18 => {
544
+ let {
545
+ source
546
+ } = _ref18;
547
+ return source.data.tableId === tableId;
548
+ },
549
+ onDrop: _ref19 => {
550
+ let {
551
+ source,
552
+ location
553
+ } = _ref19;
554
+ const drop = readRowDrop(source.data, location.current.dropTargets[0]);
555
+ if (!drop) return;
556
+ onRowDropRef.current?.(drop.fromId, drop.toId, drop.edge, drop.moveIds);
557
+ }
558
+ }), [tableId]);
483
559
  const effectiveDraggable = draggable && type === 'horizontal';
484
560
  const tableClasses = (0, _classnames.default)('ncua-table', className, {
485
561
  'ncua-table--horizontal': type === 'horizontal',
@@ -568,7 +644,8 @@ const TableComponent = /*#__PURE__*/(0, _react.forwardRef)((_ref17, ref) => {
568
644
  };
569
645
  const dndContextValue = {
570
646
  isDraggable: effectiveDraggable,
571
- tableId
647
+ tableId,
648
+ onRowDropRef
572
649
  };
573
650
  // horizontalScroll=true 시 외곽 wrapper + FloatingProvider 부착.
574
651
  // 핵심 — __h-scroll-container 는 <table>(또는 scroll-area) 만 감싸고, footer/pagination 은
@@ -645,6 +722,7 @@ TableComponent.displayName = 'Table';
645
722
  const Table = exports.Table = Object.assign(TableComponent, {
646
723
  Header,
647
724
  Body,
725
+ RowGroup,
648
726
  Row,
649
727
  DragHeaderCell,
650
728
  DragCell,
@@ -52,12 +52,17 @@ _vitest.vi.mock('@atlaskit/pragmatic-drag-and-drop-auto-scroll/element', () => (
52
52
  (0, _vitest.expect)((0, _tableDndTestUtils.draggableArgsOf)('a').element).toBe(handle);
53
53
  (0, _vitest.expect)(view.querySelectorAll('button.ncua-table__drag-handle')).toHaveLength(_tableDndTestUtils.ROW_IDS.length);
54
54
  });
55
- (0, _vitest.it)('TB-08 getInitialData 는 {id: dragId, tableId} 를 돌려준다', () => {
55
+ (0, _vitest.it)('TB-08 getInitialData 는 {id: dragId, tableId} 와 옮길 행·블록 좌표를 돌려준다', () => {
56
56
  (0, _tableTestUtils.render)((0, _tableDndTestUtils.draggableTable)(_vitest.vi.fn()));
57
57
  const tableId = (0, _tableDndTestUtils.sourceDataOf)('b').tableId;
58
+ // 병합이 없으면 블록은 자기 행 한 칸이고 옮길 행도 자기 자신뿐이다
58
59
  (0, _vitest.expect)((0, _tableDndTestUtils.draggableArgsOf)('a').getInitialData?.({})).toEqual({
59
60
  id: 'a',
60
- tableId
61
+ tableId,
62
+ moveIds: ['a'],
63
+ blockBody: 0,
64
+ blockStart: 0,
65
+ blockEnd: 0
61
66
  });
62
67
  });
63
68
  (0, _vitest.it)('TB-08 disabled 면 button 을 disabled 로 렌더하고 draggable 로 등록하지 않는다', () => {
@@ -33,14 +33,14 @@ const DRAG_OVER_BOTTOM = 'ncua-table__row--drag-over-bottom';
33
33
  const source = (0, _tableDndTestUtils.sourceDataOf)('a');
34
34
  (0, _tableDndTestUtils.dropOnMonitors)(source, [(0, _tableDndTestUtils.targetDataOf)('b', _tableDndTestUtils.LOWER_HALF_Y, source)]);
35
35
  (0, _vitest.expect)(onRowDrop).toHaveBeenCalledTimes(1);
36
- (0, _vitest.expect)(onRowDrop).toHaveBeenCalledWith('a', 'b', 'bottom');
36
+ (0, _vitest.expect)(onRowDrop).toHaveBeenCalledWith('a', 'b', 'bottom', ['a']);
37
37
  });
38
38
  (0, _vitest.it)('TB-01 다른 행의 위쪽 절반에 놓으면 edge 는 "top" 이다', () => {
39
39
  const onRowDrop = _vitest.vi.fn();
40
40
  (0, _tableTestUtils.render)((0, _tableDndTestUtils.draggableTable)(onRowDrop));
41
41
  const source = (0, _tableDndTestUtils.sourceDataOf)('c');
42
42
  (0, _tableDndTestUtils.dropOnMonitors)(source, [(0, _tableDndTestUtils.targetDataOf)('a', _tableDndTestUtils.UPPER_HALF_Y, source)]);
43
- (0, _vitest.expect)(onRowDrop).toHaveBeenCalledWith('c', 'a', 'top');
43
+ (0, _vitest.expect)(onRowDrop).toHaveBeenCalledWith('c', 'a', 'top', ['c']);
44
44
  });
45
45
  (0, _vitest.it)('TB-01 target 에 closest edge 정보가 없으면 "top" 으로 전달한다', () => {
46
46
  const onRowDrop = _vitest.vi.fn();
@@ -50,7 +50,7 @@ const DRAG_OVER_BOTTOM = 'ncua-table__row--drag-over-bottom';
50
50
  id: 'b',
51
51
  tableId: source.tableId
52
52
  }]);
53
- (0, _vitest.expect)(onRowDrop).toHaveBeenCalledWith('a', 'b', 'top');
53
+ (0, _vitest.expect)(onRowDrop).toHaveBeenCalledWith('a', 'b', 'top', ['a']);
54
54
  });
55
55
  (0, _vitest.it)('TB-01 여러 drop target 이 겹치면 가장 안쪽(첫 번째) target 기준으로 전달한다', () => {
56
56
  const onRowDrop = _vitest.vi.fn();
@@ -58,15 +58,20 @@ const DRAG_OVER_BOTTOM = 'ncua-table__row--drag-over-bottom';
58
58
  const source = (0, _tableDndTestUtils.sourceDataOf)('a');
59
59
  (0, _tableDndTestUtils.dropOnMonitors)(source, [(0, _tableDndTestUtils.targetDataOf)('c', _tableDndTestUtils.LOWER_HALF_Y, source), (0, _tableDndTestUtils.targetDataOf)('b', _tableDndTestUtils.UPPER_HALF_Y, source)]);
60
60
  (0, _vitest.expect)(onRowDrop).toHaveBeenCalledTimes(1);
61
- (0, _vitest.expect)(onRowDrop).toHaveBeenCalledWith('a', 'c', 'bottom');
61
+ (0, _vitest.expect)(onRowDrop).toHaveBeenCalledWith('a', 'c', 'bottom', ['a']);
62
62
  });
63
63
  (0, _vitest.it)('TB-01 행의 source·target data 에 이 테이블의 tableId 가 실린다', () => {
64
64
  (0, _tableTestUtils.render)((0, _tableDndTestUtils.draggableTable)(_vitest.vi.fn()));
65
65
  const source = (0, _tableDndTestUtils.sourceDataOf)('a');
66
66
  const target = (0, _tableDndTestUtils.targetDataOf)('b', _tableDndTestUtils.LOWER_HALF_Y, source);
67
+ // 블록 좌표는 병합 블록 드롭 판정에 쓰인다. 병합이 없으면 자기 행 한 칸이다
67
68
  (0, _vitest.expect)(source).toEqual({
68
69
  id: 'a',
69
- tableId: _vitest.expect.stringMatching(/^ncua-table-/)
70
+ tableId: _vitest.expect.stringMatching(/^ncua-table-/),
71
+ moveIds: ['a'],
72
+ blockBody: 0,
73
+ blockStart: 0,
74
+ blockEnd: 0
70
75
  });
71
76
  (0, _vitest.expect)(target.id).toBe('b');
72
77
  (0, _vitest.expect)(target.tableId).toBe(source.tableId);
@@ -137,7 +142,7 @@ const DRAG_OVER_BOTTOM = 'ncua-table__row--drag-over-bottom';
137
142
  id: 'b',
138
143
  tableId: source.tableId
139
144
  }]);
140
- (0, _vitest.expect)(first).toHaveBeenCalledWith('a', 'b', 'top');
145
+ (0, _vitest.expect)(first).toHaveBeenCalledWith('a', 'b', 'top', ['a']);
141
146
  (0, _vitest.expect)(second).not.toHaveBeenCalled();
142
147
  (0, _vitest.expect)((0, _tableDndTestUtils.sourceDataOf)('a', sectionOf('second')).tableId).not.toBe(source.tableId);
143
148
  });
@@ -167,7 +172,7 @@ const DRAG_OVER_BOTTOM = 'ncua-table__row--drag-over-bottom';
167
172
  id: 'c',
168
173
  tableId: foreignTarget.tableId
169
174
  }]);
170
- (0, _vitest.expect)(first).toHaveBeenCalledWith('a', 'c', 'top');
175
+ (0, _vitest.expect)(first).toHaveBeenCalledWith('a', 'c', 'top', ['a']);
171
176
  (0, _vitest.expect)(second).not.toHaveBeenCalled();
172
177
  });
173
178
  _vitest.it.todo('TB-04 다른 테이블의 행 위에 놓으면 어느 테이블의 onRowDrop 도 호출하지 않는다 (canDrop 또는 target tableId 검사)');
@@ -184,7 +189,7 @@ const DRAG_OVER_BOTTOM = 'ncua-table__row--drag-over-bottom';
184
189
  (0, _vitest.expect)((0, _tableDndTestUtils.monitors)()).toHaveLength(1);
185
190
  (0, _vitest.expect)((0, _tableDndTestUtils.monitors)()[0]).toBe(monitorBefore);
186
191
  (0, _vitest.expect)(first).not.toHaveBeenCalled();
187
- (0, _vitest.expect)(latest).toHaveBeenCalledWith('a', 'b', 'bottom');
192
+ (0, _vitest.expect)(latest).toHaveBeenCalledWith('a', 'b', 'bottom', ['a']);
188
193
  });
189
194
  (0, _vitest.it)('TB-05 onRowDrop 을 undefined 로 바꾸면 이후 drop 은 무시된다', () => {
190
195
  const first = _vitest.vi.fn();
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+
3
+ var _react = require("react");
4
+ var _client = require("react-dom/client");
5
+ var _testUtils = require("react-dom/test-utils");
6
+ var _vitest = require("vitest");
7
+ var _Table = require("../Table");
8
+ // @vitest-environment jsdom
9
+ /**
10
+ * `Table.RowGroup` 의 DOM 계약만 다룬다.
11
+ * hover·selected 배경은 CSS 캐스케이드 결과이므로 jsdom 으로 판정할 수 없다 (브라우저에서 확인).
12
+ */
13
+
14
+ let container;
15
+ let root;
16
+ (0, _vitest.beforeEach)(() => {
17
+ container = document.createElement('div');
18
+ document.body.appendChild(container);
19
+ (0, _testUtils.act)(() => {
20
+ root = (0, _client.createRoot)(container);
21
+ });
22
+ });
23
+ (0, _vitest.afterEach)(() => {
24
+ (0, _testUtils.act)(() => {
25
+ root.unmount();
26
+ });
27
+ container.remove();
28
+ });
29
+ // `children` 은 TableRowGroupProps 의 필수 prop 이라 createElement 의 props 인자에 담는다.
30
+ const rowGroupProps = className => ({
31
+ className,
32
+ children: /*#__PURE__*/(0, _react.createElement)('tr', null, /*#__PURE__*/(0, _react.createElement)('td', null, '셀'))
33
+ });
34
+ const renderRowGroup = className => {
35
+ (0, _testUtils.act)(() => {
36
+ root.render(/*#__PURE__*/(0, _react.createElement)('table', null, /*#__PURE__*/(0, _react.createElement)(_Table.Table.RowGroup, rowGroupProps(className))));
37
+ });
38
+ const tbody = container.querySelector('tbody');
39
+ if (!tbody) throw new Error('tbody 가 렌더되지 않았다.');
40
+ return tbody;
41
+ };
42
+ (0, _vitest.describe)('Table.RowGroup', () => {
43
+ (0, _vitest.it)('ncua-table__body 와 ncua-table__row-group 을 가진 tbody 를 렌더한다', () => {
44
+ const tbody = renderRowGroup();
45
+ (0, _vitest.expect)(tbody.classList.contains('ncua-table__body')).toBe(true);
46
+ (0, _vitest.expect)(tbody.classList.contains('ncua-table__row-group')).toBe(true);
47
+ });
48
+ (0, _vitest.it)('전달한 className 을 기본 클래스와 함께 붙인다', () => {
49
+ const tbody = renderRowGroup('custom-group');
50
+ (0, _vitest.expect)(tbody.classList.contains('custom-group')).toBe(true);
51
+ (0, _vitest.expect)(tbody.classList.contains('ncua-table__body')).toBe(true);
52
+ (0, _vitest.expect)(tbody.classList.contains('ncua-table__row-group')).toBe(true);
53
+ });
54
+ });