diff --git a/package-lock.json b/package-lock.json index be53c3fa2..38a93005a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12258,7 +12258,7 @@ }, "packages/DataGrid": { "name": "@3mo/data-grid", - "version": "0.38.2", + "version": "0.38.3", "license": "MIT", "dependencies": { "@a11d/equals": "x", diff --git a/packages/DataGrid/DataGrid.test.ts b/packages/DataGrid/DataGrid.test.ts index 579030347..2c90693b1 100644 --- a/packages/DataGrid/DataGrid.test.ts +++ b/packages/DataGrid/DataGrid.test.ts @@ -168,6 +168,18 @@ describe('DataGrid', () => { describe('auto-generated', () => { const fixture = new ComponentTestFixture(html``) + it('should keep the generated columns while new data has the same shape', async () => { + const columns = fixture.component.columns + const columnsChange = vi.fn() + fixture.component.addEventListener('columnsChange', columnsChange) + + fixture.component.data = [...fixture.component.data] + await fixture.updateComplete + + expect(columnsChange).not.toHaveBeenCalled() + expect(fixture.component.columns).toEqual(columns) + }) + it('should auto-generate columns', () => { const [firstColumn, secondColumn, thirdColumn] = fixture.component.columns @@ -260,8 +272,14 @@ describe('DataGrid', () => { await fixture.updateComplete expect(fixture.component.columns.map(c => c.dataSelector)).toEqual(['name']) - expect(fixture.component.columnsController.columns.definitions.programmatic.length).toBe(1) - expect(fixture.component.columnsController.columns.definitions.generated.length).toBe(0) + expect(fixture.component.columnDefinitions.programmatic.length).toBe(1) + expect(fixture.component.columnDefinitions.generated.length).toBe(0) + }) + + it('should take effect at once, before the next update', () => { + fixture.component.columns = [new DataGridColumn({ heading: 'Name', dataSelector: 'name' })] + + expect(fixture.component.columns.map(c => c.dataSelector)).toEqual(['name']) }) it('should be providable through the deprecated setColumns as well', async () => { @@ -303,7 +321,7 @@ describe('DataGrid', () => { const col = fixture.component.querySelector('mo-data-grid-column-number')! col.heading = 'Identifier' await col.updateComplete - fixture.component.columnsController.extractColumns() + fixture.component.extractColumns() await fixture.updateComplete expect(fixture.component.columns.find(c => c.dataSelector === 'id')?.heading).toEqual('Identifier') }) @@ -311,7 +329,7 @@ describe('DataGrid', () => { it('should update columns when columns connect or disconnect', async () => { const column = fixture.component.querySelector('mo-data-grid-column-number') column?.remove() - fixture.component.columnsController.extractColumns() + fixture.component.extractColumns() await fixture.updateComplete expect(fixture.component.columns.map(c => c.dataSelector)).toContain('name') @@ -322,15 +340,15 @@ describe('DataGrid', () => { fixture.component.appendChild(newCol) await new Promise(r => setTimeout(r, 20)) await newCol.updateComplete - fixture.component.columnsController.extractColumns() + fixture.component.extractColumns() await fixture.updateComplete expect(fixture.component.columns.map(c => c.dataSelector)).toContain('id') expect(fixture.component.columns.map(c => c.dataSelector)).toContain('name') }) it('should expose the definition sources with extracted definitions winning', () => { - fixture.component.columnsController.extractColumns() - const definitions = fixture.component.columnsController.columns.definitions + fixture.component.extractColumns() + const definitions = fixture.component.columnDefinitions expect(definitions.extracted.map(c => c.dataSelector)).toContain('id') expect(definitions.extracted.map(c => c.dataSelector)).toContain('name') @@ -338,7 +356,7 @@ describe('DataGrid', () => { }) it('should be iterable and array-like over the effective definitions', () => { - const definitions = fixture.component.columnsController.columns.definitions + const definitions = fixture.component.columnDefinitions expect(definitions.length).toBeGreaterThanOrEqual(2) expect(definitions.find(c => c.dataSelector === 'name')?.heading).toBe('Name') @@ -346,7 +364,7 @@ describe('DataGrid', () => { }) it('should compose anew and update the data grid when a source is assigned', async () => { - const definitions = fixture.component.columnsController.columns.definitions + const definitions = fixture.component.columnDefinitions const columnsChange = vi.fn() fixture.component.addEventListener('columnsChange', columnsChange) @@ -423,7 +441,7 @@ describe('DataGrid', () => { colText.heading = 'Full Name' await fixture.updateComplete await colText.updateComplete - fixture.component.columnsController.extractColumns() + fixture.component.extractColumns() await fixture.updateComplete expect(fixture.component.columns.find(c => c.dataSelector === 'name')?.heading).toBe('Full Name') @@ -1195,7 +1213,7 @@ describe('DataGrid', () => { get updateCompleted() { return (async () => { await this.balanceColumnElement.updateComplete - this.component.columnsController.extractColumns() + this.component.extractColumns() this.component.requestUpdate() await this.component.updateComplete for (const row of this.component.rows) { @@ -1756,4 +1774,84 @@ describe('DataGrid', () => { expect(fixture.component.virtualizationController).toBe(controller.virtualization) }) }) + + describe('Column tracks', () => { + const fixture = new ComponentTestFixture>(html` + + + + + `) + + const tracks = async () => { + await fixture.updateComplete + await new Promise(r => setTimeout(r, 30)) + await fixture.updateComplete + return fixture.component.renderRoot.querySelector('#content')?.getAttribute('columns') ?? '' + } + + const dataTrackCount = (value: string) => value.match(/\[data\]/g)?.length ?? 0 + + it('should provide a named track per visible column and none for hidden ones, as a zero track would still render a gap', async () => { + const value = await tracks() + + expect(dataTrackCount(value)).toBe(2) + expect(value).toContain('[data] max-content') + expect(value).toContain('[data] 50px') + expect(value).toContain('[padding] 1fr') + expect(value).toContain('[actions]') + + fixture.component.columns.find(c => c.dataSelector === 'b')!.hide() + const hidden = await tracks() + + expect(dataTrackCount(hidden)).toBe(1) + expect(hidden).not.toContain('50px') + }) + + it('should include the selection, details and reorder tracks only while the corresponding feature is active', async () => { + const initial = await tracks() + expect(initial).not.toContain('[selection]') + expect(initial).not.toContain('[details]') + expect(initial).not.toContain('[order]') + + fixture.component.selectability = DataGridSelectability.Multiple + expect(await tracks()).toContain('[selection]') + + fixture.component.getRowDetailsTemplate = () => html`
Details
` + expect(await tracks()).toContain('[details]') + + fixture.component.getRowDetailsTemplate = undefined + fixture.component.reorderability = true + const reorderable = await tracks() + + expect(reorderable).toContain('[order]') + expect(reorderable).not.toContain('[details]') + }) + + it('should lay out the tracks of a grid which first rendered outside the flat tree, such as slotted into a host yet to render its slot', async () => { + const host = document.createElement('div') + const root = host.attachShadow({ mode: 'open' }) + const grid = document.createElement('mo-data-grid') as DataGrid<{ a: number, b: number }> + grid.selectability = DataGridSelectability.Multiple + grid.data = [{ a: 1, b: 2 }] + const column = document.createElement('mo-data-grid-column-number') + Object.assign(column, { heading: 'A', dataSelector: 'a', width: '50px' }) + grid.append(column) + host.append(grid) + document.body.append(host) + try { + await grid.updateComplete + await new Promise(r => setTimeout(r, 30)) + await grid.updateComplete + + root.append(document.createElement('slot')) + await new Promise(requestAnimationFrame) + + const tracks = getComputedStyle(grid.renderRoot.querySelector('#content')!).gridTemplateColumns + expect(tracks).toMatch(/^\[selection\] 40px \[data\] 50px \[padding\] [\d.]+px \[actions\] 28px/) + } finally { + host.remove() + } + }) + }) }) \ No newline at end of file diff --git a/packages/DataGrid/DataGrid.ts b/packages/DataGrid/DataGrid.ts index 87aa5b1d4..7d3cb4ee6 100644 --- a/packages/DataGrid/DataGrid.ts +++ b/packages/DataGrid/DataGrid.ts @@ -1,4 +1,4 @@ -import { property, component, Component, html, css, query, type PropertyValues, event, style, literal, staticHtml, type HTMLTemplateResult, repeat, eventListener } from '@a11d/lit' +import { property, component, Component, html, css, query, type PropertyValues, event, style, literal, staticHtml, type HTMLTemplateResult, repeat } from '@a11d/lit' import { LocalStorage } from '@a11d/local-storage' import { NotificationComponent } from '@a11d/lit-application' import { Downloader } from '@3mo/downloader' @@ -8,12 +8,10 @@ import { tooltip } from '@3mo/tooltip' import { Localizer } from '@3mo/localization' import { type Scroller } from '@3mo/scroller' import { observeResize } from '@3mo/resize-observer' -import { DataGridSelectability, DataGridSelectionBehaviorOnDataChange } from './DataGridSelectionController.js' -import { type DataGridRankedSortDefinition, type DataGridSorting } from './DataGridSortingController.js' -import { DataGridController } from './DataGridController.js' -import { DataGridEditability } from './DataGridEditabilityController.js' +import { DataGridEditability, DataGridController, type DataGridRankedSortDefinition, type DataGridSorting, DataGridSelectability, DataGridSelectionBehaviorOnDataChange, type DataRecord } from './controller/index.js' +import { DataGridColumnComponentsController } from './DataGridColumnComponentsController.js' +import { DataGridColumnDefinitions } from './DataGridColumnDefinitions.js' import { type DataGridColumn, type DataGridCell, type DataGridFooter, type DataGridHeader, type DataGridRow, type DataGridReorderChange } from './index.js' -import { type DataRecord } from './DataRecord.js' import { DataGridToolbarElementStyles } from './DataGridToolbarElementStyles.js' import { DataGridPagination, type DataGridPaginationLike, type DataGridPaginationSize, type DataGridPaginationStrategy } from './DataGridPagination.js' @@ -31,7 +29,7 @@ Localizer.dictionaries.add('de', { * @element mo-data-grid * * @attr data - The data to be displayed in the DataGrid. It is an array of objects, where each object represents a row. - * @attr columns - The read-only columns of the DataGrid, composed of their definitions and modifications. Provide columns programmatically via `columns.definitions.programmatic`. + * @attr columns - The columns of the DataGrid, composed of their definitions and modifications. Assigning it gives the definitions in code, which column elements override. * @attr headerHidden - Whether the header should be hidden. * @attr page - The current page. * @attr pagination - How the rows are paged: a strategy, `pages` or `scroll`, and a size, a number or `auto` to fit the height, e.g. `pages`, `pages 50` or `50`. @@ -136,7 +134,7 @@ export class DataGrid) { - return this.controller.columns.extractColumns(...parameters) - } - - @eventListener('DataGridColumnComponent:update') - protected handleColumnChange(e: CustomEvent) { - e.stopPropagation() - this.controller.columns.extractColumns() + extractColumns() { + this.columnComponents.extractColumns() } get visibleColumns() { return this.controller.columns.columns.visible } + // Named tracks for the parts present only: a zero-width track would still render its gap. + private get columnTracks() { + const { reorderability, details, selection } = this.controller + return [ + ['order', !reorderability.enabled ? undefined : 'var(--mo-data-grid-column-reorder-width)'], + ['details', !details.hasDetails ? undefined : 'var(--mo-data-grid-column-details-width)'], + ['selection', !selection.hasSelection ? undefined : 'var(--mo-data-grid-column-selection-width)'], + ...this.visibleColumns.map(column => ['data', column.width]), + ['padding', '1fr'], + ['actions', 'var(--mo-data-grid-column-actions-width)'], + ].filter(([, width]) => width !== undefined).map(([name, width]) => `[${name}] ${width}`).join(' ') + } + getRow(data: TData) { return this.rows.find(r => r.data === data) } @@ -416,6 +421,7 @@ export class DataGrid>(this, grid => ({ get data() { return grid.data }, + get columns() { return grid.columnDefinitions.toArray() }, get subDataGridDataSelector() { return grid.subDataGridDataSelector }, get sorting() { return grid.sorting }, handleSortingChange: sorting => { @@ -449,6 +455,13 @@ export class DataGrid NotificationComponent.notifyAndThrowError(error.message), })) + readonly columnComponents = new DataGridColumnComponentsController(this) + + readonly columnDefinitions = new DataGridColumnDefinitions({ + generate: () => this.columnComponents.autoGeneratedColumns, + updated: () => this.controller.columns.syncColumns(), + }) + /** @deprecated Use `controller.columns`. */ get columnsController() { return this.controller.columns } @@ -713,7 +726,7 @@ export class DataGrid this.style.setProperty('--_content-height', `${e?.contentRect.height ?? 0}px`))} ${this.controller.virtualization.root.ref()} > - + ${this.headerTemplate} ${this.contentTemplate} diff --git a/packages/DataGrid/DataGridColumnComponentsController.ts b/packages/DataGrid/DataGridColumnComponentsController.ts new file mode 100644 index 000000000..3d7444492 --- /dev/null +++ b/packages/DataGrid/DataGridColumnComponentsController.ts @@ -0,0 +1,73 @@ +import { Controller, eventListener } from '@a11d/lit' +import { DataGridColumnComponent } from './columns/DataGridColumnComponent.js' +import { type DataGridColumn } from './controller/DataGridColumn.js' +import { type DataGrid } from './DataGrid.js' + +/** The column elements of a `mo-data-grid`: the ones it is declared with, and the ones it makes up for data it has no columns for. */ +export class DataGridColumnComponentsController extends Controller { + constructor(private readonly grid: DataGrid) { + super(grid) + } + + private initialized = false + + override hostUpdated() { + if (!this.initialized && !this.grid.columnDefinitions.extracted.length) { + this.extractColumns() + } + this.initialized = true + } + + @eventListener('DataGridColumnComponent:update') + protected handleColumnChange(e: CustomEvent) { + e.stopPropagation() + this.extractColumns() + } + + extractColumns() { + this.grid.columnDefinitions.extracted = this.columnsElements.map(c => c.column) + } + + private get columnsElements() { + const slot = this.grid.renderRoot?.querySelector('slot[name=column]') + if (!slot) { + return [] + } + const children = [...slot.children] + const assigned = [...slot.assignedElements()] + return [...assigned, ...children] + .filter((c): c is DataGridColumnComponent => c instanceof DataGridColumnComponent) + .map(c => { + c.dataGrid = this.grid + return c + }) + } + + get autoGeneratedColumns(): Array> { + if (!this.grid.data.length) { + return [] + } + + const getDefaultColumnElement = (value: unknown) => { + switch (typeof value) { + case 'number': + case 'bigint': + return 'mo-data-grid-column-number' + case 'boolean': + return 'mo-data-grid-column-boolean' + default: + return 'mo-data-grid-column-text' + } + } + const [sampleData] = this.grid.data + return Object.keys(sampleData || {}) + .filter(key => !key.startsWith('_')) + .map(key => { + const columnElement = document.createElement(getDefaultColumnElement(KeyPath.get(sampleData, key as any))) + columnElement.heading = key.replace(/([A-Z])/g, ' $1').charAt(0).toUpperCase() + key.replace(/([A-Z])/g, ' $1').slice(1) + columnElement.dataSelector = key + columnElement.dataGrid = this.grid as unknown as DataGrid + return columnElement.column + }) as unknown as Array> + } +} \ No newline at end of file diff --git a/packages/DataGrid/DataGridColumnsController/DataGridColumnDefinitions.test.ts b/packages/DataGrid/DataGridColumnDefinitions.test.ts similarity index 99% rename from packages/DataGrid/DataGridColumnsController/DataGridColumnDefinitions.test.ts rename to packages/DataGrid/DataGridColumnDefinitions.test.ts index 72f19b114..72dccd6e0 100644 --- a/packages/DataGrid/DataGridColumnsController/DataGridColumnDefinitions.test.ts +++ b/packages/DataGrid/DataGridColumnDefinitions.test.ts @@ -1,4 +1,4 @@ -import { DataGridColumn } from '../index.js' +import { DataGridColumn } from './index.js' import { DataGridColumnDefinitions } from './DataGridColumnDefinitions.js' type Person = { id: number, name: string } diff --git a/packages/DataGrid/DataGridColumnsController/DataGridColumnDefinitions.ts b/packages/DataGrid/DataGridColumnDefinitions.ts similarity index 89% rename from packages/DataGrid/DataGridColumnsController/DataGridColumnDefinitions.ts rename to packages/DataGrid/DataGridColumnDefinitions.ts index a52cd8d4f..b8c9b4148 100644 --- a/packages/DataGrid/DataGridColumnsController/DataGridColumnDefinitions.ts +++ b/packages/DataGrid/DataGridColumnDefinitions.ts @@ -1,5 +1,5 @@ -import { type DataGridColumn } from '../index.js' -import { ArrayLikeView } from './ArrayLikeView.js' +import { type DataGridColumn } from './controller/index.js' +import { ArrayLikeView } from './controller/ArrayLikeView.js' type DataGridColumnDefinitionsInit = { /** Generates definitions from the data grid's data. Only called while no other source provides any definitions. */ @@ -9,7 +9,8 @@ type DataGridColumnDefinitionsInit = { } /** - * The column definitions of a data grid: which columns exist and how they present by default. + * The column definitions of a `mo-data-grid`: which columns exist and how they present by default, which + * it hands its controller as the `columns` option. * * Definitions originate from three sources whose precedence this class owns: the first source * providing any definitions wins — `extracted` before `programmatic` before `generated`. Assigning diff --git a/packages/DataGrid/DataGridColumnHeader.ts b/packages/DataGrid/DataGridColumnHeader.ts index 4a0d91c40..046702ade 100644 --- a/packages/DataGrid/DataGridColumnHeader.ts +++ b/packages/DataGrid/DataGridColumnHeader.ts @@ -3,8 +3,8 @@ import { tooltip } from '@3mo/tooltip' import { Localizer } from '@3mo/localization' import { ResizeController } from '@3mo/resize-observer' import { ReorderabilityState } from '@3mo/reorderability' -import { DataGridColumn } from './DataGridColumn.js' -import { DataGridSortingStrategy } from './DataGridSortingController.js' +import { DataGridColumn } from './controller/DataGridColumn.js' +import { DataGridSortingStrategy } from './controller/DataGridSortingController.js' Localizer.dictionaries.add('de', { 'Sorting': 'Sortierung', diff --git a/packages/DataGrid/DataGridColumnsController/index.ts b/packages/DataGrid/DataGridColumnsController/index.ts deleted file mode 100644 index 0bff840ee..000000000 --- a/packages/DataGrid/DataGridColumnsController/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from './DataGridColumnDefinitions.js' -export * from './DataGridColumnModification.js' -export * from './DataGridColumnModifications.js' -export * from './DataGridColumns.js' -export * from './DataGridColumnsController.js' \ No newline at end of file diff --git a/packages/DataGrid/README.md b/packages/DataGrid/README.md index 38d1bb975..15927c466 100644 --- a/packages/DataGrid/README.md +++ b/packages/DataGrid/README.md @@ -86,7 +86,7 @@ rather than opening and closing rows. | `rows` | | `DataGridRow[]` | | The rendered rows, sub rows included, in the order of their records. | | `resolvedPagination` | | `{ readonly strategy: DataGridPaginationStrategy; readonly size: DataGridPaginationSize; } \| undefined` | | Resolves effective pagination configuration from property, static default, and fallback. | | `data` | `data` | `TData[]` | `"new Array()"` | The data to be displayed in the DataGrid. It is an array of objects, where each object represents a row. | -| `columns` | `columns` | `DataGridColumn[]` | | The read-only columns of the DataGrid, composed of their definitions and modifications. Provide columns programmatically via `columns.definitions.programmatic`. | +| `columns` | `columns` | `DataGridColumn[]` | | The columns of the DataGrid, composed of their definitions and modifications. Assigning it gives the definitions in code, which column elements override. | | `headerHidden` | `headerHidden` | `boolean` | `false` | Whether the header should be hidden. | | `page` | `page` | `number` | `1` | The current page. | | `pagination` | `pagination` | `DataGridPagination \| undefined` | | How the rows are paged: a strategy, `pages` or `scroll`, and a size, a number or `auto` to fit the height, e.g. `pages`, `pages 50` or `50`. | diff --git a/packages/DataGrid/columns/DataGridColumnComponent.ts b/packages/DataGrid/columns/DataGridColumnComponent.ts index b5b920ba7..2c096c460 100644 --- a/packages/DataGrid/columns/DataGridColumnComponent.ts +++ b/packages/DataGrid/columns/DataGridColumnComponent.ts @@ -1,7 +1,7 @@ import { component, Component, event, property, type HTMLTemplateResult, type PropertyValues } from '@a11d/lit' import { hasChanged } from '@a11d/equals' import { DataGrid } from '../DataGrid.js' -import { DataGridColumn, type DataGridColumnAlignment, type DataGridColumnContentStyle, type DataGridColumnMenuItems, type DataGridColumnSticky } from '../DataGridColumn.js' +import { DataGridColumn, type DataGridColumnAlignment, type DataGridColumnContentStyle, type DataGridColumnMenuItems, type DataGridColumnSticky } from '../controller/DataGridColumn.js' /** * @attr width - The width of the column diff --git a/packages/DataGrid/columns/number/DataGridColumnNumberBase.ts b/packages/DataGrid/columns/number/DataGridColumnNumberBase.ts index d39752220..a8c95c74e 100644 --- a/packages/DataGrid/columns/number/DataGridColumnNumberBase.ts +++ b/packages/DataGrid/columns/number/DataGridColumnNumberBase.ts @@ -1,7 +1,7 @@ import { type HTMLTemplateResult, property } from '@a11d/lit' import { hasChanged } from '@a11d/equals' import { DataGridColumnComponent } from '../DataGridColumnComponent.js' -import { DataGridColumn, type DataGridColumnAlignment } from '../../DataGridColumn.js' +import { DataGridColumn, type DataGridColumnAlignment } from '../../controller/DataGridColumn.js' export abstract class DataGridColumnNumberBase extends DataGridColumnComponent { @property({ type: Object, hasChanged }) formatOptions?: Intl.NumberFormatOptions diff --git a/packages/DataGrid/DataGridColumnsController/ArrayLikeView.ts b/packages/DataGrid/controller/ArrayLikeView.ts similarity index 95% rename from packages/DataGrid/DataGridColumnsController/ArrayLikeView.ts rename to packages/DataGrid/controller/ArrayLikeView.ts index 6b6133ff0..6ee165b59 100644 --- a/packages/DataGrid/DataGridColumnsController/ArrayLikeView.ts +++ b/packages/DataGrid/controller/ArrayLikeView.ts @@ -12,6 +12,11 @@ export abstract class ArrayLikeView implements ArrayLike, Iterable { get length() { return this.items.length } + /** The items as they stand: the same array until they are replaced. */ + toArray() { + return this.items + } + protected setItems(items: ReadonlyArray) { for (let index = items.length; index < this.items.length; ++index) { Reflect.deleteProperty(this, index) diff --git a/packages/DataGrid/DataGridColumn.test.ts b/packages/DataGrid/controller/DataGridColumn.test.ts similarity index 100% rename from packages/DataGrid/DataGridColumn.test.ts rename to packages/DataGrid/controller/DataGridColumn.test.ts diff --git a/packages/DataGrid/DataGridColumn.ts b/packages/DataGrid/controller/DataGridColumn.ts similarity index 96% rename from packages/DataGrid/DataGridColumn.ts rename to packages/DataGrid/controller/DataGridColumn.ts index 8e0c283ee..8551c068a 100644 --- a/packages/DataGrid/DataGridColumn.ts +++ b/packages/DataGrid/controller/DataGridColumn.ts @@ -1,6 +1,8 @@ import { css, type CSSResult, type HTMLTemplateResult } from '@a11d/lit' import { equals } from '@a11d/equals' -import type { DataGridController, DataGridColumns, DataGridSortingStrategy } from './index.js' +import type { DataGridController } from './DataGridController.js' +import type { DataGridColumns } from './DataGridColumns.js' +import type { DataGridSortingStrategy } from './DataGridSortingController.js' import type * as CSS from 'csstype' export type DataGridColumnContentStyle = diff --git a/packages/DataGrid/DataGridColumnsController/DataGridColumnModification.ts b/packages/DataGrid/controller/DataGridColumnModification.ts similarity index 90% rename from packages/DataGrid/DataGridColumnsController/DataGridColumnModification.ts rename to packages/DataGrid/controller/DataGridColumnModification.ts index 8c1c156c6..3938ff6d0 100644 --- a/packages/DataGrid/DataGridColumnsController/DataGridColumnModification.ts +++ b/packages/DataGrid/controller/DataGridColumnModification.ts @@ -1,4 +1,4 @@ -import { type DataGridColumnSticky } from '../index.js' +import { type DataGridColumnSticky } from './DataGridColumn.js' import type * as CSS from 'csstype' /** diff --git a/packages/DataGrid/DataGridColumnsController/DataGridColumnModifications.test.ts b/packages/DataGrid/controller/DataGridColumnModifications.test.ts similarity index 100% rename from packages/DataGrid/DataGridColumnsController/DataGridColumnModifications.test.ts rename to packages/DataGrid/controller/DataGridColumnModifications.test.ts diff --git a/packages/DataGrid/DataGridColumnsController/DataGridColumnModifications.ts b/packages/DataGrid/controller/DataGridColumnModifications.ts similarity index 100% rename from packages/DataGrid/DataGridColumnsController/DataGridColumnModifications.ts rename to packages/DataGrid/controller/DataGridColumnModifications.ts diff --git a/packages/DataGrid/DataGridColumnsController/DataGridColumns.test.ts b/packages/DataGrid/controller/DataGridColumns.test.ts similarity index 89% rename from packages/DataGrid/DataGridColumnsController/DataGridColumns.test.ts rename to packages/DataGrid/controller/DataGridColumns.test.ts index 9fc3ac914..aa0da77c6 100644 --- a/packages/DataGrid/DataGridColumnsController/DataGridColumns.test.ts +++ b/packages/DataGrid/controller/DataGridColumns.test.ts @@ -7,7 +7,7 @@ const column = (dataSelector: KeyPath.Of) => new DataGridColumn( const columnsWith = (...dataSelectors: Array>) => { const columns = new DataGridColumns() - columns.definitions.extracted = dataSelectors.map(column) + columns.definitions = dataSelectors.map(column) return columns } @@ -42,7 +42,7 @@ describe('DataGridColumns', () => { it('should follow the definition for fields a modification leaves undefined', () => { const columns = new DataGridColumns() - columns.definitions.extracted = [new DataGridColumn({ dataSelector: 'id', heading: 'Id', width: '80px', sticky: 'start' })] + columns.definitions = [new DataGridColumn({ dataSelector: 'id', heading: 'Id', width: '80px', sticky: 'start' })] columns.modifications.set([{ dataSelector: 'id', hidden: true }]) @@ -53,7 +53,7 @@ describe('DataGridColumns', () => { it('should let a modification pin a column as not sticky through null', () => { const columns = new DataGridColumns() - columns.definitions.extracted = [new DataGridColumn({ dataSelector: 'id', heading: 'Id', sticky: 'start' })] + columns.definitions = [new DataGridColumn({ dataSelector: 'id', heading: 'Id', sticky: 'start' })] columns.modifications.set([{ dataSelector: 'id', sticky: null }]) @@ -73,7 +73,7 @@ describe('DataGridColumns', () => { const columns = columnsWith('id') columns.modifications.set([{ dataSelector: 'balance', width: '321px' }, { dataSelector: 'id' }]) - columns.definitions.extracted = [column('id'), column('balance')] + columns.definitions = [column('id'), column('balance')] expect(columns.map(c => c.dataSelector)).toEqual(['balance', 'id']) expect(columns.get('balance')?.width).toBe('321px') @@ -83,7 +83,7 @@ describe('DataGridColumns', () => { const columns = columnsWith('id', 'name') columns.modifications.set([{ dataSelector: 'name', width: '200px' }]) - columns.definitions.extracted = [column('id'), new DataGridColumn({ dataSelector: 'name', heading: 'Full Name' })] + columns.definitions = [column('id'), new DataGridColumn({ dataSelector: 'name', heading: 'Full Name' })] expect(columns[0]?.heading).toBe('Full Name') expect(columns[0]?.width).toBe('200px') @@ -104,7 +104,7 @@ describe('DataGridColumns', () => { const prepare = vi.fn() const columns = new DataGridColumns({ prepare }) - columns.definitions.extracted = [column('id'), column('name')] + columns.definitions = [column('id'), column('name')] expect(prepare).toHaveBeenCalledTimes(2) expect(prepare).toHaveBeenCalledWith(columns[0]!) @@ -152,7 +152,7 @@ describe('DataGridColumns', () => { it('should notify', () => { const updated = vi.fn() const columns = new DataGridColumns({ updated }) - columns.definitions.extracted = [column('id')] + columns.definitions = [column('id')] updated.mockClear() columns.modify('id', { hidden: true }) @@ -230,14 +230,12 @@ describe('DataGridColumns', () => { }) describe('update', () => { - it('should compose anew and notify', () => { + it('should compose anew and notify when the definitions are given', () => { const updated = vi.fn() - let generated = new Array>() - const columns = new DataGridColumns({ generate: () => generated, updated }) + const columns = new DataGridColumns({ updated }) expect(columns.length).toBe(0) - generated = [column('id')] - columns.update() + columns.definitions = [column('id')] expect(columns.map(c => c.dataSelector)).toEqual(['id']) expect(updated).toHaveBeenCalledTimes(1) @@ -246,7 +244,7 @@ describe('DataGridColumns', () => { it('should not notify while being constructed', () => { const updated = vi.fn() - new DataGridColumns({ generate: () => [column('id')], updated }) + new DataGridColumns({ updated }) expect(updated).not.toHaveBeenCalled() }) diff --git a/packages/DataGrid/DataGridColumnsController/DataGridColumns.ts b/packages/DataGrid/controller/DataGridColumns.ts similarity index 86% rename from packages/DataGrid/DataGridColumnsController/DataGridColumns.ts rename to packages/DataGrid/controller/DataGridColumns.ts index 9bdfd2f11..8424c244c 100644 --- a/packages/DataGrid/DataGridColumnsController/DataGridColumns.ts +++ b/packages/DataGrid/controller/DataGridColumns.ts @@ -1,12 +1,9 @@ -import { type DataGridColumn } from '../index.js' +import { type DataGridColumn } from './DataGridColumn.js' import { ArrayLikeView } from './ArrayLikeView.js' -import { DataGridColumnDefinitions } from './DataGridColumnDefinitions.js' import { DataGridColumnModifications } from './DataGridColumnModifications.js' import { type DataGridColumnModification } from './DataGridColumnModification.js' type DataGridColumnsInit = { - /** Generates definitions from the data grid's data. Only called while no other definition source provides any. */ - readonly generate?: () => ReadonlyArray> /** Called for each composed column, so that the data grid can attach itself to it */ readonly prepare?: (column: DataGridColumn) => void /** Called whenever the columns have been composed anew */ @@ -16,7 +13,7 @@ type DataGridColumnsInit = { /** * The columns of a data grid, composed as `columns = modifications ⊗ definitions`: * - * - `definitions` — which columns exist and how they present by default. @see DataGridColumnDefinitions + * - `definitions` — which columns exist and how they present by default, as the host gives them. * - `modifications` — the intent about their order and presentation. @see DataGridColumnModifications * * The columns are composed by applying the modifications onto the definitions they know, in their @@ -29,10 +26,12 @@ type DataGridColumnsInit = { * composed anew whenever either layer changes, so they are never stale and never stored twice. */ export class DataGridColumns extends ArrayLikeView> { - readonly definitions = new DataGridColumnDefinitions({ - generate: () => this.init?.generate?.() ?? [], - updated: () => this.updated(), - }) + private _definitions: ReadonlyArray> = [] + get definitions() { return this._definitions } + set definitions(value) { + this._definitions = [...value] + this.updated() + } readonly modifications = new DataGridColumnModifications({ updated: () => this.updated() @@ -71,10 +70,9 @@ export class DataGridColumns extends ArrayLikeView> this.modifications.set(modifications) } - /** Composes the columns and their definitions anew, e.g. after the data grid's data changed */ + /** Composes the columns anew from the same layers. */ update() { - // Cascades back through the definitions' update notification, which composes the columns anew - this.definitions.update() + this.updated() } private updated() { @@ -111,7 +109,7 @@ export class DataGridColumns extends ArrayLikeView> private composedWith(modifications: ReadonlyArray>) { return [ ...modifications.flatMap(modification => { - const definition = this.definitions.get(modification.dataSelector) + const definition = this.definitions.find(definition => definition.dataSelector === modification.dataSelector) return !definition ? [] : [{ modification, definition }] }), ...this.definitions diff --git a/packages/DataGrid/DataGridColumnsController/DataGridColumnsController.test.ts b/packages/DataGrid/controller/DataGridColumnsController.test.ts similarity index 57% rename from packages/DataGrid/DataGridColumnsController/DataGridColumnsController.test.ts rename to packages/DataGrid/controller/DataGridColumnsController.test.ts index 7b1a2d88f..f78c11c39 100644 --- a/packages/DataGrid/DataGridColumnsController/DataGridColumnsController.test.ts +++ b/packages/DataGrid/controller/DataGridColumnsController.test.ts @@ -1,6 +1,4 @@ -import { html } from '@a11d/lit' -import { ComponentTestFixture } from '@a11d/lit-testing' -import { type DataGrid, DataGridColumn, DataGridSelectability } from '../index.js' +import { DataGridColumn } from '../index.js' import { DataGridColumnsController } from './DataGridColumnsController.js' type Data = { a: number, b: number } @@ -11,7 +9,7 @@ describe('DataGridColumnsController', () => { const state = { ...defaults, ...setup } const grid = { host: { addController: () => { }, requestUpdate: () => { }, style: document.createElement('div').style }, - options: { data: [] }, + options: { data: [], columns: [] }, selection: { get hasSelection() { return state.hasSelection } }, details: { get hasDetails() { return state.hasDetails } }, contextMenu: { get hasContextMenu() { return state.hasContextMenu } }, @@ -51,7 +49,7 @@ describe('DataGridColumnsController', () => { controller.setColumnWidth('details', 20) controller.setColumnWidth('selection', 40) controller.setColumnWidth('actions', 28) - controller.columns.definitions.programmatic = [ + controller.columns.definitions = [ new DataGridColumn({ dataSelector: 'a', heading: 'A', sticky: 'start' }), new DataGridColumn({ dataSelector: 'b', heading: 'B', sticky: 'start' }), new DataGridColumn({ dataSelector: 'c' as any, heading: 'C', sticky: 'end' }), @@ -72,7 +70,7 @@ describe('DataGridColumnsController', () => { it('should stack two sticky columns showing the same data one behind the other', () => { const controller = createController({ hasDetails: false }) controller.setColumnWidth('selection', 40) - controller.columns.definitions.programmatic = [ + controller.columns.definitions = [ new DataGridColumn({ dataSelector: 'a', heading: 'A', sticky: 'start' }), new DataGridColumn({ dataSelector: 'b', heading: 'B' }), new DataGridColumn({ dataSelector: 'a', heading: 'A again', sticky: 'start' }), @@ -90,7 +88,7 @@ describe('DataGridColumnsController', () => { it('should keep measured widths across column re-derivation, keyed by data selector, as columns are immutable value-objects', () => { const controller = createController() const definition = new DataGridColumn({ dataSelector: 'a', heading: 'A' }) - controller.columns.definitions.programmatic = [definition] + controller.columns.definitions = [definition] controller.columns.get('a')!.widthInPixels = 120 @@ -102,84 +100,4 @@ describe('DataGridColumnsController', () => { expect(rederived.widthInPixels).toBe(120) }) }) - - describe('CSS column tracks', () => { - const fixture = new ComponentTestFixture>(html` - - - - - `) - - const tracks = async () => { - await fixture.updateComplete - await new Promise(r => setTimeout(r, 30)) - await fixture.updateComplete - return fixture.component.style.getPropertyValue('--mo-data-grid-columns') - } - - const dataTrackCount = (value: string) => value.match(/\[data\]/g)?.length ?? 0 - - it('should provide a named track per visible column and none for hidden ones, as a zero track would still render a gap', async () => { - const value = await tracks() - - expect(dataTrackCount(value)).toBe(2) - expect(value).toContain('[data] max-content') - expect(value).toContain('[data] 50px') - expect(value).toContain('[padding] 1fr') - expect(value).toContain('[actions]') - - fixture.component.columns.find(c => c.dataSelector === 'b')!.hide() - const hidden = await tracks() - - expect(dataTrackCount(hidden)).toBe(1) - expect(hidden).not.toContain('50px') - }) - - it('should include the selection, details and reorder tracks only while the corresponding feature is active', async () => { - const initial = await tracks() - expect(initial).not.toContain('[selection]') - expect(initial).not.toContain('[details]') - expect(initial).not.toContain('[order]') - - fixture.component.selectability = DataGridSelectability.Multiple - expect(await tracks()).toContain('[selection]') - - fixture.component.getRowDetailsTemplate = () => html`
Details
` - expect(await tracks()).toContain('[details]') - - fixture.component.getRowDetailsTemplate = undefined - fixture.component.reorderability = true - const reorderable = await tracks() - - expect(reorderable).toContain('[order]') - expect(reorderable).not.toContain('[details]') - }) - - it('should lay out the tracks of a grid which first rendered outside the flat tree, such as slotted into a host yet to render its slot', async () => { - const host = document.createElement('div') - const root = host.attachShadow({ mode: 'open' }) - const grid = document.createElement('mo-data-grid') as DataGrid - grid.selectability = DataGridSelectability.Multiple - grid.data = [{ a: 1, b: 2 }] - const column = document.createElement('mo-data-grid-column-number') - Object.assign(column, { heading: 'A', dataSelector: 'a', width: '50px' }) - grid.append(column) - host.append(grid) - document.body.append(host) - try { - await grid.updateComplete - await new Promise(r => setTimeout(r, 30)) - await grid.updateComplete - - root.append(document.createElement('slot')) - await new Promise(requestAnimationFrame) - - const tracks = getComputedStyle(grid.renderRoot.querySelector('#content')!).gridTemplateColumns - expect(tracks).toMatch(/^\[selection\] 40px \[data\] 50px \[padding\] [\d.]+px \[actions\] 28px/) - } finally { - host.remove() - } - }) - }) }) \ No newline at end of file diff --git a/packages/DataGrid/DataGridColumnsController/DataGridColumnsController.ts b/packages/DataGrid/controller/DataGridColumnsController.ts similarity index 72% rename from packages/DataGrid/DataGridColumnsController/DataGridColumnsController.ts rename to packages/DataGrid/controller/DataGridColumnsController.ts index 52b1a239c..6b1ab9562 100644 --- a/packages/DataGrid/DataGridColumnsController/DataGridColumnsController.ts +++ b/packages/DataGrid/controller/DataGridColumnsController.ts @@ -3,25 +3,25 @@ import { equals } from '@a11d/equals' import { DirectionsByLanguage } from '@3mo/localization' import { PointerDragController, type PointerDrag } from '@3mo/pointer-controller' import { ReorderabilityController } from '@3mo/reorderability' -import { DataGridColumnComponent, type DataGrid, type DataGridColumn, type DataGridController } from '../index.js' +import { type DataGridColumn } from './DataGridColumn.js' +import { type DataGridController } from './DataGridController.js' import { DataGridColumns } from './DataGridColumns.js' /** - * Connects a data grid to its columns, feeding them the definition sources it observes — the column - * elements and the data — and reflecting them back into the data grid's rendering. + * Connects a data grid to its columns, feeding them the definitions the host gives in its options and + * reflecting them back into the data grid's rendering. The controller never sources columns: whatever + * declares them — code, column elements, the data's own keys — is the host's business. * * @see DataGridColumns for the columns themselves and the layers they are composed of. */ export class DataGridColumnsController extends Controller implements EventListenerObject { private readonly columnWidths = { reordering: 0, details: 0, selection: 0, actions: 0 } private readonly widthsInPixels = new Map, number>() - private initialized = false // Created on first access rather than initialized inline, as composing columns reads from the host private _columns?: DataGridColumns get columns() { return this._columns ??= new DataGridColumns({ - generate: () => this.autoGeneratedColumns, prepare: column => column.controller = this.grid, updated: () => this.columnsUpdated(), }) @@ -143,49 +143,22 @@ export class DataGridColumnsController extends Controller implements Even override hostUpdate() { this.syncColumns() - this.provideCssColumnsProperties() } - private programmatic?: ReadonlyArray> - private syncColumns() { + private definitions?: ReadonlyArray> + syncColumns() { const { columns } = this.grid.options - if (columns && columns !== this.programmatic) { - this.programmatic = columns - this.columns.definitions.programmatic = [...columns] + if (columns !== this.definitions) { + this.definitions = columns + this.columns.definitions = columns } } - override hostUpdated() { - if (!this.initialized && !this.columns.definitions.extracted.length) { - this.extractColumns() - } - this.initialized = true - } - private columnsUpdated() { this.grid.options.handleColumnsChange?.([...this.columns]) this.grid.host.requestUpdate() } - extractColumns() { - this.columns.definitions.extracted = this.columnsElements.map(c => c.column) - } - - private get columnsElements() { - const slot = this.grid.host.renderRoot?.querySelector('slot[name=column]') - if (!slot) { - return [] - } - const children = [...slot.children] - const assigned = [...slot.assignedElements()] - return [...assigned, ...children] - .filter((c): c is DataGridColumnComponent => c instanceof DataGridColumnComponent) - .map(c => { - c.dataGrid = this.grid.host as unknown as DataGrid - return c - }) - } - getWidthInPixels(dataSelector: KeyPath.Of) { return this.widthsInPixels.get(dataSelector) } @@ -196,63 +169,6 @@ export class DataGridColumnsController extends Controller implements Even } } - // The reason for not doing this in the CSS is that we need to trim all the 0px values out of the columns - // because the 'grid column gap' renders a gap no matter if the column is 0px or not - private provideCssColumnsProperties() { - const columns = this.cssColumns.map(c => `[${c.name}] ${c.width}`).join(' ') - if (this.grid.host.style.getPropertyValue('--mo-data-grid-columns') !== columns) { - this.grid.host.style.setProperty('--mo-data-grid-columns', columns) - } - } - - private get cssColumns() { - const order = !this.grid.reorderability.enabled ? undefined : 'var(--mo-data-grid-column-reorder-width)' - const details = !this.grid.details.hasDetails ? undefined : 'var(--mo-data-grid-column-details-width)' - const selection = !this.grid.selection.hasSelection ? undefined : 'var(--mo-data-grid-column-selection-width)' - const data = this.columns.visible - .map(c => c.width) - .filter((c): c is string => c !== undefined) - const actions = 'var(--mo-data-grid-column-actions-width)' - return [ - { name: 'order', width: order }, - { name: 'details', width: details }, - { name: 'selection', width: selection }, - ...data.map(width => ({ name: 'data', width })), - { name: 'padding', width: '1fr' }, - { name: 'actions', width: actions } - ].filter(c => c.width !== undefined) as Array<{ readonly name: string, readonly width: string }> - } - - private get autoGeneratedColumns(): Array> { - if (!this.grid.options.data.length) { - return [] - } - - const getDefaultColumnElement = (value: unknown) => { - switch (typeof value) { - case 'number': - case 'bigint': - return 'mo-data-grid-column-number' - case 'boolean': - return 'mo-data-grid-column-boolean' - default: - return 'mo-data-grid-column-text' - } - } - const [sampleData] = this.grid.options.data - return Object.keys(sampleData || {}) - .filter(key => !key.startsWith('_')) - .map(key => { - const columnElement = document.createElement(getDefaultColumnElement(KeyPath.get(sampleData, key as any))) - columnElement.heading = key.replace(/([A-Z])/g, ' $1').charAt(0).toUpperCase() + key.replace(/([A-Z])/g, ' $1').slice(1) - columnElement.dataSelector = key - columnElement.dataGrid = this.grid.host as unknown as DataGrid - const column = columnElement.column - columnElement.remove() - return column - }) as unknown as Array> - } - setColumnWidth(column: keyof typeof this.columnWidths, widthInPixels: number) { if (this.columnWidths[column] !== widthInPixels) { this.handleMetricsChange(() => this.columnWidths[column] = widthInPixels) diff --git a/packages/DataGrid/DataGridContextMenuController.test.ts b/packages/DataGrid/controller/DataGridContextMenuController.test.ts similarity index 97% rename from packages/DataGrid/DataGridContextMenuController.test.ts rename to packages/DataGrid/controller/DataGridContextMenuController.test.ts index cf7659424..c5453f1f4 100644 --- a/packages/DataGrid/DataGridContextMenuController.test.ts +++ b/packages/DataGrid/controller/DataGridContextMenuController.test.ts @@ -1,8 +1,8 @@ import { html, render } from '@a11d/lit' import { ComponentTestFixture } from '@a11d/lit-testing' import '@3mo/localization' -import './index.js' -import { type DataGrid } from './DataGrid.js' +import '../index.js' +import { type DataGrid } from '../DataGrid.js' type Person = { id: number, name: string } diff --git a/packages/DataGrid/DataGridContextMenuController.ts b/packages/DataGrid/controller/DataGridContextMenuController.ts similarity index 81% rename from packages/DataGrid/DataGridContextMenuController.ts rename to packages/DataGrid/controller/DataGridContextMenuController.ts index 38b0a5481..f4b7ae136 100644 --- a/packages/DataGrid/DataGridContextMenuController.ts +++ b/packages/DataGrid/controller/DataGridContextMenuController.ts @@ -1,5 +1,5 @@ import { Controller } from '@a11d/lit' -import { type DataGridController } from './index.js' +import { type DataGridController } from './DataGridController.js' export class DataGridContextMenuController extends Controller { constructor(private readonly grid: DataGridController) { diff --git a/packages/DataGrid/DataGridController.test.ts b/packages/DataGrid/controller/DataGridController.test.ts similarity index 99% rename from packages/DataGrid/DataGridController.test.ts rename to packages/DataGrid/controller/DataGridController.test.ts index 9812b4d71..a084b9310 100644 --- a/packages/DataGrid/DataGridController.test.ts +++ b/packages/DataGrid/controller/DataGridController.test.ts @@ -1,7 +1,7 @@ import { component, Component, html } from '@a11d/lit' import { ComponentTestFixture } from '@a11d/lit-testing' import { userEvent } from 'vitest/browser' -import { DataGrid, DataGridColumn, DataGridController, DataGridEditability, DataGridSelectability, DataGridSortingStrategy } from './index.js' +import { DataGrid, DataGridColumn, DataGridController, DataGridEditability, DataGridSelectability, DataGridSortingStrategy } from '../index.js' type Person = { id: number, name: string, age: number, reports?: Array } diff --git a/packages/DataGrid/DataGridController.ts b/packages/DataGrid/controller/DataGridController.ts similarity index 98% rename from packages/DataGrid/DataGridController.ts rename to packages/DataGrid/controller/DataGridController.ts index b3ff9458a..f74c990fc 100644 --- a/packages/DataGrid/DataGridController.ts +++ b/packages/DataGrid/controller/DataGridController.ts @@ -4,7 +4,7 @@ import { IndexabilityController, type IndexabilityItem } from '@3mo/indexability import { type ExpandabilityItemOptions } from '@3mo/expandability' import { type DataRecord } from './DataRecord.js' import { type DataGridColumn } from './DataGridColumn.js' -import { DataGridColumnsController } from './DataGridColumnsController/index.js' +import { DataGridColumnsController } from './DataGridColumnsController.js' import { DataGridSelectability, type DataGridSelectionBehaviorOnDataChange, DataGridSelectionController } from './DataGridSelectionController.js' import { type DataGridRankedSortDefinition, type DataGridSorting, DataGridSortingController } from './DataGridSortingController.js' import { DataGridContextMenuController } from './DataGridContextMenuController.js' @@ -28,8 +28,8 @@ type DataGridColumnHeaderPart = { export interface DataGridControllerOptions { /** Every datum, rendered or not. */ readonly data: ReadonlyArray - /** Columns given in code, beside those the host extracts from its markup. */ - readonly columns?: ReadonlyArray> + /** The columns' definitions. A new array re-syncs them, so a host keeps the array while nothing changed. */ + readonly columns: ReadonlyArray> /** Where a datum keeps its sub data, which makes the grid a tree grid. */ readonly subDataGridDataSelector?: KeyPath.Of diff --git a/packages/DataGrid/DataGridCsvController.test.ts b/packages/DataGrid/controller/DataGridCsvController.test.ts similarity index 99% rename from packages/DataGrid/DataGridCsvController.test.ts rename to packages/DataGrid/controller/DataGridCsvController.test.ts index d181a5be5..671cbe05a 100644 --- a/packages/DataGrid/DataGridCsvController.test.ts +++ b/packages/DataGrid/controller/DataGridCsvController.test.ts @@ -1,7 +1,7 @@ import { DataGridColumn } from './DataGridColumn.js' import { DataGridCsvController } from './DataGridCsvController.js' import { DataRecord } from './DataRecord.js' -import './index.js' +import '../index.js' type Person = { id: number, name: string, age: number, birthDate: DateTime } diff --git a/packages/DataGrid/DataGridCsvController.ts b/packages/DataGrid/controller/DataGridCsvController.ts similarity index 100% rename from packages/DataGrid/DataGridCsvController.ts rename to packages/DataGrid/controller/DataGridCsvController.ts diff --git a/packages/DataGrid/DataGridDetailsController.test.ts b/packages/DataGrid/controller/DataGridDetailsController.test.ts similarity index 99% rename from packages/DataGrid/DataGridDetailsController.test.ts rename to packages/DataGrid/controller/DataGridDetailsController.test.ts index 7a1d29620..58273baec 100644 --- a/packages/DataGrid/DataGridDetailsController.test.ts +++ b/packages/DataGrid/controller/DataGridDetailsController.test.ts @@ -1,9 +1,9 @@ import { html, type HTMLTemplateResult } from '@a11d/lit' import { ComponentTestFixture } from '@a11d/lit-testing' -import { type DataGrid } from './DataGrid.js' +import { type DataGrid } from '../DataGrid.js' import { DataGridDetailsController } from './DataGridDetailsController.js' import type { DataRecord } from './DataRecord.js' -import './index.js' +import '../index.js' type Data = string diff --git a/packages/DataGrid/DataGridDetailsController.ts b/packages/DataGrid/controller/DataGridDetailsController.ts similarity index 100% rename from packages/DataGrid/DataGridDetailsController.ts rename to packages/DataGrid/controller/DataGridDetailsController.ts diff --git a/packages/DataGrid/DataGridEditabilityController.ts b/packages/DataGrid/controller/DataGridEditabilityController.ts similarity index 100% rename from packages/DataGrid/DataGridEditabilityController.ts rename to packages/DataGrid/controller/DataGridEditabilityController.ts diff --git a/packages/DataGrid/DataGridNavigabilityController.test.ts b/packages/DataGrid/controller/DataGridNavigabilityController.test.ts similarity index 99% rename from packages/DataGrid/DataGridNavigabilityController.test.ts rename to packages/DataGrid/controller/DataGridNavigabilityController.test.ts index e91a8ef27..3c2532b03 100644 --- a/packages/DataGrid/DataGridNavigabilityController.test.ts +++ b/packages/DataGrid/controller/DataGridNavigabilityController.test.ts @@ -1,7 +1,7 @@ import { html } from '@a11d/lit' import { ComponentTestFixture } from '@a11d/lit-testing' -import { type DataGrid } from './DataGrid.js' -import './index.js' +import { type DataGrid } from '../DataGrid.js' +import '../index.js' type Person = { id: number, name: string, age: number } diff --git a/packages/DataGrid/DataGridNavigabilityController.ts b/packages/DataGrid/controller/DataGridNavigabilityController.ts similarity index 96% rename from packages/DataGrid/DataGridNavigabilityController.ts rename to packages/DataGrid/controller/DataGridNavigabilityController.ts index 33147f69f..741623e9d 100644 --- a/packages/DataGrid/DataGridNavigabilityController.ts +++ b/packages/DataGrid/controller/DataGridNavigabilityController.ts @@ -41,6 +41,11 @@ export class DataGridNavigabilityController extends Controller { /** The one cell in the tab order sits at the cursor, and an edited cell leaves the tab order to its editor. */ stampCell(cell: HTMLElement) { + if (!cell.isConnected) { + // Declared mid-template, before its row holds it: the stamp that asks for the row waits for the render to land. + queueMicrotask(() => cell.isConnected && this.stampCell(cell)) + return + } cell.role = 'gridcell' if (this.grid.editability.isEditing(cell)) { cell.removeAttribute('tabindex') @@ -53,6 +58,11 @@ export class DataGridNavigabilityController extends Controller { } private handleCursorChange() { + this.moveTabStop() + this.options?.handleChange?.() + } + + private moveTabStop() { const previous = this.tabStop const row = this.row.current ?? this.rowElements[0] const column = this.column.current ?? this.visibleColumns[0] @@ -65,7 +75,6 @@ export class DataGridNavigabilityController extends Controller { } } } - this.options?.handleChange?.() } private rowsCache?: ReadonlyArray diff --git a/packages/DataGrid/DataGridRecordsController.test.ts b/packages/DataGrid/controller/DataGridRecordsController.test.ts similarity index 100% rename from packages/DataGrid/DataGridRecordsController.test.ts rename to packages/DataGrid/controller/DataGridRecordsController.test.ts diff --git a/packages/DataGrid/DataGridRecordsController.ts b/packages/DataGrid/controller/DataGridRecordsController.ts similarity index 100% rename from packages/DataGrid/DataGridRecordsController.ts rename to packages/DataGrid/controller/DataGridRecordsController.ts diff --git a/packages/DataGrid/DataGridReorderabilityController.test.ts b/packages/DataGrid/controller/DataGridReorderabilityController.test.ts similarity index 100% rename from packages/DataGrid/DataGridReorderabilityController.test.ts rename to packages/DataGrid/controller/DataGridReorderabilityController.test.ts diff --git a/packages/DataGrid/DataGridReorderabilityController.ts b/packages/DataGrid/controller/DataGridReorderabilityController.ts similarity index 100% rename from packages/DataGrid/DataGridReorderabilityController.ts rename to packages/DataGrid/controller/DataGridReorderabilityController.ts diff --git a/packages/DataGrid/DataGridSelectionController.test.ts b/packages/DataGrid/controller/DataGridSelectionController.test.ts similarity index 100% rename from packages/DataGrid/DataGridSelectionController.test.ts rename to packages/DataGrid/controller/DataGridSelectionController.test.ts diff --git a/packages/DataGrid/DataGridSelectionController.ts b/packages/DataGrid/controller/DataGridSelectionController.ts similarity index 100% rename from packages/DataGrid/DataGridSelectionController.ts rename to packages/DataGrid/controller/DataGridSelectionController.ts diff --git a/packages/DataGrid/DataGridSortingController.test.ts b/packages/DataGrid/controller/DataGridSortingController.test.ts similarity index 100% rename from packages/DataGrid/DataGridSortingController.test.ts rename to packages/DataGrid/controller/DataGridSortingController.test.ts diff --git a/packages/DataGrid/DataGridSortingController.ts b/packages/DataGrid/controller/DataGridSortingController.ts similarity index 100% rename from packages/DataGrid/DataGridSortingController.ts rename to packages/DataGrid/controller/DataGridSortingController.ts diff --git a/packages/DataGrid/DataGridVirtualizationController.test.ts b/packages/DataGrid/controller/DataGridVirtualizationController.test.ts similarity index 100% rename from packages/DataGrid/DataGridVirtualizationController.test.ts rename to packages/DataGrid/controller/DataGridVirtualizationController.test.ts diff --git a/packages/DataGrid/DataGridVirtualizationController.ts b/packages/DataGrid/controller/DataGridVirtualizationController.ts similarity index 100% rename from packages/DataGrid/DataGridVirtualizationController.ts rename to packages/DataGrid/controller/DataGridVirtualizationController.ts diff --git a/packages/DataGrid/DataRecord.ts b/packages/DataGrid/controller/DataRecord.ts similarity index 100% rename from packages/DataGrid/DataRecord.ts rename to packages/DataGrid/controller/DataRecord.ts diff --git a/packages/DataGrid/controller/index.test.ts b/packages/DataGrid/controller/index.test.ts new file mode 100644 index 000000000..612e717af --- /dev/null +++ b/packages/DataGrid/controller/index.test.ts @@ -0,0 +1,11 @@ +import './index.js' + +describe('DataGrid controller entry', () => { + // Each spec file loads in a page of its own, so what this import registers is all that is registered. + it('should stand alone: a host of its own design takes the controller without the data grid elements', () => { + expect(customElements.get('mo-data-grid')).toBeUndefined() + expect(customElements.get('mo-data-grid-column')).toBeUndefined() + expect(customElements.get('mo-data-grid-row')).toBeUndefined() + expect(customElements.get('mo-data-grid-cell')).toBeUndefined() + }) +}) \ No newline at end of file diff --git a/packages/DataGrid/controller/index.ts b/packages/DataGrid/controller/index.ts new file mode 100644 index 000000000..c7cf9fce1 --- /dev/null +++ b/packages/DataGrid/controller/index.ts @@ -0,0 +1,19 @@ +import '../types.js' +export * from './DataRecord.js' +export * from './DataGridColumn.js' +export * from './ArrayLikeView.js' +export * from './DataGridColumnModification.js' +export * from './DataGridColumnModifications.js' +export * from './DataGridColumns.js' +export * from './DataGridColumnsController.js' +export * from './DataGridRecordsController.js' +export * from './DataGridSortingController.js' +export * from './DataGridSelectionController.js' +export * from './DataGridDetailsController.js' +export * from './DataGridNavigabilityController.js' +export * from './DataGridEditabilityController.js' +export * from './DataGridReorderabilityController.js' +export * from './DataGridVirtualizationController.js' +export * from './DataGridContextMenuController.js' +export * from './DataGridCsvController.js' +export * from './DataGridController.js' \ No newline at end of file diff --git a/packages/DataGrid/index.ts b/packages/DataGrid/index.ts index 631a0645f..df6f36261 100644 --- a/packages/DataGrid/index.ts +++ b/packages/DataGrid/index.ts @@ -25,23 +25,12 @@ import '@3mo/select-field' export * from '@3mo/reorderability' // import '@3mo/focus-ring' import './types.js' -export * from './DataGridReorderabilityController.js' -export * from './DataRecord.js' -export * from './DataGridRecordsController.js' -export * from './DataGridColumn.js' +export * from './controller/index.js' export * from './columns/index.js' -export * from './DataGridColumnsController/index.js' -export * from './DataGridSelectionController.js' -export * from './DataGridNavigabilityController.js' -export * from './DataGridEditabilityController.js' -export * from './DataGridSortingController.js' -export * from './DataGridContextMenuController.js' -export * from './DataGridDetailsController.js' -export * from './DataGridVirtualizationController.js' -export * from './DataGridCsvController.js' export * from './DataGridPagination.js' -export * from './DataGridController.js' export * from './DataGrid.js' +export * from './DataGridColumnDefinitions.js' +export * from './DataGridColumnComponentsController.js' export * from './DataGridToolbarElementStyles.js' export * from './DataGridHeader.js' export * from './DataGridColumnHeader.js' diff --git a/packages/DataGrid/package.json b/packages/DataGrid/package.json index 72a6a6ba6..923b7358d 100644 --- a/packages/DataGrid/package.json +++ b/packages/DataGrid/package.json @@ -1,6 +1,6 @@ { "name": "@3mo/data-grid", - "version": "0.38.2", + "version": "0.38.3", "description": "A web component for data grids with typed columns, sorting, selection, pagination, row details and inline editing.", "repository": { "type": "git", @@ -22,6 +22,20 @@ "type": "module", "main": "dist/index.js", "types": "dist/index.d.ts", + "exports": { + ".": { + "source": "./index.ts", + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "./controller": { + "source": "./controller/index.ts", + "types": "./dist/controller/index.d.ts", + "default": "./dist/controller/index.js" + }, + "./dist/*": "./dist/*", + "./package.json": "./package.json" + }, "customElements": "dist/custom-elements.json", "files": [ "dist", diff --git a/packages/DataGrid/rows/DataGridRow.ts b/packages/DataGrid/rows/DataGridRow.ts index d1c64190d..df982c7fe 100644 --- a/packages/DataGrid/rows/DataGridRow.ts +++ b/packages/DataGrid/rows/DataGridRow.ts @@ -5,7 +5,7 @@ import { popover } from '@3mo/popover' import { tooltip } from '@3mo/tooltip' import { ContextMenu } from '@3mo/context-menu' import { ReorderabilityState } from '@3mo/reorderability' -import { type DataGridColumn } from '../DataGridColumn.js' +import { type DataGridColumn } from '../controller/DataGridColumn.js' import { type DataGrid, type DataGridCell, DataGridPrimaryContextMenuItem, type DataRecord } from '../index.js' Localizer.dictionaries.add('de', { diff --git a/tsconfig.base.json b/tsconfig.base.json index 51c63296d..af54208e6 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -14,6 +14,7 @@ ], "module": "NodeNext", "moduleResolution": "NodeNext", + "customConditions": ["source"], "strict": true, "importHelpers": true, "noUnusedLocals": true,