@ornery/ui-grid-vanilla 0.1.10 → 1.0.0
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/README.md +196 -0
- package/demo/main.ts +0 -1
- package/dist/index.d.mts +575 -55
- package/dist/index.d.ts +575 -55
- package/dist/index.js +4894 -1503
- package/dist/index.mjs +4994 -1556
- package/package.json +11 -8
- package/scss-plugin.ts +65 -0
- package/src/adopt-styles.ts +39 -0
- package/src/attribute-bridge.ts +291 -0
- package/src/auto-height.test.ts +142 -0
- package/src/auto-resize.test.ts +206 -0
- package/src/cell-nav.test.ts +262 -0
- package/src/components/grid-body-cell/grid-body-cell.ts +23 -0
- package/src/components/grid-body-cell/index.ts +1 -0
- package/src/components/grid-cell-editor/grid-cell-editor.ts +104 -0
- package/src/components/grid-cell-editor/index.ts +1 -0
- package/src/components/grid-filter-cell/grid-filter-cell.scss +14 -0
- package/src/components/grid-filter-cell/grid-filter-cell.template.ts +20 -0
- package/src/components/grid-filter-cell/grid-filter-cell.ts +121 -0
- package/src/components/grid-filter-cell/index.ts +1 -0
- package/src/components/grid-group-row/grid-group-row.scss +24 -0
- package/src/components/grid-group-row/grid-group-row.template.ts +23 -0
- package/src/components/grid-group-row/grid-group-row.ts +94 -0
- package/src/components/grid-group-row/index.ts +1 -0
- package/src/components/grid-header-cell/grid-header-cell.ts +17 -0
- package/src/components/grid-header-cell/index.ts +1 -0
- package/src/components/grid-pagination/grid-pagination.scss +39 -0
- package/src/components/grid-pagination/grid-pagination.template.ts +40 -0
- package/src/components/grid-pagination/grid-pagination.ts +204 -0
- package/src/components/grid-pagination/index.ts +1 -0
- package/src/components/grid-template/grid-template.ts +166 -0
- package/src/components/grid-template/index.ts +1 -0
- package/src/events.ts +815 -0
- package/src/exporter.test.ts +186 -0
- package/src/focus.ts +353 -0
- package/src/framework-slots.ts +440 -0
- package/src/grid-controller.ts +1402 -18
- package/src/icons.ts +88 -0
- package/src/importer.test.ts +147 -0
- package/src/index.integration.test.ts +2 -2
- package/src/index.ts +9 -0
- package/src/infinite-scroll.test.ts +220 -0
- package/src/keyboard-navigation.test.ts +579 -0
- package/src/patch.ts +722 -0
- package/src/render.ts +465 -0
- package/src/row-edit.test.ts +191 -0
- package/src/row-selection.test.ts +550 -0
- package/src/save-state.test.ts +235 -0
- package/src/scss.d.ts +4 -0
- package/src/templates/index.ts +62 -0
- package/src/test-setup.ts +23 -0
- package/src/ui-grid-empty.template.ts +23 -0
- package/src/ui-grid-shell.scss +25 -0
- package/src/ui-grid-shell.template.ts +45 -0
- package/src/ui-grid-standalone.element.ts +741 -1112
- package/src/utils/cell-class.ts +70 -0
- package/src/utils/display-items.ts +15 -0
- package/src/utils/dom.ts +64 -0
- package/src/validate.test.ts +170 -0
- package/tsup.config.ts +10 -0
- package/vite.config.ts +10 -1
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { clearRustWasmGridEngine } from '@ornery/ui-grid-core';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
mountVanillaUiGrid,
|
|
7
|
+
type GridOptions,
|
|
8
|
+
type UiGridApi,
|
|
9
|
+
type VanillaUiGridElement,
|
|
10
|
+
} from './index';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Row-selection integration tests. Ports the behaviours from the old
|
|
14
|
+
* ui.grid.selection module and pins them against the vanilla grid's
|
|
15
|
+
* public API and DOM wiring.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
async function waitFor<T>(resolve: () => T | null | undefined, timeoutMs = 5000): Promise<T> {
|
|
19
|
+
const startedAt = Date.now();
|
|
20
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
21
|
+
const value = resolve();
|
|
22
|
+
if (value) return value;
|
|
23
|
+
await new Promise((r) => window.setTimeout(r, 10));
|
|
24
|
+
}
|
|
25
|
+
throw new Error('Timed out waiting for expected state');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const TAG = 'ui-grid-element-vanilla-selection-test';
|
|
29
|
+
|
|
30
|
+
interface Row extends Record<string, unknown> {
|
|
31
|
+
id: string;
|
|
32
|
+
name: string;
|
|
33
|
+
status: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function makeData(): Row[] {
|
|
37
|
+
return [
|
|
38
|
+
{ id: 'r1', name: 'Alpha', status: 'Active' },
|
|
39
|
+
{ id: 'r2', name: 'Beta', status: 'Pilot' },
|
|
40
|
+
{ id: 'r3', name: 'Gamma', status: 'Draft' },
|
|
41
|
+
{ id: 'r4', name: 'Delta', status: 'Active' },
|
|
42
|
+
];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function baseOptions(overrides: Partial<GridOptions> = {}): GridOptions {
|
|
46
|
+
let capturedApi: UiGridApi | undefined;
|
|
47
|
+
const options: GridOptions = {
|
|
48
|
+
id: 'row-selection-grid',
|
|
49
|
+
data: makeData(),
|
|
50
|
+
columnDefs: [
|
|
51
|
+
{ name: 'name', displayName: 'Name' },
|
|
52
|
+
{ name: 'status', displayName: 'Status' },
|
|
53
|
+
],
|
|
54
|
+
rowIdentity: (entity) => String((entity as { id: string }).id),
|
|
55
|
+
enableRowSelection: true,
|
|
56
|
+
onRegisterApi: (api) => {
|
|
57
|
+
capturedApi = api as UiGridApi;
|
|
58
|
+
},
|
|
59
|
+
...overrides,
|
|
60
|
+
};
|
|
61
|
+
Object.defineProperty(options, '__api', { enumerable: false, get: () => capturedApi });
|
|
62
|
+
return options;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function getApi(options: GridOptions): UiGridApi {
|
|
66
|
+
const api = (options as unknown as { __api: UiGridApi | undefined }).__api;
|
|
67
|
+
if (!api) throw new Error('Grid API was never registered');
|
|
68
|
+
return api;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function mountGrid(options: GridOptions): Promise<{
|
|
72
|
+
grid: VanillaUiGridElement;
|
|
73
|
+
shadow: ShadowRoot;
|
|
74
|
+
}> {
|
|
75
|
+
const target = document.getElementById('app')!;
|
|
76
|
+
const grid = await mountVanillaUiGrid(target, options, undefined, TAG);
|
|
77
|
+
const shadow = await waitFor(() => grid.shadowRoot);
|
|
78
|
+
await waitFor(() => shadow.querySelector('.body-cell[data-row="r1"]'));
|
|
79
|
+
return { grid, shadow };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function cellIn(root: ShadowRoot, rowId: string, columnName: string): HTMLElement {
|
|
83
|
+
const selector = `.body-cell[data-row="${rowId}"][data-column="${columnName}"]`;
|
|
84
|
+
const el = root.querySelector<HTMLElement>(selector);
|
|
85
|
+
if (!el) throw new Error(`Cell ${rowId}/${columnName} not rendered`);
|
|
86
|
+
return el;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function dispatchMouse(
|
|
90
|
+
el: HTMLElement,
|
|
91
|
+
type: 'mousedown' | 'mouseup' | 'mousemove' | 'click',
|
|
92
|
+
opts: {
|
|
93
|
+
shiftKey?: boolean;
|
|
94
|
+
ctrlKey?: boolean;
|
|
95
|
+
metaKey?: boolean;
|
|
96
|
+
button?: number;
|
|
97
|
+
} = {},
|
|
98
|
+
): MouseEvent {
|
|
99
|
+
const event = new MouseEvent(type, {
|
|
100
|
+
bubbles: true,
|
|
101
|
+
cancelable: true,
|
|
102
|
+
composed: true,
|
|
103
|
+
shiftKey: !!opts.shiftKey,
|
|
104
|
+
ctrlKey: !!opts.ctrlKey,
|
|
105
|
+
metaKey: !!opts.metaKey,
|
|
106
|
+
button: opts.button ?? 0,
|
|
107
|
+
});
|
|
108
|
+
el.dispatchEvent(event);
|
|
109
|
+
return event;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function pressKey(el: HTMLElement, key: string, opts: { ctrlKey?: boolean; shiftKey?: boolean } = {}): KeyboardEvent {
|
|
113
|
+
const event = new KeyboardEvent('keydown', {
|
|
114
|
+
key,
|
|
115
|
+
bubbles: true,
|
|
116
|
+
cancelable: true,
|
|
117
|
+
composed: true,
|
|
118
|
+
ctrlKey: !!opts.ctrlKey,
|
|
119
|
+
shiftKey: !!opts.shiftKey,
|
|
120
|
+
});
|
|
121
|
+
el.dispatchEvent(event);
|
|
122
|
+
return event;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
describe('vanilla grid row selection', () => {
|
|
126
|
+
beforeEach(() => {
|
|
127
|
+
document.body.innerHTML = '<div id="app"></div>';
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
afterEach(() => {
|
|
131
|
+
clearRustWasmGridEngine();
|
|
132
|
+
document.body.innerHTML = '';
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe('selection row header column', () => {
|
|
136
|
+
it('is injected when enableRowHeaderSelection is true (default)', async () => {
|
|
137
|
+
const options = baseOptions();
|
|
138
|
+
const { shadow } = await mountGrid(options);
|
|
139
|
+
expect(
|
|
140
|
+
shadow.querySelector('.header-cell[data-column="selectionRowHeaderCol"]'),
|
|
141
|
+
).not.toBeNull();
|
|
142
|
+
expect(
|
|
143
|
+
shadow.querySelectorAll('.body-cell[data-column="selectionRowHeaderCol"]').length,
|
|
144
|
+
).toBe(4);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('is NOT injected when enableRowHeaderSelection is false', async () => {
|
|
148
|
+
const options = baseOptions({ enableRowHeaderSelection: false });
|
|
149
|
+
const { shadow } = await mountGrid(options);
|
|
150
|
+
expect(
|
|
151
|
+
shadow.querySelector('.header-cell[data-column="selectionRowHeaderCol"]'),
|
|
152
|
+
).toBeNull();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('is NOT injected when enableRowSelection is false', async () => {
|
|
156
|
+
const options = baseOptions({ enableRowSelection: false });
|
|
157
|
+
const { shadow } = await mountGrid(options);
|
|
158
|
+
expect(
|
|
159
|
+
shadow.querySelector('.header-cell[data-column="selectionRowHeaderCol"]'),
|
|
160
|
+
).toBeNull();
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
describe('gridApi.selection public API', () => {
|
|
165
|
+
it('selectRow toggles isSelected and updates snapshot', async () => {
|
|
166
|
+
const options = baseOptions();
|
|
167
|
+
const { shadow } = await mountGrid(options);
|
|
168
|
+
const api = getApi(options);
|
|
169
|
+
const row = options.data[1]!;
|
|
170
|
+
api.selection.selectRow(row as unknown as Record<string, unknown>);
|
|
171
|
+
await waitFor(() =>
|
|
172
|
+
cellIn(shadow, 'r2', 'name').classList.contains('ui-grid-row-selected') ? {} : null,
|
|
173
|
+
);
|
|
174
|
+
expect(api.selection.getSelectedCount()).toBe(1);
|
|
175
|
+
expect(api.selection.getSelectedRows()).toEqual([row]);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('unSelectRow deselects a previously-selected row', async () => {
|
|
179
|
+
const options = baseOptions();
|
|
180
|
+
const { shadow } = await mountGrid(options);
|
|
181
|
+
const api = getApi(options);
|
|
182
|
+
const row = options.data[0]!;
|
|
183
|
+
api.selection.selectRow(row as unknown as Record<string, unknown>);
|
|
184
|
+
await waitFor(() => (api.selection.getSelectedCount() === 1 ? {} : null));
|
|
185
|
+
api.selection.unSelectRow(row as unknown as Record<string, unknown>);
|
|
186
|
+
await waitFor(() => (api.selection.getSelectedCount() === 0 ? {} : null));
|
|
187
|
+
expect(cellIn(shadow, 'r1', 'name').classList.contains('ui-grid-row-selected')).toBe(false);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('toggleRowSelection flips state each call', async () => {
|
|
191
|
+
const options = baseOptions();
|
|
192
|
+
await mountGrid(options);
|
|
193
|
+
const api = getApi(options);
|
|
194
|
+
const row = options.data[2]!;
|
|
195
|
+
api.selection.toggleRowSelection(row as unknown as Record<string, unknown>);
|
|
196
|
+
expect(api.selection.getSelectedCount()).toBe(1);
|
|
197
|
+
api.selection.toggleRowSelection(row as unknown as Record<string, unknown>);
|
|
198
|
+
expect(api.selection.getSelectedCount()).toBe(0);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('selectRowByVisibleIndex selects by position in visible rows', async () => {
|
|
202
|
+
const options = baseOptions();
|
|
203
|
+
await mountGrid(options);
|
|
204
|
+
const api = getApi(options);
|
|
205
|
+
api.selection.selectRowByVisibleIndex(2);
|
|
206
|
+
expect(api.selection.getSelectedCount()).toBe(1);
|
|
207
|
+
expect(api.selection.getSelectedRows()[0]!['id']).toBe('r3');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('selectRowByKey finds by entity key', async () => {
|
|
211
|
+
const options = baseOptions();
|
|
212
|
+
await mountGrid(options);
|
|
213
|
+
const api = getApi(options);
|
|
214
|
+
api.selection.selectRowByKey(true, 'status', 'Pilot');
|
|
215
|
+
expect(api.selection.getSelectedRows()[0]!['id']).toBe('r2');
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('selectAllRows selects every eligible row', async () => {
|
|
219
|
+
const options = baseOptions();
|
|
220
|
+
await mountGrid(options);
|
|
221
|
+
const api = getApi(options);
|
|
222
|
+
api.selection.selectAllRows();
|
|
223
|
+
expect(api.selection.getSelectedCount()).toBe(4);
|
|
224
|
+
expect(api.selection.getSelectAllState()).toBe(true);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('selectAllRows honors isRowSelectable', async () => {
|
|
228
|
+
const options = baseOptions({
|
|
229
|
+
isRowSelectable: (row) => (row.entity as unknown as Row).status !== 'Draft',
|
|
230
|
+
});
|
|
231
|
+
await mountGrid(options);
|
|
232
|
+
const api = getApi(options);
|
|
233
|
+
api.selection.selectAllRows();
|
|
234
|
+
// r3 has status=Draft → excluded.
|
|
235
|
+
expect(api.selection.getSelectedCount()).toBe(3);
|
|
236
|
+
expect(api.selection.getSelectedRows().some((r: any) => r.id === 'r3')).toBe(false);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('clearSelectedRows empties the selection', async () => {
|
|
240
|
+
const options = baseOptions();
|
|
241
|
+
await mountGrid(options);
|
|
242
|
+
const api = getApi(options);
|
|
243
|
+
api.selection.selectAllRows();
|
|
244
|
+
api.selection.clearSelectedRows();
|
|
245
|
+
expect(api.selection.getSelectedCount()).toBe(0);
|
|
246
|
+
expect(api.selection.getSelectAllState()).toBe(false);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('selectAllVisibleRows only selects visible rows', async () => {
|
|
250
|
+
// Filter to just "Active" rows to make some invisible.
|
|
251
|
+
const options = baseOptions();
|
|
252
|
+
const { } = await mountGrid(options);
|
|
253
|
+
const api = getApi(options);
|
|
254
|
+
api.core.setFilter('status', 'Active');
|
|
255
|
+
await waitFor(() => (api.core.getVisibleRows().length === 2 ? {} : null));
|
|
256
|
+
api.selection.selectAllVisibleRows();
|
|
257
|
+
expect(api.selection.getSelectedCount()).toBe(2);
|
|
258
|
+
const ids = api.selection.getSelectedRows().map((r: any) => r.id).sort();
|
|
259
|
+
expect(ids).toEqual(['r1', 'r4']);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('multiSelect=false keeps only one row selected at a time', async () => {
|
|
263
|
+
const options = baseOptions({ multiSelect: false });
|
|
264
|
+
await mountGrid(options);
|
|
265
|
+
const api = getApi(options);
|
|
266
|
+
api.selection.selectRow(options.data[0]! as unknown as Record<string, unknown>);
|
|
267
|
+
api.selection.selectRow(options.data[1]! as unknown as Record<string, unknown>);
|
|
268
|
+
expect(api.selection.getSelectedCount()).toBe(1);
|
|
269
|
+
expect(api.selection.getSelectedRows()[0]!['id']).toBe('r2');
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('noUnselect prevents deselection', async () => {
|
|
273
|
+
const options = baseOptions({ noUnselect: true });
|
|
274
|
+
await mountGrid(options);
|
|
275
|
+
const api = getApi(options);
|
|
276
|
+
const row = options.data[0]! as unknown as Record<string, unknown>;
|
|
277
|
+
api.selection.selectRow(row);
|
|
278
|
+
api.selection.toggleRowSelection(row); // Would normally unselect.
|
|
279
|
+
expect(api.selection.getSelectedCount()).toBe(1);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('setMultiSelect switches modes on the fly', async () => {
|
|
283
|
+
const options = baseOptions();
|
|
284
|
+
await mountGrid(options);
|
|
285
|
+
const api = getApi(options);
|
|
286
|
+
api.selection.selectRow(options.data[0]! as unknown as Record<string, unknown>);
|
|
287
|
+
api.selection.selectRow(options.data[1]! as unknown as Record<string, unknown>);
|
|
288
|
+
expect(api.selection.getSelectedCount()).toBe(2);
|
|
289
|
+
api.selection.setMultiSelect(false);
|
|
290
|
+
api.selection.selectRow(options.data[2]! as unknown as Record<string, unknown>);
|
|
291
|
+
// Single-select should clear prior selection.
|
|
292
|
+
expect(api.selection.getSelectedCount()).toBe(1);
|
|
293
|
+
expect(api.selection.getSelectedRows()[0]!['id']).toBe('r3');
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('shiftSelectRow range-selects from lastSelected', async () => {
|
|
297
|
+
const options = baseOptions();
|
|
298
|
+
await mountGrid(options);
|
|
299
|
+
const api = getApi(options);
|
|
300
|
+
api.selection.selectRow(options.data[1]! as unknown as Record<string, unknown>);
|
|
301
|
+
api.selection.shiftSelectRow(options.data[3]! as unknown as Record<string, unknown>);
|
|
302
|
+
const ids = api.selection.getSelectedRows().map((r: any) => r.id).sort();
|
|
303
|
+
expect(ids).toEqual(['r2', 'r3', 'r4']);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('getSelectedGridRows returns GridRow objects, getSelectedRows returns entities', async () => {
|
|
307
|
+
const options = baseOptions();
|
|
308
|
+
await mountGrid(options);
|
|
309
|
+
const api = getApi(options);
|
|
310
|
+
api.selection.selectRow(options.data[0]! as unknown as Record<string, unknown>);
|
|
311
|
+
const rows = api.selection.getSelectedGridRows();
|
|
312
|
+
expect(rows).toHaveLength(1);
|
|
313
|
+
expect(rows[0]!.id).toBe('r1');
|
|
314
|
+
expect(rows[0]!.entity).toBe(options.data[0]);
|
|
315
|
+
expect(api.selection.getSelectedRows()[0]).toBe(options.data[0]);
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
describe('selection events', () => {
|
|
320
|
+
it('rowSelectionChanged fires on individual toggles', async () => {
|
|
321
|
+
const options = baseOptions();
|
|
322
|
+
await mountGrid(options);
|
|
323
|
+
const api = getApi(options);
|
|
324
|
+
const listener = vi.fn();
|
|
325
|
+
api.selection.on.rowSelectionChanged(listener);
|
|
326
|
+
api.selection.selectRow(options.data[0]! as unknown as Record<string, unknown>);
|
|
327
|
+
expect(listener).toHaveBeenCalledTimes(1);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('rowSelectionChangedBatch fires for bulk selectAllRows by default', async () => {
|
|
331
|
+
const options = baseOptions();
|
|
332
|
+
await mountGrid(options);
|
|
333
|
+
const api = getApi(options);
|
|
334
|
+
const single = vi.fn();
|
|
335
|
+
const batch = vi.fn();
|
|
336
|
+
api.selection.on.rowSelectionChanged(single);
|
|
337
|
+
api.selection.on.rowSelectionChangedBatch(batch);
|
|
338
|
+
api.selection.selectAllRows();
|
|
339
|
+
expect(batch).toHaveBeenCalledTimes(1);
|
|
340
|
+
expect(batch.mock.calls[0]![0]).toHaveLength(4);
|
|
341
|
+
expect(single).toHaveBeenCalledTimes(0);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it('enableSelectionBatchEvent=false falls back to per-row events', async () => {
|
|
345
|
+
const options = baseOptions({ enableSelectionBatchEvent: false });
|
|
346
|
+
await mountGrid(options);
|
|
347
|
+
const api = getApi(options);
|
|
348
|
+
const single = vi.fn();
|
|
349
|
+
api.selection.on.rowSelectionChanged(single);
|
|
350
|
+
api.selection.selectAllRows();
|
|
351
|
+
expect(single).toHaveBeenCalledTimes(4);
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
describe('mouse interactions', () => {
|
|
356
|
+
it('clicking the row-header checkbox selects the row', async () => {
|
|
357
|
+
const options = baseOptions();
|
|
358
|
+
const { shadow } = await mountGrid(options);
|
|
359
|
+
const api = getApi(options);
|
|
360
|
+
const checkboxCell = cellIn(shadow, 'r2', 'selectionRowHeaderCol');
|
|
361
|
+
dispatchMouse(checkboxCell, 'click');
|
|
362
|
+
await waitFor(() => (api.selection.getSelectedCount() === 1 ? {} : null));
|
|
363
|
+
expect(api.selection.getSelectedRows()[0]!['id']).toBe('r2');
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it('shift-click extends selection from anchor', async () => {
|
|
367
|
+
const options = baseOptions();
|
|
368
|
+
const { shadow } = await mountGrid(options);
|
|
369
|
+
const api = getApi(options);
|
|
370
|
+
dispatchMouse(cellIn(shadow, 'r2', 'selectionRowHeaderCol'), 'click');
|
|
371
|
+
await waitFor(() => (api.selection.getSelectedCount() === 1 ? {} : null));
|
|
372
|
+
dispatchMouse(cellIn(shadow, 'r4', 'selectionRowHeaderCol'), 'click', { shiftKey: true });
|
|
373
|
+
await waitFor(() => (api.selection.getSelectedCount() >= 3 ? {} : null));
|
|
374
|
+
const ids = api.selection.getSelectedRows().map((r: any) => r.id).sort();
|
|
375
|
+
expect(ids).toEqual(['r2', 'r3', 'r4']);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it('ctrl-click toggles a single row without clearing others', async () => {
|
|
379
|
+
const options = baseOptions();
|
|
380
|
+
const { shadow } = await mountGrid(options);
|
|
381
|
+
const api = getApi(options);
|
|
382
|
+
dispatchMouse(cellIn(shadow, 'r1', 'selectionRowHeaderCol'), 'click');
|
|
383
|
+
await waitFor(() => (api.selection.getSelectedCount() === 1 ? {} : null));
|
|
384
|
+
dispatchMouse(cellIn(shadow, 'r3', 'selectionRowHeaderCol'), 'click', { ctrlKey: true });
|
|
385
|
+
await waitFor(() => (api.selection.getSelectedCount() === 2 ? {} : null));
|
|
386
|
+
const ids = api.selection.getSelectedRows().map((r: any) => r.id).sort();
|
|
387
|
+
expect(ids).toEqual(['r1', 'r3']);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it('full-row click selects when enableFullRowSelection is true', async () => {
|
|
391
|
+
const options = baseOptions({ enableFullRowSelection: true });
|
|
392
|
+
const { shadow } = await mountGrid(options);
|
|
393
|
+
const api = getApi(options);
|
|
394
|
+
dispatchMouse(cellIn(shadow, 'r2', 'name'), 'click');
|
|
395
|
+
await waitFor(() => (api.selection.getSelectedCount() === 1 ? {} : null));
|
|
396
|
+
expect(api.selection.getSelectedRows()[0]!['id']).toBe('r2');
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it('full-row click is ignored when enableFullRowSelection is false (default with header)', async () => {
|
|
400
|
+
const options = baseOptions();
|
|
401
|
+
const { shadow } = await mountGrid(options);
|
|
402
|
+
const api = getApi(options);
|
|
403
|
+
dispatchMouse(cellIn(shadow, 'r2', 'name'), 'click');
|
|
404
|
+
// No checkbox click — plain click on a data cell should not add to
|
|
405
|
+
// selection when full-row selection is off.
|
|
406
|
+
await new Promise((r) => window.setTimeout(r, 20));
|
|
407
|
+
expect(api.selection.getSelectedCount()).toBe(0);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it('full-row single click (mousedown + mouseup + click) toggles once, not twice', async () => {
|
|
411
|
+
// Regression: the drag-paint handler used to commit the starting row
|
|
412
|
+
// on mousedown AND then the click handler toggled again, producing a
|
|
413
|
+
// visible flicker and a net "no change" for single clicks. This test
|
|
414
|
+
// simulates the browser's full click sequence and asserts exactly one
|
|
415
|
+
// toggle ends up applied.
|
|
416
|
+
const options = baseOptions({ enableFullRowSelection: true });
|
|
417
|
+
const { shadow } = await mountGrid(options);
|
|
418
|
+
const api = getApi(options);
|
|
419
|
+
const events: number[] = [];
|
|
420
|
+
api.selection.on.rowSelectionChanged(() => {
|
|
421
|
+
events.push(api.selection.getSelectedCount());
|
|
422
|
+
});
|
|
423
|
+
const target = cellIn(shadow, 'r2', 'name');
|
|
424
|
+
dispatchMouse(target, 'mousedown');
|
|
425
|
+
window.dispatchEvent(new MouseEvent('mouseup', { bubbles: true }));
|
|
426
|
+
dispatchMouse(target, 'click');
|
|
427
|
+
await waitFor(() => (api.selection.getSelectedCount() === 1 ? {} : null));
|
|
428
|
+
// Exactly one selection-change event should fire. Two events (0→1→0)
|
|
429
|
+
// or zero events would both indicate flicker/re-entry.
|
|
430
|
+
expect(events).toEqual([1]);
|
|
431
|
+
expect(api.selection.getSelectedRows()[0]!['id']).toBe('r2');
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
it('click-drag paints selection across multiple rows', async () => {
|
|
435
|
+
const options = baseOptions({ enableFullRowSelection: true });
|
|
436
|
+
const { shadow } = await mountGrid(options);
|
|
437
|
+
const api = getApi(options);
|
|
438
|
+
dispatchMouse(cellIn(shadow, 'r1', 'name'), 'mousedown');
|
|
439
|
+
// Simulate dragging across r2 and r3.
|
|
440
|
+
window.dispatchEvent(
|
|
441
|
+
new MouseEvent('mousemove', { bubbles: true, composed: true, shiftKey: false }),
|
|
442
|
+
);
|
|
443
|
+
// jsdom's dispatchEvent won't route through composedPath to the right
|
|
444
|
+
// cell on a window-level event — simulate by dispatching on each cell
|
|
445
|
+
// with a mousemove that bubbles.
|
|
446
|
+
cellIn(shadow, 'r2', 'name').dispatchEvent(
|
|
447
|
+
new MouseEvent('mousemove', { bubbles: true, composed: true }),
|
|
448
|
+
);
|
|
449
|
+
cellIn(shadow, 'r3', 'name').dispatchEvent(
|
|
450
|
+
new MouseEvent('mousemove', { bubbles: true, composed: true }),
|
|
451
|
+
);
|
|
452
|
+
window.dispatchEvent(new MouseEvent('mouseup', { bubbles: true }));
|
|
453
|
+
const ids = api.selection.getSelectedRows().map((r: any) => r.id).sort();
|
|
454
|
+
// Drag guarantees start row is selected; the intermediate mousemoves
|
|
455
|
+
// paint the others as the window-level listener picks them up.
|
|
456
|
+
expect(ids).toContain('r1');
|
|
457
|
+
});
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
describe('keyboard interactions', () => {
|
|
461
|
+
it('Space on the row-header checkbox toggles selection', async () => {
|
|
462
|
+
const options = baseOptions();
|
|
463
|
+
const { shadow } = await mountGrid(options);
|
|
464
|
+
const api = getApi(options);
|
|
465
|
+
const cell = cellIn(shadow, 'r2', 'selectionRowHeaderCol');
|
|
466
|
+
cell.focus();
|
|
467
|
+
pressKey(cell, ' ');
|
|
468
|
+
expect(api.selection.getSelectedCount()).toBe(1);
|
|
469
|
+
expect(api.selection.getSelectedRows()[0]!['id']).toBe('r2');
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it('Ctrl+A selects all rows', async () => {
|
|
473
|
+
const options = baseOptions();
|
|
474
|
+
const { shadow } = await mountGrid(options);
|
|
475
|
+
const api = getApi(options);
|
|
476
|
+
const cell = cellIn(shadow, 'r1', 'name');
|
|
477
|
+
cell.focus();
|
|
478
|
+
pressKey(cell, 'a', { ctrlKey: true });
|
|
479
|
+
expect(api.selection.getSelectedCount()).toBe(4);
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
it('Ctrl+A is a no-op when multiSelect is false', async () => {
|
|
483
|
+
const options = baseOptions({ multiSelect: false });
|
|
484
|
+
const { shadow } = await mountGrid(options);
|
|
485
|
+
const api = getApi(options);
|
|
486
|
+
const cell = cellIn(shadow, 'r1', 'name');
|
|
487
|
+
cell.focus();
|
|
488
|
+
pressKey(cell, 'a', { ctrlKey: true });
|
|
489
|
+
expect(api.selection.getSelectedCount()).toBe(0);
|
|
490
|
+
});
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
describe('select-all header checkbox', () => {
|
|
494
|
+
it('clicking the header select-all checkbox selects every visible row', async () => {
|
|
495
|
+
const options = baseOptions();
|
|
496
|
+
const { shadow } = await mountGrid(options);
|
|
497
|
+
const api = getApi(options);
|
|
498
|
+
const header = shadow.querySelector<HTMLElement>(
|
|
499
|
+
'.header-cell[data-column="selectionRowHeaderCol"] [data-action="select-all"]',
|
|
500
|
+
);
|
|
501
|
+
expect(header).not.toBeNull();
|
|
502
|
+
header!.click();
|
|
503
|
+
await waitFor(() => (api.selection.getSelectedCount() === 4 ? {} : null));
|
|
504
|
+
expect(api.selection.getSelectAllState()).toBe(true);
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
it('clicking again clears the selection', async () => {
|
|
508
|
+
const options = baseOptions();
|
|
509
|
+
const { shadow } = await mountGrid(options);
|
|
510
|
+
const api = getApi(options);
|
|
511
|
+
const findHeader = (): HTMLElement | null =>
|
|
512
|
+
shadow.querySelector<HTMLElement>(
|
|
513
|
+
'.header-cell[data-column="selectionRowHeaderCol"] [data-action="select-all"]',
|
|
514
|
+
);
|
|
515
|
+
findHeader()!.click();
|
|
516
|
+
await waitFor(() => (api.selection.getSelectedCount() === 4 ? {} : null));
|
|
517
|
+
// Re-query: the header markup is rebuilt on refresh, so the prior
|
|
518
|
+
// button reference is detached from the DOM.
|
|
519
|
+
findHeader()!.click();
|
|
520
|
+
await waitFor(() => (api.selection.getSelectedCount() === 0 ? {} : null));
|
|
521
|
+
expect(api.selection.getSelectAllState()).toBe(false);
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
it('is hidden when enableSelectAll is false', async () => {
|
|
525
|
+
const options = baseOptions({ enableSelectAll: false });
|
|
526
|
+
const { shadow } = await mountGrid(options);
|
|
527
|
+
expect(
|
|
528
|
+
shadow.querySelector(
|
|
529
|
+
'.header-cell[data-column="selectionRowHeaderCol"] [data-action="select-all"]',
|
|
530
|
+
),
|
|
531
|
+
).toBeNull();
|
|
532
|
+
});
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
describe('state preservation across refresh', () => {
|
|
536
|
+
it('selected rows remain selected across a data refresh with the same ids', async () => {
|
|
537
|
+
const options = baseOptions();
|
|
538
|
+
const { shadow } = await mountGrid(options);
|
|
539
|
+
const api = getApi(options);
|
|
540
|
+
api.selection.selectRow(options.data[0]! as unknown as Record<string, unknown>);
|
|
541
|
+
api.selection.selectRow(options.data[2]! as unknown as Record<string, unknown>);
|
|
542
|
+
await waitFor(() => (api.selection.getSelectedCount() === 2 ? {} : null));
|
|
543
|
+
api.core.refresh();
|
|
544
|
+
await new Promise((r) => window.setTimeout(r, 20));
|
|
545
|
+
expect(api.selection.getSelectedCount()).toBe(2);
|
|
546
|
+
expect(cellIn(shadow, 'r1', 'name').classList.contains('ui-grid-row-selected')).toBe(true);
|
|
547
|
+
expect(cellIn(shadow, 'r3', 'name').classList.contains('ui-grid-row-selected')).toBe(true);
|
|
548
|
+
});
|
|
549
|
+
});
|
|
550
|
+
});
|