@ornery/ui-grid-vanilla 5.0.2 → 5.0.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/adopt-styles.d.ts +1 -0
- package/dist/adopt-styles.d.ts.map +1 -1
- package/dist/browser/ui-grid-element.js +254 -73
- package/dist/components/grid-filter-cell/grid-filter-cell.template.d.ts.map +1 -1
- package/dist/components/grid-group-row/grid-group-row.d.ts +3 -0
- package/dist/components/grid-group-row/grid-group-row.d.ts.map +1 -1
- package/dist/components/grid-group-row/grid-group-row.template.d.ts +1 -0
- package/dist/components/grid-group-row/grid-group-row.template.d.ts.map +1 -1
- package/dist/framework-slots.d.ts +2 -0
- package/dist/framework-slots.d.ts.map +1 -1
- package/dist/grid-controller.d.ts +7 -0
- package/dist/grid-controller.d.ts.map +1 -1
- package/dist/index.js +234 -65
- package/dist/index.mjs +236 -66
- package/dist/pagination-range-label.d.ts +8 -0
- package/dist/pagination-range-label.d.ts.map +1 -0
- package/dist/patch.d.ts +1 -1
- package/dist/patch.d.ts.map +1 -1
- package/dist/render.d.ts.map +1 -1
- package/dist/templates/index.d.ts +2 -1
- package/dist/templates/index.d.ts.map +1 -1
- package/dist/ui-grid-standalone.element.d.ts +10 -0
- package/dist/ui-grid-standalone.element.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/adopt-styles.ts +31 -1
- package/src/auto-resize.test.ts +4 -2
- package/src/components/grid-filter-cell/grid-filter-cell.template.ts +4 -3
- package/src/components/grid-group-row/grid-group-row.template.ts +6 -4
- package/src/components/grid-group-row/grid-group-row.ts +5 -1
- package/src/framework-slots.ts +38 -17
- package/src/grid-controller.ts +79 -12
- package/src/group-row.test.ts +66 -0
- package/src/index.integration.test.ts +32 -13
- package/src/pagination-range-label.test.ts +85 -0
- package/src/pagination-range-label.ts +10 -0
- package/src/pagination-range.test.ts +60 -0
- package/src/patch.ts +31 -9
- package/src/render.pagination.test.ts +37 -0
- package/src/render.ts +34 -10
- package/src/row-height.test.ts +52 -0
- package/src/scrollbar-compensation.test.ts +52 -0
- package/src/selection-pinning.test.ts +39 -0
- package/src/slot-updates.test.ts +137 -0
- package/src/templates/index.ts +8 -4
- package/src/theming-hooks.test.ts +66 -0
- package/src/ui-grid-empty.template.ts +1 -1
- package/src/ui-grid-shell.template.ts +2 -2
- package/src/ui-grid-standalone.element.ts +41 -2
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { mountVanillaUiGrid, type UiGridApi } from './index';
|
|
4
|
+
|
|
5
|
+
async function waitFor<T>(resolve: () => T | null | undefined, timeoutMs = 5000): Promise<T> {
|
|
6
|
+
const startedAt = Date.now();
|
|
7
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
8
|
+
const value = resolve();
|
|
9
|
+
if (value) return value;
|
|
10
|
+
await new Promise((next) => window.setTimeout(next, 20));
|
|
11
|
+
}
|
|
12
|
+
throw new Error('Timed out waiting for expected element state');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('group row', () => {
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
document.body.innerHTML = '<div id="app"></div>';
|
|
18
|
+
});
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
document.body.innerHTML = '';
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('shows the column header, the grouped value and a singular suffix', async () => {
|
|
24
|
+
let api: UiGridApi | undefined;
|
|
25
|
+
const grid = await mountVanillaUiGrid(
|
|
26
|
+
document.getElementById('app')!,
|
|
27
|
+
{
|
|
28
|
+
id: 'group-row',
|
|
29
|
+
enableGrouping: true,
|
|
30
|
+
grouping: { groupBy: ['current_status'] },
|
|
31
|
+
data: [
|
|
32
|
+
{ id: 'a', done: true },
|
|
33
|
+
{ id: 'b', done: false },
|
|
34
|
+
{ id: 'c', done: false },
|
|
35
|
+
],
|
|
36
|
+
columnDefs: [
|
|
37
|
+
{
|
|
38
|
+
name: 'current_status',
|
|
39
|
+
displayName: 'Status',
|
|
40
|
+
valueGetter: (row) => (row['done'] ? 'Finished <b>' : 'Open'),
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
onRegisterApi: (registered) => {
|
|
44
|
+
api = registered as UiGridApi;
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
undefined,
|
|
48
|
+
'ui-grid-element-group-row-test',
|
|
49
|
+
);
|
|
50
|
+
await waitFor(() => api);
|
|
51
|
+
const rows = await waitFor(() => {
|
|
52
|
+
const found = [...(grid.shadowRoot?.querySelectorAll('ui-grid-group-row') ?? [])];
|
|
53
|
+
return found.length === 2 && found.every((row) => row.shadowRoot?.textContent) ? found : null;
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const text = rows.map((row) => [
|
|
57
|
+
row.shadowRoot!.querySelector('strong')!.textContent,
|
|
58
|
+
row.shadowRoot!.querySelector('strong + span')!.textContent,
|
|
59
|
+
]);
|
|
60
|
+
expect(text).toEqual([
|
|
61
|
+
['Status: Finished <b>', '1 row'],
|
|
62
|
+
['Status: Open', '2 rows'],
|
|
63
|
+
]);
|
|
64
|
+
expect(rows[0].shadowRoot!.querySelector('b')).toBeNull();
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -181,9 +181,24 @@ describe('mountVanillaUiGrid integration', () => {
|
|
|
181
181
|
]);
|
|
182
182
|
});
|
|
183
183
|
|
|
184
|
-
it('
|
|
185
|
-
const enableSpy = vi.spyOn(core, 'enableUiGridWasmEngine')
|
|
186
|
-
|
|
184
|
+
it('never loads the wasm engine on its own', async () => {
|
|
185
|
+
const enableSpy = vi.spyOn(core, 'enableUiGridWasmEngine');
|
|
186
|
+
const controller = createVanillaGridController({
|
|
187
|
+
id: 'controller-wasm-opt-in',
|
|
188
|
+
data: [{ id: 'r1', name: 'Gamma' }],
|
|
189
|
+
columnDefs: [{ name: 'name' }],
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
controller.setFilter('name', 'Gamma');
|
|
193
|
+
await new Promise((resolveNext) => window.setTimeout(resolveNext, 20));
|
|
194
|
+
|
|
195
|
+
expect(enableSpy).not.toHaveBeenCalled();
|
|
196
|
+
expect(core.getWasmCoreInitializationState()).toBe('idle');
|
|
197
|
+
enableSpy.mockRestore();
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('switches to the wasm engine when a load the app started finishes', async () => {
|
|
201
|
+
const fakeRustEngine = () =>
|
|
187
202
|
core.registerRustWasmGridEngine({
|
|
188
203
|
buildPipeline(context) {
|
|
189
204
|
return {
|
|
@@ -200,7 +215,17 @@ describe('mountVanillaUiGrid integration', () => {
|
|
|
200
215
|
};
|
|
201
216
|
},
|
|
202
217
|
});
|
|
203
|
-
|
|
218
|
+
let finishLoad: (module: object) => void = () => undefined;
|
|
219
|
+
core.setUiGridWasmPackageLoader(
|
|
220
|
+
() =>
|
|
221
|
+
new Promise((resolveLoad) => {
|
|
222
|
+
finishLoad = (module) => {
|
|
223
|
+
fakeRustEngine();
|
|
224
|
+
resolveLoad(module);
|
|
225
|
+
};
|
|
226
|
+
}),
|
|
227
|
+
);
|
|
228
|
+
const enabled = core.enableUiGridWasmEngine();
|
|
204
229
|
|
|
205
230
|
const controller = createVanillaGridController({
|
|
206
231
|
id: 'controller-wasm-bootstrap',
|
|
@@ -212,22 +237,16 @@ describe('mountVanillaUiGrid integration', () => {
|
|
|
212
237
|
enableSorting: true,
|
|
213
238
|
enableFiltering: true,
|
|
214
239
|
});
|
|
240
|
+
expect(controller.getSnapshot().pipeline.pipelineMs).not.toBe(123);
|
|
215
241
|
|
|
216
|
-
|
|
217
|
-
|
|
242
|
+
finishLoad({});
|
|
243
|
+
await enabled;
|
|
218
244
|
await waitFor(() => {
|
|
219
245
|
const snapshot = controller.getSnapshot();
|
|
220
246
|
return activeGridEngineBackend() === 'rust-wasm' && snapshot.pipeline.pipelineMs === 123
|
|
221
247
|
? snapshot
|
|
222
248
|
: null;
|
|
223
249
|
});
|
|
224
|
-
|
|
225
|
-
controller.setFilter('name', 'Alpha');
|
|
226
|
-
controller.sortColumn('name', SORT_DIRECTIONS.asc);
|
|
227
|
-
|
|
228
|
-
expect(enableSpy).toHaveBeenCalledTimes(1);
|
|
229
|
-
expect(controller.getSnapshot().pipeline.pipelineMs).toBe(123);
|
|
230
|
-
enableSpy.mockRestore();
|
|
231
250
|
});
|
|
232
251
|
|
|
233
252
|
it('applies overridable SVG icons for controls', async () => {
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { clearRustWasmGridEngine } from '@ornery/ui-grid-core';
|
|
4
|
+
|
|
5
|
+
import { mountVanillaUiGrid, type GridOptions, type VanillaUiGridElement } from './index';
|
|
6
|
+
import { formatPaginationRangeLabel } from './pagination-range-label';
|
|
7
|
+
|
|
8
|
+
const TAG = 'ui-grid-element-vanilla-pagination-label-test';
|
|
9
|
+
|
|
10
|
+
interface Row extends Record<string, unknown> {
|
|
11
|
+
id: string;
|
|
12
|
+
name: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function waitFor<T>(resolve: () => T | null | undefined, timeoutMs = 5000): Promise<T> {
|
|
16
|
+
const startedAt = Date.now();
|
|
17
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
18
|
+
const value = resolve();
|
|
19
|
+
if (value) return value;
|
|
20
|
+
await new Promise((r) => window.setTimeout(r, 10));
|
|
21
|
+
}
|
|
22
|
+
throw new Error('Timed out waiting for expected state');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function externalPagedOptions(data: Row[], totalItems: number): GridOptions {
|
|
26
|
+
return {
|
|
27
|
+
id: 'pagination-label-grid',
|
|
28
|
+
data,
|
|
29
|
+
totalItems,
|
|
30
|
+
columnDefs: [{ name: 'name', displayName: 'Name' }],
|
|
31
|
+
enablePagination: true,
|
|
32
|
+
useExternalPagination: true,
|
|
33
|
+
paginationPageSizes: [10],
|
|
34
|
+
rowIdentity: (entity) => String((entity as Row).id),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function paginationLabel(grid: VanillaUiGridElement): string | null {
|
|
39
|
+
const pagination = grid.shadowRoot?.querySelector('ui-grid-pagination');
|
|
40
|
+
return pagination?.getAttribute('data-range-label') ?? null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe('pager range label', () => {
|
|
44
|
+
afterEach(() => {
|
|
45
|
+
clearRustWasmGridEngine();
|
|
46
|
+
document.body.replaceChildren();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('reads "0 of 0" after the grid updates from rows to zero rows', async () => {
|
|
50
|
+
const target = document.createElement('div');
|
|
51
|
+
document.body.appendChild(target);
|
|
52
|
+
const grid = await mountVanillaUiGrid(
|
|
53
|
+
target,
|
|
54
|
+
externalPagedOptions(
|
|
55
|
+
[
|
|
56
|
+
{ id: 'r1', name: 'Alpha' },
|
|
57
|
+
{ id: 'r2', name: 'Beta' },
|
|
58
|
+
],
|
|
59
|
+
2,
|
|
60
|
+
),
|
|
61
|
+
undefined,
|
|
62
|
+
TAG,
|
|
63
|
+
);
|
|
64
|
+
await waitFor(() => grid.shadowRoot?.querySelector('ui-grid-pagination'));
|
|
65
|
+
expect(paginationLabel(grid)).toBe('1-2 of 2');
|
|
66
|
+
|
|
67
|
+
grid.options = externalPagedOptions([], 0);
|
|
68
|
+
|
|
69
|
+
const label = await waitFor(() => {
|
|
70
|
+
const current = paginationLabel(grid);
|
|
71
|
+
return current && current !== '1-2 of 2' ? current : null;
|
|
72
|
+
});
|
|
73
|
+
expect(label).toBe('0 of 0');
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('formatPaginationRangeLabel', () => {
|
|
78
|
+
it('formats a populated page as a 1-based range', () => {
|
|
79
|
+
expect(formatPaginationRangeLabel({ firstRowIndex: 10, lastRowIndex: 19, totalItems: 42 })).toBe('11-20 of 42');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('formats an empty result as "0 of 0"', () => {
|
|
83
|
+
expect(formatPaginationRangeLabel({ firstRowIndex: 0, lastRowIndex: 0, totalItems: 0 })).toBe('0 of 0');
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Pager range label ("1-10 of 42"). Shared by the initial render and the
|
|
2
|
+
* patch path so both show the same text for empty results ("0 of 0"). */
|
|
3
|
+
export function formatPaginationRangeLabel(range: {
|
|
4
|
+
firstRowIndex: number;
|
|
5
|
+
lastRowIndex: number;
|
|
6
|
+
totalItems: number;
|
|
7
|
+
}): string {
|
|
8
|
+
if (range.totalItems === 0) return '0 of 0';
|
|
9
|
+
return `${range.firstRowIndex + 1}-${range.lastRowIndex + 1} of ${range.totalItems}`;
|
|
10
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { createVanillaGridController } from './grid-controller';
|
|
4
|
+
|
|
5
|
+
const page = (n: number) => Array.from({ length: n }, (_, i) => ({ id: `r${i}`, name: `Row ${i}` }));
|
|
6
|
+
|
|
7
|
+
describe('pagination range', () => {
|
|
8
|
+
it('describes the server page within totalItems under external pagination', () => {
|
|
9
|
+
const controller = createVanillaGridController({
|
|
10
|
+
id: 'external-range',
|
|
11
|
+
data: page(10),
|
|
12
|
+
columnDefs: [{ name: 'name' }],
|
|
13
|
+
enablePagination: true,
|
|
14
|
+
useExternalPagination: true,
|
|
15
|
+
paginationPageSize: 10,
|
|
16
|
+
totalItems: 12,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
let snapshot = controller.getSnapshot();
|
|
20
|
+
expect([snapshot.firstRowIndex, snapshot.lastRowIndex]).toEqual([0, 9]);
|
|
21
|
+
|
|
22
|
+
controller.setOptions({ ...controller.getSnapshot().options, data: page(2) });
|
|
23
|
+
controller.seekPage(2);
|
|
24
|
+
snapshot = controller.getSnapshot();
|
|
25
|
+
expect([snapshot.firstRowIndex, snapshot.lastRowIndex]).toEqual([10, 11]);
|
|
26
|
+
expect(snapshot.pipeline.visibleRows).toHaveLength(2);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('honors a declarative server page while preserving interactive page changes', () => {
|
|
30
|
+
const options = {
|
|
31
|
+
id: 'controlled-range', data: page(2), columnDefs: [{ name: 'name' }],
|
|
32
|
+
enablePagination: true, useExternalPagination: true, paginationPageSize: 10,
|
|
33
|
+
paginationCurrentPage: 2, totalItems: 32,
|
|
34
|
+
};
|
|
35
|
+
const controller = createVanillaGridController(options);
|
|
36
|
+
expect(controller.getSnapshot().firstRowIndex).toBe(10);
|
|
37
|
+
expect(controller.gridApi.pagination.getFirstRowIndex()).toBe(10);
|
|
38
|
+
controller.seekPage(3);
|
|
39
|
+
controller.setOptions({ ...options, data: page(10) });
|
|
40
|
+
expect(controller.getSnapshot().firstRowIndex).toBe(20);
|
|
41
|
+
controller.setOptions({ ...options, paginationCurrentPage: 4 });
|
|
42
|
+
expect(controller.getSnapshot().firstRowIndex).toBe(30);
|
|
43
|
+
expect(controller.gridApi.pagination.getLastRowIndex()).toBe(31);
|
|
44
|
+
controller.dispose();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('keeps client-side ranges unchanged', () => {
|
|
48
|
+
const controller = createVanillaGridController({
|
|
49
|
+
id: 'client-range',
|
|
50
|
+
data: page(12),
|
|
51
|
+
columnDefs: [{ name: 'name' }],
|
|
52
|
+
enablePagination: true,
|
|
53
|
+
paginationPageSize: 10,
|
|
54
|
+
});
|
|
55
|
+
controller.seekPage(2);
|
|
56
|
+
const snapshot = controller.getSnapshot();
|
|
57
|
+
expect([snapshot.firstRowIndex, snapshot.lastRowIndex]).toEqual([10, 11]);
|
|
58
|
+
expect(snapshot.pipeline.visibleRows).toHaveLength(2);
|
|
59
|
+
});
|
|
60
|
+
});
|
package/src/patch.ts
CHANGED
|
@@ -31,9 +31,11 @@ import {
|
|
|
31
31
|
import {
|
|
32
32
|
bodyStaticMarkup,
|
|
33
33
|
bodyVirtualMarkup,
|
|
34
|
+
virtualSpacerStyle,
|
|
34
35
|
emptyDataMarkup,
|
|
35
36
|
} from './templates';
|
|
36
37
|
import type { UiGridStandaloneElement } from './ui-grid-standalone.element';
|
|
38
|
+
import { formatPaginationRangeLabel } from './pagination-range-label';
|
|
37
39
|
import { bodyCellClass } from './utils/cell-class';
|
|
38
40
|
import { asGroupItem, isRowItem } from './utils/display-items';
|
|
39
41
|
import {
|
|
@@ -239,6 +241,7 @@ export function reconcileBodyRoot(
|
|
|
239
241
|
templateColumns: string,
|
|
240
242
|
virtualOffset: number,
|
|
241
243
|
totalVirtualHeight: number,
|
|
244
|
+
rowSize: number,
|
|
242
245
|
): HTMLElement | null {
|
|
243
246
|
const bodyViewport = root.querySelector<HTMLElement>('.grid-body-viewport');
|
|
244
247
|
if (!bodyViewport) return null;
|
|
@@ -283,7 +286,7 @@ export function reconcileBodyRoot(
|
|
|
283
286
|
return currentVirtual.querySelector<HTMLElement>('.grid-virtual-body');
|
|
284
287
|
}
|
|
285
288
|
const fresh = createFromMarkup(
|
|
286
|
-
bodyVirtualMarkup(templateColumns, totalVirtualHeight, virtualOffset, ''),
|
|
289
|
+
bodyVirtualMarkup(templateColumns, totalVirtualHeight, virtualOffset, '', rowSize),
|
|
287
290
|
);
|
|
288
291
|
if (fresh) swapBodyChild(bodyViewport, currentNode, fresh);
|
|
289
292
|
return fresh?.querySelector<HTMLElement>('.grid-virtual-body') ?? null;
|
|
@@ -342,16 +345,29 @@ export function patchFilterCells(
|
|
|
342
345
|
): void {
|
|
343
346
|
const controller = el.controller!;
|
|
344
347
|
const existing = new Map<string, HTMLElement>();
|
|
345
|
-
for (const e of filterGrid.querySelectorAll<HTMLElement>(
|
|
348
|
+
for (const e of filterGrid.querySelectorAll<HTMLElement>(
|
|
349
|
+
'ui-grid-filter-cell[data-column], .ui-grid-filter-container[data-column]',
|
|
350
|
+
)) {
|
|
346
351
|
const column = e.dataset['column'];
|
|
347
352
|
if (column) existing.set(column, e);
|
|
348
353
|
}
|
|
349
354
|
|
|
350
355
|
for (const column of snapshot.visibleColumns) {
|
|
351
|
-
const target = existing.get(column.name);
|
|
352
|
-
if (!target) continue;
|
|
353
356
|
const value = snapshot.activeFilters[column.name] ?? '';
|
|
354
357
|
const canFilter = controller.isColumnFilterable(column);
|
|
358
|
+
if (el.frameworkSlots.hasFilter(column.name)) {
|
|
359
|
+
// Framework-rendered filter cells keep their <slot> placeholder; re-stage
|
|
360
|
+
// so the flush does not report the slot as removed.
|
|
361
|
+
el.frameworkSlots.stageFilter(column, value, controller.filterPlaceholder(column), !canFilter);
|
|
362
|
+
const target = existing.get(column.name);
|
|
363
|
+
const pinOffset = controller.isPinned(column) ? controller.pinnedOffset(column) : null;
|
|
364
|
+
if (target) {
|
|
365
|
+
setAttr(target, 'style', pinOffset ? `${pinOffset.side}:${pinOffset.offset};` : '');
|
|
366
|
+
}
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
const target = existing.get(column.name);
|
|
370
|
+
if (!target) continue;
|
|
355
371
|
const pinOffset = controller.isPinned(column) ? controller.pinnedOffset(column) : null;
|
|
356
372
|
const stickyStyle = pinOffset ? `${pinOffset.side}:${pinOffset.offset};` : '';
|
|
357
373
|
setAttr(target, 'data-value', value);
|
|
@@ -398,6 +414,7 @@ export function patchBodyRegion(
|
|
|
398
414
|
templateColumns,
|
|
399
415
|
virtualOffset,
|
|
400
416
|
totalVirtualHeight,
|
|
417
|
+
snapshot.rowSize,
|
|
401
418
|
);
|
|
402
419
|
|
|
403
420
|
if (!hasRows) {
|
|
@@ -409,8 +426,9 @@ export function patchBodyRegion(
|
|
|
409
426
|
|
|
410
427
|
if (virtualizationEnabled) {
|
|
411
428
|
const spacer = root.querySelector<HTMLElement>('.grid-virtual-spacer');
|
|
412
|
-
|
|
413
|
-
|
|
429
|
+
const spacerStyle = virtualSpacerStyle(totalVirtualHeight, snapshot.rowSize);
|
|
430
|
+
if (spacer && spacer.getAttribute('style') !== spacerStyle) {
|
|
431
|
+
spacer.setAttribute('style', spacerStyle);
|
|
414
432
|
el.lastTotalVirtualHeight = totalVirtualHeight;
|
|
415
433
|
}
|
|
416
434
|
if (bodyContainer) {
|
|
@@ -685,19 +703,19 @@ export function patchGroupRow(
|
|
|
685
703
|
target: HTMLElement,
|
|
686
704
|
group: GroupItem,
|
|
687
705
|
): void {
|
|
688
|
-
const snapshot = el.snapshot!;
|
|
689
706
|
const controller = el.controller!;
|
|
690
707
|
const iconKey = group.collapsed ? 'groupCollapsed' : 'groupExpanded';
|
|
691
708
|
const icon = el.icons.resolve(iconKey);
|
|
692
709
|
setAttr(target, 'data-collapsed', group.collapsed ? 'true' : 'false');
|
|
693
710
|
setAttr(target, 'data-field', group.field);
|
|
711
|
+
setAttr(target, 'data-column-label', controller.groupColumnLabel(group));
|
|
694
712
|
setAttr(target, 'data-label', group.label);
|
|
695
713
|
setAttr(target, 'data-count', String(group.count));
|
|
696
714
|
setAttr(target, 'data-depth', String(group.depth));
|
|
697
715
|
setAttr(target, 'data-disclosure-label', controller.groupDisclosureLabel(group));
|
|
698
716
|
setAttr(target, 'data-icon-path', icon.path);
|
|
699
717
|
setAttr(target, 'data-icon-view-box', icon.viewBox ?? '0 0 24 24');
|
|
700
|
-
setAttr(target, 'data-rows-suffix',
|
|
718
|
+
setAttr(target, 'data-rows-suffix', controller.groupRowsSuffix(group));
|
|
701
719
|
}
|
|
702
720
|
|
|
703
721
|
/** Patch a body-cell element in place: class, sticky style, data-attrs,
|
|
@@ -811,7 +829,11 @@ export function patchPagination(
|
|
|
811
829
|
setAttr(
|
|
812
830
|
paginationEl,
|
|
813
831
|
'data-range-label',
|
|
814
|
-
|
|
832
|
+
formatPaginationRangeLabel({
|
|
833
|
+
firstRowIndex: snapshot.firstRowIndex,
|
|
834
|
+
lastRowIndex: snapshot.lastRowIndex,
|
|
835
|
+
totalItems: snapshot.pipeline.totalItems,
|
|
836
|
+
}),
|
|
815
837
|
);
|
|
816
838
|
setAttr(paginationEl, 'data-current-page', String(snapshot.currentPage));
|
|
817
839
|
setAttr(paginationEl, 'data-total-pages', String(snapshot.totalPages));
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import type { GridControllerSnapshot } from './grid-controller';
|
|
3
|
+
import type { UiGridStandaloneElement } from './ui-grid-standalone.element';
|
|
4
|
+
import { renderPagination } from './render';
|
|
5
|
+
|
|
6
|
+
const icon = { path: 'M0 0', viewBox: '0 0 24 24' };
|
|
7
|
+
const el = { icons: { resolve: () => icon } } as unknown as UiGridStandaloneElement;
|
|
8
|
+
|
|
9
|
+
function snapshotWith(overrides: Partial<GridControllerSnapshot>): GridControllerSnapshot {
|
|
10
|
+
return {
|
|
11
|
+
options: { enablePagination: true, useExternalPagination: true, paginationPageSizes: [10] },
|
|
12
|
+
labels: {
|
|
13
|
+
paginationPage: 'Page',
|
|
14
|
+
paginationOf: 'of',
|
|
15
|
+
paginationPrevious: 'Previous',
|
|
16
|
+
paginationNext: 'Next',
|
|
17
|
+
paginationRows: 'Rows',
|
|
18
|
+
},
|
|
19
|
+
pipeline: { totalItems: 0 },
|
|
20
|
+
currentPage: 1,
|
|
21
|
+
totalPages: 1,
|
|
22
|
+
firstRowIndex: 0,
|
|
23
|
+
lastRowIndex: 0,
|
|
24
|
+
pageSize: 10,
|
|
25
|
+
...overrides,
|
|
26
|
+
} as unknown as GridControllerSnapshot;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('renderPagination range label', () => {
|
|
30
|
+
it('shows "0 of 0" with previous and next disabled when there are no rows', () => {
|
|
31
|
+
const markup = renderPagination(el, snapshotWith({}));
|
|
32
|
+
|
|
33
|
+
expect(markup).toContain('data-range-label="0 of 0"');
|
|
34
|
+
expect(markup).toContain('data-prev-disabled="true"');
|
|
35
|
+
expect(markup).toContain('data-next-disabled="true"');
|
|
36
|
+
});
|
|
37
|
+
});
|
package/src/render.ts
CHANGED
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
slotRegistryMarkup,
|
|
43
43
|
treeToggleMarkup,
|
|
44
44
|
} from './templates';
|
|
45
|
+
import { formatPaginationRangeLabel } from './pagination-range-label';
|
|
45
46
|
import type { UiGridStandaloneElement } from './ui-grid-standalone.element';
|
|
46
47
|
import { bodyCellClass, headerCellClass } from './utils/cell-class';
|
|
47
48
|
import { asGroupItem, isRowItem } from './utils/display-items';
|
|
@@ -70,7 +71,12 @@ export function renderHeaderCell(
|
|
|
70
71
|
const inner = showSelectAll
|
|
71
72
|
? `<button type="button" class="ui-grid-selection-select-all" data-action="select-all" aria-label="Select all rows"${selectAll ? ' aria-checked="true"' : ' aria-checked="false"'}><span class="ui-grid-selection-checkbox${selectAll ? ' ui-grid-selection-checkbox-checked' : ''}"></span></button>`
|
|
72
73
|
: '';
|
|
73
|
-
|
|
74
|
+
const pinOffset = controller.isPinned(column) ? controller.pinnedOffset(column) : null;
|
|
75
|
+
const pinClass = pinOffset
|
|
76
|
+
? ` is-pinned${controller.isPinnedLeftLast(column) ? ' is-pinned-left-last' : ''}`
|
|
77
|
+
: '';
|
|
78
|
+
const stickyStyle = pinOffset ? ` style="${escapeHtml(`${pinOffset.side}:${pinOffset.offset};`)}"` : '';
|
|
79
|
+
return `<ui-grid-header-cell part="header-cell" class="header-cell ui-grid-selection-row-header${pinClass}"${stickyStyle} data-column="selectionRowHeaderCol">${inner}</ui-grid-header-cell>`;
|
|
74
80
|
}
|
|
75
81
|
|
|
76
82
|
// Framework-rendered header: emit a bare cell with a slot placeholder.
|
|
@@ -82,7 +88,16 @@ export function renderHeaderCell(
|
|
|
82
88
|
const isPinned = controller.isPinned(column);
|
|
83
89
|
const pinOffset = isPinned ? controller.pinnedOffset(column) : null;
|
|
84
90
|
const stickyStyle = pinOffset ? `${pinOffset.side}:${pinOffset.offset};` : '';
|
|
85
|
-
|
|
91
|
+
const slotClassName = headerCellClass(
|
|
92
|
+
false,
|
|
93
|
+
isPinned,
|
|
94
|
+
controller.isPinnedLeftLast(column),
|
|
95
|
+
controller.isPinnedRightFirst(column),
|
|
96
|
+
false,
|
|
97
|
+
false,
|
|
98
|
+
false,
|
|
99
|
+
);
|
|
100
|
+
return `<ui-grid-header-cell part="header-cell" class="${slotClassName}"${stickyStyle ? ` style="${escapeHtml(stickyStyle)}"` : ''} data-column="${escapeHtml(column.name)}" data-pinned="${isPinned}" data-sticky-style="${escapeHtml(stickyStyle)}"><slot name="${escapeHtml(slotName)}"></slot></ui-grid-header-cell>`;
|
|
86
101
|
}
|
|
87
102
|
|
|
88
103
|
const sortDirection = controller.getSortDirection(column);
|
|
@@ -128,7 +143,7 @@ export function renderHeaderCell(
|
|
|
128
143
|
);
|
|
129
144
|
// className/style/draggable are pre-computed here so the <ui-grid-header-cell>
|
|
130
145
|
// upgrade is a no-op at parse time. `data-*` stay for event delegation.
|
|
131
|
-
return `<ui-grid-header-cell class="${className}"${isDraggable ? ' draggable="true"' : ''}${stickyStyle ? ` style="${escapeHtml(stickyStyle)}"` : ''} data-column="${escapeHtml(column.name)}" data-sort-active="${sortDirection !== SORT_DIRECTIONS.none}" data-pinned="${isPinned}" data-pinned-left-last="${isPinnedLeftLast}" data-pinned-right-first="${isPinnedRightFirst}" data-pin-menu-open="${isPinMenuOpen}" data-drag-target="${isDragTarget}" data-dragging="${isDragging}" data-draggable="${isDraggable}" data-sticky-style="${escapeHtml(stickyStyle)}"><span class="header-label">${headerValue}</span><span class="header-actions">${canSort ? `<button type="button" class="header-action" data-action="sort" data-column="${escapeHtml(column.name)}" aria-label="${escapeHtml(sortLabel)}">${el.icons.renderControlIcon(sortIconKey)}<span class="sr-only">${escapeHtml(sortLabel)}</span></button>` : ''}${canGroup ? `<button type="button" class="chip-action${controller.isColumnGrouped(column) ? ' chip-action-active' : ''}" data-action="group" data-column="${escapeHtml(column.name)}" aria-label="${escapeHtml(groupingLabel)}">${el.icons.renderControlIcon('group')}<span class="sr-only">${escapeHtml(groupingLabel)}</span></button>` : ''}${canPin ? `<div class="pin-control${isPinMenuOpen ? ' pin-control-open' : ''}"><button type="button" class="chip-action pin-trigger${isPinned ? ' chip-action-active' : ''}" data-action="pin-trigger" data-column="${escapeHtml(column.name)}" aria-label="${escapeHtml(pinLabel)}">${el.icons.renderControlIcon('pin')}<span class="sr-only">${escapeHtml(pinLabel)}</span></button><div class="pin-menu" role="menu" aria-label="${escapeHtml(pinLabel)}"><button type="button" class="pin-menu-action" data-action="pin-left" data-column="${escapeHtml(column.name)}" aria-label="${escapeHtml(el.snapshot?.labels.pinLeft ?? 'Pin left')}">${el.icons.renderIconWithClass('control-icon', 'pinLeft')}<span class="sr-only">${escapeHtml(el.snapshot?.labels.pinLeft ?? 'Pin left')}</span></button><button type="button" class="pin-menu-action" data-action="pin-right" data-column="${escapeHtml(column.name)}" aria-label="${escapeHtml(el.snapshot?.labels.pinRight ?? 'Pin right')}">${el.icons.renderIconWithClass('control-icon', 'pinRight')}<span class="sr-only">${escapeHtml(el.snapshot?.labels.pinRight ?? 'Pin right')}</span></button></div></div>` : ''}</span>${resizerHtml}</ui-grid-header-cell>`;
|
|
146
|
+
return `<ui-grid-header-cell part="header-cell" class="${className}"${isDraggable ? ' draggable="true"' : ''}${stickyStyle ? ` style="${escapeHtml(stickyStyle)}"` : ''} data-column="${escapeHtml(column.name)}" data-sort-active="${sortDirection !== SORT_DIRECTIONS.none}" data-pinned="${isPinned}" data-pinned-left-last="${isPinnedLeftLast}" data-pinned-right-first="${isPinnedRightFirst}" data-pin-menu-open="${isPinMenuOpen}" data-drag-target="${isDragTarget}" data-dragging="${isDragging}" data-draggable="${isDraggable}" data-sticky-style="${escapeHtml(stickyStyle)}"><span class="header-label" part="header-label">${headerValue}</span><span class="header-actions" part="header-actions">${canSort ? `<button type="button" class="header-action" data-action="sort" data-column="${escapeHtml(column.name)}" aria-label="${escapeHtml(sortLabel)}">${el.icons.renderControlIcon(sortIconKey)}<span class="sr-only">${escapeHtml(sortLabel)}</span></button>` : ''}${canGroup ? `<button type="button" class="chip-action${controller.isColumnGrouped(column) ? ' chip-action-active' : ''}" data-action="group" data-column="${escapeHtml(column.name)}" aria-label="${escapeHtml(groupingLabel)}">${el.icons.renderControlIcon('group')}<span class="sr-only">${escapeHtml(groupingLabel)}</span></button>` : ''}${canPin ? `<div class="pin-control${isPinMenuOpen ? ' pin-control-open' : ''}"><button type="button" class="chip-action pin-trigger${isPinned ? ' chip-action-active' : ''}" data-action="pin-trigger" data-column="${escapeHtml(column.name)}" aria-label="${escapeHtml(pinLabel)}">${el.icons.renderControlIcon('pin')}<span class="sr-only">${escapeHtml(pinLabel)}</span></button><div class="pin-menu" part="pin-menu" role="menu" aria-label="${escapeHtml(pinLabel)}"><button type="button" class="pin-menu-action" data-action="pin-left" data-column="${escapeHtml(column.name)}" aria-label="${escapeHtml(el.snapshot?.labels.pinLeft ?? 'Pin left')}">${el.icons.renderIconWithClass('control-icon', 'pinLeft')}<span class="sr-only">${escapeHtml(el.snapshot?.labels.pinLeft ?? 'Pin left')}</span></button><button type="button" class="pin-menu-action" data-action="pin-right" data-column="${escapeHtml(column.name)}" aria-label="${escapeHtml(el.snapshot?.labels.pinRight ?? 'Pin right')}">${el.icons.renderIconWithClass('control-icon', 'pinRight')}<span class="sr-only">${escapeHtml(el.snapshot?.labels.pinRight ?? 'Pin right')}</span></button></div></div>` : ''}</span>${resizerHtml}</ui-grid-header-cell>`;
|
|
132
147
|
}
|
|
133
148
|
|
|
134
149
|
/** Render the filter-row cell for one column. Selection column gets a bare
|
|
@@ -138,7 +153,8 @@ export function renderFilterCell(el: UiGridStandaloneElement, column: GridColumn
|
|
|
138
153
|
if (column.name === 'selectionRowHeaderCol') {
|
|
139
154
|
const pinOffset = el.controller!.isPinned(column) ? el.controller!.pinnedOffset(column) : null;
|
|
140
155
|
const stickyStyle = pinOffset ? `${pinOffset.side}:${pinOffset.offset};` : '';
|
|
141
|
-
|
|
156
|
+
const pinClass = pinOffset ? ' is-pinned' : '';
|
|
157
|
+
return `<div part="filter-cell" class="filter-cell ui-grid-selection-row-header${pinClass}" data-column="selectionRowHeaderCol"${stickyStyle ? ` style="${escapeHtml(stickyStyle)}"` : ''}></div>`;
|
|
142
158
|
}
|
|
143
159
|
const value = el.snapshot?.activeFilters[column.name] ?? '';
|
|
144
160
|
const controller = el.controller!;
|
|
@@ -149,10 +165,13 @@ export function renderFilterCell(el: UiGridStandaloneElement, column: GridColumn
|
|
|
149
165
|
if (el.frameworkSlots.hasFilter(column.name)) {
|
|
150
166
|
el.frameworkSlots.stageFilter(column, value, controller.filterPlaceholder(column), !canFilter);
|
|
151
167
|
const slotName = filterSlotName(column);
|
|
152
|
-
|
|
168
|
+
const pinClass = pinOffset
|
|
169
|
+
? ` is-pinned${controller.isPinnedLeftLast(column) ? ' is-pinned-left-last' : ''}${controller.isPinnedRightFirst(column) ? ' is-pinned-right-first' : ''}`
|
|
170
|
+
: '';
|
|
171
|
+
return `<div part="filter-cell" class="filter-cell ui-grid-filter-container${pinClass}" data-column="${escapeHtml(column.name)}"${stickyStyle ? ` style="${escapeHtml(stickyStyle)}"` : ''}><slot name="${escapeHtml(slotName)}"></slot></div>`;
|
|
153
172
|
}
|
|
154
173
|
|
|
155
|
-
return `<ui-grid-filter-cell data-column="${escapeHtml(column.name)}" data-value="${escapeHtml(value)}" data-placeholder="${escapeHtml(controller.filterPlaceholder(column))}" data-disabled="${!canFilter}" data-pinned="${controller.isPinned(column)}" data-pinned-left-last="${controller.isPinnedLeftLast(column)}" data-pinned-right-first="${controller.isPinnedRightFirst(column)}" data-sticky-style="${escapeHtml(stickyStyle)}"></ui-grid-filter-cell>`;
|
|
174
|
+
return `<ui-grid-filter-cell part="filter-cell" data-column="${escapeHtml(column.name)}" data-value="${escapeHtml(value)}" data-placeholder="${escapeHtml(controller.filterPlaceholder(column))}" data-disabled="${!canFilter}" data-pinned="${controller.isPinned(column)}" data-pinned-left-last="${controller.isPinnedLeftLast(column)}" data-pinned-right-first="${controller.isPinnedRightFirst(column)}" data-sticky-style="${escapeHtml(stickyStyle)}"></ui-grid-filter-cell>`;
|
|
156
175
|
}
|
|
157
176
|
|
|
158
177
|
/** Render one display-item: group row, expandable row, or the cells of a
|
|
@@ -169,12 +188,12 @@ export function renderDisplayItem(
|
|
|
169
188
|
if (el.frameworkSlots.hasGroupRow()) {
|
|
170
189
|
el.frameworkSlots.stageGroupRow(group);
|
|
171
190
|
const slotName = groupRowSlotName(group);
|
|
172
|
-
return `<div class="group-row ui-grid-row ui-grid-group-row" data-group="${escapeHtml(group.id)}" style="grid-column:1 / -1"><slot name="${escapeHtml(slotName)}"></slot></div>`;
|
|
191
|
+
return `<div part="group-row" class="group-row ui-grid-row ui-grid-group-row" data-group="${escapeHtml(group.id)}" style="grid-column:1 / -1"><slot name="${escapeHtml(slotName)}"></slot></div>`;
|
|
173
192
|
}
|
|
174
193
|
const iconKey = group.collapsed ? 'groupCollapsed' : 'groupExpanded';
|
|
175
194
|
const icon = el.icons.resolve(iconKey);
|
|
176
195
|
const disclosureLabel = el.controller.groupDisclosureLabel(group);
|
|
177
|
-
return `<ui-grid-group-row data-action="toggle-group" data-group="${escapeHtml(group.id)}" data-collapsed="${group.collapsed ? 'true' : 'false'}" data-field="${escapeHtml(group.field)}" data-label="${escapeHtml(group.label)}" data-count="${group.count}" data-depth="${group.depth}" data-disclosure-label="${escapeHtml(disclosureLabel)}" data-icon-path="${icon.path}" data-icon-view-box="${icon.viewBox ?? '0 0 24 24'}" data-rows-suffix="${escapeHtml(el.
|
|
196
|
+
return `<ui-grid-group-row part="group-row" data-action="toggle-group" data-group="${escapeHtml(group.id)}" data-collapsed="${group.collapsed ? 'true' : 'false'}" data-field="${escapeHtml(group.field)}" data-column-label="${escapeHtml(el.controller.groupColumnLabel(group))}" data-label="${escapeHtml(group.label)}" data-count="${group.count}" data-depth="${group.depth}" data-disclosure-label="${escapeHtml(disclosureLabel)}" data-icon-path="${icon.path}" data-icon-view-box="${icon.viewBox ?? '0 0 24 24'}" data-rows-suffix="${escapeHtml(el.controller.groupRowsSuffix(group))}"></ui-grid-group-row>`;
|
|
178
197
|
}
|
|
179
198
|
|
|
180
199
|
if (item.kind === 'expandable') {
|
|
@@ -268,7 +287,7 @@ export function renderBodyCell(
|
|
|
268
287
|
const titleAttr = isCellInvalid
|
|
269
288
|
? ` title="${escapeHtml(controller.gridApi.validate.getTitleFormattedErrors(row.entity, column))}"`
|
|
270
289
|
: '';
|
|
271
|
-
return `<ui-grid-body-cell class="${className}" tabindex="0"${stickyStyle ? ` style="${escapeHtml(stickyStyle)}"` : ''}${titleAttr} data-row="${escapeHtml(rowId)}" data-column="${escapeHtml(columnName)}" data-odd="${displayIndex % 2 !== 0}" data-align="${escapeHtml(align)}" data-pinned="${isPinned}" data-pinned-left-last="${isPinnedLeftLast}" data-pinned-right-first="${isPinnedRightFirst}" data-focused="${isFocused}" data-editing="${editing}" data-sticky-style="${escapeHtml(stickyStyle)}"><div class="cell-shell" style="padding-inline-start:${escapeHtml(controller.cellIndent(row, column))}">${treeToggle}${expandToggle}<div class="cell-content">${content}</div></div></ui-grid-body-cell>`;
|
|
290
|
+
return `<ui-grid-body-cell part="body-cell" class="${className}" tabindex="0"${stickyStyle ? ` style="${escapeHtml(stickyStyle)}"` : ''}${titleAttr} data-row="${escapeHtml(rowId)}" data-column="${escapeHtml(columnName)}" data-odd="${displayIndex % 2 !== 0}" data-align="${escapeHtml(align)}" data-pinned="${isPinned}" data-pinned-left-last="${isPinnedLeftLast}" data-pinned-right-first="${isPinnedRightFirst}" data-focused="${isFocused}" data-editing="${editing}" data-sticky-style="${escapeHtml(stickyStyle)}"><div class="cell-shell" style="padding-inline-start:${escapeHtml(controller.cellIndent(row, column))}">${treeToggle}${expandToggle}<div class="cell-content">${content}</div></div></ui-grid-body-cell>`;
|
|
272
291
|
}
|
|
273
292
|
|
|
274
293
|
/** Render the contents of `.cell-shell` (tree/expand toggles + the
|
|
@@ -331,7 +350,12 @@ export function renderPagination(
|
|
|
331
350
|
const pageSizes = snapshot.options.paginationPageSizes ?? [10, 25, 50, 100];
|
|
332
351
|
const prevIcon = el.icons.resolve('paginationPrev');
|
|
333
352
|
const nextIcon = el.icons.resolve('paginationNext');
|
|
334
|
-
|
|
353
|
+
const rangeLabel = formatPaginationRangeLabel({
|
|
354
|
+
firstRowIndex: snapshot.firstRowIndex,
|
|
355
|
+
lastRowIndex: snapshot.lastRowIndex,
|
|
356
|
+
totalItems: snapshot.pipeline.totalItems,
|
|
357
|
+
});
|
|
358
|
+
return `<ui-grid-pagination part="pagination" data-range-label="${escapeHtml(rangeLabel)}" data-current-page="${snapshot.currentPage}" data-total-pages="${snapshot.totalPages}" data-page-label="${escapeHtml(snapshot.labels.paginationPage)}" data-of-label="${escapeHtml(snapshot.labels.paginationOf)}" data-prev-label="${escapeHtml(snapshot.labels.paginationPrevious)}" data-next-label="${escapeHtml(snapshot.labels.paginationNext)}" data-rows-label="${escapeHtml(snapshot.labels.paginationRows)}" data-prev-icon-path="${prevIcon.path}" data-prev-icon-view-box="${prevIcon.viewBox ?? '0 0 24 24'}" data-next-icon-path="${nextIcon.path}" data-next-icon-view-box="${nextIcon.viewBox ?? '0 0 24 24'}" data-page-sizes="${escapeHtml(JSON.stringify(pageSizes))}" data-page-size="${snapshot.pageSize}" data-prev-disabled="${snapshot.currentPage <= 1}" data-next-disabled="${snapshot.currentPage >= snapshot.totalPages}"></ui-grid-pagination>`;
|
|
335
359
|
}
|
|
336
360
|
|
|
337
361
|
/** Hidden `<slot>` registry block so the element's shadow DOM exposes a
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { GRID_CORE_CSS } from '@ornery/ui-grid-core';
|
|
4
|
+
import { mountVanillaUiGrid, type UiGridApi } from './index';
|
|
5
|
+
|
|
6
|
+
async function waitFor<T>(resolve: () => T | null | undefined, timeoutMs = 5000): Promise<T> {
|
|
7
|
+
const startedAt = Date.now();
|
|
8
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
9
|
+
const value = resolve();
|
|
10
|
+
if (value) return value;
|
|
11
|
+
await new Promise((next) => window.setTimeout(next, 20));
|
|
12
|
+
}
|
|
13
|
+
throw new Error('Timed out waiting for expected element state');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe('virtual row height', () => {
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
document.body.innerHTML = '<div id="app"></div>';
|
|
19
|
+
});
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
document.body.innerHTML = '';
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('holds virtualized rows to rowHeight', async () => {
|
|
25
|
+
let api: UiGridApi | undefined;
|
|
26
|
+
const grid = await mountVanillaUiGrid(
|
|
27
|
+
document.getElementById('app')!,
|
|
28
|
+
{
|
|
29
|
+
id: 'row-height',
|
|
30
|
+
rowHeight: 60,
|
|
31
|
+
virtualizationThreshold: 5,
|
|
32
|
+
data: Array.from({ length: 50 }, (_, i) => ({ id: `r${i}`, name: `Row ${i}` })),
|
|
33
|
+
columnDefs: [{ name: 'name' }],
|
|
34
|
+
onRegisterApi: (registered) => {
|
|
35
|
+
api = registered as UiGridApi;
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
undefined,
|
|
39
|
+
'ui-grid-element-row-height-test',
|
|
40
|
+
);
|
|
41
|
+
await waitFor(() => api);
|
|
42
|
+
const spacer = await waitFor(() =>
|
|
43
|
+
grid.shadowRoot?.querySelector<HTMLElement>('.grid-virtual-spacer'),
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
expect(spacer.style.getPropertyValue('--ui-grid-row-height')).toBe('60px');
|
|
47
|
+
expect(spacer.style.height).toBe('3000px');
|
|
48
|
+
expect(GRID_CORE_CSS).toMatch(
|
|
49
|
+
/\.grid-virtual-body > :not\(\.expandable-row\) \{[^}]*height: var\(--ui-grid-row-height\)/,
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
});
|