@ornery/ui-grid-vanilla 0.1.10 → 1.0.1

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.
Files changed (63) hide show
  1. package/AGENTS.md +70 -0
  2. package/README.md +196 -0
  3. package/demo/main.ts +0 -1
  4. package/dist/index.d.mts +582 -55
  5. package/dist/index.d.ts +582 -55
  6. package/dist/index.js +4959 -1429
  7. package/dist/index.mjs +5137 -1560
  8. package/package.json +11 -8
  9. package/scss-plugin.ts +65 -0
  10. package/src/adopt-styles.ts +39 -0
  11. package/src/attribute-bridge.ts +291 -0
  12. package/src/auto-height.test.ts +142 -0
  13. package/src/auto-resize.test.ts +207 -0
  14. package/src/cell-nav.test.ts +262 -0
  15. package/src/components/grid-body-cell/grid-body-cell.ts +23 -0
  16. package/src/components/grid-body-cell/index.ts +1 -0
  17. package/src/components/grid-cell-editor/grid-cell-editor.ts +104 -0
  18. package/src/components/grid-cell-editor/index.ts +1 -0
  19. package/src/components/grid-filter-cell/grid-filter-cell.scss +14 -0
  20. package/src/components/grid-filter-cell/grid-filter-cell.template.ts +20 -0
  21. package/src/components/grid-filter-cell/grid-filter-cell.ts +121 -0
  22. package/src/components/grid-filter-cell/index.ts +1 -0
  23. package/src/components/grid-group-row/grid-group-row.scss +24 -0
  24. package/src/components/grid-group-row/grid-group-row.template.ts +23 -0
  25. package/src/components/grid-group-row/grid-group-row.ts +94 -0
  26. package/src/components/grid-group-row/index.ts +1 -0
  27. package/src/components/grid-header-cell/grid-header-cell.ts +17 -0
  28. package/src/components/grid-header-cell/index.ts +1 -0
  29. package/src/components/grid-pagination/grid-pagination.scss +39 -0
  30. package/src/components/grid-pagination/grid-pagination.template.ts +40 -0
  31. package/src/components/grid-pagination/grid-pagination.ts +204 -0
  32. package/src/components/grid-pagination/index.ts +1 -0
  33. package/src/components/grid-template/grid-template.ts +166 -0
  34. package/src/components/grid-template/index.ts +1 -0
  35. package/src/events.ts +824 -0
  36. package/src/exporter.test.ts +186 -0
  37. package/src/focus.ts +379 -0
  38. package/src/framework-slots.ts +440 -0
  39. package/src/grid-controller.ts +1532 -21
  40. package/src/icons.ts +88 -0
  41. package/src/importer.test.ts +147 -0
  42. package/src/index.integration.test.ts +2 -2
  43. package/src/index.ts +9 -0
  44. package/src/infinite-scroll.test.ts +220 -0
  45. package/src/keyboard-navigation.test.ts +579 -0
  46. package/src/patch.ts +722 -0
  47. package/src/render.ts +465 -0
  48. package/src/row-edit.test.ts +191 -0
  49. package/src/row-selection.test.ts +550 -0
  50. package/src/save-state.test.ts +235 -0
  51. package/src/scss.d.ts +4 -0
  52. package/src/templates/index.ts +62 -0
  53. package/src/test-setup.ts +23 -0
  54. package/src/ui-grid-empty.template.ts +23 -0
  55. package/src/ui-grid-shell.scss +25 -0
  56. package/src/ui-grid-shell.template.ts +45 -0
  57. package/src/ui-grid-standalone.element.ts +751 -1112
  58. package/src/utils/cell-class.ts +70 -0
  59. package/src/utils/display-items.ts +15 -0
  60. package/src/utils/dom.ts +64 -0
  61. package/src/validate.test.ts +170 -0
  62. package/tsup.config.ts +10 -0
  63. package/vite.config.ts +10 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ornery/ui-grid-vanilla",
