@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.
- package/dist/cjs/assets/scripts/table/scrollbar/const.js +50 -0
- package/dist/cjs/assets/scripts/table/scrollbar/drag.js +75 -0
- package/dist/cjs/assets/scripts/table/scrollbar/geometry.js +69 -0
- package/dist/cjs/assets/scripts/table/scrollbar/isolate.js +36 -0
- package/dist/cjs/src/components/data-display/table/Table.js +1 -0
- package/dist/cjs/src/components/data-display/table/__tests__/useTableScrollbars.characterisation.test.js +390 -0
- package/dist/cjs/src/components/data-display/table/__tests__/useTableScrollbars.regression.test.js +339 -0
- package/dist/cjs/src/components/data-display/table/useTableScrollbars.js +115 -86
- package/dist/cjs/src/generated/scoped-css.js +1 -1
- package/dist/esm/assets/scripts/table/scrollbar/const.js +44 -0
- package/dist/esm/assets/scripts/table/scrollbar/drag.js +67 -0
- package/dist/esm/assets/scripts/table/scrollbar/geometry.js +60 -0
- package/dist/esm/assets/scripts/table/scrollbar/isolate.js +27 -0
- package/dist/esm/src/components/data-display/table/Table.js +1 -0
- package/dist/esm/src/components/data-display/table/__tests__/useTableScrollbars.characterisation.test.js +387 -0
- package/dist/esm/src/components/data-display/table/__tests__/useTableScrollbars.regression.test.js +336 -0
- package/dist/esm/src/components/data-display/table/useTableScrollbars.js +85 -86
- package/dist/esm/src/generated/scoped-css.js +1 -1
- package/dist/temp/assets/scripts/table/scrollbar/const.d.ts +46 -0
- package/dist/temp/assets/scripts/table/scrollbar/const.js +44 -0
- package/dist/temp/assets/scripts/table/scrollbar/drag.d.ts +18 -0
- package/dist/temp/assets/scripts/table/scrollbar/drag.js +49 -0
- package/dist/temp/assets/scripts/table/scrollbar/geometry.d.ts +47 -0
- package/dist/temp/assets/scripts/table/scrollbar/geometry.js +37 -0
- package/dist/temp/assets/scripts/table/scrollbar/isolate.d.ts +4 -0
- package/dist/temp/assets/scripts/table/scrollbar/isolate.js +30 -0
- package/dist/temp/src/components/data-display/table/Table.js +1 -0
- package/dist/temp/src/components/data-display/table/__tests__/useTableScrollbars.characterisation.test.d.ts +1 -0
- package/dist/temp/src/components/data-display/table/__tests__/useTableScrollbars.characterisation.test.js +282 -0
- package/dist/temp/src/components/data-display/table/__tests__/useTableScrollbars.regression.test.d.ts +1 -0
- package/dist/temp/src/components/data-display/table/__tests__/useTableScrollbars.regression.test.js +244 -0
- package/dist/temp/src/components/data-display/table/useTableScrollbars.d.ts +2 -6
- package/dist/temp/src/components/data-display/table/useTableScrollbars.js +100 -72
- package/dist/temp/src/generated/scoped-css.js +1 -1
- package/dist/temp/src/hooks/useIsomorphicLayoutEffect.d.ts +2 -2
- package/dist/types/assets/scripts/table/scrollbar/const.d.ts +46 -0
- package/dist/types/assets/scripts/table/scrollbar/drag.d.ts +18 -0
- package/dist/types/assets/scripts/table/scrollbar/geometry.d.ts +47 -0
- package/dist/types/assets/scripts/table/scrollbar/isolate.d.ts +4 -0
- package/dist/types/src/components/data-display/table/__tests__/useTableScrollbars.characterisation.test.d.ts +1 -0
- package/dist/types/src/components/data-display/table/__tests__/useTableScrollbars.regression.test.d.ts +1 -0
- package/dist/types/src/components/data-display/table/useTableScrollbars.d.ts +2 -6
- package/dist/types/src/hooks/useIsomorphicLayoutEffect.d.ts +2 -2
- package/dist/ui-admin/assets/styles/style.css +29 -0
- 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
|
+
});
|