@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,235 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { SORT_DIRECTIONS, clearRustWasmGridEngine } from '@ornery/ui-grid-core';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
createVanillaGridController,
|
|
7
|
+
mountVanillaUiGrid,
|
|
8
|
+
type GridOptions,
|
|
9
|
+
type UiGridApi,
|
|
10
|
+
type VanillaUiGridElement,
|
|
11
|
+
} from './index';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* saveState integration tests. Ports the per-field save/restore behaviour
|
|
15
|
+
* from the old ui.grid.saveState module.
|
|
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-savestate-test';
|
|
29
|
+
|
|
30
|
+
interface Row extends Record<string, unknown> {
|
|
31
|
+
id: string;
|
|
32
|
+
name: string;
|
|
33
|
+
status: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function baseOptions(overrides: Partial<GridOptions> = {}): GridOptions {
|
|
37
|
+
let capturedApi: UiGridApi | undefined;
|
|
38
|
+
const data: Row[] = [
|
|
39
|
+
{ id: 'r1', name: 'Alpha', status: 'Active' },
|
|
40
|
+
{ id: 'r2', name: 'Beta', status: 'Pilot' },
|
|
41
|
+
{ id: 'r3', name: 'Gamma', status: 'Draft' },
|
|
42
|
+
];
|
|
43
|
+
const options: GridOptions = {
|
|
44
|
+
id: 'savestate-grid',
|
|
45
|
+
data,
|
|
46
|
+
columnDefs: [
|
|
47
|
+
{ name: 'name', displayName: 'Name' },
|
|
48
|
+
{ name: 'status', displayName: 'Status' },
|
|
49
|
+
],
|
|
50
|
+
enableRowSelection: true,
|
|
51
|
+
enableFullRowSelection: true,
|
|
52
|
+
rowIdentity: (entity) => String((entity as Row).id),
|
|
53
|
+
onRegisterApi: (api) => {
|
|
54
|
+
capturedApi = api as UiGridApi;
|
|
55
|
+
},
|
|
56
|
+
...overrides,
|
|
57
|
+
};
|
|
58
|
+
Object.defineProperty(options, '__api', { enumerable: false, get: () => capturedApi });
|
|
59
|
+
return options;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function getApi(options: GridOptions): UiGridApi {
|
|
63
|
+
const api = (options as unknown as { __api: UiGridApi | undefined }).__api;
|
|
64
|
+
if (!api) throw new Error('Grid API was never registered');
|
|
65
|
+
return api;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function mountGrid(options: GridOptions): Promise<{
|
|
69
|
+
grid: VanillaUiGridElement;
|
|
70
|
+
shadow: ShadowRoot;
|
|
71
|
+
}> {
|
|
72
|
+
const target = document.getElementById('app')!;
|
|
73
|
+
const grid = await mountVanillaUiGrid(target, options, undefined, TAG);
|
|
74
|
+
const shadow = await waitFor(() => grid.shadowRoot);
|
|
75
|
+
await waitFor(() => shadow.querySelector('.body-cell[data-row="r1"]'));
|
|
76
|
+
return { grid, shadow };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
describe('vanilla grid saveState', () => {
|
|
80
|
+
beforeEach(() => {
|
|
81
|
+
document.body.innerHTML = '<div id="app"></div>';
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
afterEach(() => {
|
|
85
|
+
clearRustWasmGridEngine();
|
|
86
|
+
document.body.innerHTML = '';
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('getState returns sort, filter, grouping, pinning by default', () => {
|
|
90
|
+
const controller = createVanillaGridController({
|
|
91
|
+
id: 'save-controller',
|
|
92
|
+
data: [{ id: 'r1', name: 'Alpha' }] as Row[],
|
|
93
|
+
columnDefs: [{ name: 'name' }],
|
|
94
|
+
});
|
|
95
|
+
controller.setFilter('name', 'Al');
|
|
96
|
+
controller.sortColumn('name', SORT_DIRECTIONS.asc);
|
|
97
|
+
const state = controller.getState();
|
|
98
|
+
expect(state.sortState).toEqual({ columnName: 'name', direction: SORT_DIRECTIONS.asc });
|
|
99
|
+
expect(state.activeFilters).toEqual({ name: 'Al' });
|
|
100
|
+
expect(state.groupByColumns).toEqual([]);
|
|
101
|
+
expect(state.pinnedColumns).toEqual({});
|
|
102
|
+
expect(state.columnOrder).toEqual(['name']);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('per-field saveSort=false excludes sort from the snapshot', () => {
|
|
106
|
+
const controller = createVanillaGridController({
|
|
107
|
+
id: 'save-controller',
|
|
108
|
+
data: [{ id: 'r1', name: 'Alpha' }] as Row[],
|
|
109
|
+
columnDefs: [{ name: 'name' }],
|
|
110
|
+
saveSort: false,
|
|
111
|
+
});
|
|
112
|
+
controller.sortColumn('name', SORT_DIRECTIONS.asc);
|
|
113
|
+
const state = controller.getState();
|
|
114
|
+
expect(state.sortState).toBeUndefined();
|
|
115
|
+
// Filter is still serialized — each flag is independent.
|
|
116
|
+
expect(state.activeFilters).toEqual({});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('setState restores sort and filter independently', () => {
|
|
120
|
+
const controller = createVanillaGridController({
|
|
121
|
+
id: 'save-controller',
|
|
122
|
+
data: [
|
|
123
|
+
{ id: 'r1', name: 'Alpha' },
|
|
124
|
+
{ id: 'r2', name: 'Beta' },
|
|
125
|
+
] as Row[],
|
|
126
|
+
columnDefs: [{ name: 'name' }],
|
|
127
|
+
enableSorting: true,
|
|
128
|
+
enableFiltering: true,
|
|
129
|
+
rowIdentity: (entity) => String((entity as Row).id),
|
|
130
|
+
});
|
|
131
|
+
controller.setState({
|
|
132
|
+
sortState: { columnName: 'name', direction: SORT_DIRECTIONS.desc },
|
|
133
|
+
activeFilters: { name: 'Beta' },
|
|
134
|
+
});
|
|
135
|
+
expect(controller.getSnapshot().pipeline.visibleRows.map((r) => r.id)).toEqual(['r2']);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('saveSelection captures selectedRowIds and restores cleanly', async () => {
|
|
139
|
+
const options = baseOptions();
|
|
140
|
+
await mountGrid(options);
|
|
141
|
+
const api = getApi(options);
|
|
142
|
+
api.selection.selectRow(options.data[0]! as unknown as Record<string, unknown>);
|
|
143
|
+
api.selection.selectRow(options.data[2]! as unknown as Record<string, unknown>);
|
|
144
|
+
const state = api.saveState.save();
|
|
145
|
+
expect((state as unknown as { selectedRowIds: string[] }).selectedRowIds.sort()).toEqual(['r1', 'r3']);
|
|
146
|
+
api.selection.clearSelectedRows();
|
|
147
|
+
expect(api.selection.getSelectedCount()).toBe(0);
|
|
148
|
+
api.saveState.restore(state);
|
|
149
|
+
expect(api.selection.getSelectedCount()).toBe(2);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('saveFocus captures focusedCell and restores it', async () => {
|
|
153
|
+
const options = baseOptions();
|
|
154
|
+
const { shadow } = await mountGrid(options);
|
|
155
|
+
const api = getApi(options);
|
|
156
|
+
const cell = shadow.querySelector<HTMLElement>('.body-cell[data-row="r2"][data-column="status"]')!;
|
|
157
|
+
cell.click();
|
|
158
|
+
const state = api.saveState.save();
|
|
159
|
+
expect((state as unknown as { focusedCell: { rowId: string } | null }).focusedCell?.rowId).toBe('r2');
|
|
160
|
+
// Clear focus, then restore.
|
|
161
|
+
cell.blur();
|
|
162
|
+
api.saveState.restore({});
|
|
163
|
+
expect(api.cellNav.getFocusedCell()?.row.id).toBe('r2');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('saveGroupingExpandedStates=true captures collapsed groups', async () => {
|
|
167
|
+
const controller = createVanillaGridController({
|
|
168
|
+
id: 'save-controller',
|
|
169
|
+
data: [{ id: 'r1', name: 'Alpha', status: 'Active' }] as Row[],
|
|
170
|
+
columnDefs: [{ name: 'name' }, { name: 'status' }],
|
|
171
|
+
enableGrouping: true,
|
|
172
|
+
grouping: { groupBy: ['status'] },
|
|
173
|
+
saveGroupingExpandedStates: true,
|
|
174
|
+
});
|
|
175
|
+
controller.setCollapsedGroup('status::Active', true);
|
|
176
|
+
const state = controller.getState();
|
|
177
|
+
expect(state.collapsedGroups).toEqual({ 'status::Active': true });
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('saveScroll flag captures and restores viewport scroll position', async () => {
|
|
181
|
+
const options = baseOptions({
|
|
182
|
+
saveScroll: true,
|
|
183
|
+
data: Array.from({ length: 100 }, (_, i) => ({
|
|
184
|
+
id: `r${i}`,
|
|
185
|
+
name: `Row ${i}`,
|
|
186
|
+
status: 'Active',
|
|
187
|
+
})),
|
|
188
|
+
rowHeight: 20,
|
|
189
|
+
virtualizationThreshold: 1,
|
|
190
|
+
});
|
|
191
|
+
const { shadow } = await mountGrid(options);
|
|
192
|
+
const api = getApi(options);
|
|
193
|
+
// After the body/viewport split, vertical scroll lives on
|
|
194
|
+
// .grid-body-viewport (the inner overflow wrapper). The .grid-table
|
|
195
|
+
// only owns horizontal overflow now.
|
|
196
|
+
const bodyViewport = shadow.querySelector<HTMLElement>('.grid-body-viewport')!;
|
|
197
|
+
bodyViewport.scrollTop = 400;
|
|
198
|
+
bodyViewport.dispatchEvent(new Event('scroll', { bubbles: true }));
|
|
199
|
+
await new Promise((r) => requestAnimationFrame(() => r(undefined)));
|
|
200
|
+
await new Promise((r) => requestAnimationFrame(() => r(undefined)));
|
|
201
|
+
const state = api.saveState.save();
|
|
202
|
+
expect((state as unknown as { scrollTop: number }).scrollTop).toBe(400);
|
|
203
|
+
// Scroll back to 0, then restore.
|
|
204
|
+
bodyViewport.scrollTop = 0;
|
|
205
|
+
api.saveState.restore(state);
|
|
206
|
+
await new Promise((r) => requestAnimationFrame(() => r(undefined)));
|
|
207
|
+
expect(bodyViewport.scrollTop).toBe(400);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('saveScroll=true implicitly disables saveFocus', async () => {
|
|
211
|
+
const controller = createVanillaGridController({
|
|
212
|
+
id: 'save-controller',
|
|
213
|
+
data: [{ id: 'r1', name: 'Alpha' }] as Row[],
|
|
214
|
+
columnDefs: [{ name: 'name' }],
|
|
215
|
+
saveScroll: true,
|
|
216
|
+
});
|
|
217
|
+
controller.setCellNavFocus('r1', 'name');
|
|
218
|
+
const state = controller.getState();
|
|
219
|
+
expect(state.focusedCell).toBeUndefined();
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('setState is a no-op for fields that are undefined', () => {
|
|
223
|
+
const controller = createVanillaGridController({
|
|
224
|
+
id: 'save-controller',
|
|
225
|
+
data: [{ id: 'r1', name: 'Alpha' }] as Row[],
|
|
226
|
+
columnDefs: [{ name: 'name' }],
|
|
227
|
+
});
|
|
228
|
+
controller.setFilter('name', 'Al');
|
|
229
|
+
const before = controller.getState();
|
|
230
|
+
// setState({}) should preserve everything.
|
|
231
|
+
controller.setState({});
|
|
232
|
+
const after = controller.getState();
|
|
233
|
+
expect(after.activeFilters).toEqual(before.activeFilters);
|
|
234
|
+
});
|
|
235
|
+
});
|
package/src/scss.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* String template helpers for grid markup fragments.
|
|
3
|
+
*
|
|
4
|
+
* These are simple functions that return HTML strings for use in innerHTML
|
|
5
|
+
* concatenation (the hot render path). They are NOT .html template imports —
|
|
6
|
+
* .html templates should only be used where connect() is called against
|
|
7
|
+
* a DOM target (shell, components with shadow DOM).
|
|
8
|
+
*
|
|
9
|
+
* This avoids the DOM→string→DOM round-trip that toHTML() would cause.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export function iconMarkup(svgClass: string, viewBox: string, path: string): string {
|
|
13
|
+
return `<svg class="${svgClass}" viewBox="${viewBox}" aria-hidden="true" focusable="false"><path d="${path}"></path></svg>`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function slotRegistryMarkup(cellSlots: string): string {
|
|
17
|
+
return `<div hidden class="slot-registry">${cellSlots}<slot name="expandable-row"></slot></div>`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function filterRowMarkup(templateColumns: string, filterCells: string): string {
|
|
21
|
+
return `<div class="filter-grid ui-grid-header" style="grid-template-columns:${templateColumns}">${filterCells}</div>`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function bodyVirtualMarkup(templateColumns: string, totalVirtualHeight: number, virtualOffset: number, bodyContent: string): string {
|
|
25
|
+
return `<div class="grid-virtual-spacer" style="height:${totalVirtualHeight}px"><div class="body-grid ui-grid-canvas grid-virtual-body" style="grid-template-columns:${templateColumns};top:${virtualOffset}px">${bodyContent}</div></div>`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function bodyStaticMarkup(templateColumns: string, bodyContent: string): string {
|
|
29
|
+
return `<div class="body-grid ui-grid-canvas" style="grid-template-columns:${templateColumns}">${bodyContent}</div>`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
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>`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
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>`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function treeToggleMarkup(rowId: string, toggleLabel: string, iconViewBox: string, iconPath: string): string {
|
|
41
|
+
return `<button type="button" class="row-toggle" data-action="toggle-tree" data-row="${rowId}" aria-label="${toggleLabel}">${iconMarkup('toggle-icon', iconViewBox, iconPath)}<span class="sr-only">${toggleLabel}</span></button>`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function expandToggleMarkup(rowId: string, toggleLabel: string, iconViewBox: string, iconPath: string): string {
|
|
45
|
+
return `<button type="button" class="row-toggle row-toggle-expand" data-action="toggle-expand" data-row="${rowId}" aria-label="${toggleLabel}">${iconMarkup('toggle-icon', iconViewBox, iconPath)}<span class="sr-only">${toggleLabel}</span></button>`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function cellEditorMarkup(rowId: string, columnName: string, inputType: string, editingValue: string): string {
|
|
49
|
+
return `<ui-grid-cell-editor data-row="${rowId}" data-column="${columnName}" data-type="${inputType}" data-value="${editingValue}"></ui-grid-cell-editor>`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function cellValueMarkup(displayValue: string): string {
|
|
53
|
+
return `<span class="cell-value">${displayValue}</span>`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function defaultExpandableMarkup(label: string): string {
|
|
57
|
+
return `<p>${label}</p>`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function resizerMarkup(columnName: string, headerValue: string): string {
|
|
61
|
+
return `<button type="button" class="column-resizer" data-action="resize" data-column="${columnName}" aria-label="Resize ${headerValue} column" title="Drag to resize, double-click to auto fit"></button>`;
|
|
62
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vitest setup file — polyfills APIs that jsdom does not fully implement
|
|
3
|
+
* so that tests exercise the real adoptedStyleSheets code path.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
if (typeof CSSStyleSheet !== 'undefined' && !CSSStyleSheet.prototype.replaceSync) {
|
|
7
|
+
CSSStyleSheet.prototype.replaceSync = function (text: string) {
|
|
8
|
+
(this as any).__cssText = text;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const originalAttachShadow = HTMLElement.prototype.attachShadow;
|
|
13
|
+
HTMLElement.prototype.attachShadow = function (init: ShadowRootInit): ShadowRoot {
|
|
14
|
+
const shadow = originalAttachShadow.call(this, init);
|
|
15
|
+
if (!shadow.adoptedStyleSheets) {
|
|
16
|
+
Object.defineProperty(shadow, 'adoptedStyleSheets', {
|
|
17
|
+
value: [],
|
|
18
|
+
writable: true,
|
|
19
|
+
configurable: true,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
return shadow;
|
|
23
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import shellCss from './ui-grid-shell.scss';
|
|
2
|
+
import { adoptStyles } from './adopt-styles';
|
|
3
|
+
|
|
4
|
+
const STYLE_KEY = {};
|
|
5
|
+
|
|
6
|
+
interface EmptyContext {
|
|
7
|
+
message: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default function emptyTemplate(ctx: EmptyContext) {
|
|
11
|
+
return {
|
|
12
|
+
connect(root?: ShadowRoot): void {
|
|
13
|
+
if (!root) return;
|
|
14
|
+
adoptStyles(root, shellCss, STYLE_KEY);
|
|
15
|
+
root.innerHTML =
|
|
16
|
+
`<section class="grid-shell ui-grid-shell">` +
|
|
17
|
+
`<div class="empty-state ui-grid-no-row-overlay">` +
|
|
18
|
+
`<strong>${ctx.message}</strong>` +
|
|
19
|
+
`</div>` +
|
|
20
|
+
`</section>`;
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Grid shell — imports all core partials for the main grid layout.
|
|
2
|
+
// Partials with only mixins require an explicit @include to emit rules.
|
|
3
|
+
@use "@ornery/ui-grid-core/src/lib/styles/tokens";
|
|
4
|
+
@use "@ornery/ui-grid-core/src/lib/styles/inherit";
|
|
5
|
+
@use "@ornery/ui-grid-core/src/lib/styles/shell";
|
|
6
|
+
@use "@ornery/ui-grid-core/src/lib/styles/frame";
|
|
7
|
+
@use "@ornery/ui-grid-core/src/lib/styles/grid-table";
|
|
8
|
+
@use "@ornery/ui-grid-core/src/lib/styles/header-cell";
|
|
9
|
+
@use "@ornery/ui-grid-core/src/lib/styles/filter-cell";
|
|
10
|
+
@use "@ornery/ui-grid-core/src/lib/styles/body-cell";
|
|
11
|
+
@use "@ornery/ui-grid-core/src/lib/styles/group-row";
|
|
12
|
+
@use "@ornery/ui-grid-core/src/lib/styles/virtualization";
|
|
13
|
+
@use "@ornery/ui-grid-core/src/lib/styles/alignment";
|
|
14
|
+
@use "@ornery/ui-grid-core/src/lib/styles/empty-state";
|
|
15
|
+
@use "@ornery/ui-grid-core/src/lib/styles/pagination";
|
|
16
|
+
@use "@ornery/ui-grid-core/src/lib/styles/utilities";
|
|
17
|
+
@use "@ornery/ui-grid-core/src/lib/styles/status-pill";
|
|
18
|
+
@use "@ornery/ui-grid-core/src/lib/styles/selection";
|
|
19
|
+
@use "@ornery/ui-grid-core/src/lib/styles/row-edit";
|
|
20
|
+
@use "@ornery/ui-grid-core/src/lib/styles/validate";
|
|
21
|
+
|
|
22
|
+
// Mixin-only partials: emit class-based rules for the grid shell shadow DOM.
|
|
23
|
+
@include filter-cell.rules;
|
|
24
|
+
@include group-row.rules;
|
|
25
|
+
@include pagination.rules;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import shellCss from './ui-grid-shell.scss';
|
|
2
|
+
import { adoptStyles } from './adopt-styles';
|
|
3
|
+
|
|
4
|
+
const STYLE_KEY = {};
|
|
5
|
+
|
|
6
|
+
interface ShellContext {
|
|
7
|
+
shadowRoot?: ShadowRoot | null;
|
|
8
|
+
slotRegistry: string;
|
|
9
|
+
gridTitle: string;
|
|
10
|
+
gridTableStyle: string;
|
|
11
|
+
templateColumns: string;
|
|
12
|
+
headerContent: string;
|
|
13
|
+
filterRowContent: string;
|
|
14
|
+
bodyViewportStyle: string;
|
|
15
|
+
bodyContent: string;
|
|
16
|
+
paginationContent: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default function gridShellTemplate(ctx: ShellContext) {
|
|
20
|
+
return {
|
|
21
|
+
connect(root?: ShadowRoot): void {
|
|
22
|
+
const target = root ?? ctx.shadowRoot;
|
|
23
|
+
if (!target) return;
|
|
24
|
+
adoptStyles(target, shellCss, STYLE_KEY);
|
|
25
|
+
target.innerHTML =
|
|
26
|
+
`${ctx.slotRegistry}` +
|
|
27
|
+
`<section class="grid-frame ui-grid" role="grid" aria-label="${ctx.gridTitle}">` +
|
|
28
|
+
`<div class="grid-table ui-grid-contents-wrapper" style="${ctx.gridTableStyle}">` +
|
|
29
|
+
`<div class="grid-header-strip">` +
|
|
30
|
+
`<div class="header-grid ui-grid-header ui-grid-header-canvas" style="grid-template-columns:${ctx.templateColumns}">` +
|
|
31
|
+
ctx.headerContent +
|
|
32
|
+
`</div>` +
|
|
33
|
+
`</div>` +
|
|
34
|
+
`<div class="grid-filter-strip">` +
|
|
35
|
+
ctx.filterRowContent +
|
|
36
|
+
`</div>` +
|
|
37
|
+
`<div class="grid-body-viewport" style="${ctx.bodyViewportStyle}">` +
|
|
38
|
+
ctx.bodyContent +
|
|
39
|
+
`</div>` +
|
|
40
|
+
ctx.paginationContent +
|
|
41
|
+
`</div>` +
|
|
42
|
+
`</section>`;
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|