@ncds/ui-admin 1.8.22-alpha.2 → 1.8.22-alpha.3

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 (45) hide show
  1. package/dist/cjs/assets/scripts/table/scrollbar/const.js +50 -0
  2. package/dist/cjs/assets/scripts/table/scrollbar/drag.js +75 -0
  3. package/dist/cjs/assets/scripts/table/scrollbar/geometry.js +69 -0
  4. package/dist/cjs/assets/scripts/table/scrollbar/isolate.js +36 -0
  5. package/dist/cjs/src/components/data-display/table/Table.js +1 -0
  6. package/dist/cjs/src/components/data-display/table/__tests__/useTableScrollbars.characterisation.test.js +390 -0
  7. package/dist/cjs/src/components/data-display/table/__tests__/useTableScrollbars.regression.test.js +339 -0
  8. package/dist/cjs/src/components/data-display/table/useTableScrollbars.js +115 -86
  9. package/dist/cjs/src/generated/scoped-css.js +1 -1
  10. package/dist/esm/assets/scripts/table/scrollbar/const.js +44 -0
  11. package/dist/esm/assets/scripts/table/scrollbar/drag.js +67 -0
  12. package/dist/esm/assets/scripts/table/scrollbar/geometry.js +60 -0
  13. package/dist/esm/assets/scripts/table/scrollbar/isolate.js +27 -0
  14. package/dist/esm/src/components/data-display/table/Table.js +1 -0
  15. package/dist/esm/src/components/data-display/table/__tests__/useTableScrollbars.characterisation.test.js +387 -0
  16. package/dist/esm/src/components/data-display/table/__tests__/useTableScrollbars.regression.test.js +336 -0
  17. package/dist/esm/src/components/data-display/table/useTableScrollbars.js +85 -86
  18. package/dist/esm/src/generated/scoped-css.js +1 -1
  19. package/dist/temp/assets/scripts/table/scrollbar/const.d.ts +46 -0
  20. package/dist/temp/assets/scripts/table/scrollbar/const.js +44 -0
  21. package/dist/temp/assets/scripts/table/scrollbar/drag.d.ts +18 -0
  22. package/dist/temp/assets/scripts/table/scrollbar/drag.js +49 -0
  23. package/dist/temp/assets/scripts/table/scrollbar/geometry.d.ts +47 -0
  24. package/dist/temp/assets/scripts/table/scrollbar/geometry.js +37 -0
  25. package/dist/temp/assets/scripts/table/scrollbar/isolate.d.ts +4 -0
  26. package/dist/temp/assets/scripts/table/scrollbar/isolate.js +30 -0
  27. package/dist/temp/src/components/data-display/table/Table.js +1 -0
  28. package/dist/temp/src/components/data-display/table/__tests__/useTableScrollbars.characterisation.test.d.ts +1 -0
  29. package/dist/temp/src/components/data-display/table/__tests__/useTableScrollbars.characterisation.test.js +282 -0
  30. package/dist/temp/src/components/data-display/table/__tests__/useTableScrollbars.regression.test.d.ts +1 -0
  31. package/dist/temp/src/components/data-display/table/__tests__/useTableScrollbars.regression.test.js +244 -0
  32. package/dist/temp/src/components/data-display/table/useTableScrollbars.d.ts +2 -6
  33. package/dist/temp/src/components/data-display/table/useTableScrollbars.js +100 -72
  34. package/dist/temp/src/generated/scoped-css.js +1 -1
  35. package/dist/temp/src/hooks/useIsomorphicLayoutEffect.d.ts +2 -2
  36. package/dist/types/assets/scripts/table/scrollbar/const.d.ts +46 -0
  37. package/dist/types/assets/scripts/table/scrollbar/drag.d.ts +18 -0
  38. package/dist/types/assets/scripts/table/scrollbar/geometry.d.ts +47 -0
  39. package/dist/types/assets/scripts/table/scrollbar/isolate.d.ts +4 -0
  40. package/dist/types/src/components/data-display/table/__tests__/useTableScrollbars.characterisation.test.d.ts +1 -0
  41. package/dist/types/src/components/data-display/table/__tests__/useTableScrollbars.regression.test.d.ts +1 -0
  42. package/dist/types/src/components/data-display/table/useTableScrollbars.d.ts +2 -6
  43. package/dist/types/src/hooks/useIsomorphicLayoutEffect.d.ts +2 -2
  44. package/dist/ui-admin/assets/styles/style.css +29 -0
  45. package/package.json +1 -1