3
- "version": "0.1.10",
3
+ "version": "1.0.1",
4
4
  "description": "Vanilla DOM wrapper for the Rust/WASM-backed @ornery/ui-grid-core custom element",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -16,18 +16,21 @@
16
16
  }
17
17
  },
18
18
  "dependencies": {
19
- "@ornery/ui-grid-core": "0.1.10"
19
+ "@ornery/ui-grid-core": "1.0.0"
20
20
  },
21
21
  "devDependencies": {
22
- "typescript": "~5.8.0",
22
+ "happy-dom": "^20.9.0",
23
+ "jsdom": "^26.0.0",
24
+ "sass": "^1.77.0",
23
25
  "tsup": "^8.0.0",
26
+ "typescript": "~5.8.0",
24
27
  "vite": "^6.0.0",
25
- "vitest": "^4.1.0",
26
- "jsdom": "^26.0.0"
28
+ "vitest": "^4.1.0"
27
29
  },
28
30
  "scripts": {
29
- "start": "vite serve",
30
- "build": "npm run build --prefix ../ui-grid-core && tsup src/index.ts --format esm,cjs --dts --tsconfig tsconfig.json",
31
- "test": "vitest run"
31
+ "start": "SASS_PATH=node_modules vite serve",
32
+ "build": "npm run build --prefix ../ui-grid-core && SASS_PATH=node_modules tsup",
33
+ "watch": "npm run build --prefix ../ui-grid-core && SASS_PATH=node_modules tsup --watch",
34
+ "test": "SASS_PATH=node_modules vitest run"
32
35
  }
33
36
  }
