@ncds/ui-admin 1.8.28-alpha.3 → 1.8.28-alpha.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/assets/scripts/table/rowSpanBlock.js +97 -0
- package/dist/cjs/src/components/data-display/table/Table.js +135 -57
- package/dist/cjs/src/components/data-display/table/__tests__/Table.dnd.dragCell.test.js +7 -2
- package/dist/cjs/src/components/data-display/table/__tests__/Table.dnd.test.js +13 -8
- package/dist/cjs/src/components/data-display/table/__tests__/Table.rowGroup.test.js +54 -0
- package/dist/cjs/src/components/data-display/table/__tests__/Table.rowGroupDnd.test.js +212 -0
- package/dist/cjs/src/components/data-display/table/__tests__/dnd-context.test.js +5 -2
- package/dist/cjs/src/components/data-display/table/__tests__/tableSelectableStyle.test.js +75 -0
- package/dist/cjs/src/components/data-display/table/dnd-context.js +4 -1
- package/dist/cjs/src/components/data-display/table/dnd-move.js +46 -0
- package/dist/cjs/src/components/data-display/table/dnd-preview.js +8 -3
- package/dist/cjs/src/components/overlays/postcode-search-modal/PostcodeSearchModal.js +16 -2
- package/dist/cjs/src/components/overlays/postcode-search-modal/__tests__/PostcodeSearchModal.test.js +127 -0
- package/dist/cjs/src/generated/scoped-css.js +1 -1
- package/dist/esm/assets/scripts/table/rowSpanBlock.js +85 -0
- package/dist/esm/src/components/data-display/table/Table.js +135 -57
- package/dist/esm/src/components/data-display/table/__tests__/Table.dnd.dragCell.test.js +7 -2
- package/dist/esm/src/components/data-display/table/__tests__/Table.dnd.test.js +13 -8
- package/dist/esm/src/components/data-display/table/__tests__/Table.rowGroup.test.js +51 -0
- package/dist/esm/src/components/data-display/table/__tests__/Table.rowGroupDnd.test.js +210 -0
- package/dist/esm/src/components/data-display/table/__tests__/dnd-context.test.js +5 -2
- package/dist/esm/src/components/data-display/table/__tests__/tableSelectableStyle.test.js +72 -0
- package/dist/esm/src/components/data-display/table/dnd-context.js +4 -1
- package/dist/esm/src/components/data-display/table/dnd-move.js +38 -0
- package/dist/esm/src/components/data-display/table/dnd-preview.js +8 -3
- package/dist/esm/src/components/overlays/postcode-search-modal/PostcodeSearchModal.js +16 -2
- package/dist/esm/src/components/overlays/postcode-search-modal/__tests__/PostcodeSearchModal.test.js +127 -0
- package/dist/esm/src/generated/scoped-css.js +1 -1
- package/dist/temp/assets/scripts/table/rowSpanBlock.d.ts +29 -0
- package/dist/temp/assets/scripts/table/rowSpanBlock.js +77 -0
- package/dist/temp/src/components/data-display/table/Table.d.ts +7 -3
- package/dist/temp/src/components/data-display/table/Table.js +107 -47
- package/dist/temp/src/components/data-display/table/__tests__/Table.dnd.dragCell.test.js +10 -2
- package/dist/temp/src/components/data-display/table/__tests__/Table.dnd.test.js +16 -8
- package/dist/temp/src/components/data-display/table/__tests__/Table.rowGroup.test.d.ts +1 -0
- package/dist/temp/src/components/data-display/table/__tests__/Table.rowGroup.test.js +52 -0
- package/dist/temp/src/components/data-display/table/__tests__/Table.rowGroupDnd.test.d.ts +1 -0
- package/dist/temp/src/components/data-display/table/__tests__/Table.rowGroupDnd.test.js +137 -0
- package/dist/temp/src/components/data-display/table/__tests__/dnd-context.test.js +2 -2
- package/dist/temp/src/components/data-display/table/__tests__/tableSelectableStyle.test.d.ts +1 -0
- package/dist/temp/src/components/data-display/table/__tests__/tableSelectableStyle.test.js +71 -0
- package/dist/temp/src/components/data-display/table/dnd-context.d.ts +3 -0
- package/dist/temp/src/components/data-display/table/dnd-context.js +1 -0
- package/dist/temp/src/components/data-display/table/dnd-move.d.ts +17 -0
- package/dist/temp/src/components/data-display/table/dnd-move.js +31 -0
- package/dist/temp/src/components/data-display/table/dnd-preview.d.ts +5 -1
- package/dist/temp/src/components/data-display/table/dnd-preview.js +7 -3
- package/dist/temp/src/components/data-display/table/types.d.ts +14 -5
- package/dist/temp/src/components/overlays/postcode-search-modal/PostcodeSearchModal.js +16 -2
- package/dist/temp/src/components/overlays/postcode-search-modal/__tests__/PostcodeSearchModal.test.js +92 -0
- package/dist/temp/src/generated/scoped-css.js +1 -1
- package/dist/types/assets/scripts/table/rowSpanBlock.d.ts +29 -0
- package/dist/types/src/components/data-display/table/Table.d.ts +7 -3
- package/dist/types/src/components/data-display/table/__tests__/Table.rowGroup.test.d.ts +1 -0
- package/dist/types/src/components/data-display/table/__tests__/Table.rowGroupDnd.test.d.ts +1 -0
- package/dist/types/src/components/data-display/table/__tests__/tableSelectableStyle.test.d.ts +1 -0
- package/dist/types/src/components/data-display/table/dnd-context.d.ts +3 -0
- package/dist/types/src/components/data-display/table/dnd-move.d.ts +17 -0
- package/dist/types/src/components/data-display/table/dnd-preview.d.ts +5 -1
- package/dist/types/src/components/data-display/table/types.d.ts +14 -5
- package/dist/ui-admin/assets/styles/style.css +60 -4
- package/package.json +1 -1
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
/**
|
|
3
|
+
* 묶음은 스타일 레이어이므로 드래그앤드롭 동작이 묶음 유무와 무관해야 한다.
|
|
4
|
+
* 드롭 자체는 실제 포인터 이벤트가 필요해 jsdom 으로 재현할 수 없어, 감시 등록 횟수와 콜백 전달만 고정한다.
|
|
5
|
+
*/
|
|
6
|
+
import { createElement } from 'react';
|
|
7
|
+
import { createRoot } from 'react-dom/client';
|
|
8
|
+
import { act } from 'react-dom/test-utils';
|
|
9
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
10
|
+
const monitorCalls = [];
|
|
11
|
+
vi.mock('@atlaskit/pragmatic-drag-and-drop/element/adapter', () => ({
|
|
12
|
+
dropTargetForElements: () => () => undefined,
|
|
13
|
+
draggable: () => () => undefined,
|
|
14
|
+
monitorForElements: (args) => {
|
|
15
|
+
monitorCalls.push(args);
|
|
16
|
+
return () => undefined;
|
|
17
|
+
},
|
|
18
|
+
}));
|
|
19
|
+
vi.mock('@atlaskit/pragmatic-drag-and-drop/element/disable-native-drag-preview', () => ({
|
|
20
|
+
disableNativeDragPreview: () => undefined,
|
|
21
|
+
}));
|
|
22
|
+
vi.mock('@atlaskit/pragmatic-drag-and-drop-auto-scroll/element', () => ({
|
|
23
|
+
autoScrollForElements: () => () => undefined,
|
|
24
|
+
}));
|
|
25
|
+
vi.mock('@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge', () => ({
|
|
26
|
+
attachClosestEdge: (data) => data,
|
|
27
|
+
extractClosestEdge: () => 'bottom',
|
|
28
|
+
}));
|
|
29
|
+
import { planMoveIds } from '../dnd-move';
|
|
30
|
+
const { Table } = await import('../Table');
|
|
31
|
+
let container;
|
|
32
|
+
let root;
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
monitorCalls.length = 0;
|
|
35
|
+
container = document.createElement('div');
|
|
36
|
+
document.body.appendChild(container);
|
|
37
|
+
act(() => {
|
|
38
|
+
root = createRoot(container);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
afterEach(() => {
|
|
42
|
+
act(() => {
|
|
43
|
+
root.unmount();
|
|
44
|
+
});
|
|
45
|
+
container.remove();
|
|
46
|
+
});
|
|
47
|
+
const rowProps = (id) => ({ key: id, dragId: id, children: createElement(Table.Cell, null, id) });
|
|
48
|
+
const row = (id) => createElement(Table.Row, rowProps(id));
|
|
49
|
+
const groupProps = (key, onRowDrop, ids) => ({
|
|
50
|
+
key,
|
|
51
|
+
onRowDrop,
|
|
52
|
+
children: ids.map(row),
|
|
53
|
+
});
|
|
54
|
+
const group = (key, onRowDrop, ids) => createElement(Table.RowGroup, groupProps(key, onRowDrop, ids));
|
|
55
|
+
const bodyProps = (onRowDrop, ids) => ({ onRowDrop, children: ids.map(row) });
|
|
56
|
+
const plainBodyProps = (ids) => ({ children: ids.map(row) });
|
|
57
|
+
const tableProps = (children) => ({ type: 'horizontal', draggable: true, children });
|
|
58
|
+
const renderTable = (children) => {
|
|
59
|
+
act(() => {
|
|
60
|
+
root.render(createElement(Table, tableProps(children)));
|
|
61
|
+
});
|
|
62
|
+
};
|
|
63
|
+
describe('Table.RowGroup — 드래그앤드롭', () => {
|
|
64
|
+
it('묶음이 여러 개여도 드롭 감시는 한 번만 등록된다', () => {
|
|
65
|
+
const onRowDrop = vi.fn();
|
|
66
|
+
renderTable([
|
|
67
|
+
group('g1', onRowDrop, ['a', 'b']),
|
|
68
|
+
group('g2', onRowDrop, ['c', 'd']),
|
|
69
|
+
group('g3', onRowDrop, ['e']),
|
|
70
|
+
]);
|
|
71
|
+
expect(monitorCalls).toHaveLength(1);
|
|
72
|
+
});
|
|
73
|
+
it('묶음에 넘긴 onRowDrop 이 드롭 1회에 1번만 호출된다', () => {
|
|
74
|
+
const onRowDrop = vi.fn();
|
|
75
|
+
renderTable([group('g1', onRowDrop, ['a', 'b']), group('g2', onRowDrop, ['c', 'd'])]);
|
|
76
|
+
for (const call of monitorCalls) {
|
|
77
|
+
call.onDrop({
|
|
78
|
+
source: { data: { id: 'a' } },
|
|
79
|
+
location: { current: { dropTargets: [{ data: { id: 'c' } }] } },
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
expect(onRowDrop).toHaveBeenCalledTimes(1);
|
|
83
|
+
expect(onRowDrop).toHaveBeenCalledWith('a', 'c', 'bottom', ['a']);
|
|
84
|
+
});
|
|
85
|
+
it('다중 선택 드래그는 고른 행과 그 병합 블록을 모두 넘긴다', () => {
|
|
86
|
+
const onRowDrop = vi.fn();
|
|
87
|
+
renderTable(createElement(Table.Body, bodyProps(onRowDrop, ['a', 'b', 'c', 'd'])));
|
|
88
|
+
const rows = Array.from(container.querySelectorAll('tbody tr'));
|
|
89
|
+
// a 가 b 를 덮고, c 는 혼자다 — a·c 를 고르면 b 도 따라와야 한다
|
|
90
|
+
rows[0].cells[0].rowSpan = 2;
|
|
91
|
+
rows[0].classList.add('ncua-table__row--selected');
|
|
92
|
+
rows[2].classList.add('ncua-table__row--selected');
|
|
93
|
+
monitorCalls[0].onDrop({
|
|
94
|
+
source: { data: { id: 'a', moveIds: planMoveIds(rows[0]) } },
|
|
95
|
+
location: { current: { dropTargets: [{ data: { id: 'd' } }] } },
|
|
96
|
+
});
|
|
97
|
+
expect(onRowDrop).toHaveBeenCalledWith('a', 'd', 'bottom', ['a', 'b', 'c']);
|
|
98
|
+
});
|
|
99
|
+
it('고르지 않은 행을 잡으면 그 행의 블록만 넘긴다', () => {
|
|
100
|
+
const onRowDrop = vi.fn();
|
|
101
|
+
renderTable(createElement(Table.Body, bodyProps(onRowDrop, ['a', 'b', 'c', 'd'])));
|
|
102
|
+
const rows = Array.from(container.querySelectorAll('tbody tr'));
|
|
103
|
+
rows[0].cells[0].rowSpan = 2;
|
|
104
|
+
rows[2].classList.add('ncua-table__row--selected');
|
|
105
|
+
monitorCalls[0].onDrop({
|
|
106
|
+
source: { data: { id: 'a', moveIds: planMoveIds(rows[0]) } },
|
|
107
|
+
location: { current: { dropTargets: [{ data: { id: 'd' } }] } },
|
|
108
|
+
});
|
|
109
|
+
expect(onRowDrop).toHaveBeenCalledWith('a', 'd', 'bottom', ['a', 'b']);
|
|
110
|
+
});
|
|
111
|
+
it('onRowDrop 을 거두면 드롭이 더 이상 호출되지 않는다', () => {
|
|
112
|
+
const onRowDrop = vi.fn();
|
|
113
|
+
const drop = () => monitorCalls[0].onDrop({
|
|
114
|
+
source: { data: { id: 'a' } },
|
|
115
|
+
location: { current: { dropTargets: [{ data: { id: 'b' } }] } },
|
|
116
|
+
});
|
|
117
|
+
renderTable(createElement(Table.Body, bodyProps(onRowDrop, ['a', 'b'])));
|
|
118
|
+
drop();
|
|
119
|
+
expect(onRowDrop).toHaveBeenCalledTimes(1);
|
|
120
|
+
act(() => {
|
|
121
|
+
root.render(createElement(Table, tableProps(createElement(Table.Body, plainBodyProps(['a', 'b'])))));
|
|
122
|
+
});
|
|
123
|
+
drop();
|
|
124
|
+
expect(onRowDrop).toHaveBeenCalledTimes(1);
|
|
125
|
+
});
|
|
126
|
+
it('Table.Body 의 onRowDrop 동작은 묶음 도입 전과 같다', () => {
|
|
127
|
+
const onRowDrop = vi.fn();
|
|
128
|
+
renderTable(createElement(Table.Body, bodyProps(onRowDrop, ['a', 'b'])));
|
|
129
|
+
expect(monitorCalls).toHaveLength(1);
|
|
130
|
+
monitorCalls[0].onDrop({
|
|
131
|
+
source: { data: { id: 'a' } },
|
|
132
|
+
location: { current: { dropTargets: [{ data: { id: 'b' } }] } },
|
|
133
|
+
});
|
|
134
|
+
expect(onRowDrop).toHaveBeenCalledTimes(1);
|
|
135
|
+
expect(onRowDrop).toHaveBeenCalledWith('a', 'b', 'bottom', ['a']);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -35,8 +35,8 @@ describe('dnd-context — Provider 밖 기본값 (RSD-05)', () => {
|
|
|
35
35
|
expect(row).toEqual({ dragId: undefined, setIsDragging: expect.any(Function) });
|
|
36
36
|
expect(() => row?.setIsDragging(true)).not.toThrow();
|
|
37
37
|
});
|
|
38
|
-
it('RSD-05 TableDndContext 기본값은 드래그 비활성 · 빈 tableId
|
|
38
|
+
it('RSD-05 TableDndContext 기본값은 드래그 비활성 · 빈 tableId · 비어 있는 드롭 핸들러 자리다', () => {
|
|
39
39
|
const { table } = readDefaults();
|
|
40
|
-
expect(table).toEqual({ isDraggable: false, tableId: '' });
|
|
40
|
+
expect(table).toEqual({ isDraggable: false, tableId: '', onRowDropRef: { current: undefined } });
|
|
41
41
|
});
|
|
42
42
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { compile as compileSass } from 'sass';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
// box-shadow 는 jsdom 에서 계산되지 않으므로, 컴파일된 CSS 에서 규칙을 검사한다.
|
|
5
|
+
// (page-title/__tests__/pageTitleStyle.test.ts 와 같은 방식)
|
|
6
|
+
const TABLE_SCSS_PATH = path.resolve(__dirname, '../../../../../assets/styles/components/table/_index.scss');
|
|
7
|
+
const compiledTableCss = compileSass(TABLE_SCSS_PATH).css;
|
|
8
|
+
/** 선택자 → 선언 목록. 같은 선택자가 여러 규칙에 나오면 선언을 이어 붙인다. */
|
|
9
|
+
const parseRules = (css) => {
|
|
10
|
+
const rules = new Map();
|
|
11
|
+
for (const [, selectorList, body] of css.matchAll(/([^{}]+)\{([^{}]*)\}/g)) {
|
|
12
|
+
const declarations = body
|
|
13
|
+
.split(';')
|
|
14
|
+
.map((declaration) => declaration.trim().replace(/\s+/g, ' '))
|
|
15
|
+
.filter(Boolean);
|
|
16
|
+
for (const selector of selectorList.split(',').map((selector) => selector.trim().replace(/\s+/g, ' '))) {
|
|
17
|
+
rules.set(selector, [...(rules.get(selector) ?? []), ...declarations]);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return rules;
|
|
21
|
+
};
|
|
22
|
+
const rules = parseRules(compiledTableCss);
|
|
23
|
+
const declarationsOf = (selector) => rules.get(selector) ?? [];
|
|
24
|
+
const SELECTABLE = '.ncua-table--selectable';
|
|
25
|
+
const IN_DATA_GRID = '.ncua-table--in-data-grid.ncua-table--selectable';
|
|
26
|
+
const UNSELECTED_ROW = '.ncua-table__body .ncua-table__row:not(.ncua-table__row--selected)';
|
|
27
|
+
const HEADER_FIRST_CELL = '.ncua-table__header-cell:first-child';
|
|
28
|
+
const COMPENSATION_LINE = 'box-shadow: inset 1px 0 0 0 var(--gray-100)';
|
|
29
|
+
const NO_SHADOW = 'box-shadow: none';
|
|
30
|
+
describe('Table selectable — 좌측 border 보상선', () => {
|
|
31
|
+
it('행(tr)이 보상선을 갖는다 — 병합 셀이 덮은 행의 첫 셀은 표 왼쪽 끝이 아니다', () => {
|
|
32
|
+
// Given: 컴파일된 Table CSS 에서
|
|
33
|
+
// When: selectable 의 보상 규칙을 찾으면
|
|
34
|
+
const declarations = declarationsOf(`${SELECTABLE} ${UNSELECTED_ROW}`);
|
|
35
|
+
// Then: 셀이 아니라 행 전체에 보상선을 둔다
|
|
36
|
+
expect(declarations).toContain(COMPENSATION_LINE);
|
|
37
|
+
});
|
|
38
|
+
it('헤더 첫 칸도 보상선을 갖는다', () => {
|
|
39
|
+
// Given/When: 헤더 첫 칸 규칙을 찾으면
|
|
40
|
+
// Then: 기존 보상선이 그대로다
|
|
41
|
+
expect(declarationsOf(`${SELECTABLE} ${HEADER_FIRST_CELL}`)).toContain(COMPENSATION_LINE);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
describe('Table selectable — DataGrid 안에서는 보상선을 거둔다', () => {
|
|
45
|
+
it('행(tr)의 보상선이 제거된다 — DataGrid wrapper 가 외곽 border 를 담당한다', () => {
|
|
46
|
+
// Given: 컴파일된 Table CSS 에서
|
|
47
|
+
// When: in-data-grid 조합의 행 규칙을 찾으면
|
|
48
|
+
const declarations = declarationsOf(`${IN_DATA_GRID} ${UNSELECTED_ROW}`);
|
|
49
|
+
// Then: 보상선을 지워 DataGrid 외곽선과 겹치지 않는다
|
|
50
|
+
expect(declarations).toContain(NO_SHADOW);
|
|
51
|
+
});
|
|
52
|
+
it('헤더 첫 칸의 보상선이 제거된다', () => {
|
|
53
|
+
// Given/When: in-data-grid 조합의 헤더 첫 칸 규칙을 찾으면
|
|
54
|
+
// Then: 보상선을 지운다
|
|
55
|
+
expect(declarationsOf(`${IN_DATA_GRID} ${HEADER_FIRST_CELL}`)).toContain(NO_SHADOW);
|
|
56
|
+
});
|
|
57
|
+
it('보상선을 받는 모든 자리에 제거 규칙이 대응한다 — 한쪽만 넓어지면 선이 남는다', () => {
|
|
58
|
+
// Given: 보상선을 받는 selectable 선택자를 모두 모으고
|
|
59
|
+
const compensated = [...rules]
|
|
60
|
+
.filter(([selector, declarations]) => selector.startsWith(SELECTABLE) && declarations.includes(COMPENSATION_LINE))
|
|
61
|
+
.map(([selector]) => selector);
|
|
62
|
+
// When: 각 선택자의 in-data-grid 대응 규칙을 찾으면
|
|
63
|
+
const uncovered = compensated.filter((selector) => {
|
|
64
|
+
const counterpart = selector.replace(SELECTABLE, IN_DATA_GRID);
|
|
65
|
+
return !declarationsOf(counterpart).includes(NO_SHADOW);
|
|
66
|
+
});
|
|
67
|
+
// Then: 대응 없이 남는 자리가 없다
|
|
68
|
+
expect(compensated.length).toBeGreaterThan(0);
|
|
69
|
+
expect(uncovered).toEqual([]);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { type MutableRefObject } from 'react';
|
|
2
|
+
import type { TableRowDropHandler } from './types';
|
|
1
3
|
type TableDndContextValue = {
|
|
2
4
|
isDraggable: boolean;
|
|
3
5
|
tableId: string;
|
|
6
|
+
onRowDropRef: MutableRefObject<TableRowDropHandler | undefined>;
|
|
4
7
|
};
|
|
5
8
|
declare const TableDndContext: import("react").Context<TableDndContextValue>;
|
|
6
9
|
type RowDndContextValue = {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
declare const DRAG_ID_ATTRIBUTE = "data-draggable-id";
|
|
2
|
+
/** 행 위치는 `tbody` 기준이라 묶음이 여러 개면 서로 겹친다 — 어느 `tbody` 인지 함께 본다 */
|
|
3
|
+
export declare const locateRow: (rowEl: HTMLTableRowElement) => {
|
|
4
|
+
bodyIndex: number;
|
|
5
|
+
body: HTMLTableSectionElement;
|
|
6
|
+
index: number;
|
|
7
|
+
} | null;
|
|
8
|
+
/** 병합이 덮는 행은 따로 옮길 수 없다. 드래그 단위를 그 행이 속한 병합 블록으로 넓힌다. */
|
|
9
|
+
export declare const findRowSpanBlock: (rowEl: HTMLTableRowElement) => {
|
|
10
|
+
bodyIndex: number;
|
|
11
|
+
rows: HTMLTableRowElement[];
|
|
12
|
+
start: number;
|
|
13
|
+
end: number;
|
|
14
|
+
} | null;
|
|
15
|
+
/** 이번 드래그로 옮겨야 하는 행 전부. 고른 행이 여럿이면 각자의 병합 블록까지 끌고 온다. */
|
|
16
|
+
export declare const planMoveIds: (rowEl: HTMLTableRowElement) => string[];
|
|
17
|
+
export { DRAG_ID_ATTRIBUTE };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { computeRowSpanBlock, expandRowsToBlocks, locateRow as locateInBody, readRowSpans, } from '../../../../assets/scripts/table/rowSpanBlock';
|
|
2
|
+
const DRAG_ID_ATTRIBUTE = 'data-draggable-id';
|
|
3
|
+
const toDragIds = (rows) => rows.map((row) => row.getAttribute(DRAG_ID_ATTRIBUTE)).filter((id) => id !== null);
|
|
4
|
+
/** 행 위치는 `tbody` 기준이라 묶음이 여러 개면 서로 겹친다 — 어느 `tbody` 인지 함께 본다 */
|
|
5
|
+
export const locateRow = (rowEl) => {
|
|
6
|
+
const located = locateInBody(rowEl);
|
|
7
|
+
const table = located?.body.closest('table');
|
|
8
|
+
if (!located || !table)
|
|
9
|
+
return null;
|
|
10
|
+
return { ...located, bodyIndex: Array.from(table.tBodies).indexOf(located.body) };
|
|
11
|
+
};
|
|
12
|
+
/** 병합이 덮는 행은 따로 옮길 수 없다. 드래그 단위를 그 행이 속한 병합 블록으로 넓힌다. */
|
|
13
|
+
export const findRowSpanBlock = (rowEl) => {
|
|
14
|
+
const located = locateRow(rowEl);
|
|
15
|
+
if (!located)
|
|
16
|
+
return null;
|
|
17
|
+
const { body, bodyIndex, index } = located;
|
|
18
|
+
const block = computeRowSpanBlock(readRowSpans(body), index);
|
|
19
|
+
return { ...block, bodyIndex, rows: Array.from(body.rows).slice(block.start, block.end + 1) };
|
|
20
|
+
};
|
|
21
|
+
/** 이번 드래그로 옮겨야 하는 행 전부. 고른 행이 여럿이면 각자의 병합 블록까지 끌고 온다. */
|
|
22
|
+
export const planMoveIds = (rowEl) => {
|
|
23
|
+
const tableEl = rowEl.closest('table');
|
|
24
|
+
const selectedRows = tableEl
|
|
25
|
+
? Array.from(tableEl.querySelectorAll('tbody > .ncua-table__row--selected'))
|
|
26
|
+
: [];
|
|
27
|
+
const isMultiSelect = selectedRows.length > 1 && selectedRows.includes(rowEl);
|
|
28
|
+
const rows = isMultiSelect ? expandRowsToBlocks(selectedRows) : (findRowSpanBlock(rowEl)?.rows ?? [rowEl]);
|
|
29
|
+
return toDragIds(rows);
|
|
30
|
+
};
|
|
31
|
+
export { DRAG_ID_ATTRIBUTE };
|
|
@@ -2,5 +2,9 @@ declare const TABLE_DND_ID_RADIX = 36;
|
|
|
2
2
|
declare const TABLE_DND_ID_SLICE_START = 2;
|
|
3
3
|
declare const TABLE_DND_ID_SLICE_END = 9;
|
|
4
4
|
declare const PREVIEW_BADGE_SIZE = 24;
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* @param rows 미리보기에 그릴 행. 병합 묶음은 한 건이라도 행이 여러 개다
|
|
7
|
+
* @param itemCount 옮기는 건수. 병합 묶음은 행 수와 다르므로, 생략하면 행 수를 건수로 본다
|
|
8
|
+
*/
|
|
9
|
+
declare const buildDragPreview: (rows: HTMLElement[], rowRect: DOMRect, itemCount?: number) => HTMLDivElement;
|
|
6
10
|
export { TABLE_DND_ID_RADIX, TABLE_DND_ID_SLICE_START, TABLE_DND_ID_SLICE_END, PREVIEW_BADGE_SIZE, buildDragPreview };
|
|
@@ -84,11 +84,15 @@ const buildRowCard = ({ row, cardLeft, cardTop, cardWidth, rowHeight, zIndex, pa
|
|
|
84
84
|
wrapper.appendChild(table);
|
|
85
85
|
return wrapper;
|
|
86
86
|
};
|
|
87
|
-
|
|
87
|
+
/**
|
|
88
|
+
* @param rows 미리보기에 그릴 행. 병합 묶음은 한 건이라도 행이 여러 개다
|
|
89
|
+
* @param itemCount 옮기는 건수. 병합 묶음은 행 수와 다르므로, 생략하면 행 수를 건수로 본다
|
|
90
|
+
*/
|
|
91
|
+
const buildDragPreview = (rows, rowRect, itemCount = rows.length) => {
|
|
88
92
|
const visibleCount = Math.min(rows.length, PREVIEW_MAX_ROWS);
|
|
89
93
|
const hasSummary = rows.length > visibleCount;
|
|
90
94
|
const totalCards = visibleCount + (hasSummary ? 1 : 0);
|
|
91
|
-
const isMulti =
|
|
95
|
+
const isMulti = itemCount > 1;
|
|
92
96
|
const parentTable = rows[0]?.closest('table') ?? null;
|
|
93
97
|
const rowWidth = Math.round(rowRect.width);
|
|
94
98
|
const rowHeight = Math.round(rowRect.height);
|
|
@@ -99,7 +103,7 @@ const buildDragPreview = (rows, rowRect) => {
|
|
|
99
103
|
height: `${PREVIEW_BADGE_HALF + rowHeight + PREVIEW_STACK_OFFSET * (totalCards - 1)}px`,
|
|
100
104
|
});
|
|
101
105
|
if (isMulti)
|
|
102
|
-
container.appendChild(buildCountBadge(
|
|
106
|
+
container.appendChild(buildCountBadge(itemCount));
|
|
103
107
|
if (hasSummary)
|
|
104
108
|
container.appendChild(buildSummaryCard(rowRect));
|
|
105
109
|
// 뒤쪽 행 카드부터 렌더해서 앞쪽이 위로 올라오게
|
|
@@ -30,14 +30,21 @@ export type TableHeaderProps = {
|
|
|
30
30
|
children: ReactNode;
|
|
31
31
|
className?: string;
|
|
32
32
|
};
|
|
33
|
+
/**
|
|
34
|
+
* draggable=true 테이블에서 행이 재정렬될 때 호출된다.
|
|
35
|
+
* fromId → toId 로 이동, edge는 toId 행의 위(top)/아래(bottom) 삽입 위치.
|
|
36
|
+
* @param moveIds 이번 드래그로 옮겨야 하는 행 전부 — 다중 선택과 각 행의 병합 블록을 모두 반영한다
|
|
37
|
+
*/
|
|
38
|
+
export type TableRowDropHandler = (fromId: string, toId: string, edge: 'top' | 'bottom', moveIds: string[]) => void;
|
|
33
39
|
export type TableBodyProps = {
|
|
34
40
|
children: ReactNode;
|
|
35
41
|
className?: string;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
42
|
+
onRowDrop?: TableRowDropHandler;
|
|
43
|
+
};
|
|
44
|
+
export type TableRowGroupProps = {
|
|
45
|
+
children: ReactNode;
|
|
46
|
+
className?: string;
|
|
47
|
+
onRowDrop?: TableRowDropHandler;
|
|
41
48
|
};
|
|
42
49
|
export type TableRowProps = Omit<ComponentProps<'tr'>, 'ref'> & {
|
|
43
50
|
selected?: boolean;
|
|
@@ -48,6 +55,8 @@ export type TableRowProps = Omit<ComponentProps<'tr'>, 'ref'> & {
|
|
|
48
55
|
export type TableDragCellProps = {
|
|
49
56
|
disabled?: boolean;
|
|
50
57
|
className?: string;
|
|
58
|
+
/** 묶음에서 여러 행을 덮을 때 지정한다. 다른 칸과 달리 병합되지 않으면 묶음 첫 칸만 쪼개져 보인다. */
|
|
59
|
+
rowSpan?: number;
|
|
51
60
|
/** 드래그 셀에 함께 배치할 콘텐츠 (예: CheckboxInput). 아이콘 우측 8px gap으로 배치된다. */
|
|
52
61
|
children?: ReactNode;
|
|
53
62
|
};
|
|
@@ -14,6 +14,8 @@ const PAGE_SIZE = 15;
|
|
|
14
14
|
const PAGER_WINDOW = 10;
|
|
15
15
|
/** 검색 조합 안내 문구를 띄우는 기준 — 결과가 이 수를 넘을 때만 노출한다 */
|
|
16
16
|
const SEARCH_HINT_MIN_TOTAL_COUNT = 30;
|
|
17
|
+
/** 기획서 케이스 2 — 이 길이 미만이면 검색하지 않는다 */
|
|
18
|
+
const MIN_SEARCH_QUERY_LENGTH = 2;
|
|
17
19
|
/** 반응형 전환 기준 — assets/styles/base/_variable.scss 의 $breakpoint 와 동일 */
|
|
18
20
|
const MOBILE_BREAKPOINT = 768;
|
|
19
21
|
/** 본문 스크롤 컨테이너. Modal.Content 가 overflow-y:auto 를 갖는다. */
|
|
@@ -35,6 +37,8 @@ const ERROR_HINT = {
|
|
|
35
37
|
};
|
|
36
38
|
/** 선택 표시를 판정하는 키. 우편번호만으로는 같은 건물의 다른 지번을 구분하지 못한다. */
|
|
37
39
|
const resultKey = ({ zipCode, roadAddress, jibunAddress }) => `${zipCode}|${roadAddress}|${jibunAddress}`;
|
|
40
|
+
/** 앞뒤 공백만 자르고 센다. 가운데 공백은 글자 수에 포함한다. */
|
|
41
|
+
const isSearchQueryTooShort = (query) => query.trim().length < MIN_SEARCH_QUERY_LENGTH;
|
|
38
42
|
/** SSR(도큐사우루스 등)에서는 초기값을 false(=PC)로 두고 mount 이후 갱신한다. */
|
|
39
43
|
const useIsMobileViewport = () => {
|
|
40
44
|
const [isMobile, setIsMobile] = useState(false);
|
|
@@ -85,6 +89,10 @@ const PostcodeSearchModal = ({ open, onClose, zIndex, className, searchQuery, on
|
|
|
85
89
|
// 검색이 실행된 검색어. 입력 중에는 갱신되지 않는다.
|
|
86
90
|
// 초기값은 마운트 시점의 searchQuery — 사용처가 검색어를 채운 채로 열면 그 검색은 이미 실행된 것으로 본다.
|
|
87
91
|
const [searchedQuery, setSearchedQuery] = useState(searchQuery);
|
|
92
|
+
// 검색어 길이 검사에 걸렸을 때의 오류코드. 사용처 errorCode 보다 앞선다.
|
|
93
|
+
// 사용처가 결과를 지우지 않아도 이 값이 있는 동안 목록을 숨긴다(늦게 온 응답 포함).
|
|
94
|
+
const [validationErrorCode, setValidationErrorCode] = useState();
|
|
95
|
+
const activeErrorCode = validationErrorCode ?? errorCode;
|
|
88
96
|
// Modal 은 닫혀도 이 컴포넌트를 언마운트하지 않아 내부 상태가 다음 열기까지 남는다.
|
|
89
97
|
// 열리는 순간 이전 세션의 선택·검색 상태를 지운다.
|
|
90
98
|
// biome-ignore lint/correctness/useExhaustiveDependencies: searchQuery 를 deps 에 넣으면 입력할 때마다 초기화된다
|
|
@@ -93,8 +101,9 @@ const PostcodeSearchModal = ({ open, onClose, zIndex, className, searchQuery, on
|
|
|
93
101
|
return;
|
|
94
102
|
setSelectedKey(undefined);
|
|
95
103
|
setSearchedQuery(searchQuery);
|
|
104
|
+
setValidationErrorCode(undefined);
|
|
96
105
|
}, [open]);
|
|
97
|
-
const hasError = Boolean(
|
|
106
|
+
const hasError = Boolean(activeErrorCode);
|
|
98
107
|
const hasResults = !hasError && results.length > 0;
|
|
99
108
|
// 결과가 적으면 굳이 검색 조합을 안내하지 않는다.
|
|
100
109
|
const showSearchHint = hasResults && totalCount > SEARCH_HINT_MIN_TOTAL_COUNT;
|
|
@@ -107,6 +116,11 @@ const PostcodeSearchModal = ({ open, onClose, zIndex, className, searchQuery, on
|
|
|
107
116
|
onSearchQueryChange(event.target.value);
|
|
108
117
|
};
|
|
109
118
|
const handleSearch = () => {
|
|
119
|
+
if (isSearchQueryTooShort(searchQuery)) {
|
|
120
|
+
setValidationErrorCode('E0008');
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
setValidationErrorCode(undefined);
|
|
110
124
|
setSearchedQuery(searchQuery);
|
|
111
125
|
onSearch();
|
|
112
126
|
};
|
|
@@ -119,7 +133,7 @@ const PostcodeSearchModal = ({ open, onClose, zIndex, className, searchQuery, on
|
|
|
119
133
|
};
|
|
120
134
|
return (_jsxs(Modal, { isOpen: open, onClose: onClose, size: "md", zIndex: zIndex, className: className, children: [_jsx(Modal.Header, { title: MODAL_TITLE, onClose: onClose, showDivider: true }), _jsx(Modal.Content, { children: _jsxs("div", { className: "ncua-postcode-search-modal", ref: rootRef, children: [_jsxs("div", { className: classnames('ncua-postcode-search-modal__search-area', {
|
|
121
135
|
'is-fixed': hasResults && isContentScrolled,
|
|
122
|
-
}), children: [_jsxs("div", { className: "ncua-postcode-search-modal__search", children: [_jsx(InputBase, { className: "ncua-postcode-search-modal__search-input", size: "sm", fullWidth: true, value: searchQuery, clearText: !!searchQuery, onClearText: () => onSearchQueryChange(''), onChange: handleSearchQueryChange, onKeyDown: handleSearchKeyDown, placeholder: SEARCH_PLACEHOLDER, destructive: hasError, hintText:
|
|
136
|
+
}), children: [_jsxs("div", { className: "ncua-postcode-search-modal__search", children: [_jsx(InputBase, { className: "ncua-postcode-search-modal__search-input", size: "sm", fullWidth: true, value: searchQuery, clearText: !!searchQuery, onClearText: () => onSearchQueryChange(''), onChange: handleSearchQueryChange, onKeyDown: handleSearchKeyDown, placeholder: SEARCH_PLACEHOLDER, destructive: hasError, hintText: activeErrorCode ? (ERROR_HINT[activeErrorCode] ?? DEFAULT_ERROR_HINT) : undefined }), _jsx(Button, { className: "ncua-postcode-search-modal__search-button", label: SEARCH_BUTTON_LABEL, size: "sm", hierarchy: "secondary", onClick: handleSearch })] }), showSearchHint && (_jsxs("div", { children: [_jsx("p", { className: "ncua-postcode-search-modal__result-title", children: "\uC544\uB798 \uC870\uD569\uC73C\uB85C \uAC80\uC0C9 \uC2DC \uC8FC\uC18C\uB97C \uB354 \uC27D\uAC8C \uCC3E\uC744 \uC218 \uC788\uC5B4\uC694." }), _jsx("p", { className: "ncua-postcode-search-modal__result-desc", children: "\uB3C4\uB85C\uBA85+\uAC74\uBB3C\uBC88\uD638, \uC9C0\uC5ED\uBA85+\uC9C0\uBC88, \uC9C0\uC5ED\uBA85+\uAC74\uBB3C\uBA85(\uC544\uD30C\uD2B8\uBA85)" })] }))] }), !hasResults && _jsx(PostcodeSearchModal.Tip, {}), hasResults && (_jsxs(_Fragment, { children: [_jsxs("p", { className: "ncua-postcode-search-modal__count", children: ["\uAC80\uC0C9 ", _jsx("b", { children: totalCount }), "\uAC1C"] }), _jsx("ul", { className: "ncua-postcode-search-modal__list", children: results.map((result, index) => (_jsx("li", { className: "ncua-postcode-search-modal__item", children: _jsx(Radio, { name: RESULT_RADIO_NAME, value: result.zipCode, checked: selectedKey === resultKey(result), onChange: () => {
|
|
123
137
|
setSelectedKey(resultKey(result));
|
|
124
138
|
onSelect(result);
|
|
125
139
|
}, text: _jsxs(_Fragment, { children: [_jsxs("span", { className: "ncua-postcode-search-modal__addr-line", children: [_jsx("span", { className: "ncua-postcode-search-modal__badge", children: ROAD_BADGE_LABEL }), _jsx("span", { className: "ncua-postcode-search-modal__item-road", children: result.roadAddress }), _jsx("span", { className: "ncua-postcode-search-modal__item-zip", children: result.zipCode })] }), _jsxs("span", { className: "ncua-postcode-search-modal__addr-line", children: [_jsx("span", { className: "ncua-postcode-search-modal__badge", children: JIBUN_BADGE_LABEL }), _jsx("span", { className: "ncua-postcode-search-modal__item-jibun", children: result.jibunAddress })] })] }) }) }, `${result.zipCode}-${index}`))) }), _jsx("div", { className: "ncua-postcode-search-modal__pagination", children: _jsx(Pagination, { totalCount: totalCount, itemCountPerPage: pageSize, pageCount: PAGER_WINDOW, currentPage: currentPage, breakPoint: isMobile ? 'mo' : 'pc', onPageChange: onPageChange }) })] })), showEmptyState && (_jsx("div", { className: "ncua-postcode-search-modal__empty", children: _jsx(EmptyState, { title: EMPTY_TITLE, description: EMPTY_DESCRIPTION }) }))] }) })] }));
|
|
@@ -501,3 +501,95 @@ describe('PostcodeSearchModal — 검색 영역 고정 표시', () => {
|
|
|
501
501
|
expect(isFixedSearchArea(el)).toBe(false);
|
|
502
502
|
});
|
|
503
503
|
});
|
|
504
|
+
describe('PostcodeSearchModal — 검색어 2글자 미만 검사 (#678)', () => {
|
|
505
|
+
const SHORT_QUERY_HINT = '검색어를 두 글자 이상 입력해주세요.';
|
|
506
|
+
const searchButtonOf = (el) => el.querySelector('.ncua-postcode-search-modal__search-button');
|
|
507
|
+
const clickSearch = (el) => {
|
|
508
|
+
act(() => {
|
|
509
|
+
searchButtonOf(el)?.click();
|
|
510
|
+
});
|
|
511
|
+
};
|
|
512
|
+
it.each(['', ' ', '가', ' 가 '])('trim 후 2글자 미만("%s")이면 onSearch 를 부르지 않고 오류 문구를 보인다', (searchQuery) => {
|
|
513
|
+
// Given: 검색어가 trim 후 2글자 미만인 상태에서
|
|
514
|
+
const onSearch = vi.fn();
|
|
515
|
+
const el = render(baseProps({ searchQuery, onSearch }));
|
|
516
|
+
// When: 검색 버튼을 누르면
|
|
517
|
+
clickSearch(el);
|
|
518
|
+
// Then: 검색은 실행되지 않고 입력칸이 오류 상태가 된다
|
|
519
|
+
expect(onSearch).not.toHaveBeenCalled();
|
|
520
|
+
expect(hintOf(el)?.textContent).toBe(SHORT_QUERY_HINT);
|
|
521
|
+
expect(inputRootOf(el)?.classList.contains('destructive')).toBe(true);
|
|
522
|
+
});
|
|
523
|
+
it.each(['가나', ' 가나 '])('trim 후 2글자 이상("%s")이면 onSearch 를 부르고 오류가 없다', (searchQuery) => {
|
|
524
|
+
// Given: 검색어가 trim 후 2글자 이상인 상태에서
|
|
525
|
+
const onSearch = vi.fn();
|
|
526
|
+
const el = render(baseProps({ searchQuery, onSearch }));
|
|
527
|
+
// When: 검색 버튼을 누르면
|
|
528
|
+
clickSearch(el);
|
|
529
|
+
// Then: 검색이 실행되고 입력칸은 오류 상태가 아니다
|
|
530
|
+
expect(onSearch).toHaveBeenCalledTimes(1);
|
|
531
|
+
expect(hintOf(el)).toBeNull();
|
|
532
|
+
expect(inputRootOf(el)?.classList.contains('destructive')).toBe(false);
|
|
533
|
+
});
|
|
534
|
+
it('결과를 보여 주는 중에 1글자로 검색하면 목록·페이지네이션을 숨기고 Tip 을 보인다', () => {
|
|
535
|
+
// Given: 결과가 보이는 상태에서 검색어를 1글자로 바꾸고
|
|
536
|
+
const results = makeResults(SMALL_RESULT_COUNT);
|
|
537
|
+
render(baseProps({ searchQuery: '판교로', results, totalCount: SMALL_RESULT_COUNT }));
|
|
538
|
+
const el = render(baseProps({ searchQuery: '가', results, totalCount: SMALL_RESULT_COUNT }));
|
|
539
|
+
expect(itemsOf(el)).toHaveLength(SMALL_RESULT_COUNT);
|
|
540
|
+
// When: 검색 버튼을 누르면
|
|
541
|
+
clickSearch(el);
|
|
542
|
+
// Then: 사용처가 결과를 지우지 않아도 지난 목록은 보이지 않는다
|
|
543
|
+
expect(el.querySelector('.ncua-postcode-search-modal__list')).toBeNull();
|
|
544
|
+
expect(paginationOf(el)).toBeNull();
|
|
545
|
+
expect(tipOf(el)).not.toBeNull();
|
|
546
|
+
});
|
|
547
|
+
it('검사에 걸린 뒤 2글자 이상으로 다시 검색하면 오류가 풀리고 onSearch 를 부른다', () => {
|
|
548
|
+
// Given: 1글자로 검색해 검사에 걸린 상태에서
|
|
549
|
+
const onSearch = vi.fn();
|
|
550
|
+
const el = render(baseProps({ searchQuery: '가', onSearch }));
|
|
551
|
+
clickSearch(el);
|
|
552
|
+
// When: 2글자로 고쳐 다시 검색하면
|
|
553
|
+
render(baseProps({ searchQuery: '가나', onSearch }));
|
|
554
|
+
clickSearch(el);
|
|
555
|
+
// Then: 오류가 풀리고 검색이 실행된다
|
|
556
|
+
expect(onSearch).toHaveBeenCalledTimes(1);
|
|
557
|
+
expect(hintOf(el)).toBeNull();
|
|
558
|
+
expect(inputRootOf(el)?.classList.contains('destructive')).toBe(false);
|
|
559
|
+
});
|
|
560
|
+
it('검사에 걸린 뒤 모달을 닫았다 다시 열면 오류가 없다', () => {
|
|
561
|
+
// Given: 1글자로 검색해 검사에 걸린 상태에서
|
|
562
|
+
const el = render(baseProps({ searchQuery: '가' }));
|
|
563
|
+
clickSearch(el);
|
|
564
|
+
// When: 모달을 닫았다가 다시 열면
|
|
565
|
+
render(baseProps({ open: false, searchQuery: '가' }));
|
|
566
|
+
const reopened = render(baseProps({ searchQuery: '가' }));
|
|
567
|
+
// Then: 지난 검사 오류는 남지 않는다
|
|
568
|
+
expect(hintOf(reopened)).toBeNull();
|
|
569
|
+
expect(inputRootOf(reopened)?.classList.contains('destructive')).toBe(false);
|
|
570
|
+
});
|
|
571
|
+
it('앱 errorCode(E0006) 가 있어도 1글자로 검색하면 검사 오류 문구(E0008)를 보인다', () => {
|
|
572
|
+
// Given: 사용처가 E0006 을 넘긴 상태에서 검색어를 1글자로 바꾸고
|
|
573
|
+
const el = render(baseProps({ searchQuery: '가', errorCode: 'E0006' }));
|
|
574
|
+
// When: 검색 버튼을 누르면
|
|
575
|
+
clickSearch(el);
|
|
576
|
+
// Then: 컴포넌트 검사 오류가 앞선다
|
|
577
|
+
expect(hintOf(el)?.textContent).toBe(SHORT_QUERY_HINT);
|
|
578
|
+
});
|
|
579
|
+
it('Enter 로 1글자 검색해도 버튼과 같이 onSearch 를 부르지 않고 오류 문구를 보인다', () => {
|
|
580
|
+
// Given: 검색어가 1글자인 상태에서
|
|
581
|
+
const onSearch = vi.fn();
|
|
582
|
+
const el = render(baseProps({ searchQuery: '가', onSearch }));
|
|
583
|
+
const input = el.querySelector('.ncua-input input');
|
|
584
|
+
if (!input)
|
|
585
|
+
throw new Error('검색 인풋이 렌더되지 않았습니다');
|
|
586
|
+
// When: Enter 를 누르면
|
|
587
|
+
act(() => {
|
|
588
|
+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
|
|
589
|
+
});
|
|
590
|
+
// Then: 버튼을 누른 것과 결과가 같다
|
|
591
|
+
expect(onSearch).not.toHaveBeenCalled();
|
|
592
|
+
expect(hintOf(el)?.textContent).toBe(SHORT_QUERY_HINT);
|
|
593
|
+
expect(inputRootOf(el)?.classList.contains('destructive')).toBe(true);
|
|
594
|
+
});
|
|
595
|
+
});
|