diff --git a/libs/components/src/lib/data-grid/data-grid-cell.spec.ts b/libs/components/src/lib/data-grid/data-grid-cell.spec.ts index fee99b8d0f..0ed4609d47 100644 --- a/libs/components/src/lib/data-grid/data-grid-cell.spec.ts +++ b/libs/components/src/lib/data-grid/data-grid-cell.spec.ts @@ -182,32 +182,45 @@ describe('vwc-data-grid-cell', () => { expect(hasActiveClassBeforeFocus).toBeFalsy(); expect(baseElement?.classList.contains('active')).toBeTruthy(); }); + + it('should ignore additional focusin events', async () => { + const spy = jest.fn(); + element.addEventListener('cell-focused', spy); + + element.dispatchEvent(new Event('focusin')); + element.dispatchEvent(new Event('focusin')); + + expect(spy).toHaveBeenCalledTimes(1); + }); }); describe('handleKeydown', () => { - it('should focus on target element with enter key', async () => { - element.cellType = 'default'; - element.columnDefinition = { - columnDataKey: 'name', - cellFocusTargetCallback: () => { - return elementToFocus; - }, - cellInternalFocusQueue: true, - }; - element.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' })); - expect(document.activeElement).toEqual(elementToFocus); - }); + it.each(['Enter', 'F2'])( + 'should focus on target element with %s key', + async (key) => { + element.cellType = 'default'; + element.columnDefinition = { + columnDataKey: 'name', + cellFocusTargetCallback: () => { + return elementToFocus; + }, + cellInternalFocusQueue: true, + }; + element.dispatchEvent(new KeyboardEvent('keydown', { key })); + expect(document.activeElement).toEqual(elementToFocus); + } + ); - it('should focus on target element with F2 key', async () => { - element.cellType = 'default'; + it('should focus on header target element when cellType is columnheader', async () => { + element.cellType = 'columnheader'; element.columnDefinition = { columnDataKey: 'name', - cellFocusTargetCallback: () => { + headerCellFocusTargetCallback: () => { return elementToFocus; }, - cellInternalFocusQueue: true, + headerCellInternalFocusQueue: true, }; - element.dispatchEvent(new KeyboardEvent('keydown', { key: 'F2' })); + element.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' })); expect(document.activeElement).toEqual(elementToFocus); }); @@ -222,6 +235,22 @@ describe('vwc-data-grid-cell', () => { element.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' })); expect(document.activeElement).toEqual(element); }); + + it('should not move focus if a child is already focused', async () => { + element.cellType = 'default'; + element.columnDefinition = { + columnDataKey: 'name', + cellFocusTargetCallback: () => { + return elementToFocus; + }, + cellInternalFocusQueue: true, + }; + const focusedChild = document.createElement('input'); + element.appendChild(focusedChild); + focusedChild.focus(); + element.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' })); + expect(document.activeElement).toBe(focusedChild); + }); }); }); diff --git a/libs/components/src/lib/data-grid/data-grid-cell.template.ts b/libs/components/src/lib/data-grid/data-grid-cell.template.ts index 7ab64d0934..2fa2d1b51b 100644 --- a/libs/components/src/lib/data-grid/data-grid-cell.template.ts +++ b/libs/components/src/lib/data-grid/data-grid-cell.template.ts @@ -5,7 +5,7 @@ import type { VividElementDefinitionContext } from '../../shared/design-system/d import { DataGridCellRole, DataGridCellSortStates } from './data-grid.options'; import type { DataGridCell } from './data-grid-cell'; -function shouldShowSortIcons(x: T): boolean { +function shouldShowSortIcons(x: DataGridCell): boolean { if (x.columnDefinition) { x.ariaSort = !x.columnDefinition.sortable ? null diff --git a/libs/components/src/lib/data-grid/data-grid-cell.ts b/libs/components/src/lib/data-grid/data-grid-cell.ts index 8b7650e4e4..94614c1595 100644 --- a/libs/components/src/lib/data-grid/data-grid-cell.ts +++ b/libs/components/src/lib/data-grid/data-grid-cell.ts @@ -1,18 +1,46 @@ import { - type ColumnDefinition, - DataGridCell as FoundationDataGridCell, -} from '@microsoft/fast-foundation'; -import { attr } from '@microsoft/fast-element'; -import type { DataGridCellSortStates } from './data-grid.options'; + attr, + html, + type HTMLView, + observable, + type ViewTemplate, +} from '@microsoft/fast-element'; +import { + eventFocusIn, + eventFocusOut, + eventKeyDown, + keyEnter, + keyEscape, + keyFunction2, +} from '@microsoft/fast-web-utilities'; +import { VividElement } from '../../shared/foundation/vivid-element/vivid-element'; +import { DataGridCellSortStates, DataGridCellTypes } from './data-grid.options'; +import type { ColumnDefinition } from './data-grid'; declare interface ColumnDefinitionExtended extends ColumnDefinition { sortDirection?: DataGridCellSortStates | null; sortable?: boolean | undefined; } -declare interface DataGridCellExtension { - columnDefinition: ColumnDefinitionExtended | null; -} +const defaultCellContentsTemplate: ViewTemplate = html` + +`; + +const defaultHeaderCellContentsTemplate: ViewTemplate = html` + +`; /** * @public @@ -22,55 +50,304 @@ declare interface DataGridCellExtension { * @event {CustomEvent<{cell: HTMLElement, row: HTMLElement, isHeaderCell: boolean, columnDataKey: string}>} cell-click - Event that fires when a cell is clicked * @event {CustomEvent} cell-focused - Fires a custom 'cell-focused' event when focus is on the cell or its contents */ -export class DataGridCell extends FoundationDataGridCell { +export class DataGridCell extends VividElement { /** - * Indicates the selected status. + * The type of cell * * @public - * HTML Attribute: aria-selected + * @remarks + * HTML Attribute: cell-type */ - @attr({ attribute: 'aria-selected', mode: 'fromView' }) - override ariaSelected: string | null = null; + @attr({ attribute: 'cell-type' }) + // eslint-disable-next-line @nrwl/nx/workspace/no-attribute-default-value + cellType: DataGridCellTypes = DataGridCellTypes.default; + /** + * @internal + */ + cellTypeChanged(): void { + if (this.$fastController.isConnected) { + this.updateCellView(); + } + } /** - * Indicates the sort status. + * The column index of the cell. + * This will be applied to the css grid-column-index value + * applied to the cell * * @public - * HTML Attribute: aria-sort + * @remarks + * HTML Attribute: grid-column */ - @attr({ attribute: 'aria-sort' }) override ariaSort: string | null = null; + @attr({ attribute: 'grid-column' }) + gridColumn!: string; + /** + * @internal + */ + gridColumnChanged(): void { + if (this.$fastController.isConnected) { + this.updateCellStyle(); + } + } - ariaSelectedChanged(_: string | null, selectedState: string | null) { - this.shadowRoot!.querySelector('.base')?.classList.toggle( - 'selected', - selectedState === 'true' - ); + /** + * The base data for the parent row + * + * @public + */ + @observable + rowData: object | null = null; + + /** + * The base data for the column + * + * @public + */ + @observable + columnDefinition: ColumnDefinitionExtended | null = null; + /** + * @internal + */ + columnDefinitionChanged( + _oldValue: ColumnDefinitionExtended | null, + _newValue: ColumnDefinitionExtended | null + ): void { + if (this.$fastController.isConnected) { + this.updateCellView(); + } } - override connectedCallback() { + private isActiveCell = false; + private customCellView: HTMLView | null = null; + + /** + * @internal + */ + override connectedCallback(): void { super.connectedCallback(); + + this.addEventListener(eventFocusIn, this.handleFocusin as EventListener); + this.addEventListener(eventFocusOut, this.handleFocusout as EventListener); + this.addEventListener(eventKeyDown, this.handleKeydown as EventListener); + + this.style.gridColumn = `${ + this.columnDefinition?.gridColumn === undefined + ? 0 + : this.columnDefinition.gridColumn + }`; + + this.updateCellView(); + this.updateCellStyle(); + this.ariaSelectedChanged(null, this.ariaSelected); } - override handleFocusin(e: FocusEvent) { - super.handleFocusin(e); + /** + * @internal + */ + override disconnectedCallback(): void { + super.disconnectedCallback(); + + this.removeEventListener(eventFocusIn, this.handleFocusin as EventListener); + this.removeEventListener( + eventFocusOut, + this.handleFocusout as EventListener + ); + this.removeEventListener(eventKeyDown, this.handleKeydown as EventListener); + + this.disconnectCellView(); + } + + handleFocusin(_: FocusEvent): void { this.shadowRoot!.querySelector('.base')!.classList.add('active'); + + if (this.isActiveCell) { + return; + } + + this.isActiveCell = true; + + switch (this.cellType) { + case DataGridCellTypes.columnHeader: + if ( + this.columnDefinition !== null && + this.columnDefinition.headerCellInternalFocusQueue !== true && + typeof this.columnDefinition.headerCellFocusTargetCallback === + 'function' + ) { + // move focus to the focus target + const focusTarget: HTMLElement = + this.columnDefinition.headerCellFocusTargetCallback(this); + if (focusTarget !== null) { + focusTarget.focus(); + } + } + break; + + default: + if ( + this.columnDefinition !== null && + this.columnDefinition.cellInternalFocusQueue !== true && + typeof this.columnDefinition.cellFocusTargetCallback === 'function' + ) { + // move focus to the focus target + const focusTarget: HTMLElement = + this.columnDefinition.cellFocusTargetCallback(this); + if (focusTarget !== null) { + focusTarget.focus(); + } + } + break; + } + + this.$emit('cell-focused', this); } - override handleFocusout(e: FocusEvent) { - super.handleFocusout(e); + handleFocusout(_: FocusEvent): void { this.shadowRoot!.querySelector('.base')!.classList.remove('active'); + + if ( + this !== document.activeElement && + !this.contains(document.activeElement) + ) { + this.isActiveCell = false; + } } - constructor() { - super(); - (this as any).updateCellStyle = () => { - if (this.gridColumn && !this.gridColumn.includes('undefined')) { - this.style.gridColumn = this.gridColumn; - } else { - this.style.removeProperty('grid-column'); - } - }; + handleKeydown(e: KeyboardEvent): void { + if ( + e.defaultPrevented || + this.columnDefinition === null || + (this.cellType === DataGridCellTypes.default && + this.columnDefinition.cellInternalFocusQueue !== true) || + (this.cellType === DataGridCellTypes.columnHeader && + this.columnDefinition.headerCellInternalFocusQueue !== true) + ) { + return; + } + + switch (e.key) { + case keyEnter: + case keyFunction2: + if ( + this.contains(document.activeElement) && + document.activeElement !== this + ) { + return; + } + + switch (this.cellType) { + case DataGridCellTypes.columnHeader: + if ( + this.columnDefinition.headerCellFocusTargetCallback !== undefined + ) { + const focusTarget: HTMLElement = + this.columnDefinition.headerCellFocusTargetCallback(this); + if (focusTarget !== null) { + focusTarget.focus(); + } + e.preventDefault(); + } + break; + + default: + if (this.columnDefinition.cellFocusTargetCallback !== undefined) { + const focusTarget: HTMLElement = + this.columnDefinition.cellFocusTargetCallback(this); + if (focusTarget !== null) { + focusTarget.focus(); + } + e.preventDefault(); + } + break; + } + break; + + case keyEscape: + if ( + this.contains(document.activeElement) && + document.activeElement !== this + ) { + this.focus(); + e.preventDefault(); + } + break; + } + } + + private updateCellView(): void { + this.disconnectCellView(); + + if (this.columnDefinition === null) { + return; + } + + switch (this.cellType) { + case DataGridCellTypes.columnHeader: + if (this.columnDefinition.headerCellTemplate !== undefined) { + this.customCellView = this.columnDefinition.headerCellTemplate.render( + this, + this + ); + } else { + this.customCellView = defaultHeaderCellContentsTemplate.render( + this, + this + ); + } + break; + + case DataGridCellTypes.rowHeader: + case DataGridCellTypes.default: + if (this.columnDefinition.cellTemplate !== undefined) { + this.customCellView = this.columnDefinition.cellTemplate.render( + this, + this + ); + } else { + this.customCellView = defaultCellContentsTemplate.render(this, this); + } + break; + } + } + + private disconnectCellView(): void { + if (this.customCellView !== null) { + this.customCellView.dispose(); + this.customCellView = null; + } + } + + private updateCellStyle = (): void => { + if (this.gridColumn && !this.gridColumn.includes('undefined')) { + this.style.gridColumn = this.gridColumn; + } else { + this.style.removeProperty('grid-column'); + } + }; + + /** + * Indicates the selected status. + * + * @public + * HTML Attribute: aria-selected + */ + @attr({ attribute: 'aria-selected', mode: 'fromView' }) + override ariaSelected: string | null = null; + + /** + * Indicates the sort status. + * + * @public + * HTML Attribute: aria-sort + */ + @attr({ attribute: 'aria-sort' }) override ariaSort: string | null = null; + + ariaSelectedChanged(_: string | null, selectedState: string | null) { + this.shadowRoot!.querySelector('.base')?.classList.toggle( + 'selected', + selectedState === 'true' + ); } #getColumnDataKey() { @@ -107,7 +384,3 @@ export class DataGridCell extends FoundationDataGridCell { return true; } } - -export interface DataGridCell extends DataGridCellExtension { - columnDefinition: ColumnDefinitionExtended | null; -} diff --git a/libs/components/src/lib/data-grid/data-grid-row.spec.ts b/libs/components/src/lib/data-grid/data-grid-row.spec.ts index 6942890a9b..082d50c0ce 100644 --- a/libs/components/src/lib/data-grid/data-grid-row.spec.ts +++ b/libs/components/src/lib/data-grid/data-grid-row.spec.ts @@ -2,6 +2,7 @@ import { html } from '@microsoft/fast-element'; import { axe, elementUpdated, fixture, getBaseElement } from '@vivid-nx/shared'; import { DataGridRow } from './data-grid-row'; import '.'; +import { DataGridCell } from './data-grid-cell.ts'; const COMPONENT_TAG = 'vwc-data-grid-row'; @@ -170,6 +171,47 @@ describe('vwc-data-grid-row', () => { expect(firstCellFocusedAfterArrowLeft).toBeTruthy(); }); + it('should reset focus index when the row loses focus', async () => { + element.columnDefinitions = [ + { columnDataKey: 'name' }, + { columnDataKey: 'age' }, + { columnDataKey: 'set' }, + ]; + element.rowData = { name: 'John', age: 30, set: 'set' }; + await elementUpdated(element); + const cells = Array.from( + element.querySelectorAll('vwc-data-grid-cell') + ) as DataGridCell[]; + + element.dispatchEvent( + new KeyboardEvent('keydown', { key: 'ArrowRight' }) + ); + expect(document.activeElement).toBe(cells[1]); + + cells[1].blur(); + + element.dispatchEvent( + new KeyboardEvent('keydown', { key: 'ArrowRight' }) + ); + expect(document.activeElement).toBe(cells[1]); + }); + + it('should ignore key press event when their default is prevented', async () => { + const dataGridCellTagName = 'button'; + element.columnDefinitions = [ + { columnDataKey: 'name' }, + { columnDataKey: 'age' }, + ]; + element.cellItemTemplate = html`<${dataGridCellTagName} role="cell">`; + await elementUpdated(element); + element.addEventListener('keydown', (e) => e.preventDefault(), true); + + element.dispatchEvent( + new KeyboardEvent('keydown', { key: 'ArrowLeft', cancelable: true }) + ); + expect(document.activeElement).toBe(document.body); + }); + it('should move focus edges on home or end keys press', async () => { const dataGridCellTagName = 'button'; element.columnDefinitions = [ diff --git a/libs/components/src/lib/data-grid/data-grid-row.ts b/libs/components/src/lib/data-grid/data-grid-row.ts index 0324f885b5..8fc285e663 100644 --- a/libs/components/src/lib/data-grid/data-grid-row.ts +++ b/libs/components/src/lib/data-grid/data-grid-row.ts @@ -1,5 +1,23 @@ -import { DataGridRow as FoundationDataGridRow } from '@microsoft/fast-foundation'; -import { attr } from '@microsoft/fast-element'; +import { + attr, + observable, + type RepeatBehavior, + RepeatDirective, + type ViewTemplate, +} from '@microsoft/fast-element'; +import { + eventFocusOut, + eventKeyDown, + keyEnd, + keyHome, +} from '@microsoft/fast-web-utilities'; +import { + keyArrowLeft, + keyArrowRight, +} from '@microsoft/fast-web-utilities/dist/key-codes'; +import { VividElement } from '../../shared/foundation/vivid-element/vivid-element'; +import { DataGridRowTypes } from './data-grid.options'; +import type { ColumnDefinition } from './data-grid'; /** * @public @@ -8,7 +26,256 @@ import { attr } from '@microsoft/fast-element'; * @event {CustomEvent<{cell: HTMLElement, row: HTMLElement, isHeaderCell: boolean, columnDataKey: string}>} cell-click - Event that fires when a cell is clicked * @event {CustomEvent} row-focused - Fires a custom 'row-focused' event when focus is on an element (usually a cell or its contents) in the row */ -export class DataGridRow extends FoundationDataGridRow { +export class DataGridRow extends VividElement { + /** + * String that gets applied to the the css gridTemplateColumns attribute for the row + *x + * @public + * @remarks + * HTML Attribute: grid-template-columns + */ + @attr({ attribute: 'grid-template-columns' }) + // @ts-expect-error Type is incorrectly non-optional + gridTemplateColumns: string; + /** + * @internal + */ + gridTemplateColumnsChanged(): void { + if (this.$fastController.isConnected) { + this.updateRowStyle(); + } + } + + /** + * The type of row + * + * @public + * @remarks + * HTML Attribute: row-type + */ + @attr({ attribute: 'row-type' }) + // eslint-disable-next-line @nrwl/nx/workspace/no-attribute-default-value + rowType: DataGridRowTypes = DataGridRowTypes.default; + /** + * @internal + */ + rowTypeChanged(): void { + if (this.$fastController.isConnected) { + this.updateItemTemplate(); + } + } + + /** + * The base data for this row + * + * @public + */ + @observable + rowData: object | null = null; + + /** + * The column definitions of the row + * + * @public + */ + @observable + columnDefinitions: ColumnDefinition[] | null = null; + + /** + * The template used to render cells in generated rows. + * + * @public + */ + @observable + cellItemTemplate?: ViewTemplate; + /** + * @internal + */ + cellItemTemplateChanged(): void { + this.updateItemTemplate(); + } + + /** + * The template used to render header cells in generated rows. + * + * @public + */ + @observable + headerCellItemTemplate?: ViewTemplate; + /** + * @internal + */ + headerCellItemTemplateChanged(): void { + this.updateItemTemplate(); + } + + /** + * The index of the row in the parent grid. + * This is typically set programmatically by the parent grid. + * + * @public + */ + @observable + rowIndex!: number; + + /** + * The cell item template currently in use. + * + * @internal + */ + @observable + activeCellItemTemplate?: ViewTemplate; + + /** + * The default cell item template. Set by the component templates. + * + * @internal + */ + @observable + defaultCellItemTemplate?: ViewTemplate; + + /** + * The default header cell item template. Set by the component templates. + * + * @internal + */ + @observable + defaultHeaderCellItemTemplate?: ViewTemplate; + + /** + * Children that are cells + * + * @internal + */ + @observable + cellElements!: HTMLElement[]; + + private cellsRepeatBehavior: RepeatBehavior | null = null; + private cellsPlaceholder: Node | null = null; + + /** + * @internal + */ + slottedCellElements!: HTMLElement[]; + + /** + * @internal + */ + focusColumnIndex = 0; + + /** + * @internal + */ + override connectedCallback(): void { + super.connectedCallback(); + + // note that row elements can be reused with a different data object + // as the parent grid's repeat behavior reacts to changes in the data set. + if (this.cellsRepeatBehavior === null) { + this.cellsPlaceholder = document.createComment(''); + this.appendChild(this.cellsPlaceholder); + + this.updateItemTemplate(); + + this.cellsRepeatBehavior = new RepeatDirective( + (x) => x.columnDefinitions, + (x) => x.activeCellItemTemplate, + { positioning: true } + ).createBehavior(this.cellsPlaceholder); + /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */ + this.$fastController.addBehaviors([this.cellsRepeatBehavior!]); + } + + this.addEventListener('cell-focused', this.handleCellFocus); + this.addEventListener(eventFocusOut, this.handleFocusout as EventListener); + this.addEventListener(eventKeyDown, this.handleKeydown as EventListener); + + this.updateRowStyle(); + } + + /** + * @internal + */ + override disconnectedCallback(): void { + super.disconnectedCallback(); + + this.removeEventListener('cell-focused', this.handleCellFocus); + this.removeEventListener( + eventFocusOut, + this.handleFocusout as EventListener + ); + this.removeEventListener(eventKeyDown, this.handleKeydown as EventListener); + } + + handleFocusout(_: FocusEvent): void { + if (!this.contains(document.activeElement)) { + this.focusColumnIndex = 0; + } + } + + handleCellFocus(e: Event): void { + this.focusColumnIndex = this.cellElements.indexOf(e.target as HTMLElement); + this.$emit('row-focused', this); + } + + handleKeydown(e: KeyboardEvent): void { + if (e.defaultPrevented) { + return; + } + let newFocusColumnIndex = 0; + switch (e.key) { + case keyArrowLeft: + // focus left one cell + newFocusColumnIndex = Math.max(0, this.focusColumnIndex - 1); + (this.cellElements[newFocusColumnIndex] as HTMLElement).focus(); + e.preventDefault(); + break; + + case keyArrowRight: + // focus right one cell + newFocusColumnIndex = Math.min( + this.cellElements.length - 1, + this.focusColumnIndex + 1 + ); + (this.cellElements[newFocusColumnIndex] as HTMLElement).focus(); + e.preventDefault(); + break; + + case keyHome: + if (!e.ctrlKey) { + (this.cellElements[0] as HTMLElement).focus(); + e.preventDefault(); + } + break; + case keyEnd: + if (!e.ctrlKey) { + // focus last cell of the row + ( + this.cellElements[this.cellElements.length - 1] as HTMLElement + ).focus(); + e.preventDefault(); + } + break; + } + } + + private updateItemTemplate(): void { + this.activeCellItemTemplate = + this.rowType === DataGridRowTypes.default && + this.cellItemTemplate !== undefined + ? this.cellItemTemplate + : this.rowType === DataGridRowTypes.default && + this.cellItemTemplate === undefined + ? this.defaultCellItemTemplate + : this.headerCellItemTemplate !== undefined + ? this.headerCellItemTemplate + : this.defaultHeaderCellItemTemplate; + } + + private updateRowStyle = (): void => { + this.style.gridTemplateColumns = this.gridTemplateColumns; + }; + /** * Indicates the selected status. * diff --git a/libs/components/src/lib/data-grid/data-grid.spec.ts b/libs/components/src/lib/data-grid/data-grid.spec.ts index 36432494e6..7e3defae44 100644 --- a/libs/components/src/lib/data-grid/data-grid.spec.ts +++ b/libs/components/src/lib/data-grid/data-grid.spec.ts @@ -6,6 +6,8 @@ import { DataGridRow } from './data-grid-row.ts'; const COMPONENT_TAG = 'vwc-data-grid'; +Element.prototype.scrollIntoView = jest.fn(); + function setMockRows(element: DataGrid) { element.rowElementTag = 'div'; element.rowItemTemplate = html` @@ -53,6 +55,32 @@ describe('vwc-data-grid', () => { expect(element.getAttribute('tabindex')).toEqual('-1'); }); + + it('should set tabindex to -1 if noTabbing is changed to false while element has focus', async () => { + element.noTabbing = true; + element.focus(); + + element.noTabbing = false; + + expect(element.getAttribute('tabindex')).toEqual('-1'); + }); + + it('should set the tabindex to -1 if noTabbing is true on focusout', async () => { + element.noTabbing = true; + element.focus(); + + element.blur(); + + expect(element.getAttribute('tabindex')).toEqual('-1'); + }); + + it('should set the tabindex to 0 if noTabbing is false on focusout', async () => { + element.focus(); + + element.blur(); + + expect(element.getAttribute('tabindex')).toEqual('0'); + }); }); describe('rowsData', () => { @@ -150,6 +178,14 @@ describe('vwc-data-grid', () => { element.generateHeader = 'none'; expect(getGeneratedHeader()).toBeNull(); }); + + it('should use headerCellItemTemplate for header cells', async () => { + element.headerCellItemTemplate = html`Custom`; + await elementUpdated(element); + + expect(getGeneratedHeader()!.children[0].textContent).toBe('Custom'); + expect(getGeneratedHeader()!.children[0].textContent).toBe('Custom'); + }); }); describe('gridTemplateColumns', () => { @@ -356,6 +392,138 @@ describe('vwc-data-grid', () => { }); }); + describe('keyboard navigation', () => { + const setupData = async () => { + element.rowsData = [ + { id: '1', name: 'Person 1', age: '20' }, + { id: '2', name: 'Person 2', age: '30' }, + { id: '3', name: 'Person 3', age: '40' }, + { id: '4', name: 'Person 4', age: '50' }, + { id: '5', name: 'Person 5', age: '60' }, + ]; + await elementUpdated(element); + await elementUpdated(element); + + const rows = Array.from(element.querySelectorAll('[role="row"]')); + const cells = rows.map( + (row) => + Array.from( + row.querySelectorAll('[role="columnheader"],[role="gridcell"]') + ) as HTMLElement[] + ); + + for (let i = 0; i < rows.length; i++) { + Object.defineProperty(rows[i], 'offsetTop', { + value: i * 100, + }); + Object.defineProperty(rows[i], 'offsetHeight', { + value: 100, + }); + } + + Object.defineProperty(element, 'scrollHeight', { + value: rows.length * 600, + }); + Object.defineProperty(element, 'clientHeight', { + value: 200, + }); + Object.defineProperty(element, 'offsetHeight', { + value: 200, + }); + + return { rows, cells }; + }; + + const pressKey = (key: string, options?: KeyboardEventInit) => { + document.activeElement!.dispatchEvent( + new KeyboardEvent('keydown', { + key, + cancelable: true, + bubbles: true, + ...options, + }) + ); + }; + + it('should allow navigating between cells with arrow keys', async () => { + const { cells } = await setupData(); + cells[0][0].focus(); + + pressKey('ArrowRight'); + expect(document.activeElement).toBe(cells[0][1]); + + pressKey('ArrowDown'); + expect(document.activeElement).toBe(cells[1][1]); + + pressKey('ArrowLeft'); + expect(document.activeElement).toBe(cells[1][0]); + + pressKey('ArrowUp'); + expect(document.activeElement).toBe(cells[0][0]); + }); + + it('should move to the first/last cell when pressing ctrl + Home/End', async () => { + const { cells } = await setupData(); + cells[1][1].focus(); + + pressKey('End', { ctrlKey: true }); + expect(document.activeElement).toBe(cells[5][2]); + + pressKey('Home', { ctrlKey: true }); + expect(document.activeElement).toBe(cells[0][0]); + }); + + it('should move up/down one page when pressing PageUp/PageDown', async () => { + const { cells } = await setupData(); + cells[0][0].focus(); + + pressKey('PageDown'); + expect(document.activeElement === cells[2][0]).toBe(true); + + pressKey('PageDown'); + expect(document.activeElement === cells[4][0]).toBe(true); + + pressKey('PageDown'); + expect(document.activeElement === cells[5][0]).toBe(true); + + pressKey('PageDown'); + expect(document.activeElement === cells[5][0]).toBe(true); + + pressKey('PageUp'); + expect(document.activeElement === cells[3][0]).toBe(true); + + pressKey('PageUp'); + expect(document.activeElement === cells[0][0]).toBe(true); + + pressKey('PageUp'); + expect(document.activeElement === cells[0][0]).toBe(true); + }); + + it('should update scrollTop to consider sticky header height', async () => { + element.generateHeader = 'sticky'; + const { rows, cells } = await setupData(); + Object.defineProperty(rows[0], 'clientHeight', { + value: 50, + }); + + cells[0][0].focus(); + pressKey('PageDown'); + expect(element.scrollTop).toBe(150); + }); + + it('should not throw an error when there are now rows', async () => { + element.generateHeader = 'none'; + element.rowsData = []; + await elementUpdated(element); + element.focus(); + + expect(() => pressKey('ArrowDown')).not.toThrow(); + expect(() => pressKey('ArrowUp')).not.toThrow(); + expect(() => pressKey('PageUp')).not.toThrow(); + expect(() => pressKey('PageDown')).not.toThrow(); + }); + }); + describe('a11y', () => { it('should pass html a11y test', async () => { element.rowsData = [ diff --git a/libs/components/src/lib/data-grid/data-grid.ts b/libs/components/src/lib/data-grid/data-grid.ts index ec065137b4..51afbb6080 100644 --- a/libs/components/src/lib/data-grid/data-grid.ts +++ b/libs/components/src/lib/data-grid/data-grid.ts @@ -1,5 +1,24 @@ -import { DataGrid as FoundationDataGrid } from '@microsoft/fast-foundation'; -import { attr, DOM, observable, Observable } from '@microsoft/fast-element'; +import { + attr, + DOM, + observable, + Observable, + RepeatBehavior, + RepeatDirective, + type ViewTemplate, +} from '@microsoft/fast-element'; +import { + eventFocus, + eventFocusOut, + eventKeyDown, + keyArrowDown, + keyArrowUp, + keyEnd, + keyHome, + keyPageDown, + keyPageUp, +} from '@microsoft/fast-web-utilities'; +import { VividElement } from '../../shared/foundation/vivid-element/vivid-element'; import type { DataGridCell } from './data-grid-cell'; import type { DataGridRow } from './data-grid-row'; import { DataGridRowTypes, GenerateHeaderOptions } from './data-grid.options'; @@ -23,13 +42,675 @@ export type ValueOf = T[keyof T]; export type DataGridSelectionMode = ValueOf; +/** + * Defines a column in the grid + * + * @public + */ +export interface ColumnDefinition { + /** + * Identifies the data item to be displayed in this column + * (i.e. how the data item is labelled in each row) + */ + columnDataKey: string; + + /** + * Sets the css grid-column property on the cell which controls its placement in + * the parent row. If left unset the cells will set this value to match the index + * of their column in the parent collection of ColumnDefinitions. + */ + gridColumn?: string; + + /** + * Column title, if not provided columnDataKey is used as title + */ + title?: string; + + /** + * Header cell template + */ + headerCellTemplate?: ViewTemplate; + + /** + * Whether the header cell has an internal focus queue + */ + headerCellInternalFocusQueue?: boolean; + + /** + * Callback function that returns the element to focus in a custom cell. + * When headerCellInternalFocusQueue is false this function is called when the cell is first focused + * to immediately move focus to a cell element, for example a cell that is a checkbox could move + * focus directly to the checkbox. + * When headerCellInternalFocusQueue is true this function is called when the user hits Enter or F2 + */ + headerCellFocusTargetCallback?: (cell: DataGridCell) => HTMLElement; + + /** + * cell template + */ + cellTemplate?: ViewTemplate; + + /** + * Whether the cell has an internal focus queue + */ + cellInternalFocusQueue?: boolean; + + /** + * Callback function that returns the element to focus in a custom cell. + * When cellInternalFocusQueue is false this function is called when the cell is first focused + * to immediately move focus to a cell element, for example a cell that is a checkbox could move + * focus directly to the checkbox. + * When cellInternalFocusQueue is true this function is called when the user hits Enter or F2 + */ + + cellFocusTargetCallback?: (cell: DataGridCell) => HTMLElement; + + /** + * Whether this column is the row header + */ + isRowHeader?: boolean; +} + /** * @public * @component data-grid * @slot - Default slot. * @event {CustomEvent<{cell: HTMLElement, row: HTMLElement, isHeaderCell: boolean, columnDataKey: string}>} cell-click - Event that fires when a cell is clicked */ -export class DataGrid extends FoundationDataGrid { +export class DataGrid extends VividElement { + /** + * generates a gridTemplateColumns based on columndata array + */ + private static generateTemplateColumns( + columnDefinitions: ColumnDefinition[] + ): string { + let templateColumns = ''; + columnDefinitions.forEach((_: ColumnDefinition) => { + templateColumns = `${templateColumns}${ + templateColumns === '' ? '' : ' ' + }${'1fr'}`; + }); + return templateColumns; + } + + /** + * When true the component will not add itself to the tab queue. + * Default is false. + * + * @public + * @remarks + * HTML Attribute: no-tabbing + */ + @attr({ attribute: 'no-tabbing', mode: 'boolean' }) + noTabbing = false; + /** + * @internal + */ + noTabbingChanged(): void { + if (this.noTabbing) { + this.setAttribute('tabIndex', '-1'); + } else { + this.setAttribute( + 'tabIndex', + this.contains(document.activeElement) ? '-1' : '0' + ); + } + } + + /** + * Whether the grid should automatically generate a header row and its type + * + * @public + * @remarks + * HTML Attribute: generate-header + */ + @attr({ attribute: 'generate-header' }) + // eslint-disable-next-line @nrwl/nx/workspace/no-attribute-default-value + generateHeader: GenerateHeaderOptions = GenerateHeaderOptions.default; + /** + * @internal + */ + generateHeaderChanged(): void { + if (this.$fastController.isConnected) { + this.toggleGeneratedHeader(); + } + } + + /** + * String that gets applied to the the css gridTemplateColumns attribute of child rows + * + * @public + * @remarks + * HTML Attribute: grid-template-columns + */ + @attr({ attribute: 'grid-template-columns' }) + // @ts-expect-error Type is incorrectly non-optional + gridTemplateColumns: string; + /** + * @internal + */ + gridTemplateColumnsChanged(): void { + if (this.$fastController.isConnected) { + this.updateRowIndexes(); + } + } + + /** + * The data being displayed in the grid + * + * @public + */ + @observable + rowsData: object[] = []; + /** + * @internal + */ + rowsDataChanged() { + if (this.columnDefinitions === null && this.rowsData.length > 0) { + this.columnDefinitions = DataGrid.generateColumns(this.rowsData[0]); + } + if (this.$fastController.isConnected) { + this.toggleGeneratedHeader(); + } + } + + /** + * The column definitions of the grid + * + * @public + */ + @observable + columnDefinitions: ColumnDefinition[] | null = null; + /** + * @internal + */ + columnDefinitionsChanged(): void { + if (this.columnDefinitions === null) { + this.generatedGridTemplateColumns = ''; + return; + } + this.generatedGridTemplateColumns = DataGrid.generateTemplateColumns( + this.columnDefinitions + ); + if (this.$fastController.isConnected) { + this.columnDefinitionsStale = true; + this.queueRowIndexUpdate(); + } + } + + /** + * The template to use for the programmatic generation of rows + * + * @public + */ + @observable + rowItemTemplate!: ViewTemplate; + + /** + * The template used to render cells in generated rows. + * + * @public + */ + @observable + cellItemTemplate?: ViewTemplate; + + /** + * The template used to render header cells in generated rows. + * + * @public + */ + @observable + headerCellItemTemplate?: ViewTemplate; + /** + * @internal + */ + headerCellItemTemplateChanged() { + if (this.$fastController.isConnected) { + if (this.generatedHeader !== null) { + this.generatedHeader.headerCellItemTemplate = + this.headerCellItemTemplate; + } + } + } + + /** + * The index of the row that will receive focus the next time the + * grid is focused. This value changes as focus moves to different + * rows within the grid. Changing this value when focus is already + * within the grid moves focus to the specified row. + * + * @public + */ + @observable + focusRowIndex = 0; + /** + * @internal + */ + focusRowIndexChanged() { + if (this.$fastController.isConnected) { + this.queueFocusUpdate(); + } + } + + /** + * The index of the column that will receive focus the next time the + * grid is focused. This value changes as focus moves to different rows + * within the grid. Changing this value when focus is already within + * the grid moves focus to the specified column. + * + * @public + */ + @observable + focusColumnIndex = 0; + /** + * @internal + */ + focusColumnIndexChanged() { + if (this.$fastController.isConnected) { + this.queueFocusUpdate(); + } + } + + /** + * The default row item template. Set by the component templates. + * + * @internal + */ + @observable + defaultRowItemTemplate!: ViewTemplate; + + /** + * Set by the component templates. + * + */ + @observable + rowElementTag!: string; + + /** + * Children that are rows + * + * @internal + */ + @observable + rowElements!: HTMLElement[]; + + private rowsRepeatBehavior: RepeatBehavior | null = null; + private rowsPlaceholder: Node | null = null; + + private generatedHeader: DataGridRow | null = null; + + private isUpdatingFocus = false; + private pendingFocusUpdate = false; + + private observer!: MutationObserver; + + private rowindexUpdateQueued = false; + private columnDefinitionsStale = true; + + private generatedGridTemplateColumns = ''; + + /** + * @internal + */ + override connectedCallback() { + super.connectedCallback(); + + if (this.rowItemTemplate === undefined) { + this.rowItemTemplate = this.defaultRowItemTemplate; + } + + this.rowsPlaceholder = document.createComment(''); + this.appendChild(this.rowsPlaceholder); + + this.toggleGeneratedHeader(); + + this.rowsRepeatBehavior = new RepeatDirective( + (x) => x.rowsData, + (x) => x.rowItemTemplate, + { positioning: true } + ).createBehavior(this.rowsPlaceholder); + + /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */ + this.$fastController.addBehaviors([this.rowsRepeatBehavior!]); + + this.addEventListener('row-focused', this.handleRowFocus); + this.addEventListener(eventFocus, this.handleFocus as EventListener); + this.addEventListener(eventKeyDown, this.handleKeydown as EventListener); + this.addEventListener(eventFocusOut, this.handleFocusOut as EventListener); + + this.observer = new MutationObserver(this.onChildListChange); + // only observe if nodes are added or removed + this.observer.observe(this, { childList: true }); + + DOM.queueUpdate(this.queueRowIndexUpdate); + + Observable.getNotifier(this).subscribe( + this.#changeHandler, + 'columnDefinitions' + ); + } + + /** + * @internal + */ + override disconnectedCallback(): void { + super.disconnectedCallback(); + + this.removeEventListener('row-focused', this.handleRowFocus); + this.removeEventListener(eventFocus, this.handleFocus as EventListener); + this.removeEventListener(eventKeyDown, this.handleKeydown as EventListener); + this.removeEventListener( + eventFocusOut, + this.handleFocusOut as EventListener + ); + + // disconnect observer + this.observer.disconnect(); + + this.rowsPlaceholder = null; + this.generatedHeader = null; + + Observable.getNotifier(this).unsubscribe( + this.#changeHandler, + 'columnDefinitions' + ); + } + + /** + * @internal + */ + handleRowFocus(e: Event): void { + this.isUpdatingFocus = true; + const focusRow: DataGridRow = e.target as DataGridRow; + this.focusRowIndex = this.rowElements.indexOf(focusRow); + this.focusColumnIndex = focusRow.focusColumnIndex; + this.setAttribute('tabIndex', '-1'); + this.isUpdatingFocus = false; + } + + /** + * @internal + */ + handleFocus(_: FocusEvent): void { + this.focusOnCell(this.focusRowIndex, this.focusColumnIndex, true); + } + + /** + * @internal + */ + handleFocusOut(e: FocusEvent): void { + if ( + e.relatedTarget === null || + !this.contains(e.relatedTarget as Element) + ) { + this.setAttribute('tabIndex', this.noTabbing ? '-1' : '0'); + } + } + + /** + * @internal + */ + handleKeydown(e: KeyboardEvent): void { + if (e.defaultPrevented) { + return; + } + + let newFocusRowIndex: number; + const maxIndex = this.rowElements.length - 1; + const currentGridBottom: number = this.offsetHeight + this.scrollTop; + const lastRow: HTMLElement = this.rowElements[maxIndex] as HTMLElement; + + switch (e.key) { + case keyArrowUp: + e.preventDefault(); + // focus up one row + this.focusOnCell(this.focusRowIndex - 1, this.focusColumnIndex, true); + break; + + case keyArrowDown: + e.preventDefault(); + // focus down one row + this.focusOnCell(this.focusRowIndex + 1, this.focusColumnIndex, true); + break; + + case keyPageUp: + e.preventDefault(); + if (this.rowElements.length === 0) { + this.focusOnCell(0, 0, false); + break; + } + if (this.focusRowIndex === 0) { + this.focusOnCell(0, this.focusColumnIndex, false); + return; + } + + newFocusRowIndex = this.focusRowIndex - 1; + + for (newFocusRowIndex; newFocusRowIndex >= 0; newFocusRowIndex--) { + const thisRow: HTMLElement = this.rowElements[newFocusRowIndex]; + if (thisRow.offsetTop < this.scrollTop) { + this.scrollTop = + thisRow.offsetTop + thisRow.clientHeight - this.clientHeight; + break; + } + } + + this.focusOnCell(newFocusRowIndex, this.focusColumnIndex, false); + break; + + case keyPageDown: + e.preventDefault(); + if (this.rowElements.length === 0) { + this.focusOnCell(0, 0, false); + break; + } + + // focus down one "page" + if ( + this.focusRowIndex >= maxIndex || + lastRow.offsetTop + lastRow.offsetHeight <= currentGridBottom + ) { + this.focusOnCell(maxIndex, this.focusColumnIndex, false); + return; + } + + newFocusRowIndex = this.focusRowIndex + 1; + + for ( + newFocusRowIndex; + newFocusRowIndex <= maxIndex; + newFocusRowIndex++ + ) { + const thisRow: HTMLElement = this.rowElements[ + newFocusRowIndex + ] as HTMLElement; + if (thisRow.offsetTop + thisRow.offsetHeight > currentGridBottom) { + let stickyHeaderOffset = 0; + if ( + this.generateHeader === GenerateHeaderOptions.sticky && + this.generatedHeader !== null + ) { + stickyHeaderOffset = this.generatedHeader.clientHeight; + } + this.scrollTop = thisRow.offsetTop - stickyHeaderOffset; + break; + } + } + + this.focusOnCell(newFocusRowIndex, this.focusColumnIndex, false); + + break; + + case keyHome: + if (e.ctrlKey) { + e.preventDefault(); + // focus first cell of first row + this.focusOnCell(0, 0, true); + } + break; + + case keyEnd: + if (e.ctrlKey && this.columnDefinitions !== null) { + e.preventDefault(); + // focus last cell of last row + this.focusOnCell( + this.rowElements.length - 1, + this.columnDefinitions.length - 1, + true + ); + } + break; + } + } + + private focusOnCell = ( + rowIndex: number, + columnIndex: number, + scrollIntoView: boolean + ): void => { + if (this.rowElements.length === 0) { + this.focusRowIndex = 0; + this.focusColumnIndex = 0; + return; + } + + const focusRowIndex = Math.max( + 0, + Math.min(this.rowElements.length - 1, rowIndex) + ); + const focusRow: Element = this.rowElements[focusRowIndex]; + + const cells: NodeListOf = focusRow.querySelectorAll( + '[role="cell"], [role="gridcell"], [role="columnheader"], [role="rowheader"]' + ); + + const focusColumnIndex = Math.max( + 0, + Math.min(cells.length - 1, columnIndex) + ); + + const focusTarget: HTMLElement = cells[focusColumnIndex] as HTMLElement; + + if ( + scrollIntoView && + this.scrollHeight !== this.clientHeight && + ((focusRowIndex < this.focusRowIndex && this.scrollTop > 0) || + (focusRowIndex > this.focusRowIndex && + this.scrollTop < this.scrollHeight - this.clientHeight)) + ) { + focusTarget.scrollIntoView({ block: 'center', inline: 'center' }); + } + + focusTarget.focus(); + }; + + private queueFocusUpdate(): void { + if (this.isUpdatingFocus && this.contains(document.activeElement)) { + return; + } + if (this.pendingFocusUpdate === false) { + this.pendingFocusUpdate = true; + DOM.queueUpdate(() => this.updateFocus()); + } + } + + private updateFocus(): void { + this.pendingFocusUpdate = false; + this.focusOnCell(this.focusRowIndex, this.focusColumnIndex, true); + } + + private toggleGeneratedHeader(): void { + if (this.generatedHeader !== null) { + this.removeChild(this.generatedHeader); + this.generatedHeader = null; + } + + if ( + this.generateHeader !== GenerateHeaderOptions.none && + this.columnDefinitions !== null + ) { + const generatedHeaderElement: HTMLElement = document.createElement( + this.rowElementTag + ); + this.generatedHeader = generatedHeaderElement as unknown as DataGridRow; + this.generatedHeader.columnDefinitions = this.columnDefinitions; + this.generatedHeader.gridTemplateColumns = this.gridTemplateColumns; + this.generatedHeader.rowType = + this.generateHeader === GenerateHeaderOptions.sticky + ? DataGridRowTypes.stickyHeader + : DataGridRowTypes.header; + /* istanbul ignore next */ + if (this.firstChild !== null || this.rowsPlaceholder !== null) { + this.insertBefore( + generatedHeaderElement, + this.firstChild !== null ? this.firstChild : this.rowsPlaceholder + ); + } + return; + } + } + + private onChildListChange = ( + mutations: MutationRecord[], + /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ + _: MutationObserver + ): void => { + if (mutations && mutations.length) { + mutations.forEach((mutation: MutationRecord): void => { + mutation.addedNodes.forEach((newNode: Node): void => { + if ( + newNode.nodeType === 1 && + (newNode as Element).getAttribute('role') === 'row' + ) { + (newNode as DataGridRow).columnDefinitions = this.columnDefinitions; + } + }); + }); + + this.queueRowIndexUpdate(); + } + }; + + private queueRowIndexUpdate = (): void => { + if (!this.rowindexUpdateQueued) { + this.rowindexUpdateQueued = true; + DOM.queueUpdate(this.updateRowIndexes); + } + }; + + private updateRowIndexes = (): void => { + let newGridTemplateColumns = this.gridTemplateColumns; + + if (newGridTemplateColumns === undefined) { + // try to generate columns based on manual rows + if ( + this.generatedGridTemplateColumns === '' && + this.rowElements.length > 0 + ) { + const firstRow: DataGridRow = this.rowElements[0] as DataGridRow; + this.generatedGridTemplateColumns = new Array( + firstRow.cellElements.length + ) + .fill('1fr') + .join(' '); + } + + newGridTemplateColumns = this.generatedGridTemplateColumns; + } + + this.rowElements.forEach((element: Element, index: number): void => { + const thisRow = element as DataGridRow; + thisRow.rowIndex = index; + thisRow.gridTemplateColumns = newGridTemplateColumns; + if (this.columnDefinitionsStale) { + thisRow.columnDefinitions = this.columnDefinitions; + } + }); + + this.rowindexUpdateQueued = false; + this.columnDefinitionsStale = false; + }; + /** * * Rows slot observer: @@ -152,75 +833,18 @@ export class DataGrid extends FoundationDataGrid { super(); this.addEventListener('click', this.#handleClick); this.addEventListener('keydown', this.#handleKeypress); - - // Override toggleGeneratedHeader to generate the header row even if the grid is empty - const privates = this as unknown as { - generatedHeader: DataGridRow | null; - rowsPlaceholder: HTMLElement | null; - rowElementTag: string; - toggleGeneratedHeader: () => void; - }; - privates.toggleGeneratedHeader = () => { - if (privates.generatedHeader !== null) { - this.removeChild(privates.generatedHeader); - privates.generatedHeader = null; - } - - if ( - this.generateHeader !== GenerateHeaderOptions.none && - this.columnDefinitions !== null - ) { - const generatedHeaderElement: HTMLElement = document.createElement( - this.rowElementTag - ); - privates.generatedHeader = - generatedHeaderElement as unknown as DataGridRow; - privates.generatedHeader.columnDefinitions = this.columnDefinitions; - privates.generatedHeader.gridTemplateColumns = this.gridTemplateColumns; - privates.generatedHeader.rowType = - this.generateHeader === GenerateHeaderOptions.sticky - ? DataGridRowTypes.stickyHeader - : DataGridRowTypes.header; - /* istanbul ignore next */ - if (this.firstChild !== null || privates.rowsPlaceholder !== null) { - this.insertBefore( - generatedHeaderElement, - this.firstChild !== null - ? this.firstChild - : privates.rowsPlaceholder - ); - } - return; - } - }; } #changeHandler = { handleChange(dataGrid: DataGrid, propertyName: string) { if (propertyName === 'columnDefinitions') { if (dataGrid.$fastController.isConnected) { - (dataGrid as any).toggleGeneratedHeader(); + dataGrid.toggleGeneratedHeader(); } } }, }; - override connectedCallback() { - super.connectedCallback(); - Observable.getNotifier(this).subscribe( - this.#changeHandler, - 'columnDefinitions' - ); - } - - override disconnectedCallback() { - super.disconnectedCallback(); - Observable.getNotifier(this).unsubscribe( - this.#changeHandler, - 'columnDefinitions' - ); - } - #setSelectedState = ( cell: DataGridCell | DataGridRow, selectedState: boolean @@ -300,7 +924,7 @@ export class DataGrid extends FoundationDataGrid { } }; - static override generateColumns(rowData: any) { + static generateColumns(rowData: any) { return Object.keys(rowData).map((property, index) => { return { columnDataKey: property, diff --git a/libs/wrapper-gen/componentOverrides.ts b/libs/wrapper-gen/componentOverrides.ts index 127613507e..3f7c73d768 100644 --- a/libs/wrapper-gen/componentOverrides.ts +++ b/libs/wrapper-gen/componentOverrides.ts @@ -38,6 +38,15 @@ ComponentRegister.addComponentOverride('data-grid', (component) => { }); }); +ComponentRegister.addComponentOverride('data-grid-cell', (component) => { + component.attributes.push({ + name: 'columnDefinition', + description: 'Object representing the column definition.', + type: [{ text: 'object', vuePropType: 'Object' }], + forwardTo: { type: 'property', name: 'columnDefinition' }, + }); +}); + ComponentRegister.addComponentOverride('searchable-select', (component) => { component.attributes.push({ name: 'values', diff --git a/libs/wrapper-gen/src/generator/customElementDeclarations.ts b/libs/wrapper-gen/src/generator/customElementDeclarations.ts index 10da4160d4..9e2b038220 100644 --- a/libs/wrapper-gen/src/generator/customElementDeclarations.ts +++ b/libs/wrapper-gen/src/generator/customElementDeclarations.ts @@ -363,15 +363,6 @@ const VividMixins: Record = { type: { text: 'string' }, }, ], - DataGridCellExtension: [ - { - name: 'columnDefinition', - description: - 'Extends the data grid cell definition to hold more options.', - type: { text: 'object' }, - fieldName: 'columnDefinition', - }, - ], Localized: [], Anchored: [ {