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

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 (58) 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/components/feedback-and-status/badge/Badge.js +1 -1
  10. package/dist/cjs/src/components/overlays/modal/Modal.js +46 -41
  11. package/dist/cjs/src/components/overlays/postcode-search-modal/PostcodeSearchModal.js +2 -2
  12. package/dist/cjs/src/generated/scoped-css.js +1 -1
  13. package/dist/esm/assets/scripts/table/scrollbar/const.js +44 -0
  14. package/dist/esm/assets/scripts/table/scrollbar/drag.js +67 -0
  15. package/dist/esm/assets/scripts/table/scrollbar/geometry.js +60 -0
  16. package/dist/esm/assets/scripts/table/scrollbar/isolate.js +27 -0
  17. package/dist/esm/src/components/data-display/table/Table.js +1 -0
  18. package/dist/esm/src/components/data-display/table/__tests__/useTableScrollbars.characterisation.test.js +387 -0
  19. package/dist/esm/src/components/data-display/table/__tests__/useTableScrollbars.regression.test.js +336 -0
  20. package/dist/esm/src/components/data-display/table/useTableScrollbars.js +85 -86
  21. package/dist/esm/src/components/feedback-and-status/badge/Badge.js +1 -1
  22. package/dist/esm/src/components/overlays/modal/Modal.js +47 -42
  23. package/dist/esm/src/components/overlays/postcode-search-modal/PostcodeSearchModal.js +2 -2
  24. package/dist/esm/src/generated/scoped-css.js +1 -1
  25. package/dist/temp/assets/scripts/table/scrollbar/const.d.ts +46 -0
  26. package/dist/temp/assets/scripts/table/scrollbar/const.js +44 -0
  27. package/dist/temp/assets/scripts/table/scrollbar/drag.d.ts +18 -0
  28. package/dist/temp/assets/scripts/table/scrollbar/drag.js +49 -0
  29. package/dist/temp/assets/scripts/table/scrollbar/geometry.d.ts +47 -0
  30. package/dist/temp/assets/scripts/table/scrollbar/geometry.js +37 -0
  31. package/dist/temp/assets/scripts/table/scrollbar/isolate.d.ts +4 -0
  32. package/dist/temp/assets/scripts/table/scrollbar/isolate.js +30 -0
  33. package/dist/temp/src/components/data-display/table/Table.js +1 -0
  34. package/dist/temp/src/components/data-display/table/__tests__/useTableScrollbars.characterisation.test.d.ts +1 -0
  35. package/dist/temp/src/components/data-display/table/__tests__/useTableScrollbars.characterisation.test.js +282 -0
  36. package/dist/temp/src/components/data-display/table/__tests__/useTableScrollbars.regression.test.d.ts +1 -0
  37. package/dist/temp/src/components/data-display/table/__tests__/useTableScrollbars.regression.test.js +244 -0
  38. package/dist/temp/src/components/data-display/table/useTableScrollbars.d.ts +2 -6
  39. package/dist/temp/src/components/data-display/table/useTableScrollbars.js +100 -72
  40. package/dist/temp/src/components/feedback-and-status/badge/Badge.d.ts +1 -1
  41. package/dist/temp/src/components/feedback-and-status/badge/Badge.js +1 -1
  42. package/dist/temp/src/components/overlays/modal/Modal.d.ts +5 -5
  43. package/dist/temp/src/components/overlays/modal/Modal.js +11 -14
  44. package/dist/temp/src/components/overlays/postcode-search-modal/PostcodeSearchModal.js +1 -1
  45. package/dist/temp/src/generated/scoped-css.js +1 -1
  46. package/dist/temp/src/hooks/useIsomorphicLayoutEffect.d.ts +2 -2
  47. package/dist/types/assets/scripts/table/scrollbar/const.d.ts +46 -0
  48. package/dist/types/assets/scripts/table/scrollbar/drag.d.ts +18 -0
  49. package/dist/types/assets/scripts/table/scrollbar/geometry.d.ts +47 -0
  50. package/dist/types/assets/scripts/table/scrollbar/isolate.d.ts +4 -0
  51. package/dist/types/src/components/data-display/table/__tests__/useTableScrollbars.characterisation.test.d.ts +1 -0
  52. package/dist/types/src/components/data-display/table/__tests__/useTableScrollbars.regression.test.d.ts +1 -0
  53. package/dist/types/src/components/data-display/table/useTableScrollbars.d.ts +2 -6
  54. package/dist/types/src/components/feedback-and-status/badge/Badge.d.ts +1 -1
  55. package/dist/types/src/components/overlays/modal/Modal.d.ts +5 -5
  56. package/dist/types/src/hooks/useIsomorphicLayoutEffect.d.ts +2 -2
  57. package/dist/ui-admin/assets/styles/style.css +57 -28
  58. package/package.json +1 -1
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.TABLE_HEADER_HEIGHT = exports.SCROLLBAR_TRACK_OFFSET = exports.SCROLLBAR_THUMB_MIN_HEIGHT = exports.SCROLLBAR_OWNER_VALUES = exports.SCROLLBAR_OWNERS = exports.SCROLLBAR_CLASS_NAMES = exports.SCROLLBAR_ATTRS = exports.OPT_OUT_VALUE = exports.LOG_PREFIX = exports.H_SCROLLBAR_THUMB_MIN_WIDTH = exports.H_SCROLLBAR_SIDE_GAP = exports.DEFAULT_AXIS = exports.AXIS_VALUES = void 0;
7
+ /** Table 커스텀 스크롤바의 클래스명·속성명·수치 상수. React 와 CDN 이 함께 참조한다. */
8
+ /** `_table.scss` 의 `--ncua-table-header-height` 와 같은 값 */
9
+ const TABLE_HEADER_HEIGHT = exports.TABLE_HEADER_HEIGHT = 40;
10
+ /** `.ncua-table__scrollbar-thumb { height }` 와 같은 값 */
11
+ const SCROLLBAR_THUMB_MIN_HEIGHT = exports.SCROLLBAR_THUMB_MIN_HEIGHT = 40;
12
+ /** `.ncua-table__h-scrollbar-thumb { width }` 와 같은 값 */
13
+ const H_SCROLLBAR_THUMB_MIN_WIDTH = exports.H_SCROLLBAR_THUMB_MIN_WIDTH = 40;
14
+ /** `.ncua-table__h-scrollbar { left/right: var(--spacing-s) }` 와 같은 값 */
15
+ const H_SCROLLBAR_SIDE_GAP = exports.H_SCROLLBAR_SIDE_GAP = 8;
16
+ /** 세로 트랙 상하 여백 합계. 헤더 회피분은 `TABLE_HEADER_HEIGHT` 로 따로 뺀다 */
17
+ const SCROLLBAR_TRACK_OFFSET = exports.SCROLLBAR_TRACK_OFFSET = 16;
18
+ const SCROLLBAR_CLASS_NAMES = exports.SCROLLBAR_CLASS_NAMES = {
19
+ table: 'ncua-table',
20
+ tableWrapper: 'ncua-table-wrapper',
21
+ dataGrid: 'ncua-data-grid',
22
+ scrollArea: 'ncua-table__scroll-area',
23
+ scrollContainer: 'ncua-table__scroll-container',
24
+ scrollbar: 'ncua-table__scrollbar',
25
+ scrollbarThumb: 'ncua-table__scrollbar-thumb',
26
+ hScrollContainer: 'ncua-table__h-scroll-container',
27
+ hScrollInner: 'ncua-table__h-scroll-inner',
28
+ hScrollbar: 'ncua-table__h-scrollbar',
29
+ hScrollbarThumb: 'ncua-table__h-scrollbar-thumb',
30
+ tableElement: 'ncua-table__table'
31
+ };
32
+ /** `optOut` 을 제외한 전부가 런타임에만 부착된다. 마크업에 직접 기재하지 않는다. */
33
+ const SCROLLBAR_ATTRS = exports.SCROLLBAR_ATTRS = {
34
+ ready: 'data-scrollbar-ready',
35
+ /** `ready` 와 항상 함께 부착·제거한다 */
36
+ owner: 'data-scrollbar-owner',
37
+ generated: 'data-scrollbar-generated',
38
+ fallbackArmed: 'data-ncua-scrollbar-fallback',
39
+ dragging: 'data-dragging',
40
+ optOut: 'data-ncua-scrollbar'
41
+ };
42
+ const SCROLLBAR_OWNERS = exports.SCROLLBAR_OWNERS = {
43
+ cdn: 'cdn',
44
+ react: 'react'
45
+ };
46
+ const SCROLLBAR_OWNER_VALUES = exports.SCROLLBAR_OWNER_VALUES = Object.values(SCROLLBAR_OWNERS);
47
+ const OPT_OUT_VALUE = exports.OPT_OUT_VALUE = 'off';
48
+ const AXIS_VALUES = exports.AXIS_VALUES = ['both', 'x', 'y'];
49
+ const DEFAULT_AXIS = exports.DEFAULT_AXIS = 'both';
50
+ const LOG_PREFIX = exports.LOG_PREFIX = '[ncua.TableScrollbar]';
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.startThumbDrag = void 0;
7
+ var _const = require("./const");
8
+ var _geometry = require("./geometry");
9
+ var _isolate = require("./isolate");
10
+ /**
11
+ * 썸 드래그 와이어링. React 훅과 CDN 바인딩이 공유한다.
12
+ * 모듈 평가 시점에는 `document`·`window` 를 참조하지 않는다 — SSR 경로에서 함께 평가된다.
13
+ */
14
+
15
+ const startThumbDrag = (event, input) => {
16
+ const {
17
+ axis,
18
+ scrollEl,
19
+ thumbEl,
20
+ draggingTarget,
21
+ measureDragBasisSize,
22
+ releaseSignal
23
+ } = input;
24
+ event.preventDefault();
25
+ const isVertical = axis === 'y';
26
+ // 측정을 속성 부착보다 앞에 둔다. 부착이 스타일을 무효화한 뒤 읽으면 강제 재계산이 1회 붙는다.
27
+ const startPointer = isVertical ? event.clientY : event.clientX;
28
+ const startOffset = isVertical ? scrollEl.scrollTop : scrollEl.scrollLeft;
29
+ const scrollSize = isVertical ? scrollEl.scrollHeight : scrollEl.scrollWidth;
30
+ const clientSize = isVertical ? scrollEl.clientHeight : scrollEl.clientWidth;
31
+ const thumbSize = isVertical ? thumbEl.offsetHeight : thumbEl.offsetWidth;
32
+ const dragBasisSize = measureDragBasisSize(clientSize);
33
+ draggingTarget.setAttribute(_const.SCROLLBAR_ATTRS.dragging, '');
34
+ const dragController = new AbortController();
35
+ const dragSignal = dragController.signal;
36
+ // 리스너 해제를 먼저 끝낸다. 뒤 문장이 실패해도 드래그가 계속 붙어 있지 않게 한다.
37
+ const endDrag = (0, _isolate.isolate)('드래그 종료', () => {
38
+ dragController.abort();
39
+ draggingTarget.removeAttribute(_const.SCROLLBAR_ATTRS.dragging);
40
+ });
41
+ const onMove = (0, _isolate.isolate)('드래그 이동', moveEvent => {
42
+ const delta = (0, _geometry.computeDragScrollDelta)({
43
+ pointerDelta: (isVertical ? moveEvent.clientY : moveEvent.clientX) - startPointer,
44
+ scrollSize,
45
+ clientSize,
46
+ dragBasisSize,
47
+ thumbSize
48
+ });
49
+ if (isVertical) {
50
+ scrollEl.scrollTop = startOffset + delta;
51
+ return;
52
+ }
53
+ scrollEl.scrollLeft = startOffset + delta;
54
+ });
55
+ document.addEventListener('mousemove', onMove, {
56
+ signal: dragSignal
57
+ });
58
+ document.addEventListener('mouseup', endDrag, {
59
+ signal: dragSignal
60
+ });
61
+ // 창 밖 릴리즈·탭 전환으로 mouseup 을 놓치는 경우가 상시 발생한다.
62
+ document.addEventListener('pointercancel', endDrag, {
63
+ signal: dragSignal
64
+ });
65
+ window.addEventListener('blur', endDrag, {
66
+ signal: dragSignal
67
+ });
68
+ // `dragSignal` 을 함께 넘기지 않으면 정상 종료한 드래그의 리스너가 해제 신호에 남아
69
+ // 반복할수록 누적된다.
70
+ releaseSignal?.addEventListener('abort', endDrag, {
71
+ once: true,
72
+ signal: dragSignal
73
+ });
74
+ };
75
+ exports.startThumbDrag = startThumbDrag;
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.computeThumbGeometry = exports.computeHorizontalTrackBox = exports.computeDragScrollDelta = exports.DRAG_BASIS = void 0;
7
+ /**
8
+ * 썸 치수·위치 산출. React 훅과 CDN 바인딩이 공유한다.
9
+ * SSR 경로에서도 평가되므로 `document`·`window` 를 참조하지 않는다.
10
+ */
11
+ const HIDDEN = {
12
+ visible: false,
13
+ size: 0,
14
+ offset: 0
15
+ };
16
+ const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
17
+ const computeThumbGeometry = metrics => {
18
+ const {
19
+ scrollOffset,
20
+ scrollSize,
21
+ clientSize,
22
+ trackSize,
23
+ thumbMinSize
24
+ } = metrics;
25
+ if (!(scrollSize > clientSize)) return HIDDEN;
26
+ if (!(trackSize > 0)) return HIDDEN;
27
+ const size = Math.min(trackSize, Math.max(thumbMinSize, clientSize / scrollSize * trackSize));
28
+ const travel = trackSize - size;
29
+ if (travel <= 0) return {
30
+ visible: true,
31
+ size,
32
+ offset: 0
33
+ };
34
+ const offset = clamp(scrollOffset / (scrollSize - clientSize) * travel, 0, travel);
35
+ return {
36
+ visible: true,
37
+ size,
38
+ offset
39
+ };
40
+ };
41
+ /** 호출자가 넘겨야 하는 축별 `dragBasisSize` 기준 */
42
+ exports.computeThumbGeometry = computeThumbGeometry;
43
+ const DRAG_BASIS = exports.DRAG_BASIS = {
44
+ y: 'clientSize',
45
+ x: 'trackSize'
46
+ };
47
+ /**
48
+ * 포인터 이동량(px) → 스크롤 오프셋 변화량(px).
49
+ * 환산 분모 기준이 축마다 다르므로 `dragBasisSize` 는 호출자가 `DRAG_BASIS` 대로 넘긴다.
50
+ */
51
+ const computeDragScrollDelta = input => {
52
+ const {
53
+ pointerDelta,
54
+ scrollSize,
55
+ clientSize,
56
+ dragBasisSize,
57
+ thumbSize
58
+ } = input;
59
+ const denominator = dragBasisSize - thumbSize;
60
+ if (denominator <= 0) return 0;
61
+ return pointerDelta * ((scrollSize - clientSize) / denominator);
62
+ };
63
+ /** 가로 트랙은 스크롤 컨테이너 안에 있어 함께 밀리므로, 되돌릴 오프셋과 폭을 산출한다. */
64
+ exports.computeDragScrollDelta = computeDragScrollDelta;
65
+ const computeHorizontalTrackBox = input => ({
66
+ translateX: input.scrollLeft,
67
+ width: Math.max(0, input.clientWidth - input.sideGap * 2)
68
+ });
69
+ exports.computeHorizontalTrackBox = computeHorizontalTrackBox;
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.runIsolated = exports.isolate = void 0;
7
+ var _const = require("./const");
8
+ /**
9
+ * 라이브러리가 스스로 부르는 콜백(리스너·옵저버·정리 루프)을 감싼다.
10
+ * 소비처 페이지로 예외가 새면 그 페이지의 전역 오류 처리기가 발화하므로 여기서 가둔다.
11
+ * 생성자처럼 호출자가 결과를 판정해야 하는 경로에는 쓰지 않는다.
12
+ */
13
+
14
+ /** 스크롤·리사이즈는 초당 수십 회 불리므로 같은 지점의 실패는 1회만 기록한다 */
15
+ const isolate = (label, run) => {
16
+ let reported = false;
17
+ return function () {
18
+ try {
19
+ run(...arguments);
20
+ } catch (error) {
21
+ if (reported) return;
22
+ reported = true;
23
+ console.error(`${_const.LOG_PREFIX} ${label} 중 오류가 발생했습니다. 이후 같은 오류는 기록을 생략합니다.`, error);
24
+ }
25
+ };
26
+ };
27
+ /** 1회성 호출용. 실패해도 나머지 정리·순회가 이어지도록 한다 */
28
+ exports.isolate = isolate;
29
+ const runIsolated = (label, run) => {
30
+ try {
31
+ run();
32
+ } catch (error) {
33
+ console.error(`${_const.LOG_PREFIX} ${label} 중 오류가 발생했습니다.`, error);
34
+ }
35
+ };
36
+ exports.runIsolated = runIsolated;
@@ -496,6 +496,7 @@ const TableComponent = /*#__PURE__*/(0, _react.forwardRef)((_ref17, ref) => {
496
496
  footerContent,
497
497
  paginationContent
498
498
  } = sortChildren(children);
499
+ // maxHeight 를 본문 영역 높이로 해석해 헤더 높이를 가산한다.
499
500
  const scrollStyle = fixedHeader && maxHeight ? {
500
501
  maxHeight: typeof maxHeight === 'number' ? `${maxHeight + _useTableScrollbars.TABLE_HEADER_HEIGHT}px` : `calc(${maxHeight} + ${_useTableScrollbars.TABLE_HEADER_HEIGHT}px)`
501
502
  } : undefined;
@@ -0,0 +1,390 @@
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 _useTableScrollbars = require("../useTableScrollbars");
8
+ // @vitest-environment jsdom
9
+ /**
10
+ * 특성화 테스트. 기대값은 "이래야 한다"가 아니라 "현재 이렇게 동작한다"이며,
11
+ * 산출 로직을 CDN 과 공유한 뒤에도 같은 값이 나오는지 판정하는 기준선이다.
12
+ */
13
+
14
+ const observers = [];
15
+ class ResizeObserverStub {
16
+ constructor(callback) {
17
+ this.record = {
18
+ callback,
19
+ targets: [],
20
+ disconnected: false
21
+ };
22
+ observers.push(this.record);
23
+ }
24
+ observe(target) {
25
+ this.record.targets.push(target);
26
+ }
27
+ unobserve() {
28
+ // 전체 해제만 검증 대상이다.
29
+ }
30
+ disconnect() {
31
+ this.record.disconnected = true;
32
+ }
33
+ }
34
+ /** jsdom 은 레이아웃을 계산하지 않으므로 측정 속성을 직접 정의한다. */
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 render = element => {
47
+ (0, _testUtils.act)(() => {
48
+ root.render(element);
49
+ });
50
+ };
51
+ const VerticalHarness = _ref => {
52
+ let {
53
+ enabled = true
54
+ } = _ref;
55
+ const scrollContainerRef = (0, _react.useRef)(null);
56
+ const scrollAreaRef = (0, _react.useRef)(null);
57
+ const scrollbarRef = (0, _react.useRef)(null);
58
+ const thumbRef = (0, _react.useRef)(null);
59
+ const {
60
+ handleThumbMouseDown
61
+ } = (0, _useTableScrollbars.useTableVerticalScrollbar)({
62
+ enabled,
63
+ layoutKey: false,
64
+ scrollContainerRef,
65
+ scrollAreaRef,
66
+ scrollbarRef,
67
+ thumbRef
68
+ });
69
+ return /*#__PURE__*/(0, _react.createElement)('div', {
70
+ className: 'ncua-table ncua-table--horizontal ncua-table--fixed-header'
71
+ }, /*#__PURE__*/(0, _react.createElement)('div', {
72
+ ref: scrollAreaRef,
73
+ className: 'ncua-table__scroll-area'
74
+ }, /*#__PURE__*/(0, _react.createElement)('div', {
75
+ ref: scrollContainerRef,
76
+ className: 'ncua-table__scroll-container'
77
+ }, /*#__PURE__*/(0, _react.createElement)('table', {
78
+ className: 'ncua-table__table'
79
+ }, /*#__PURE__*/(0, _react.createElement)('tbody', null))), /*#__PURE__*/(0, _react.createElement)('div', {
80
+ ref: scrollbarRef,
81
+ className: 'ncua-table__scrollbar',
82
+ 'aria-hidden': true
83
+ }, /*#__PURE__*/(0, _react.createElement)('div', {
84
+ ref: thumbRef,
85
+ className: 'ncua-table__scrollbar-thumb',
86
+ onMouseDown: handleThumbMouseDown
87
+ }))));
88
+ };
89
+ const HorizontalHarness = _ref2 => {
90
+ let {
91
+ enabled = true
92
+ } = _ref2;
93
+ const hScrollContainerRef = (0, _react.useRef)(null);
94
+ const hScrollbarRef = (0, _react.useRef)(null);
95
+ const hThumbRef = (0, _react.useRef)(null);
96
+ const {
97
+ handleHThumbMouseDown
98
+ } = (0, _useTableScrollbars.useTableHorizontalScrollbar)({
99
+ enabled,
100
+ hScrollContainerRef,
101
+ hScrollbarRef,
102
+ hThumbRef
103
+ });
104
+ return /*#__PURE__*/(0, _react.createElement)('div', {
105
+ className: 'ncua-table ncua-table--horizontal'
106
+ }, /*#__PURE__*/(0, _react.createElement)('div', {
107
+ ref: hScrollContainerRef,
108
+ className: 'ncua-table__h-scroll-container'
109
+ }, /*#__PURE__*/(0, _react.createElement)('div', {
110
+ className: 'ncua-table__h-scroll-inner'
111
+ }, /*#__PURE__*/(0, _react.createElement)('table', {
112
+ className: 'ncua-table__table'
113
+ }, /*#__PURE__*/(0, _react.createElement)('tbody', null))), /*#__PURE__*/(0, _react.createElement)('div', {
114
+ ref: hScrollbarRef,
115
+ className: 'ncua-table__h-scrollbar',
116
+ 'aria-hidden': true
117
+ }, /*#__PURE__*/(0, _react.createElement)('div', {
118
+ ref: hThumbRef,
119
+ className: 'ncua-table__h-scrollbar-thumb',
120
+ onMouseDown: handleHThumbMouseDown
121
+ }))));
122
+ };
123
+ const query = selector => {
124
+ const found = container.querySelector(selector);
125
+ if (!found) throw new Error(`선택자에 해당하는 요소가 없습니다: ${selector}`);
126
+ return found;
127
+ };
128
+ const fireScroll = element => {
129
+ (0, _testUtils.act)(() => {
130
+ element.dispatchEvent(new Event('scroll'));
131
+ });
132
+ };
133
+ (0, _vitest.beforeEach)(() => {
134
+ _vitest.vi.stubGlobal('ResizeObserver', ResizeObserverStub);
135
+ observers.length = 0;
136
+ container = document.createElement('div');
137
+ document.body.appendChild(container);
138
+ (0, _testUtils.act)(() => {
139
+ root = (0, _client.createRoot)(container);
140
+ });
141
+ });
142
+ (0, _vitest.afterEach)(() => {
143
+ (0, _testUtils.act)(() => {
144
+ root.unmount();
145
+ });
146
+ container.remove();
147
+ _vitest.vi.unstubAllGlobals();
148
+ _vitest.vi.restoreAllMocks();
149
+ });
150
+ /**
151
+ * 가상 레이아웃 수치. 기대값은 구현 상수를 쓰지 않고 리터럴로 적는다 —
152
+ * 상수를 참조하면 값이 바뀔 때 기대값도 함께 움직여 수치 회귀를 놓친다.
153
+ */
154
+ const LAYOUT = {
155
+ viewportHeight: 200,
156
+ contentHeight: 800,
157
+ shortContentHeight: 400,
158
+ trackHeight: 150,
159
+ thumbHeight: 40,
160
+ scrollTop: 300,
161
+ areaHeight: 240,
162
+ viewportWidth: 600,
163
+ contentWidth: 1500,
164
+ thumbWidth: 234,
165
+ scrollLeft: 120,
166
+ pointerDelta: 10
167
+ };
168
+ /** toBeCloseTo 자릿수 */
169
+ const PRECISION = 5;
170
+ /** (1500 - 600) / (584 - 234) 비율로 10px 을 끌었을 때의 scrollLeft */
171
+ const EXPECTED_HORIZONTAL_SCROLL_LEFT = 25.714285;
172
+ (0, _vitest.describe)('useTableVerticalScrollbar', () => {
173
+ const setUpOverflow = () => {
174
+ render(/*#__PURE__*/(0, _react.createElement)(VerticalHarness));
175
+ const scrollEl = query('.ncua-table__scroll-container');
176
+ const thumbEl = query('.ncua-table__scrollbar-thumb');
177
+ const trackEl = query('.ncua-table__scrollbar');
178
+ defineMetrics(scrollEl, {
179
+ clientHeight: LAYOUT.viewportHeight,
180
+ scrollHeight: LAYOUT.contentHeight,
181
+ scrollTop: 0
182
+ });
183
+ defineMetrics(trackEl, {
184
+ clientHeight: LAYOUT.trackHeight
185
+ });
186
+ return {
187
+ scrollEl,
188
+ thumbEl
189
+ };
190
+ };
191
+ (0, _vitest.it)('오버플로 발생 시 트랙 길이 기준으로 썸 높이를 산출한다', () => {
192
+ const {
193
+ scrollEl,
194
+ thumbEl
195
+ } = setUpOverflow();
196
+ fireScroll(scrollEl);
197
+ // max(40, (200 / 800) * 150) = max(40, 37.5) = 40
198
+ (0, _vitest.expect)(thumbEl.style.height).toBe('40px');
199
+ (0, _vitest.expect)(thumbEl.style.transform).toBe('translateY(0px)');
200
+ });
201
+ (0, _vitest.it)('스크롤 오프셋에 비례해 썸을 이동시킨다', () => {
202
+ const {
203
+ scrollEl,
204
+ thumbEl
205
+ } = setUpOverflow();
206
+ defineMetrics(scrollEl, {
207
+ scrollTop: LAYOUT.scrollTop
208
+ });
209
+ fireScroll(scrollEl);
210
+ // (300 / (800 - 200)) * (150 - 40) = 0.5 * 110 = 55
211
+ (0, _vitest.expect)(thumbEl.style.transform).toBe('translateY(55px)');
212
+ });
213
+ (0, _vitest.it)('트랙 clientHeight 가 0 이면 scroll-area 높이에서 헤더·여백을 차감한 값을 트랙 길이로 쓴다', () => {
214
+ render(/*#__PURE__*/(0, _react.createElement)(VerticalHarness));
215
+ const scrollEl = query('.ncua-table__scroll-container');
216
+ const thumbEl = query('.ncua-table__scrollbar-thumb');
217
+ const areaEl = query('.ncua-table__scroll-area');
218
+ defineMetrics(scrollEl, {
219
+ clientHeight: LAYOUT.viewportHeight,
220
+ scrollHeight: LAYOUT.shortContentHeight,
221
+ scrollTop: 0
222
+ });
223
+ defineMetrics(query('.ncua-table__scrollbar'), {
224
+ clientHeight: 0
225
+ });
226
+ defineMetrics(areaEl, {
227
+ clientHeight: LAYOUT.areaHeight
228
+ });
229
+ fireScroll(scrollEl);
230
+ // trackHeight = 240 - 40(헤더) - 16(상하 여백) = 184 → max(40, (200/400) * 184) = 92
231
+ (0, _vitest.expect)(thumbEl.style.height).toBe('92px');
232
+ });
233
+ (0, _vitest.it)('오버플로가 없으면 썸 높이를 0 으로 둔다', () => {
234
+ render(/*#__PURE__*/(0, _react.createElement)(VerticalHarness));
235
+ const scrollEl = query('.ncua-table__scroll-container');
236
+ const thumbEl = query('.ncua-table__scrollbar-thumb');
237
+ defineMetrics(scrollEl, {
238
+ clientHeight: LAYOUT.shortContentHeight,
239
+ scrollHeight: LAYOUT.shortContentHeight,
240
+ scrollTop: 0
241
+ });
242
+ fireScroll(scrollEl);
243
+ (0, _vitest.expect)(thumbEl.style.height).toBe('0px');
244
+ });
245
+ (0, _vitest.it)('썸 드래그는 (scrollHeight − clientHeight) / (clientHeight − thumbHeight) 비율로 환산한다', () => {
246
+ const {
247
+ scrollEl,
248
+ thumbEl
249
+ } = setUpOverflow();
250
+ defineMetrics(thumbEl, {
251
+ offsetHeight: 40
252
+ });
253
+ const areaEl = query('.ncua-table__scroll-area');
254
+ (0, _testUtils.act)(() => {
255
+ thumbEl.dispatchEvent(new MouseEvent('mousedown', {
256
+ bubbles: true,
257
+ clientY: 100
258
+ }));
259
+ });
260
+ (0, _vitest.expect)(areaEl.hasAttribute('data-dragging')).toBe(true);
261
+ (0, _testUtils.act)(() => {
262
+ document.dispatchEvent(new MouseEvent('mousemove', {
263
+ clientY: 110
264
+ }));
265
+ });
266
+ // ratio = (800 - 200) / (200 - 40) = 3.75 → 10px 이동 = 37.5
267
+ const verticalRatio = (LAYOUT.contentHeight - LAYOUT.viewportHeight) / (LAYOUT.viewportHeight - LAYOUT.thumbHeight);
268
+ (0, _vitest.expect)(scrollEl.scrollTop).toBe(LAYOUT.pointerDelta * verticalRatio);
269
+ (0, _testUtils.act)(() => {
270
+ document.dispatchEvent(new MouseEvent('mouseup'));
271
+ });
272
+ (0, _vitest.expect)(areaEl.hasAttribute('data-dragging')).toBe(false);
273
+ });
274
+ (0, _vitest.it)('mouseup 이후에는 포인터 이동이 스크롤에 반영되지 않는다', () => {
275
+ const {
276
+ scrollEl,
277
+ thumbEl
278
+ } = setUpOverflow();
279
+ defineMetrics(thumbEl, {
280
+ offsetHeight: 40
281
+ });
282
+ (0, _testUtils.act)(() => {
283
+ thumbEl.dispatchEvent(new MouseEvent('mousedown', {
284
+ bubbles: true,
285
+ clientY: 100
286
+ }));
287
+ document.dispatchEvent(new MouseEvent('mouseup'));
288
+ document.dispatchEvent(new MouseEvent('mousemove', {
289
+ clientY: 400
290
+ }));
291
+ });
292
+ (0, _vitest.expect)(scrollEl.scrollTop).toBe(0);
293
+ });
294
+ (0, _vitest.it)('enabled 가 false 이면 바인딩하지 않는다', () => {
295
+ render(/*#__PURE__*/(0, _react.createElement)(VerticalHarness, {
296
+ enabled: false
297
+ }));
298
+ (0, _vitest.expect)(observers).toHaveLength(0);
299
+ });
300
+ (0, _vitest.it)('언마운트 시 ResizeObserver 를 해제한다', () => {
301
+ render(/*#__PURE__*/(0, _react.createElement)(VerticalHarness));
302
+ (0, _testUtils.act)(() => {
303
+ root.unmount();
304
+ });
305
+ (0, _vitest.expect)(observers.every(observer => observer.disconnected)).toBe(true);
306
+ (0, _testUtils.act)(() => {
307
+ root = (0, _client.createRoot)(container);
308
+ });
309
+ });
310
+ });
311
+ (0, _vitest.describe)('useTableHorizontalScrollbar', () => {
312
+ const setUpOverflow = () => {
313
+ render(/*#__PURE__*/(0, _react.createElement)(HorizontalHarness));
314
+ const hScrollEl = query('.ncua-table__h-scroll-container');
315
+ const hThumbEl = query('.ncua-table__h-scrollbar-thumb');
316
+ const hTrackEl = query('.ncua-table__h-scrollbar');
317
+ defineMetrics(hScrollEl, {
318
+ clientWidth: LAYOUT.viewportWidth,
319
+ scrollWidth: LAYOUT.contentWidth,
320
+ scrollLeft: 0
321
+ });
322
+ return {
323
+ hScrollEl,
324
+ hThumbEl,
325
+ hTrackEl
326
+ };
327
+ };
328
+ (0, _vitest.it)('트랙 wrapper 를 스크롤 오프셋만큼 보정하고 좌우 여백을 차감한 폭으로 맞춘다', () => {
329
+ const {
330
+ hScrollEl,
331
+ hTrackEl
332
+ } = setUpOverflow();
333
+ defineMetrics(hScrollEl, {
334
+ scrollLeft: LAYOUT.scrollLeft
335
+ });
336
+ fireScroll(hScrollEl);
337
+ (0, _vitest.expect)(hTrackEl.style.transform).toBe('translateX(120px)');
338
+ // 600 - 8(좌) - 8(우) = 584
339
+ (0, _vitest.expect)(hTrackEl.style.width).toBe('584px');
340
+ });
341
+ (0, _vitest.it)('썸 폭은 트랙 유효 길이 기준으로 산출한다', () => {
342
+ const {
343
+ hScrollEl,
344
+ hThumbEl
345
+ } = setUpOverflow();
346
+ fireScroll(hScrollEl);
347
+ // trackWidth = 600 - 16 = 584 → max(40, (600 / 1500) * 584) = 233.6
348
+ (0, _vitest.expect)(hThumbEl.style.width).toBe('233.60000000000002px');
349
+ (0, _vitest.expect)(hThumbEl.style.transform).toBe('translateX(0px)');
350
+ });
351
+ (0, _vitest.it)('오버플로가 없으면 썸 폭을 0 으로 둔다', () => {
352
+ render(/*#__PURE__*/(0, _react.createElement)(HorizontalHarness));
353
+ const hScrollEl = query('.ncua-table__h-scroll-container');
354
+ const hThumbEl = query('.ncua-table__h-scrollbar-thumb');
355
+ defineMetrics(hScrollEl, {
356
+ clientWidth: LAYOUT.viewportWidth,
357
+ scrollWidth: LAYOUT.viewportWidth,
358
+ scrollLeft: 0
359
+ });
360
+ fireScroll(hScrollEl);
361
+ (0, _vitest.expect)(hThumbEl.style.width).toBe('0px');
362
+ });
363
+ (0, _vitest.it)('썸 드래그는 (scrollWidth − clientWidth) / (trackWidth − thumbWidth) 비율로 환산한다', () => {
364
+ const {
365
+ hScrollEl,
366
+ hThumbEl
367
+ } = setUpOverflow();
368
+ defineMetrics(hThumbEl, {
369
+ offsetWidth: LAYOUT.thumbWidth
370
+ });
371
+ (0, _testUtils.act)(() => {
372
+ hThumbEl.dispatchEvent(new MouseEvent('mousedown', {
373
+ bubbles: true,
374
+ clientX: 50
375
+ }));
376
+ });
377
+ (0, _vitest.expect)(hScrollEl.hasAttribute('data-dragging')).toBe(true);
378
+ (0, _testUtils.act)(() => {
379
+ document.dispatchEvent(new MouseEvent('mousemove', {
380
+ clientX: 60
381
+ }));
382
+ });
383
+ // ratio = (1500 - 600) / (584 - 234) = 900 / 350 → 10px 이동 = 25.714…
384
+ (0, _vitest.expect)(hScrollEl.scrollLeft).toBeCloseTo(EXPECTED_HORIZONTAL_SCROLL_LEFT, PRECISION);
385
+ (0, _testUtils.act)(() => {
386
+ document.dispatchEvent(new MouseEvent('mouseup'));
387
+ });
388
+ (0, _vitest.expect)(hScrollEl.hasAttribute('data-dragging')).toBe(false);
389
+ });
390
+ });