@ornery/ui-grid-vanilla 5.0.1 → 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 +317 -89
- 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 -1
- package/dist/grid-controller.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +234 -67
- package/dist/index.mjs +239 -69
- 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 +5 -5
- 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 +78 -15
- package/src/group-row.test.ts +66 -0
- package/src/index.integration.test.ts +34 -12
- package/src/index.ts +2 -1
- 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
- package/tsup.config.ts +1 -0
|
@@ -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
|
+
});
|
|
@@ -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('body scrollbar compensation', () => {
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
document.body.innerHTML = '<div id="app"></div>';
|
|
19
|
+
});
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
document.body.innerHTML = '';
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('narrows the header and filter strips by the body scrollbar width', async () => {
|
|
25
|
+
let api: UiGridApi | undefined;
|
|
26
|
+
const grid = await mountVanillaUiGrid(
|
|
27
|
+
document.getElementById('app')!,
|
|
28
|
+
{
|
|
29
|
+
id: 'scrollbar-compensation',
|
|
30
|
+
enableFiltering: true,
|
|
31
|
+
data: [{ id: 'r1', name: 'Alpha' }],
|
|
32
|
+
columnDefs: [{ name: 'name' }],
|
|
33
|
+
onRegisterApi: (registered) => {
|
|
34
|
+
api = registered as UiGridApi;
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
undefined,
|
|
38
|
+
'ui-grid-element-scrollbar-test',
|
|
39
|
+
);
|
|
40
|
+
await waitFor(() => api);
|
|
41
|
+
const root = grid.shadowRoot!;
|
|
42
|
+
const viewport = await waitFor(() => root.querySelector<HTMLElement>('.grid-body-viewport'));
|
|
43
|
+
Object.defineProperty(viewport, 'offsetWidth', { configurable: true, value: 1000 });
|
|
44
|
+
Object.defineProperty(viewport, 'clientWidth', { configurable: true, value: 985 });
|
|
45
|
+
|
|
46
|
+
api!.core.refresh();
|
|
47
|
+
|
|
48
|
+
const frame = root.querySelector<HTMLElement>('.grid-frame')!;
|
|
49
|
+
await waitFor(() => frame.style.getPropertyValue('--ui-grid-scrollbar-width') === '15px');
|
|
50
|
+
expect(GRID_CORE_CSS).toContain('width: calc(100% - var(--ui-grid-scrollbar-width, 0px))');
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { createVanillaGridController } from './grid-controller';
|
|
4
|
+
|
|
5
|
+
const make = (pinClient: boolean) =>
|
|
6
|
+
createVanillaGridController({
|
|
7
|
+
id: 'selection-pinning',
|
|
8
|
+
data: [{ id: 'r1', client: 'A', amount: 1 }],
|
|
9
|
+
columnDefs: [
|
|
10
|
+
{ name: 'client', width: '200px', ...(pinClient ? { pinnedLeft: true } : {}) },
|
|
11
|
+
{ name: 'amount', width: '120px' },
|
|
12
|
+
],
|
|
13
|
+
enablePinning: true,
|
|
14
|
+
enableRowSelection: true,
|
|
15
|
+
enableRowHeaderSelection: true,
|
|
16
|
+
selectionRowHeaderWidth: 30,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const column = (controller: ReturnType<typeof make>, name: string) =>
|
|
20
|
+
controller.getSnapshot().visibleColumns.find((candidate) => candidate.name === name)!;
|
|
21
|
+
|
|
22
|
+
describe('selection column pinning', () => {
|
|
23
|
+
it('pins the selection column with left-pinned columns and offsets them past it', () => {
|
|
24
|
+
const controller = make(true);
|
|
25
|
+
const selection = column(controller, 'selectionRowHeaderCol');
|
|
26
|
+
const client = column(controller, 'client');
|
|
27
|
+
|
|
28
|
+
expect(controller.isPinned(selection)).toBe(true);
|
|
29
|
+
expect(controller.pinnedOffset(selection)).toEqual({ side: 'left', offset: '0px' });
|
|
30
|
+
expect(controller.pinnedOffset(client)).toEqual({ side: 'left', offset: 'calc(30px)' });
|
|
31
|
+
expect(controller.isPinnedLeftLast(client)).toBe(true);
|
|
32
|
+
expect(controller.getSnapshot().pinnedColumns).toEqual({ client: 'left' });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('leaves the selection column unpinned when nothing is pinned left', () => {
|
|
36
|
+
const controller = make(false);
|
|
37
|
+
expect(controller.isPinned(column(controller, 'selectionRowHeaderCol'))).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { createVanillaGridController } from './grid-controller';
|
|
4
|
+
import { mountVanillaUiGrid, type UiGridApi } from './index';
|
|
5
|
+
import type { FrameworkFilterSlot, FrameworkSlotDelta } from './framework-slots';
|
|
6
|
+
|
|
7
|
+
async function waitFor<T>(resolve: () => T | null | undefined, timeoutMs = 5000): Promise<T> {
|
|
8
|
+
const startedAt = Date.now();
|
|
9
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
10
|
+
const value = resolve();
|
|
11
|
+
if (value) return value;
|
|
12
|
+
await new Promise((next) => window.setTimeout(next, 20));
|
|
13
|
+
}
|
|
14
|
+
throw new Error('Timed out waiting for expected element state');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('framework slot updates', () => {
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
document.body.innerHTML = '<div id="app"></div>';
|
|
20
|
+
});
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
document.body.innerHTML = '';
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
async function mount(id: string) {
|
|
26
|
+
const tag = 'ui-grid-element-slot-updates-test';
|
|
27
|
+
let api: UiGridApi | undefined;
|
|
28
|
+
const grid = await mountVanillaUiGrid(
|
|
29
|
+
document.getElementById('app')!,
|
|
30
|
+
{
|
|
31
|
+
id,
|
|
32
|
+
enableFiltering: true,
|
|
33
|
+
data: [
|
|
34
|
+
{ id: 'r1', name: 'Alpha' },
|
|
35
|
+
{ id: 'r2', name: 'Beta' },
|
|
36
|
+
],
|
|
37
|
+
columnDefs: [{ name: 'name' }],
|
|
38
|
+
onRegisterApi: (registered) => {
|
|
39
|
+
api = registered as UiGridApi;
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
undefined,
|
|
43
|
+
tag,
|
|
44
|
+
);
|
|
45
|
+
await waitFor(() => api);
|
|
46
|
+
await waitFor(() => grid.shadowRoot?.querySelector('.body-cell'));
|
|
47
|
+
return { grid, api: api! };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
it('turns filter slots on after the first render', async () => {
|
|
51
|
+
const { grid } = await mount('slot-toggle');
|
|
52
|
+
const root = grid.shadowRoot!;
|
|
53
|
+
expect(root.querySelector('ui-grid-filter-cell[data-column="name"]')).not.toBeNull();
|
|
54
|
+
|
|
55
|
+
const deltas: FrameworkSlotDelta<FrameworkFilterSlot>[] = [];
|
|
56
|
+
grid.addEventListener('filterSlotsChanged', (event) =>
|
|
57
|
+
deltas.push((event as CustomEvent<FrameworkSlotDelta<FrameworkFilterSlot>>).detail),
|
|
58
|
+
);
|
|
59
|
+
grid.setFrameworkRenderedSlots({ filters: ['name'] });
|
|
60
|
+
|
|
61
|
+
await waitFor(() => root.querySelector('slot[name="filter-name"]'));
|
|
62
|
+
expect(root.querySelector('ui-grid-filter-cell[data-column="name"]')).toBeNull();
|
|
63
|
+
expect(deltas.at(-1)?.added.map((slot) => slot.slotName)).toEqual(['filter-name']);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('reports a filter slot whose value changed as updated', async () => {
|
|
67
|
+
const { grid } = await mount('slot-updated');
|
|
68
|
+
grid.setFrameworkRenderedSlots({ filters: ['name'] });
|
|
69
|
+
await waitFor(() => grid.shadowRoot!.querySelector('slot[name="filter-name"]'));
|
|
70
|
+
|
|
71
|
+
const deltas: FrameworkSlotDelta<FrameworkFilterSlot>[] = [];
|
|
72
|
+
grid.addEventListener('filterSlotsChanged', (event) =>
|
|
73
|
+
deltas.push((event as CustomEvent<FrameworkSlotDelta<FrameworkFilterSlot>>).detail),
|
|
74
|
+
);
|
|
75
|
+
(grid as unknown as { controller: { setFilter(name: string, value: string): void } }).controller.setFilter('name', 'Al');
|
|
76
|
+
|
|
77
|
+
const delta = await waitFor(() => deltas.find((d) => (d.updated ?? []).length > 0));
|
|
78
|
+
expect(delta.added).toEqual([]);
|
|
79
|
+
expect(delta.removed).toEqual([]);
|
|
80
|
+
expect(delta.updated?.map((slot) => [slot.slotName, slot.value])).toEqual([['filter-name', 'Al']]);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it.each(['left', 'right'] as const)('keeps %s-pinned filter slots aligned after resizing', async (side) => {
|
|
84
|
+
const { grid } = await mount(`slot-resize-${side}`);
|
|
85
|
+
const pinned = side === 'left' ? { pinnedLeft: true } : { pinnedRight: true };
|
|
86
|
+
grid.options = {
|
|
87
|
+
...grid.options,
|
|
88
|
+
enablePinning: true,
|
|
89
|
+
columnDefs: [
|
|
90
|
+
{ name: 'name', width: '180px', ...pinned },
|
|
91
|
+
{ name: 'other', width: '120px', ...pinned },
|
|
92
|
+
],
|
|
93
|
+
};
|
|
94
|
+
const filtered = side === 'left' ? 'other' : 'name';
|
|
95
|
+
const resized = side === 'left' ? 'name' : 'other';
|
|
96
|
+
grid.setFrameworkRenderedSlots({ filters: [filtered] });
|
|
97
|
+
const slot = await waitFor(() => grid.shadowRoot!.querySelector(`slot[name="filter-${filtered}"]`));
|
|
98
|
+
const wrapper = slot.parentElement!;
|
|
99
|
+
expect(wrapper.style[side]).toBe(side === 'left' ? 'calc(180px)' : 'calc(120px)');
|
|
100
|
+
const controller = (grid as unknown as { controller: ReturnType<typeof createVanillaGridController> }).controller;
|
|
101
|
+
controller.setColumnWidthOverride(resized, 240);
|
|
102
|
+
await waitFor(() => grid.shadowRoot!.querySelector(`[data-column="${resized}"]`));
|
|
103
|
+
expect(wrapper.style[side]).toBe('calc(240px)');
|
|
104
|
+
expect(wrapper.querySelector('slot')).toBe(slot);
|
|
105
|
+
expect(wrapper.classList.contains('is-pinned')).toBe(true);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('fires emptyStateSlotsChanged and the original singular name', async () => {
|
|
109
|
+
const { grid } = await mount('empty-event');
|
|
110
|
+
const seen: string[] = [];
|
|
111
|
+
grid.addEventListener('emptyStateSlotsChanged', () => seen.push('plural'));
|
|
112
|
+
grid.addEventListener('emptyStateSlotChanged', () => seen.push('singular'));
|
|
113
|
+
|
|
114
|
+
grid.setFrameworkRenderedSlots({ emptyState: true });
|
|
115
|
+
grid.options = { ...grid.options, data: [] };
|
|
116
|
+
|
|
117
|
+
await waitFor(() => seen.length >= 2);
|
|
118
|
+
expect(seen.slice(0, 2)).toEqual(['plural', 'singular']);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe('columnSizeChanged', () => {
|
|
123
|
+
it('fires with the column and its new width when a column is resized', () => {
|
|
124
|
+
const controller = createVanillaGridController({
|
|
125
|
+
id: 'column-size',
|
|
126
|
+
data: [{ id: 'r1', name: 'Alpha' }],
|
|
127
|
+
columnDefs: [{ name: 'name' }],
|
|
128
|
+
});
|
|
129
|
+
const events: Array<[string, string]> = [];
|
|
130
|
+
controller.gridApi.core.on.columnSizeChanged((name, width) => events.push([name, width]));
|
|
131
|
+
|
|
132
|
+
controller.setColumnWidthOverride('name', 180.4);
|
|
133
|
+
controller.setColumnWidthOverride('name', 180);
|
|
134
|
+
|
|
135
|
+
expect(events).toEqual([['name', '180px']]);
|
|
136
|
+
});
|
|
137
|
+
});
|
package/src/templates/index.ts
CHANGED
|
@@ -21,8 +21,12 @@ export function filterRowMarkup(templateColumns: string, filterCells: string): s
|
|
|
21
21
|
return `<div class="filter-grid ui-grid-header" style="grid-template-columns:${templateColumns}">${filterCells}</div>`;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
export function
|
|
25
|
-
return
|
|
24
|
+
export function virtualSpacerStyle(totalVirtualHeight: number, rowSize: number): string {
|
|
25
|
+
return `height:${totalVirtualHeight}px;--ui-grid-row-height:${rowSize}px`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function bodyVirtualMarkup(templateColumns: string, totalVirtualHeight: number, virtualOffset: number, bodyContent: string, rowSize: number): string {
|
|
29
|
+
return `<div class="grid-virtual-spacer" style="${virtualSpacerStyle(totalVirtualHeight, rowSize)}"><div class="body-grid ui-grid-canvas grid-virtual-body" style="grid-template-columns:${templateColumns};top:${virtualOffset}px">${bodyContent}</div></div>`;
|
|
26
30
|
}
|
|
27
31
|
|
|
28
32
|
export function bodyStaticMarkup(templateColumns: string, bodyContent: string): string {
|
|
@@ -30,11 +34,11 @@ export function bodyStaticMarkup(templateColumns: string, bodyContent: string):
|
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
export function emptyDataMarkup(heading: string, description: string): string {
|
|
33
|
-
return `<div class="empty-state ui-grid-no-row-overlay"><strong>${heading}</strong><p>${description}</p></div>`;
|
|
37
|
+
return `<div part="empty-state" class="empty-state ui-grid-no-row-overlay"><strong>${heading}</strong><p>${description}</p></div>`;
|
|
34
38
|
}
|
|
35
39
|
|
|
36
40
|
export function expandableRowMarkup(expandableContent: string): string {
|
|
37
|
-
return `<div class="expandable-row ui-grid-row ui-grid-expandable-row" style="grid-column:1 / -1">${expandableContent}</div>`;
|
|
41
|
+
return `<div part="expandable-row" class="expandable-row ui-grid-row ui-grid-expandable-row" style="grid-column:1 / -1">${expandableContent}</div>`;
|
|
38
42
|
}
|
|
39
43
|
|
|
40
44
|
export function treeToggleMarkup(rowId: string, toggleLabel: string, iconViewBox: string, iconPath: string): string {
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { adoptStyles } from './adopt-styles';
|
|
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('theming hooks', () => {
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
document.body.innerHTML = '<div id="app"></div>';
|
|
19
|
+
});
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
document.body.innerHTML = '';
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
async function mount() {
|
|
25
|
+
let api: UiGridApi | undefined;
|
|
26
|
+
const grid = await mountVanillaUiGrid(
|
|
27
|
+
document.getElementById('app')!,
|
|
28
|
+
{
|
|
29
|
+
id: 'theming-hooks',
|
|
30
|
+
enableFiltering: true,
|
|
31
|
+
enablePinning: true,
|
|
32
|
+
data: [{ id: 'r1', name: 'Alpha' }],
|
|
33
|
+
columnDefs: [{ name: 'name' }],
|
|
34
|
+
onRegisterApi: (registered) => {
|
|
35
|
+
api = registered as UiGridApi;
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
undefined,
|
|
39
|
+
'ui-grid-element-theming-test',
|
|
40
|
+
);
|
|
41
|
+
await waitFor(() => api);
|
|
42
|
+
await waitFor(() => grid.shadowRoot?.querySelector('.body-cell'));
|
|
43
|
+
return grid;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
it('renders the documented part hooks', async () => {
|
|
47
|
+
const root = (await mount()).shadowRoot!;
|
|
48
|
+
const parts = new Set([...root.querySelectorAll('[part]')].map((node) => node.getAttribute('part')));
|
|
49
|
+
for (const part of ['grid-frame', 'header', 'header-cell', 'header-label', 'header-actions', 'pin-menu', 'filter-cell', 'body-cell']) {
|
|
50
|
+
expect(parts, part).toContain(part);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('keeps addStyles sheets after the grid sheets, and removes them', async () => {
|
|
55
|
+
const grid = await mount();
|
|
56
|
+
const root = grid.shadowRoot!;
|
|
57
|
+
const remove = grid.addStyles('.body-cell{color:red}');
|
|
58
|
+
const added = root.adoptedStyleSheets.at(-1)!;
|
|
59
|
+
|
|
60
|
+
adoptStyles(root, '.late{color:blue}', {});
|
|
61
|
+
expect(root.adoptedStyleSheets.at(-1)).toBe(added);
|
|
62
|
+
|
|
63
|
+
remove();
|
|
64
|
+
expect(root.adoptedStyleSheets).not.toContain(added);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -14,7 +14,7 @@ export default function emptyTemplate(ctx: EmptyContext) {
|
|
|
14
14
|
adoptStyles(root, shellCss, STYLE_KEY);
|
|
15
15
|
root.innerHTML =
|
|
16
16
|
`<section class="grid-shell ui-grid-shell">` +
|
|
17
|
-
`<div class="empty-state ui-grid-no-row-overlay">` +
|
|
17
|
+
`<div part="empty-state" class="empty-state ui-grid-no-row-overlay">` +
|
|
18
18
|
`<strong>${ctx.message}</strong>` +
|
|
19
19
|
`</div>` +
|
|
20
20
|
`</section>`;
|
|
@@ -24,10 +24,10 @@ export default function gridShellTemplate(ctx: ShellContext) {
|
|
|
24
24
|
adoptStyles(target, shellCss, STYLE_KEY);
|
|
25
25
|
target.innerHTML =
|
|
26
26
|
`${ctx.slotRegistry}` +
|
|
27
|
-
`<section class="grid-frame ui-grid" role="grid" aria-label="${ctx.gridTitle}">` +
|
|
27
|
+
`<section class="grid-frame ui-grid" part="grid-frame" role="grid" aria-label="${ctx.gridTitle}">` +
|
|
28
28
|
`<div class="grid-table ui-grid-contents-wrapper" style="${ctx.gridTableStyle}">` +
|
|
29
29
|
`<div class="grid-header-strip">` +
|
|
30
|
-
`<div class="header-grid ui-grid-header ui-grid-header-canvas" style="grid-template-columns:${ctx.templateColumns}">` +
|
|
30
|
+
`<div class="header-grid ui-grid-header ui-grid-header-canvas" part="header" style="grid-template-columns:${ctx.templateColumns}">` +
|
|
31
31
|
ctx.headerContent +
|
|
32
32
|
`</div>` +
|
|
33
33
|
`</div>` +
|
|
@@ -85,6 +85,7 @@ import {
|
|
|
85
85
|
type GridSaveState,
|
|
86
86
|
type VanillaGridController,
|
|
87
87
|
} from './grid-controller';
|
|
88
|
+
import { addUserStyles } from './adopt-styles';
|
|
88
89
|
import emptyTemplate from './ui-grid-empty.template';
|
|
89
90
|
import gridShellTemplate from './ui-grid-shell.template';
|
|
90
91
|
import {
|
|
@@ -117,6 +118,11 @@ export type VanillaUiGridElement = HTMLElement & {
|
|
|
117
118
|
* full contract.
|
|
118
119
|
*/
|
|
119
120
|
setFrameworkRenderedSlots(config: FrameworkRenderedSlotsConfig): void;
|
|
121
|
+
/**
|
|
122
|
+
* Add CSS to the grid's shadow root, after the grid's own styles so it wins
|
|
123
|
+
* at equal specificity. Returns a function that removes it.
|
|
124
|
+
*/
|
|
125
|
+
addStyles(cssText: string): () => void;
|
|
120
126
|
};
|
|
121
127
|
|
|
122
128
|
type VanillaGridOptions = GridOptions & {
|
|
@@ -214,6 +220,11 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
214
220
|
this.ensureController(this.buildEffectiveOptions(this.activeOptions));
|
|
215
221
|
}
|
|
216
222
|
|
|
223
|
+
/** Append application CSS after the grid styles; return a removal callback. */
|
|
224
|
+
addStyles(cssText: string): () => void {
|
|
225
|
+
return addUserStyles(this.ensureShadowRoot(), cssText);
|
|
226
|
+
}
|
|
227
|
+
|
|
217
228
|
/**
|
|
218
229
|
* Declare which slots are framework-rendered. When a slot is flagged, the
|
|
219
230
|
* element renders a `<slot name="…">` placeholder at the usual site instead
|
|
@@ -240,6 +251,9 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
240
251
|
// hasCell() / hasHeader() / etc. — values not captured by the
|
|
241
252
|
// row-level fingerprint.
|
|
242
253
|
this.lastRowStateFingerprints.clear();
|
|
254
|
+
// The patch path keeps existing nodes (e.g. a <ui-grid-filter-cell>
|
|
255
|
+
// that should now be a <slot>), so the next pass must be a full render.
|
|
256
|
+
this.lastStructureKey = null;
|
|
243
257
|
this.render();
|
|
244
258
|
}
|
|
245
259
|
}
|
|
@@ -707,6 +721,9 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
707
721
|
this.templateObserver?.disconnect();
|
|
708
722
|
this.templateObserver = null;
|
|
709
723
|
this.teardownAutoResize();
|
|
724
|
+
this.scrollbarObserver?.disconnect();
|
|
725
|
+
this.scrollbarObserver = null;
|
|
726
|
+
this.observedBodyViewport = null;
|
|
710
727
|
if (this.scrollFrame !== null) {
|
|
711
728
|
cancelAnimationFrame(this.scrollFrame);
|
|
712
729
|
this.scrollFrame = null;
|
|
@@ -1050,9 +1067,30 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
1050
1067
|
});
|
|
1051
1068
|
}
|
|
1052
1069
|
|
|
1070
|
+
this.syncScrollbarCompensation(root);
|
|
1053
1071
|
this.autoAdjustHeight(snapshot);
|
|
1054
1072
|
}
|
|
1055
1073
|
|
|
1074
|
+
private scrollbarObserver: ResizeObserver | null = null;
|
|
1075
|
+
private observedBodyViewport: HTMLElement | null = null;
|
|
1076
|
+
|
|
1077
|
+
// The header and filter strips have no scrollbar, so they are narrowed by the
|
|
1078
|
+
// body viewport's to keep right-pinned cells and scroll range aligned (#7402).
|
|
1079
|
+
private syncScrollbarCompensation(root: ShadowRoot): void {
|
|
1080
|
+
const frame = root.querySelector<HTMLElement>('.grid-frame');
|
|
1081
|
+
const viewport = root.querySelector<HTMLElement>('.grid-body-viewport');
|
|
1082
|
+
if (!frame || !viewport) return;
|
|
1083
|
+
const width = `${Math.max(0, viewport.offsetWidth - viewport.clientWidth)}px`;
|
|
1084
|
+
if (frame.style.getPropertyValue('--ui-grid-scrollbar-width') !== width) {
|
|
1085
|
+
frame.style.setProperty('--ui-grid-scrollbar-width', width);
|
|
1086
|
+
}
|
|
1087
|
+
if (viewport === this.observedBodyViewport || typeof ResizeObserver === 'undefined') return;
|
|
1088
|
+
this.scrollbarObserver ??= new ResizeObserver(() => this.syncScrollbarCompensation(root));
|
|
1089
|
+
if (this.observedBodyViewport) this.scrollbarObserver.unobserve(this.observedBodyViewport);
|
|
1090
|
+
this.scrollbarObserver.observe(viewport);
|
|
1091
|
+
this.observedBodyViewport = viewport;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1056
1094
|
/** @internal */
|
|
1057
1095
|
private inAutoAdjust = false;
|
|
1058
1096
|
private lastAutoHeight = 0;
|
|
@@ -1259,7 +1297,7 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
1259
1297
|
const body =
|
|
1260
1298
|
snapshot.pipeline.displayItems.length > 0
|
|
1261
1299
|
? virtualizationEnabled
|
|
1262
|
-
? bodyVirtualMarkup(templateColumns, totalVirtualHeight, virtualOffset, bodyContent)
|
|
1300
|
+
? bodyVirtualMarkup(templateColumns, totalVirtualHeight, virtualOffset, bodyContent, snapshot.rowSize)
|
|
1263
1301
|
: bodyStaticMarkup(templateColumns, bodyContent)
|
|
1264
1302
|
: this.renderEmptyState(
|
|
1265
1303
|
options.emptyMessage ?? labels.emptyHeading,
|
|
@@ -1318,8 +1356,9 @@ export class UiGridStandaloneElement extends HTMLElement {
|
|
|
1318
1356
|
templateColumns: string,
|
|
1319
1357
|
virtualOffset: number,
|
|
1320
1358
|
totalVirtualHeight: number,
|
|
1359
|
+
rowSize: number,
|
|
1321
1360
|
): HTMLElement | null {
|
|
1322
|
-
return reconcileBodyRoot(this, root, kind, options, labels, templateColumns, virtualOffset, totalVirtualHeight);
|
|
1361
|
+
return reconcileBodyRoot(this, root, kind, options, labels, templateColumns, virtualOffset, totalVirtualHeight, rowSize);
|
|
1323
1362
|
}
|
|
1324
1363
|
|
|
1325
1364
|
private reconcilePagination(
|
package/tsup.config.ts
CHANGED