diff --git a/packages/grid/src/vaadin-grid-column-group-mixin.js b/packages/grid/src/vaadin-grid-column-group-mixin.js index ba95f99b33..44ed78a0b3 100644 --- a/packages/grid/src/vaadin-grid-column-group-mixin.js +++ b/packages/grid/src/vaadin-grid-column-group-mixin.js @@ -3,11 +3,10 @@ * Copyright (c) 2016 - 2023 Vaadin Ltd. * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/ */ -import { FlattenedNodesObserver } from '@polymer/polymer/lib/utils/flattened-nodes-observer.js'; import { animationFrame } from '@vaadin/component-base/src/async.js'; import { Debouncer } from '@vaadin/component-base/src/debounce.js'; import { ColumnBaseMixin } from './vaadin-grid-column-mixin.js'; -import { updateColumnOrders } from './vaadin-grid-helpers.js'; +import { ColumnObserver, updateColumnOrders } from './vaadin-grid-helpers.js'; /** * A mixin providing common vaadin-grid-column-group functionality. @@ -331,29 +330,24 @@ export const GridColumnGroupMixin = (superClass) => * @protected */ _getChildColumns(el) { - return FlattenedNodesObserver.getFlattenedNodes(el).filter(this._isColumnElement); + return ColumnObserver.getColumns(el); } /** @private */ _addNodeObserver() { - this._observer = new FlattenedNodesObserver(this, (info) => { - if ( - info.addedNodes.filter(this._isColumnElement).length > 0 || - info.removedNodes.filter(this._isColumnElement).length > 0 - ) { - // Prevent synchronization of the hidden state to child columns. - // If the group is currently auto-hidden, and a visible column is added, - // we don't want the other columns to become visible as well. - this._preventHiddenSynchronization = true; - this._rootColumns = this._getChildColumns(this); - this._childColumns = this._rootColumns; - this._updateVisibleChildColumns(this._childColumns); - this._preventHiddenSynchronization = false; - - // Update the column tree - if (this._grid && this._grid._debounceUpdateColumnTree) { - this._grid._debounceUpdateColumnTree(); - } + this._observer = new ColumnObserver(this, () => { + // Prevent synchronization of the hidden state to child columns. + // If the group is currently auto-hidden, and a visible column is added, + // we don't want the other columns to become visible as well. + this._preventHiddenSynchronization = true; + this._rootColumns = this._getChildColumns(this); + this._childColumns = this._rootColumns; + this._updateVisibleChildColumns(this._childColumns); + this._preventHiddenSynchronization = false; + + // Update the column tree + if (this._grid && this._grid._debounceUpdateColumnTree) { + this._grid._debounceUpdateColumnTree(); } }); this._observer.flush(); diff --git a/packages/grid/src/vaadin-grid-dynamic-columns-mixin.js b/packages/grid/src/vaadin-grid-dynamic-columns-mixin.js index 17c536117a..0a158d1539 100644 --- a/packages/grid/src/vaadin-grid-dynamic-columns-mixin.js +++ b/packages/grid/src/vaadin-grid-dynamic-columns-mixin.js @@ -3,10 +3,9 @@ * Copyright (c) 2016 - 2023 Vaadin Ltd. * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/ */ -import { FlattenedNodesObserver } from '@polymer/polymer/lib/utils/flattened-nodes-observer.js'; import { microTask, timeOut } from '@vaadin/component-base/src/async.js'; import { Debouncer } from '@vaadin/component-base/src/debounce.js'; -import { updateCellState } from './vaadin-grid-helpers.js'; +import { ColumnObserver, updateCellState } from './vaadin-grid-helpers.js'; function arrayEquals(arr1, arr2) { if (!arr1 || !arr2 || arr1.length !== arr2.length) { @@ -58,7 +57,7 @@ export const DynamicColumnsMixin = (superClass) => * @protected */ _getChildColumns(el) { - return FlattenedNodesObserver.getFlattenedNodes(el).filter(this._isColumnElement); + return ColumnObserver.getColumns(el); } /** @private */ @@ -77,7 +76,7 @@ export const DynamicColumnsMixin = (superClass) => /** @private */ _getColumnTree() { - const rootColumns = FlattenedNodesObserver.getFlattenedNodes(this).filter(this._isColumnElement); + const rootColumns = ColumnObserver.getColumns(this); const columnTree = [rootColumns]; let c = rootColumns; @@ -116,17 +115,14 @@ export const DynamicColumnsMixin = (superClass) => /** @private */ _addNodeObserver() { - this._observer = new FlattenedNodesObserver(this, (info) => { - const hasColumnElements = (nodeCollection) => nodeCollection.filter(this._isColumnElement).length > 0; - if (hasColumnElements(info.addedNodes) || hasColumnElements(info.removedNodes)) { - const allRemovedCells = info.removedNodes.flatMap((c) => c._allCells); - const filterNotConnected = (element) => - allRemovedCells.filter((cell) => cell && cell._content.contains(element)).length; - - this.__removeSorters(this._sorters.filter(filterNotConnected)); - this.__removeFilters(this._filters.filter(filterNotConnected)); - this._debounceUpdateColumnTree(); - } + this._observer = new ColumnObserver(this, (_addedColumns, removedColumns) => { + const allRemovedCells = removedColumns.flatMap((c) => c._allCells); + const filterNotConnected = (element) => + allRemovedCells.filter((cell) => cell && cell._content.contains(element)).length; + + this.__removeSorters(this._sorters.filter(filterNotConnected)); + this.__removeFilters(this._filters.filter(filterNotConnected)); + this._debounceUpdateColumnTree(); this._debouncerCheckImports = Debouncer.debounce( this._debouncerCheckImports, diff --git a/packages/grid/src/vaadin-grid-helpers.js b/packages/grid/src/vaadin-grid-helpers.js index 69d5d1fd0f..47c9fd01b1 100644 --- a/packages/grid/src/vaadin-grid-helpers.js +++ b/packages/grid/src/vaadin-grid-helpers.js @@ -3,6 +3,8 @@ * Copyright (c) 2016 - 2023 Vaadin Ltd. * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/ */ +import { microTask } from '@vaadin/component-base/src/async.js'; +import { Debouncer } from '@vaadin/component-base/src/debounce.js'; import { addValueToAttribute, removeValueFromAttribute } from '@vaadin/component-base/src/dom-utils.js'; /** @@ -170,3 +172,95 @@ export function updateCellState(cell, attribute, value, part, oldPart) { // Add new part to the cell attribute updatePart(cell, value, part || `${attribute}-cell`); } + +/** + * A helper for observing flattened child column list of an element. + */ +export class ColumnObserver { + constructor(host, callback) { + this.__host = host; + this.__callback = callback; + this.__currentSlots = []; + + this.__onMutation = this.__onMutation.bind(this); + this.__observer = new MutationObserver(this.__onMutation); + this.__observer.observe(host, { + childList: true, + }); + + // The observer callback is invoked once initially. + this.__initialCallDebouncer = Debouncer.debounce(this.__initialCallDebouncer, microTask, () => this.__onMutation()); + } + + disconnect() { + this.__observer.disconnect(); + this.__initialCallDebouncer.cancel(); + this.__toggleSlotChangeListeners(false); + } + + flush() { + this.__onMutation(); + } + + __toggleSlotChangeListeners(add) { + this.__currentSlots.forEach((slot) => { + if (add) { + slot.addEventListener('slotchange', this.__onMutation); + } else { + slot.removeEventListener('slotchange', this.__onMutation); + } + }); + } + + __onMutation() { + // Detect if this is the initial call + const initialCall = !this.__currentColumns; + this.__currentColumns ||= []; + + // Detect added and removed columns or if the columns order has changed + const columns = ColumnObserver.getColumns(this.__host); + const addedColumns = columns.filter((column) => !this.__currentColumns.includes(column)); + const removedColumns = this.__currentColumns.filter((column) => !columns.includes(column)); + const orderChanged = this.__currentColumns.some((column, index) => column !== columns[index]); + this.__currentColumns = columns; + + // Update the list of child slots and toggle their slotchange listeners + this.__toggleSlotChangeListeners(false); + this.__currentSlots = [...this.__host.children].filter((child) => child instanceof HTMLSlotElement); + this.__toggleSlotChangeListeners(true); + + // Invoke the callback if there are changes in the child columns or if this is the initial call + const invokeCallback = initialCall || addedColumns.length || removedColumns.length || orderChanged; + if (invokeCallback) { + this.__callback(addedColumns, removedColumns); + } + } + + /** + * Default filter for column elements. + */ + static __isColumnElement(node) { + return node.nodeType === Node.ELEMENT_NODE && /\bcolumn\b/u.test(node.localName); + } + + static getColumns(host) { + const columns = []; + + // A temporary workaround for backwards compatibility + const isColumnElement = host._isColumnElement || ColumnObserver.__isColumnElement; + + [...host.children].forEach((child) => { + if (isColumnElement(child)) { + // The child is a column element, add it to the list + columns.push(child); + } else if (child instanceof HTMLSlotElement) { + // The child is a slot, add all assigned column elements to the list + [...child.assignedElements({ flatten: true })] + .filter((assignedElement) => isColumnElement(assignedElement)) + .forEach((assignedElement) => columns.push(assignedElement)); + } + }); + + return columns; + } +} diff --git a/packages/grid/test/column-observer.test.js b/packages/grid/test/column-observer.test.js new file mode 100644 index 0000000000..7b3c99a491 --- /dev/null +++ b/packages/grid/test/column-observer.test.js @@ -0,0 +1,216 @@ +import { expect } from '@esm-bundle/chai'; +import { aTimeout, fixtureSync, nextFrame } from '@vaadin/testing-helpers'; +import sinon from 'sinon'; +import { ColumnObserver } from '../src/vaadin-grid-helpers.js'; + +function createColumn() { + return document.createElement('vaadin-grid-column'); +} + +class Wrapper extends HTMLElement { + constructor() { + super(); + this.attachShadow({ mode: 'open' }).innerHTML = ` +
+ `; + } +} + +customElements.define('wrapper-component', Wrapper); + +describe('column observer', () => { + let wrapper; + let host; + let spy; + let observer; + + beforeEach(async () => { + wrapper = fixtureSync('