package/scss-plugin.ts ADDED
@@ -0,0 +1,65 @@
1
+ import * as sass from 'sass';
2
+ import path from 'path';
3
+ import type { Plugin } from 'vite';
4
+ import type { Plugin as EsbuildPlugin } from 'esbuild';
5
+ import fs from 'fs';
6
+
7
+ const SCSS_SUFFIX = '?inline-css';
8
+
9
+ export function scssInlinePlugin(): Plugin {
10
+ return {
11
+ name: 'scss-inline',
12
+ enforce: 'pre',
13
+ resolveId(source, importer) {
14
+ if (source.endsWith('.scss') && importer && !source.includes('node_modules')) {
15
+ const resolved = path.resolve(path.dirname(importer), source);
16
+ return resolved + SCSS_SUFFIX;
17
+ }
18
+ return null;
19
+ },
20
+ load(id) {
21
+ if (!id.endsWith(SCSS_SUFFIX)) return null;
22
+ const filePath = id.slice(0, -SCSS_SUFFIX.length);
23
+ const result = sass.compile(filePath, {
24
+ style: 'compressed',
25
+ loadPaths: [
26
+ path.resolve(process.cwd(), 'node_modules'),
27
+ path.resolve(process.cwd(), '../../node_modules'),
28
+ ],
29
+ });
30
+ return `export default ${JSON.stringify(result.css)};`;
31
+ },
32
+ };
33
+ }
34
+
35
+ export function scssInlineEsbuild(): EsbuildPlugin {
36
+ return {
37
+ name: 'scss-inline',
38
+ setup(build) {
39
+ build.onResolve({ filter: /\.scss$/ }, (args) => {
40
+ if (args.resolveDir && !args.path.includes('node_modules')) {
41
+ return {
42
+ path: path.resolve(args.resolveDir, args.path),
43
+ namespace: 'scss-inline',
44
+ };
45
+ }
46
+ return null;
47
+ });
48
+
49
+ build.onLoad({ filter: /.*/, namespace: 'scss-inline' }, (args) => {
50
+ const result = sass.compile(args.path, {
51
+ style: 'compressed',
52
+ loadPaths: [
53
+ path.resolve(process.cwd(), 'node_modules'),
54
+ path.resolve(process.cwd(), '..'),
55
+ path.resolve(process.cwd(), '../../node_modules'),
56
+ ],
57
+ });
58
+ return {
59
+ contents: `export default ${JSON.stringify(result.css)};`,
60
+ loader: 'js',
61
+ };
62
+ });
63
+ },
64
+ };
65
+ }
@@ -0,0 +1,39 @@
1
+ let sheetsSupported: boolean | null = null;
2
+
3
+ function supportsAdoptedStyleSheets(): boolean {
4
+ if (sheetsSupported === null) {
5
+ try {
6
+ const s = new CSSStyleSheet();
7
+ s.replaceSync('');
8
+ sheetsSupported = true;
9
+ } catch {
10
+ sheetsSupported = false;
11
+ }
12
+ }
13
+ return sheetsSupported;
14
+ }
15
+
16
+ const sheetCache = new WeakMap<object, CSSStyleSheet>();
17
+
18
+ export function adoptStyles(shadowRoot: ShadowRoot, css: string, key: object): void {
19
+ if (!supportsAdoptedStyleSheets()) {
20
+ let styleEl = shadowRoot.querySelector('style[data-grid-styles]') as HTMLStyleElement | null;
21
+ if (!styleEl) {
22
+ styleEl = document.createElement('style');
23
+ styleEl.setAttribute('data-grid-styles', '');
24
+ styleEl.textContent = css;
25
+ shadowRoot.prepend(styleEl);
26
+ }
27
+ return;
28
+ }
29
+
30
+ let sheet = sheetCache.get(key);
31
+ if (!sheet) {
32
+ sheet = new CSSStyleSheet();
33
+ sheet.replaceSync(css);
34
+ sheetCache.set(key, sheet);
35
+ }
36
+ if (!shadowRoot.adoptedStyleSheets.includes(sheet)) {
37
+ shadowRoot.adoptedStyleSheets = [...shadowRoot.adoptedStyleSheets, sheet];
38
+ }
39
+ }
@@ -0,0 +1,291 @@
1
+ /**
2
+ * Declarative attribute bridge.
3
+ *
4
+ * The grid element accepts its full `GridOptions` shape through HTML
5
+ * attributes so it can be driven declaratively from any framework (or
6
+ * plain HTML). This module owns:
7
+ *
8
+ * - `OBSERVED_GRID_ATTRIBUTES`: the list passed to `observedAttributes`
9
+ * - `parseGridAttributeOptions(el)`: reads attributes off an element and
10
+ * returns the `Partial<GridOptions>` they describe
11
+ * - the individual attribute parsers (boolean / tri-state / number / JSON)
12
+ *
13
+ * The element keeps its own `attributeOptions` cache and `buildEffectiveOptions`
14
+ * merge so JS-set properties can override attribute values, but the parsing
15
+ * itself lives here.
16
+ */
17
+
18
+ import type { GridColumnDef, GridOptions, GridRecord } from '@ornery/ui-grid-core';
19
+
20
+ /** The list passed to `static observedAttributes`. Every kebab-case name
21
+ * that corresponds to a GridOptions field lives here. */
22
+ export const OBSERVED_GRID_ATTRIBUTES: readonly string[] = [
23
+ // Scalar attributes
24
+ 'grid-id',
25
+ 'title',
26
+ 'row-height',
27
+ 'header-row-height',
28
+ 'pagination-page-size',
29
+ 'pagination-current-page',
30
+ 'total-items',
31
+ 'virtualization-threshold',
32
+ 'tree-children-field',
33
+ 'tree-indent',
34
+ 'expandable-row-height',
35
+ 'expandable-row-header-width',
36
+ 'empty-message',
37
+ 'infinite-scroll-rows-from-end',
38
+ // JSON attributes
39
+ 'column-defs',
40
+ 'data',
41
+ 'grouping',
42
+ 'pagination-page-sizes',
43
+ // Boolean flags
44
+ 'enable-sorting',
45
+ 'enable-filtering',
46
+ 'enable-grouping',
47
+ 'enable-pinning',
48
+ 'enable-column-moving',
49
+ 'enable-column-resizing',
50
+ 'enable-cell-edit',
51
+ 'enable-cell-edit-on-focus',
52
+ 'enable-pagination',
53
+ 'enable-pagination-controls',
54
+ 'use-external-pagination',
55
+ 'enable-expandable',
56
+ 'enable-tree-view',
57
+ 'show-tree-expand-no-children',
58
+ 'tree-row-header-always-visible',
59
+ 'enable-auto-resize',
60
+ 'enable-virtualization',
61
+ 'enable-infinite-scroll',
62
+ 'infinite-scroll-up',
63
+ 'infinite-scroll-down',
64
+ // Selection — ported from ui.grid.selection options.
65
+ 'enable-row-selection',
66
+ 'multi-select',
67
+ 'no-unselect',
68
+ 'modifier-keys-to-multi-select',
69
+ 'enable-row-header-selection',
70
+ 'enable-full-row-selection',
71
+ 'enable-focus-row-on-row-header-click',
72
+ 'enable-select-row-on-focus',
73
+ 'enable-select-all',
74
+ 'enable-selection-batch-event',
75
+ 'enable-footer-total-selected',
76
+ 'selection-row-header-width',
77
+ ];
78
+
79
+ /**
80
+ * Read every supported attribute off the element and produce the
81
+ * corresponding `Partial<GridOptions>`. Missing attributes are simply
82
+ * absent from the result — the element merges this with its imperative
83
+ * `options` setter so JS-set properties win.
84
+ */
85
+ export function parseGridAttributeOptions(el: Element): Partial<GridOptions> {
86
+ const out: Partial<GridOptions> = {};
87
+
88
+ // Scalar string attributes
89
+ const gridId = el.getAttribute('grid-id');
90
+ if (gridId !== null) out.id = gridId;
91
+
92
+ const title = el.getAttribute('title');
93
+ if (title !== null) out.title = title;
94
+
95
+ const emptyMessage = el.getAttribute('empty-message');
96
+ if (emptyMessage !== null) out.emptyMessage = emptyMessage;
97
+
98
+ const treeChildrenField = el.getAttribute('tree-children-field');
99
+ if (treeChildrenField !== null) out.treeChildrenField = treeChildrenField;
100
+
101
+ // Scalar number attributes
102
+ const rowHeight = parseNumber(el, 'row-height');
103
+ if (rowHeight !== undefined) out.rowHeight = rowHeight;
104
+
105
+ const headerRowHeight = parseNumber(el, 'header-row-height');
106
+ if (headerRowHeight !== undefined) out.headerRowHeight = headerRowHeight;
107
+
108
+ const paginationPageSize = parseNumber(el, 'pagination-page-size');
109
+ if (paginationPageSize !== undefined) out.paginationPageSize = paginationPageSize;
110
+
111
+ const paginationCurrentPage = parseNumber(el, 'pagination-current-page');
112
+ if (paginationCurrentPage !== undefined) out.paginationCurrentPage = paginationCurrentPage;
113
+
114
+ const totalItems = parseNumber(el, 'total-items');
115
+ if (totalItems !== undefined) out.totalItems = totalItems;
116
+
117
+ const virtualizationThreshold = parseNumber(el, 'virtualization-threshold');
118
+ if (virtualizationThreshold !== undefined) out.virtualizationThreshold = virtualizationThreshold;
119
+
120
+ const treeIndent = parseNumber(el, 'tree-indent');
121
+ if (treeIndent !== undefined) out.treeIndent = treeIndent;
122
+
123
+ const expandableRowHeight = parseNumber(el, 'expandable-row-height');
124
+ if (expandableRowHeight !== undefined) out.expandableRowHeight = expandableRowHeight;
125
+
126
+ const expandableRowHeaderWidth = parseNumber(el, 'expandable-row-header-width');
127
+ if (expandableRowHeaderWidth !== undefined) out.expandableRowHeaderWidth = expandableRowHeaderWidth;
128
+
129
+ const infiniteScrollRowsFromEnd = parseNumber(el, 'infinite-scroll-rows-from-end');
130
+ if (infiniteScrollRowsFromEnd !== undefined) out.infiniteScrollRowsFromEnd = infiniteScrollRowsFromEnd;
131
+
132
+ // Boolean attributes
133
+ const enableSorting = parseBoolean(el, 'enable-sorting');
134
+ if (enableSorting !== undefined) out.enableSorting = enableSorting;
135
+
136
+ const enableFiltering = parseBoolean(el, 'enable-filtering');
137
+ if (enableFiltering !== undefined) out.enableFiltering = enableFiltering;
138
+
139
+ const enableGrouping = parseBoolean(el, 'enable-grouping');
140
+ if (enableGrouping !== undefined) out.enableGrouping = enableGrouping;
141
+
142
+ const enablePinning = parseBoolean(el, 'enable-pinning');
143
+ if (enablePinning !== undefined) out.enablePinning = enablePinning;
144
+
145
+ const enableColumnMoving = parseBoolean(el, 'enable-column-moving');
146
+ if (enableColumnMoving !== undefined) out.enableColumnMoving = enableColumnMoving;
147
+
148
+ const enableColumnResizing = parseBoolean(el, 'enable-column-resizing');
149
+ if (enableColumnResizing !== undefined) out.enableColumnResizing = enableColumnResizing;
150
+
151
+ const enableCellEdit = parseBoolean(el, 'enable-cell-edit');
152
+ if (enableCellEdit !== undefined) out.enableCellEdit = enableCellEdit;
153
+
154
+ const enableCellEditOnFocus = parseBoolean(el, 'enable-cell-edit-on-focus');
155
+ if (enableCellEditOnFocus !== undefined) out.enableCellEditOnFocus = enableCellEditOnFocus;
156
+
157
+ const enablePagination = parseBoolean(el, 'enable-pagination');
158
+ if (enablePagination !== undefined) out.enablePagination = enablePagination;
159
+
160
+ const enablePaginationControls = parseBoolean(el, 'enable-pagination-controls');
161
+ if (enablePaginationControls !== undefined) out.enablePaginationControls = enablePaginationControls;
162
+
163
+ const useExternalPagination = parseBoolean(el, 'use-external-pagination');
164
+ if (useExternalPagination !== undefined) out.useExternalPagination = useExternalPagination;
165
+
166
+ const enableExpandable = parseBoolean(el, 'enable-expandable');
167
+ if (enableExpandable !== undefined) out.enableExpandable = enableExpandable;
168
+
169
+ const enableTreeView = parseBoolean(el, 'enable-tree-view');
170
+ if (enableTreeView !== undefined) out.enableTreeView = enableTreeView;
171
+
172
+ const showTreeExpandNoChildren = parseBoolean(el, 'show-tree-expand-no-children');
173
+ if (showTreeExpandNoChildren !== undefined) out.showTreeExpandNoChildren = showTreeExpandNoChildren;
174
+
175
+ const treeRowHeaderAlwaysVisible = parseBoolean(el, 'tree-row-header-always-visible');
176
+ if (treeRowHeaderAlwaysVisible !== undefined) out.treeRowHeaderAlwaysVisible = treeRowHeaderAlwaysVisible;
177
+
178
+ const enableAutoResize = parseBoolean(el, 'enable-auto-resize');
179
+ if (enableAutoResize !== undefined) out.enableAutoResize = enableAutoResize;
180
+
181
+ const enableVirtualization = parseBoolean(el, 'enable-virtualization');
182
+ if (enableVirtualization !== undefined) out.enableVirtualization = enableVirtualization;
183
+
184
+ const enableInfiniteScroll = parseTriStateBoolean(el, 'enable-infinite-scroll');
185
+ if (enableInfiniteScroll !== undefined) out.enableInfiniteScroll = enableInfiniteScroll;
186
+
187
+ const infiniteScrollUp = parseBoolean(el, 'infinite-scroll-up');
188
+ if (infiniteScrollUp !== undefined) out.infiniteScrollUp = infiniteScrollUp;
189
+
190
+ const infiniteScrollDown = parseBoolean(el, 'infinite-scroll-down');
191
+ if (infiniteScrollDown !== undefined) out.infiniteScrollDown = infiniteScrollDown;
192
+
193
+ // Selection — most flags default to true in the old grid, so we use
194
+ // the tri-state parser to let consumers opt out with attr="false".
195
+ const enableRowSelection = parseTriStateBoolean(el, 'enable-row-selection');
196
+ if (enableRowSelection !== undefined) out.enableRowSelection = enableRowSelection;
197
+
198
+ const multiSelect = parseTriStateBoolean(el, 'multi-select');
199
+ if (multiSelect !== undefined) out.multiSelect = multiSelect;
200
+
201
+ const noUnselect = parseTriStateBoolean(el, 'no-unselect');
202
+ if (noUnselect !== undefined) out.noUnselect = noUnselect;
203
+
204
+ const modifierKeysToMultiSelect = parseTriStateBoolean(el, 'modifier-keys-to-multi-select');
205
+ if (modifierKeysToMultiSelect !== undefined) out.modifierKeysToMultiSelect = modifierKeysToMultiSelect;
206
+
207
+ const enableRowHeaderSelection = parseTriStateBoolean(el, 'enable-row-header-selection');
208
+ if (enableRowHeaderSelection !== undefined) out.enableRowHeaderSelection = enableRowHeaderSelection;
209
+
210
+ const enableFullRowSelection = parseTriStateBoolean(el, 'enable-full-row-selection');
211
+ if (enableFullRowSelection !== undefined) out.enableFullRowSelection = enableFullRowSelection;
212
+
213
+ const enableFocusRowOnRowHeaderClick = parseTriStateBoolean(
214
+ el,
215
+ 'enable-focus-row-on-row-header-click',
216
+ );
217
+ if (enableFocusRowOnRowHeaderClick !== undefined) out.enableFocusRowOnRowHeaderClick = enableFocusRowOnRowHeaderClick;
218
+
219
+ const enableSelectRowOnFocus = parseTriStateBoolean(el, 'enable-select-row-on-focus');
220
+ if (enableSelectRowOnFocus !== undefined) out.enableSelectRowOnFocus = enableSelectRowOnFocus;
221
+
222
+ const enableSelectAll = parseTriStateBoolean(el, 'enable-select-all');
223
+ if (enableSelectAll !== undefined) out.enableSelectAll = enableSelectAll;
224
+
225
+ const enableSelectionBatchEvent = parseTriStateBoolean(el, 'enable-selection-batch-event');
226
+ if (enableSelectionBatchEvent !== undefined) out.enableSelectionBatchEvent = enableSelectionBatchEvent;
227
+
228
+ const enableFooterTotalSelected = parseTriStateBoolean(el, 'enable-footer-total-selected');
229
+ if (enableFooterTotalSelected !== undefined) out.enableFooterTotalSelected = enableFooterTotalSelected;
230
+
231
+ const selectionRowHeaderWidth = parseNumber(el, 'selection-row-header-width');
232
+ if (selectionRowHeaderWidth !== undefined) out.selectionRowHeaderWidth = selectionRowHeaderWidth;
233
+
234
+ // JSON attributes
235
+ const columnDefs = parseJson<GridColumnDef[]>(el, 'column-defs');
236
+ if (columnDefs !== undefined) out.columnDefs = columnDefs;
237
+
238
+ const data = parseJson<GridRecord[]>(el, 'data');
239
+ if (data !== undefined) out.data = data;
240
+
241
+ const grouping = parseJson(el, 'grouping');
242
+ if (grouping !== undefined && grouping !== null) {
243
+ // Grouping is typed as `GridGroupingOptions` in core; we stay loose
244
+ // here because the parse-time check already validated shape via
245
+ // JSON.parse, and downstream merges don't care about the cast.
246
+ out.grouping = grouping as GridOptions['grouping'];
247
+ }
248
+
249
+ const paginationPageSizes = parseJson<number[] | null>(el, 'pagination-page-sizes');
250
+ if (paginationPageSizes !== undefined) out.paginationPageSizes = paginationPageSizes;
251
+
252
+ return out;
253
+ }
254
+
255
+ /** Present attribute → true. Absent → undefined (no override). */
256
+ export function parseBoolean(el: Element, name: string): boolean | undefined {
257
+ return el.hasAttribute(name) ? true : undefined;
258
+ }
259
+
260
+ /**
261
+ * Tri-state boolean. Present + empty/"true" → true; "false"/"0"/"off" →
262
+ * false; absent → undefined. Use for flags that default to true in the
263
+ * model so consumers can opt out with `attr="false"` without removing
264
+ * the attribute.
265
+ */
266
+ export function parseTriStateBoolean(el: Element, name: string): boolean | undefined {
267
+ if (!el.hasAttribute(name)) return undefined;
268
+ const raw = el.getAttribute(name);
269
+ if (raw === null) return undefined;
270
+ const normalized = raw.trim().toLowerCase();
271
+ if (normalized === 'false' || normalized === '0' || normalized === 'off') return false;
272
+ return true;
273
+ }
274
+
275
+ export function parseNumber(el: Element, name: string): number | undefined {
276
+ const raw = el.getAttribute(name);
277
+ if (raw === null) return undefined;
278
+ const parsed = Number(raw);
279
+ return Number.isFinite(parsed) ? parsed : undefined;
280
+ }
281
+
282
+ export function parseJson<T = unknown>(el: Element, name: string): T | undefined {
283
+ const raw = el.getAttribute(name);
284
+ if (raw === null) return undefined;
285
+ try {
286
+ return JSON.parse(raw) as T;
287
+ } catch (e) {
288
+ console.warn(`<ui-grid-element>: invalid JSON in "${name}" attribute`, e);
289
+ return undefined;
290
+ }
291
+ }
@@ -0,0 +1,142 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
2
+ import {
3
+ defineStandaloneUiGridElement,
4
+ mountVanillaUiGrid,
5
+ type GridOptions,
6
+ } from './index';
7
+
8
+ function createRows(count: number) {
9
+ return Array.from({ length: count }, (_, i) => ({
10
+ id: `row-${i}`,
11
+ name: `Row ${i}`,
12
+ value: i * 10,
13
+ }));
14
+ }
15
+
16
+ const BASE_OPTIONS: GridOptions = {
17
+ id: 'auto-height-test',
18
+ columnDefs: [
19
+ { name: 'name', displayName: 'Name' },
20
+ { name: 'value', displayName: 'Value' },
21
+ ],
22
+ data: createRows(50),
23
+ rowHeight: 40,
24
+ headerRowHeight: 50,
25
+ enableSorting: false,
26
+ enableFiltering: false,
27
+ enableVirtualization: true,
28
+ virtualizationThreshold: 1,
29
+ };
30
+
31
+ describe('autoAdjustHeight (matches old ui-grid enableMinHeightCheck)', () => {
32
+ let container: HTMLElement;
33
+
34
+ beforeEach(async () => {
35
+ container = document.createElement('div');
36
+ document.body.appendChild(container);
37
+ await defineStandaloneUiGridElement('ui-grid-auto-height-test');
38
+ });
39
+
40
+ afterEach(() => {
41
+ document.body.removeChild(container);
42
+ });
43
+
44
+ it('sets host height when parent provides no height (minRowsToShow default=10)', async () => {
45
+ const grid = await mountVanillaUiGrid(
46
+ container,
47
+ { ...BASE_OPTIONS },
48
+ undefined,
49
+ 'ui-grid-auto-height-test',
50
+ );
51
+
52
+ // Default minRowsToShow = 10, rowHeight = 40, headerRowHeight = 50
53
+ // Expected min height = 50 (header) + 0 (no filter) + 10 * 40 = 450
54
+ // Note: jsdom clientHeight is always 0, so autoAdjust always fires.
55
+ const expectedMin = 50 + 10 * 40;
56
+ expect(grid.style.height).toBe(`${expectedMin}px`);
57
+ });
58
+
59
+ it('respects custom minRowsToShow', async () => {
60
+ const grid = await mountVanillaUiGrid(
61
+ container,
62
+ { ...BASE_OPTIONS, minRowsToShow: 5 },
63
+ undefined,
64
+ 'ui-grid-auto-height-test',
65
+ );
66
+
67
+ // 50 (header) + 5 * 40 = 250
68
+ const expectedMin = 50 + 5 * 40;
69
+ expect(grid.style.height).toBe(`${expectedMin}px`);
70
+ });
71
+
72
+ it('includes filter row height in calculation when filtering enabled', async () => {
73
+ const grid = await mountVanillaUiGrid(
74
+ container,
75
+ { ...BASE_OPTIONS, enableFiltering: true, minRowsToShow: 5 },
76
+ undefined,
77
+ 'ui-grid-auto-height-test',
78
+ );
79
+
80
+ // With filtering, the filter strip adds height. The exact measured value
81
+ // depends on rendering, but the total should exceed header + rows alone.
82
+ const headerPlusRows = 50 + 5 * 40;
83
+ expect(parseInt(grid.style.height)).toBeGreaterThanOrEqual(headerPlusRows);
84
+ });
85
+
86
+ it('does not adjust height when enableMinHeightCheck is false', async () => {
87
+ const grid = await mountVanillaUiGrid(
88
+ container,
89
+ { ...BASE_OPTIONS, enableMinHeightCheck: false },
90
+ undefined,
91
+ 'ui-grid-auto-height-test',
92
+ );
93
+
94
+ expect(grid.style.height).toBe('');
95
+ });
96
+
97
+ it('does not override height when parent provides sufficient height', async () => {
98
+ container.style.height = '800px';
99
+ container.style.display = 'grid';
100
+
101
+ const grid = await mountVanillaUiGrid(
102
+ container,
103
+ { ...BASE_OPTIONS },
104
+ undefined,
105
+ 'ui-grid-auto-height-test',
106
+ );
107
+
108
+ // Grid fills parent — autoAdjust should not fire because clientHeight >= minHeight
109
+ // Note: in jsdom clientHeight is always 0, so autoAdjust will fire.
110
+ // This test verifies the logic path exists; real browser testing
111
+ // confirms the parent-height case.
112
+ expect(grid.style.height).toBeTruthy();
113
+ });
114
+
115
+ it('body viewport has overflow-y:auto for scrolling', async () => {
116
+ const grid = await mountVanillaUiGrid(
117
+ container,
118
+ { ...BASE_OPTIONS },
119
+ undefined,
120
+ 'ui-grid-auto-height-test',
121
+ );
122
+
123
+ const shadow = grid.shadowRoot!;
124
+ const bodyViewport = shadow.querySelector<HTMLElement>('.grid-body-viewport');
125
+ expect(bodyViewport).not.toBeNull();
126
+ expect(bodyViewport!.classList.contains('grid-body-viewport')).toBe(true);
127
+ });
128
+
129
+ it('grid-frame fills the host element height', async () => {
130
+ const grid = await mountVanillaUiGrid(
131
+ container,
132
+ { ...BASE_OPTIONS },
133
+ undefined,
134
+ 'ui-grid-auto-height-test',
135
+ );
136
+
137
+ const shadow = grid.shadowRoot!;
138
+ const gridFrame = shadow.querySelector<HTMLElement>('.grid-frame');
139
+ expect(gridFrame).not.toBeNull();
140
+ // .grid-frame has height:100% in CSS to fill the host
141
+ });
142
+ });