@@ -0,0 +1,339 @@
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 _const = require("../../../../../assets/scripts/table/scrollbar/const");
8
+ var _useTableScrollbars = require("../useTableScrollbars");
9
+ // @vitest-environment jsdom
10
+ /**
11
+ * 훅에 새로 추가한 동작만 다룬다.
12
+ * 기존 동작 무변경은 `useTableScrollbars.characterisation.test.ts` 가 고정한다.
13
+ */
14
+
15
+ const observers = [];
16
+ class ResizeObserverStub {
17
+ constructor(callback) {
18
+ this.record = {
19
+ callback,
20
+ targets: [],
21
+ disconnected: false
22
+ };
23
+ observers.push(this.record);
24
+ }
25
+ observe(target) {
26
+ this.record.targets.push(target);
27
+ }
28
+ unobserve() {
29
+ // 전체 해제만 검증 대상이다.
30
+ }
31
+ disconnect() {
32
+ this.record.disconnected = true;
33
+ }
34
+ }
35
+ const defineMetrics = (element, metrics) => {
36
+ for (const [key, value] of Object.entries(metrics)) {
37
+ Object.defineProperty(element, key, {
38
+ configurable: true,
39
+ writable: true,
40
+ value
41
+ });
42
+ }
43
+ };
44
+ let container;
45
+ let root;
46
+ const probe = {
47
+ markerAtPassiveEffect: false
48
+ };
49
+ const render = element => {
50
+ (0, _testUtils.act)(() => {
51
+ root.render(element);
52
+ });
53
+ };
54
+ const VerticalHarness = _ref => {
55
+ let {
56
+ enabled = true
57
+ } = _ref;
58
+ const scrollContainerRef = (0, _react.useRef)(null);
59
+ const scrollAreaRef = (0, _react.useRef)(null);
60
+ const scrollbarRef = (0, _react.useRef)(null);
61
+ const thumbRef = (0, _react.useRef)(null);
62
+ // 훅보다 먼저 선언한 passive effect. 훅이 layout effect 를 쓰면 이 시점에 이미 부착돼 있다.
63
+ (0, _react.useEffect)(() => {
64
+ probe.markerAtPassiveEffect = scrollContainerRef.current?.hasAttribute(_const.SCROLLBAR_ATTRS.ready) ?? false;
65
+ });
66
+ const {
67
+ handleThumbMouseDown
68
+ } = (0, _useTableScrollbars.useTableVerticalScrollbar)({
69
+ enabled,
70
+ layoutKey: false,
71
+ scrollContainerRef,
72
+ scrollAreaRef,
73
+ scrollbarRef,
74
+ thumbRef
75
+ });
76
+ return /*#__PURE__*/(0, _react.createElement)('div', {
77
+ className: 'ncua-table ncua-table--fixed-header'
78
+ }, /*#__PURE__*/(0, _react.createElement)('div', {
79
+ ref: scrollAreaRef,
80
+ className: 'ncua-table__scroll-area'
81
+ }, /*#__PURE__*/(0, _react.createElement)('div', {
82
+ ref: scrollContainerRef,
83
+ className: 'ncua-table__scroll-container'
84
+ }, /*#__PURE__*/(0, _react.createElement)('table', {
85
+ className: 'ncua-table__table'
86
+ }, /*#__PURE__*/(0, _react.createElement)('tbody', null))), /*#__PURE__*/(0, _react.createElement)('div', {
87
+ ref: scrollbarRef,
88
+ className: 'ncua-table__scrollbar'
89
+ }, /*#__PURE__*/(0, _react.createElement)('div', {
90
+ ref: thumbRef,
91
+ className: 'ncua-table__scrollbar-thumb',
92
+ onMouseDown: handleThumbMouseDown
93
+ }))));
94
+ };
95
+ const HorizontalHarness = () => {
96
+ const hScrollContainerRef = (0, _react.useRef)(null);
97
+ const hScrollbarRef = (0, _react.useRef)(null);
98
+ const hThumbRef = (0, _react.useRef)(null);
99
+ const {
100
+ handleHThumbMouseDown
101
+ } = (0, _useTableScrollbars.useTableHorizontalScrollbar)({
102
+ enabled: true,
103
+ hScrollContainerRef,
104
+ hScrollbarRef,
105
+ hThumbRef
106
+ });
107
+ return /*#__PURE__*/(0, _react.createElement)('div', {
108
+ className: 'ncua-table'
109
+ }, /*#__PURE__*/(0, _react.createElement)('div', {
110
+ ref: hScrollContainerRef,
111
+ className: 'ncua-table__h-scroll-container'
112
+ }, /*#__PURE__*/(0, _react.createElement)('div', {
113
+ className: 'ncua-table__h-scroll-inner'
114
+ }, /*#__PURE__*/(0, _react.createElement)('table', {
115
+ className: 'ncua-table__table'
116
+ }, /*#__PURE__*/(0, _react.createElement)('tbody', null))), /*#__PURE__*/(0, _react.createElement)('div', {
117
+ ref: hScrollbarRef,
118
+ className: 'ncua-table__h-scrollbar'
119
+ }, /*#__PURE__*/(0, _react.createElement)('div', {
120
+ ref: hThumbRef,
121
+ className: 'ncua-table__h-scrollbar-thumb',
122
+ onMouseDown: handleHThumbMouseDown
123
+ }))));
124
+ };
125
+ /** 누적 여부를 드러내기 위한 반복 드래그 횟수 */
126
+ const DRAG_REPEAT = 5;
127
+ const query = selector => container.querySelector(selector);
128
+ (0, _vitest.beforeEach)(() => {
129
+ _vitest.vi.stubGlobal('ResizeObserver', ResizeObserverStub);
130
+ observers.length = 0;
131
+ probe.markerAtPassiveEffect = false;
132
+ container = document.createElement('div');
133
+ document.body.appendChild(container);
134
+ (0, _testUtils.act)(() => {
135
+ root = (0, _client.createRoot)(container);
136
+ });
137
+ });
138
+ (0, _vitest.afterEach)(() => {
139
+ (0, _testUtils.act)(() => {
140
+ root.unmount();
141
+ });
142
+ container.remove();
143
+ _vitest.vi.unstubAllGlobals();
144
+ _vitest.vi.restoreAllMocks();
145
+ });
146
+ (0, _vitest.describe)('표식 부착', () => {
147
+ (0, _vitest.it)('마운트 시 스크롤 컨테이너에 data-scrollbar-ready 를 부착한다', () => {
148
+ render(/*#__PURE__*/(0, _react.createElement)(VerticalHarness));
149
+ (0, _vitest.expect)(query('.ncua-table__scroll-container').hasAttribute(_const.SCROLLBAR_ATTRS.ready)).toBe(true);
150
+ });
151
+ (0, _vitest.it)('표식을 layout effect 에서 부착해 페인트 전에 반영한다', () => {
152
+ render(/*#__PURE__*/(0, _react.createElement)(VerticalHarness));
153
+ (0, _vitest.expect)(probe.markerAtPassiveEffect).toBe(true);
154
+ });
155
+ (0, _vitest.it)('소유자 표식(react)을 함께 부착한다', () => {
156
+ render(/*#__PURE__*/(0, _react.createElement)(VerticalHarness));
157
+ (0, _vitest.expect)(query('.ncua-table__scroll-container').getAttribute(_const.SCROLLBAR_ATTRS.owner)).toBe(_const.SCROLLBAR_OWNERS.react);
158
+ });
159
+ (0, _vitest.it)('언마운트 시 표식과 소유자 표식을 모두 제거한다', () => {
160
+ render(/*#__PURE__*/(0, _react.createElement)(VerticalHarness));
161
+ const scrollEl = query('.ncua-table__scroll-container');
162
+ (0, _testUtils.act)(() => {
163
+ root.unmount();
164
+ });
165
+ (0, _vitest.expect)(scrollEl.hasAttribute(_const.SCROLLBAR_ATTRS.ready)).toBe(false);
166
+ (0, _vitest.expect)(scrollEl.hasAttribute(_const.SCROLLBAR_ATTRS.owner)).toBe(false);
167
+ (0, _testUtils.act)(() => {
168
+ root = (0, _client.createRoot)(container);
169
+ });
170
+ });
171
+ (0, _vitest.it)('가로축도 자기 스크롤 컨테이너에 표식을 부착한다', () => {
172
+ render(/*#__PURE__*/(0, _react.createElement)(HorizontalHarness));
173
+ (0, _vitest.expect)(query('.ncua-table__h-scroll-container').hasAttribute(_const.SCROLLBAR_ATTRS.ready)).toBe(true);
174
+ });
175
+ (0, _vitest.it)('타 소유자가 선점한 컨테이너는 덮어쓰지 않고 바인딩도 하지 않는다', () => {
176
+ render(/*#__PURE__*/(0, _react.createElement)(VerticalHarness, {
177
+ enabled: false
178
+ }));
179
+ const scrollEl = query('.ncua-table__scroll-container');
180
+ scrollEl.setAttribute(_const.SCROLLBAR_ATTRS.ready, '');
181
+ scrollEl.setAttribute(_const.SCROLLBAR_ATTRS.owner, _const.SCROLLBAR_OWNERS.cdn);
182
+ observers.length = 0;
183
+ render(/*#__PURE__*/(0, _react.createElement)(VerticalHarness, {
184
+ enabled: true
185
+ }));
186
+ (0, _vitest.expect)(scrollEl.getAttribute(_const.SCROLLBAR_ATTRS.owner)).toBe(_const.SCROLLBAR_OWNERS.cdn);
187
+ (0, _vitest.expect)(observers).toHaveLength(0);
188
+ });
189
+ (0, _vitest.it)('타 소유자가 선점한 표식은 언마운트에서도 제거하지 않는다', () => {
190
+ render(/*#__PURE__*/(0, _react.createElement)(VerticalHarness, {
191
+ enabled: false
192
+ }));
193
+ const scrollEl = query('.ncua-table__scroll-container');
194
+ scrollEl.setAttribute(_const.SCROLLBAR_ATTRS.ready, '');
195
+ scrollEl.setAttribute(_const.SCROLLBAR_ATTRS.owner, _const.SCROLLBAR_OWNERS.cdn);
196
+ render(/*#__PURE__*/(0, _react.createElement)(VerticalHarness, {
197
+ enabled: true
198
+ }));
199
+ (0, _testUtils.act)(() => {
200
+ root.unmount();
201
+ });
202
+ (0, _vitest.expect)(scrollEl.hasAttribute(_const.SCROLLBAR_ATTRS.ready)).toBe(true);
203
+ (0, _vitest.expect)(scrollEl.getAttribute(_const.SCROLLBAR_ATTRS.owner)).toBe(_const.SCROLLBAR_OWNERS.cdn);
204
+ (0, _testUtils.act)(() => {
205
+ root = (0, _client.createRoot)(container);
206
+ });
207
+ });
208
+ });
209
+ (0, _vitest.describe)('ResizeObserver 관찰 대상', () => {
210
+ (0, _vitest.it)('세로축이 스크롤 콘텐츠 루트(<table>)를 함께 관찰한다', () => {
211
+ render(/*#__PURE__*/(0, _react.createElement)(VerticalHarness));
212
+ (0, _vitest.expect)(observers[0]?.targets).toContain(query('.ncua-table__table'));
213
+ });
214
+ (0, _vitest.it)('컨테이너 치수 불변 + 콘텐츠 증가 통지에 썸 높이를 재산출한다', () => {
215
+ render(/*#__PURE__*/(0, _react.createElement)(VerticalHarness));
216
+ const scrollEl = query('.ncua-table__scroll-container');
217
+ const thumbEl = query('.ncua-table__scrollbar-thumb');
218
+ defineMetrics(scrollEl, {
219
+ clientHeight: 200,
220
+ scrollHeight: 400,
221
+ scrollTop: 0
222
+ });
223
+ defineMetrics(query('.ncua-table__scrollbar'), {
224
+ clientHeight: 160
225
+ });
226
+ (0, _testUtils.act)(() => {
227
+ observers[0]?.callback();
228
+ });
229
+ (0, _vitest.expect)(thumbEl.style.height).toBe('80px');
230
+ // 컨테이너 박스는 그대로인 채 콘텐츠만 늘어난 경우
231
+ defineMetrics(scrollEl, {
232
+ scrollHeight: 800
233
+ });
234
+ (0, _testUtils.act)(() => {
235
+ observers[0]?.callback();
236
+ });
237
+ (0, _vitest.expect)(thumbEl.style.height).toBe('40px');
238
+ });
239
+ (0, _vitest.it)('가로축이 __h-scroll-inner 와 콘텐츠 루트를 함께 관찰한다', () => {
240
+ render(/*#__PURE__*/(0, _react.createElement)(HorizontalHarness));
241
+ (0, _vitest.expect)(observers[0]?.targets).toContain(query('.ncua-table__h-scroll-inner'));
242
+ (0, _vitest.expect)(observers[0]?.targets).toContain(query('.ncua-table__table'));
243
+ });
244
+ });
245
+ (0, _vitest.describe)('드래그 해제 경로', () => {
246
+ (0, _vitest.it)('pointercancel 로도 드래그가 종료된다', () => {
247
+ render(/*#__PURE__*/(0, _react.createElement)(VerticalHarness));
248
+ const scrollEl = query('.ncua-table__scroll-container');
249
+ const thumbEl = query('.ncua-table__scrollbar-thumb');
250
+ const areaEl = query('.ncua-table__scroll-area');
251
+ defineMetrics(scrollEl, {
252
+ clientHeight: 200,
253
+ scrollHeight: 800,
254
+ scrollTop: 0
255
+ });
256
+ defineMetrics(thumbEl, {
257
+ offsetHeight: 40
258
+ });
259
+ (0, _testUtils.act)(() => {
260
+ thumbEl.dispatchEvent(new MouseEvent('mousedown', {
261
+ bubbles: true,
262
+ clientY: 100
263
+ }));
264
+ });
265
+ (0, _testUtils.act)(() => {
266
+ document.dispatchEvent(new Event('pointercancel'));
267
+ document.dispatchEvent(new MouseEvent('mousemove', {
268
+ clientY: 400
269
+ }));
270
+ });
271
+ (0, _vitest.expect)(scrollEl.scrollTop).toBe(0);
272
+ (0, _vitest.expect)(areaEl.hasAttribute(_const.SCROLLBAR_ATTRS.dragging)).toBe(false);
273
+ });
274
+ (0, _vitest.it)('드래그를 반복해도 해제 신호에 리스너가 누적되지 않는다', () => {
275
+ render(/*#__PURE__*/(0, _react.createElement)(VerticalHarness));
276
+ const scrollEl = query('.ncua-table__scroll-container');
277
+ const thumbEl = query('.ncua-table__scrollbar-thumb');
278
+ const areaEl = query('.ncua-table__scroll-area');
279
+ defineMetrics(scrollEl, {
280
+ clientHeight: 200,
281
+ scrollHeight: 800,
282
+ scrollTop: 0
283
+ });
284
+ defineMetrics(thumbEl, {
285
+ offsetHeight: 40
286
+ });
287
+ // 누적이 있으면 잔존 클로저가 언마운트 시 드래그 횟수만큼 추가 발화한다.
288
+ for (let i = 0; i < DRAG_REPEAT; i += 1) {
289
+ (0, _testUtils.act)(() => {
290
+ thumbEl.dispatchEvent(new MouseEvent('mousedown', {
291
+ bubbles: true,
292
+ clientY: 100
293
+ }));
294
+ document.dispatchEvent(new MouseEvent('mouseup'));
295
+ });
296
+ }
297
+ const removeSpy = _vitest.vi.spyOn(areaEl, 'removeAttribute');
298
+ (0, _testUtils.act)(() => {
299
+ root.unmount();
300
+ });
301
+ const draggingRemovals = removeSpy.mock.calls.filter(_ref2 => {
302
+ let [name] = _ref2;
303
+ return name === _const.SCROLLBAR_ATTRS.dragging;
304
+ });
305
+ (0, _vitest.expect)(draggingRemovals).toHaveLength(0);
306
+ (0, _testUtils.act)(() => {
307
+ root = (0, _client.createRoot)(container);
308
+ });
309
+ });
310
+ (0, _vitest.it)('언마운트가 드래그보다 먼저 일어나도 document 리스너가 남지 않는다', () => {
311
+ render(/*#__PURE__*/(0, _react.createElement)(VerticalHarness));
312
+ const scrollEl = query('.ncua-table__scroll-container');
313
+ const thumbEl = query('.ncua-table__scrollbar-thumb');
314
+ defineMetrics(scrollEl, {
315
+ clientHeight: 200,
316
+ scrollHeight: 800,
317
+ scrollTop: 0
318
+ });
319
+ defineMetrics(thumbEl, {
320
+ offsetHeight: 40
321
+ });
322
+ (0, _testUtils.act)(() => {
323
+ thumbEl.dispatchEvent(new MouseEvent('mousedown', {
324
+ bubbles: true,
325
+ clientY: 100
326
+ }));
327
+ });
328
+ (0, _testUtils.act)(() => {
329
+ root.unmount();
330
+ });
331
+ document.dispatchEvent(new MouseEvent('mousemove', {
332
+ clientY: 400
333
+ }));
334
+ (0, _vitest.expect)(scrollEl.scrollTop).toBe(0);
335
+ (0, _testUtils.act)(() => {
336
+ root = (0, _client.createRoot)(container);
337
+ });
338
+ });
339
+ });
@@ -3,71 +3,57 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.useTableVerticalScrollbar = exports.useTableHorizontalScrollbar = exports.TABLE_HEADER_HEIGHT = exports.SCROLLBAR_TRACK_OFFSET = exports.SCROLLBAR_THUMB_MIN_HEIGHT = exports.H_SCROLLBAR_THUMB_MIN_WIDTH = exports.H_SCROLLBAR_SIDE_GAP = void 0;
7
- var _react = require("react");
8
- // ──────────────────────────────────────────────
9
- // 상수 — Table.tsx 와 _table.scss 양쪽에서 동기화 필요
10
- // ──────────────────────────────────────────────
11
- // $table-header-height
12
- const TABLE_HEADER_HEIGHT = exports.TABLE_HEADER_HEIGHT = 40;
13
- // 세로/가로 thumb 최소 크기
14
- const SCROLLBAR_THUMB_MIN_HEIGHT = exports.SCROLLBAR_THUMB_MIN_HEIGHT = 40;
15
- const H_SCROLLBAR_THUMB_MIN_WIDTH = exports.H_SCROLLBAR_THUMB_MIN_WIDTH = 40;
16
- // SCSS .ncua-table__h-scrollbar { left/right: var(--spacing-s) = 8px } 와 동기화
17
- const H_SCROLLBAR_SIDE_GAP = exports.H_SCROLLBAR_SIDE_GAP = 8;
18
- // 세로 트랙 상하 여백 합계 — top 8 + bottom 8 = 16 (header 회피분 40 은 별도 처리)
19
- // biome-ignore lint/style/useExportsLast: 상수는 문서 주석과 함께 상단에 정의
20
- const SCROLLBAR_TRACK_OFFSET = exports.SCROLLBAR_TRACK_OFFSET = 16;
21
- const startDrag = (e, options) => {
22
- e.preventDefault();
23
- const {
24
- axis,
25
- scrollEl,
26
- thumbEl,
27
- draggingTarget = scrollEl,
28
- sideGap = 0
29
- } = options;
30
- draggingTarget.setAttribute('data-dragging', '');
31
- if (axis === 'y') {
32
- const startY = e.clientY;
33
- const startScrollTop = scrollEl.scrollTop;
34
- const {
35
- scrollHeight,
36
- clientHeight
37
- } = scrollEl;
38
- const thumbHeight = thumbEl.offsetHeight;
39
- const ratio = (scrollHeight - clientHeight) / (clientHeight - thumbHeight);
40
- const onMove = ev => {
41
- scrollEl.scrollTop = startScrollTop + (ev.clientY - startY) * ratio;
42
- };
43
- const onUp = () => {
44
- draggingTarget.removeAttribute('data-dragging');
45
- document.removeEventListener('mousemove', onMove);
46
- document.removeEventListener('mouseup', onUp);
47
- };
48
- document.addEventListener('mousemove', onMove);
49
- document.addEventListener('mouseup', onUp);
50
- return;
6
+ Object.defineProperty(exports, "H_SCROLLBAR_SIDE_GAP", {
7
+ enumerable: true,
8
+ get: function () {
9
+ return _const.H_SCROLLBAR_SIDE_GAP;
51
10
  }
52
- const startX = e.clientX;
53
- const startScrollLeft = scrollEl.scrollLeft;
54
- const {
55
- scrollWidth,
56
- clientWidth
57
- } = scrollEl;
58
- const thumbWidth = thumbEl.offsetWidth;
59
- const trackWidth = clientWidth - sideGap * 2;
60
- const ratio = (scrollWidth - clientWidth) / (trackWidth - thumbWidth);
61
- const onMove = ev => {
62
- scrollEl.scrollLeft = startScrollLeft + (ev.clientX - startX) * ratio;
63
- };
64
- const onUp = () => {
65
- draggingTarget.removeAttribute('data-dragging');
66
- document.removeEventListener('mousemove', onMove);
67
- document.removeEventListener('mouseup', onUp);
68
- };
69
- document.addEventListener('mousemove', onMove);
70
- document.addEventListener('mouseup', onUp);
11
+ });
12
+ Object.defineProperty(exports, "H_SCROLLBAR_THUMB_MIN_WIDTH", {
13
+ enumerable: true,
14
+ get: function () {
15
+ return _const.H_SCROLLBAR_THUMB_MIN_WIDTH;
16
+ }
17
+ });
18
+ Object.defineProperty(exports, "SCROLLBAR_THUMB_MIN_HEIGHT", {
19
+ enumerable: true,
20
+ get: function () {
21
+ return _const.SCROLLBAR_THUMB_MIN_HEIGHT;
22
+ }
23
+ });
24
+ Object.defineProperty(exports, "SCROLLBAR_TRACK_OFFSET", {
25
+ enumerable: true,
26
+ get: function () {
27
+ return _const.SCROLLBAR_TRACK_OFFSET;
28
+ }
29
+ });
30
+ Object.defineProperty(exports, "TABLE_HEADER_HEIGHT", {
31
+ enumerable: true,
32
+ get: function () {
33
+ return _const.TABLE_HEADER_HEIGHT;
34
+ }
35
+ });
36
+ exports.useTableVerticalScrollbar = exports.useTableHorizontalScrollbar = void 0;
37
+ var _react = require("react");
38
+ var _const = require("../../../../assets/scripts/table/scrollbar/const");
39
+ var _drag = require("../../../../assets/scripts/table/scrollbar/drag");
40
+ var _geometry = require("../../../../assets/scripts/table/scrollbar/geometry");
41
+ var _useIsomorphicLayoutEffect = require("../../../hooks/useIsomorphicLayoutEffect");
42
+ // CDN 바인딩과 같은 값을 쓰기 위해 재노출한다. 정의는 scrollbar/const.ts 에 있다.
43
+
44
+ /** 타 소유자가 선점한 컨테이너는 덮어쓰지 않는다. CDN 생성자의 소유권 검사와 대칭이다 */
45
+ const claimMarkers = el => {
46
+ const owner = el.getAttribute(_const.SCROLLBAR_ATTRS.owner);
47
+ if (el.hasAttribute(_const.SCROLLBAR_ATTRS.ready) && owner !== null && owner !== _const.SCROLLBAR_OWNERS.react) return false;
48
+ el.setAttribute(_const.SCROLLBAR_ATTRS.ready, '');
49
+ el.setAttribute(_const.SCROLLBAR_ATTRS.owner, _const.SCROLLBAR_OWNERS.react);
50
+ return true;
51
+ };
52
+ /** 자기가 붙인 표식만 뗀다. CDN 인스턴스가 살아 있는 표식을 훅이 떼면 양쪽이 모두 어긋난다 */
53
+ const releaseMarkers = el => {
54
+ if (el.getAttribute(_const.SCROLLBAR_ATTRS.owner) !== _const.SCROLLBAR_OWNERS.react) return;
55
+ el.removeAttribute(_const.SCROLLBAR_ATTRS.ready);
56
+ el.removeAttribute(_const.SCROLLBAR_ATTRS.owner);
71
57
  };
72
58
  const useTableVerticalScrollbar = _ref => {
73
59
  let {
@@ -78,27 +64,40 @@ const useTableVerticalScrollbar = _ref => {
78
64
  scrollbarRef,
79
65
  thumbRef
80
66
  } = _ref;
81
- // biome-ignore lint/correctness/useExhaustiveDependencies: layoutKey는 직접 참조하지 않지만 horizontalScroll 토글로 스크롤바 DOM이 리마운트될 때 effect를 재바인딩하기 위한 트리거로 의도적으로 포함한다.
82
- (0, _react.useEffect)(() => {
67
+ const releaseRef = (0, _react.useRef)(null);
68
+ // 표식을 페인트 전에 붙여야 스크롤바가 한 프레임 늦게 나타나지 않는다.
69
+ // layoutKey 는 직접 참조하지 않지만, horizontalScroll 토글로 스크롤바 DOM 이 리마운트될 때
70
+ // effect 를 재바인딩하기 위한 트리거로 의존성 배열에 넣는다.
71
+ (0, _useIsomorphicLayoutEffect.useIsomorphicLayoutEffect)(() => {
83
72
  if (!enabled) return;
84
73
  const scrollEl = scrollContainerRef.current;
85
74
  const thumbEl = thumbRef.current;
86
75
  if (!scrollEl || !thumbEl) return;
76
+ // 소유자 표식이 있어야 CDN 자동 초기화가 이 테이블을 건드리지 않는다.
77
+ // 선점 실패 시 리스너·옵저버 등록 전에 빠져야 이중 구동이 생기지 않는다.
78
+ if (!claimMarkers(scrollEl)) return;
79
+ const controller = new AbortController();
80
+ releaseRef.current = controller;
87
81
  const update = () => {
88
82
  const {
89
83
  scrollTop,
90
84
  scrollHeight,
91
85
  clientHeight
92
86
  } = scrollEl;
93
- if (scrollHeight <= clientHeight) {
87
+ const trackSize = scrollbarRef.current?.clientHeight || (scrollAreaRef.current?.clientHeight ?? clientHeight) - _const.TABLE_HEADER_HEIGHT - _const.SCROLLBAR_TRACK_OFFSET;
88
+ const geometry = (0, _geometry.computeThumbGeometry)({
89
+ scrollOffset: scrollTop,
90
+ scrollSize: scrollHeight,
91
+ clientSize: clientHeight,
92
+ trackSize,
93
+ thumbMinSize: _const.SCROLLBAR_THUMB_MIN_HEIGHT
94
+ });
95
+ if (!geometry.visible) {
94
96
  thumbEl.style.height = '0';
95
97
  return;
96
98
  }
97
- const trackHeight = scrollbarRef.current?.clientHeight || (scrollAreaRef.current?.clientHeight ?? clientHeight) - TABLE_HEADER_HEIGHT - SCROLLBAR_TRACK_OFFSET;
98
- const thumbHeight = Math.max(SCROLLBAR_THUMB_MIN_HEIGHT, clientHeight / scrollHeight * trackHeight);
99
- const thumbTop = scrollTop / (scrollHeight - clientHeight) * (trackHeight - thumbHeight);
100
- thumbEl.style.height = `${thumbHeight}px`;
101
- thumbEl.style.transform = `translateY(${thumbTop}px)`;
99
+ thumbEl.style.height = `${geometry.size}px`;
100
+ thumbEl.style.transform = `translateY(${geometry.offset}px)`;
102
101
  };
103
102
  scrollEl.addEventListener('scroll', update, {
104
103
  passive: true
@@ -107,10 +106,16 @@ const useTableVerticalScrollbar = _ref => {
107
106
  observer.observe(scrollEl);
108
107
  if (scrollAreaRef.current) observer.observe(scrollAreaRef.current);
109
108
  if (scrollbarRef.current) observer.observe(scrollbarRef.current);
109
+ // 컨테이너는 maxHeight 로 고정되므로 콘텐츠 루트까지 관찰해야 행 추가가 통지된다.
110
+ const contentEl = scrollEl.querySelector('.ncua-table__table');
111
+ if (contentEl) observer.observe(contentEl);
110
112
  update();
111
113
  return () => {
112
114
  scrollEl.removeEventListener('scroll', update);
113
115
  observer.disconnect();
116
+ controller.abort();
117
+ releaseRef.current = null;
118
+ releaseMarkers(scrollEl);
114
119
  };
115
120
  }, [enabled, layoutKey, scrollContainerRef, scrollAreaRef, scrollbarRef, thumbRef]);
116
121
  const handleThumbMouseDown = e => {
@@ -118,11 +123,13 @@ const useTableVerticalScrollbar = _ref => {
118
123
  const thumbEl = thumbRef.current;
119
124
  const areaEl = scrollAreaRef.current;
120
125
  if (!scrollEl || !thumbEl) return;
121
- startDrag(e, {
126
+ (0, _drag.startThumbDrag)(e, {
122
127
  axis: 'y',
123
128
  scrollEl,
124
129
  thumbEl,
125
- draggingTarget: areaEl ?? scrollEl
130
+ draggingTarget: areaEl ?? scrollEl,
131
+ measureDragBasisSize: clientSize => clientSize,
132
+ releaseSignal: releaseRef.current?.signal
126
133
  });
127
134
  };
128
135
  return {
@@ -137,51 +144,73 @@ const useTableHorizontalScrollbar = _ref2 => {
137
144
  hScrollbarRef,
138
145
  hThumbRef
139
146
  } = _ref2;
140
- (0, _react.useEffect)(() => {
147
+ const releaseRef = (0, _react.useRef)(null);
148
+ (0, _useIsomorphicLayoutEffect.useIsomorphicLayoutEffect)(() => {
141
149
  if (!enabled) return;
142
150
  const hScrollEl = hScrollContainerRef.current;
143
151
  const hScrollbarEl = hScrollbarRef.current;
144
152
  const hThumbEl = hThumbRef.current;
145
153
  if (!hScrollEl || !hScrollbarEl || !hThumbEl) return;
154
+ if (!claimMarkers(hScrollEl)) return;
155
+ const controller = new AbortController();
156
+ releaseRef.current = controller;
146
157
  const update = () => {
147
158
  const {
148
159
  scrollLeft,
149
160
  scrollWidth,
150
161
  clientWidth
151
162
  } = hScrollEl;
152
- if (scrollWidth <= clientWidth) {
163
+ const box = (0, _geometry.computeHorizontalTrackBox)({
164
+ clientWidth,
165
+ scrollLeft,
166
+ sideGap: _const.H_SCROLLBAR_SIDE_GAP
167
+ });
168
+ const geometry = (0, _geometry.computeThumbGeometry)({
169
+ scrollOffset: scrollLeft,
170
+ scrollSize: scrollWidth,
171
+ clientSize: clientWidth,
172
+ trackSize: box.width,
173
+ thumbMinSize: _const.H_SCROLLBAR_THUMB_MIN_WIDTH
174
+ });
175
+ if (!geometry.visible) {
153
176
  hThumbEl.style.width = '0';
154
177
  return;
155
178
  }
156
- // transform으로 스크롤 오프셋 보정 — reflow 없이 compositor-only 이동
157
- hScrollbarEl.style.transform = `translateX(${scrollLeft}px)`;
158
- hScrollbarEl.style.width = `${clientWidth - H_SCROLLBAR_SIDE_GAP * 2}px`;
159
- const trackWidth = clientWidth - H_SCROLLBAR_SIDE_GAP * 2;
160
- const thumbWidth = Math.max(H_SCROLLBAR_THUMB_MIN_WIDTH, clientWidth / scrollWidth * trackWidth);
161
- const thumbLeft = scrollLeft / (scrollWidth - clientWidth) * (trackWidth - thumbWidth);
162
- hThumbEl.style.width = `${thumbWidth}px`;
163
- hThumbEl.style.transform = `translateX(${thumbLeft}px)`;
179
+ hScrollbarEl.style.transform = `translateX(${box.translateX}px)`;
180
+ hScrollbarEl.style.width = `${box.width}px`;
181
+ hThumbEl.style.width = `${geometry.size}px`;
182
+ hThumbEl.style.transform = `translateX(${geometry.offset}px)`;
164
183
  };
165
184
  hScrollEl.addEventListener('scroll', update, {
166
185
  passive: true
167
186
  });
168
187
  const ro = new ResizeObserver(update);
169
188
  ro.observe(hScrollEl);
189
+ // 컨테이너 폭이 그대로인 채 콘텐츠 폭만 변하는 경우를 포착한다.
190
+ const innerEl = hScrollEl.querySelector('.ncua-table__h-scroll-inner');
191
+ if (innerEl) ro.observe(innerEl);
192
+ const contentEl = hScrollEl.querySelector('.ncua-table__table');
193
+ if (contentEl) ro.observe(contentEl);
170
194
  update();
171
195
  return () => {
172
196
  hScrollEl.removeEventListener('scroll', update);
173
197
  ro.disconnect();
198
+ controller.abort();
199
+ releaseRef.current = null;
200
+ releaseMarkers(hScrollEl);
174
201
  };
175
202
  }, [enabled, hScrollContainerRef, hScrollbarRef, hThumbRef]);
176
203
  const handleHThumbMouseDown = e => {
177
204
  const hScrollEl = hScrollContainerRef.current;
178
205
  const hThumbEl = hThumbRef.current;
179
206
  if (!hScrollEl || !hThumbEl) return;
180
- startDrag(e, {
207
+ (0, _drag.startThumbDrag)(e, {
181
208
  axis: 'x',
182
209
  scrollEl: hScrollEl,
183
210
  thumbEl: hThumbEl,
184
- sideGap: H_SCROLLBAR_SIDE_GAP
211
+ draggingTarget: hScrollEl,
212
+ measureDragBasisSize: clientSize => clientSize - _const.H_SCROLLBAR_SIDE_GAP * 2,
213
+ releaseSignal: releaseRef.current?.signal
185
214
  });
186
215
  };
187
216
  return {