From 6d94a700a7366d502fc108853c3bc27b2fd93e8d Mon Sep 17 00:00:00 2001 From: Tomi Virkki Date: Tue, 10 Oct 2023 10:39:54 +0300 Subject: [PATCH] fix: defer grid cell-focus event with lazy data provider (#6618) --- .../src/vaadin-grid-data-provider-mixin.js | 1 + .../vaadin-grid-keyboard-navigation-mixin.js | 17 ++- .../grid/test/keyboard-navigation.test.js | 133 ++++++++++++++++++ 3 files changed, 149 insertions(+), 2 deletions(-) diff --git a/packages/grid/src/vaadin-grid-data-provider-mixin.js b/packages/grid/src/vaadin-grid-data-provider-mixin.js index dab02694658..b1cf24eb602 100644 --- a/packages/grid/src/vaadin-grid-data-provider-mixin.js +++ b/packages/grid/src/vaadin-grid-data-provider-mixin.js @@ -433,6 +433,7 @@ export const DataProviderMixin = (superClass) => }); this.__scrollToPendingIndexes(); + this.__dispatchPendingBodyCellFocus(); }); // If the grid is not loading anything, flush the debouncer immediately diff --git a/packages/grid/src/vaadin-grid-keyboard-navigation-mixin.js b/packages/grid/src/vaadin-grid-keyboard-navigation-mixin.js index 0ac70a13f5e..1e20e2858b4 100644 --- a/packages/grid/src/vaadin-grid-keyboard-navigation-mixin.js +++ b/packages/grid/src/vaadin-grid-keyboard-navigation-mixin.js @@ -841,9 +841,12 @@ export const KeyboardNavigationMixin = (superClass) => } if (cell) { - // Fire a public event for cell. const context = this.getEventContext(e); - cell.dispatchEvent(new CustomEvent('cell-focus', { bubbles: true, composed: true, detail: { context } })); + this.__pendingBodyCellFocus = this.loading && context.section === 'body'; + if (!this.__pendingBodyCellFocus) { + // Fire a cell-focus event for the cell + cell.dispatchEvent(new CustomEvent('cell-focus', { bubbles: true, composed: true, detail: { context } })); + } this._focusedCell = cell._focusButton || cell; if (isKeyboardActive() && e.target === cell) { @@ -857,6 +860,16 @@ export const KeyboardNavigationMixin = (superClass) => this._detectFocusedItemIndex(e); } + /** + * @private + */ + __dispatchPendingBodyCellFocus() { + // If the body cell focus was pending, dispatch the event once loading is done + if (this.__pendingBodyCellFocus && this.shadowRoot.activeElement === this._itemsFocusable) { + this._itemsFocusable.dispatchEvent(new Event('focusin', { bubbles: true, composed: true })); + } + } + /** * Get the focusable element depending on the current focus mode. * It can be a row, a cell, or a focusable div inside a cell. diff --git a/packages/grid/test/keyboard-navigation.test.js b/packages/grid/test/keyboard-navigation.test.js index 8003539f4a1..65d7f1cdac9 100644 --- a/packages/grid/test/keyboard-navigation.test.js +++ b/packages/grid/test/keyboard-navigation.test.js @@ -155,6 +155,10 @@ function focusFirstFooterCell() { focusWithMouse(grid.$.footer.children[0].children[0]); } +function focusFirstBodyCell() { + focusWithMouse(grid.$.items.children[0].children[0]); +} + function tabToHeader() { grid._headerFocusable.focus(); } @@ -2321,3 +2325,132 @@ describe('hierarchical data', () => { expect(grid.shadowRoot.activeElement.index).to.equal(itemsOnEachLevel - 1); }); }); + +describe('lazy data provider', () => { + let dataProviderCallbacks; + let cellFocusSpy; + + function flushDataProvider() { + dataProviderCallbacks.forEach((cb) => cb()); + dataProviderCallbacks = []; + } + + function lazyDataProvider({ page, pageSize }, callback) { + const items = [...Array(pageSize).keys()].map((i) => { + return { + name: `name-${page * pageSize + i}`, + }; + }); + + dataProviderCallbacks.push(() => callback(items, 1000)); + } + + beforeEach(() => { + dataProviderCallbacks = []; + grid = fixtureSync(` + + + + `); + cellFocusSpy = sinon.spy(); + grid.addEventListener('cell-focus', cellFocusSpy); + + grid.dataProvider = lazyDataProvider; + flushGrid(grid); + flushDataProvider(); + focusFirstBodyCell(); + cellFocusSpy.resetHistory(); + }); + + it('should dispatch cell-focused event for lazily loaded item', async () => { + const expectedContext = { + column: grid.querySelector('vaadin-grid-column'), + detailsOpened: false, + expanded: false, + index: 999, + item: { name: 'name-999' }, + level: 0, + section: 'body', + selected: false, + }; + + // Keyboard navigate to the last row cell + ctrlEnd(); + + flushDataProvider(); + await nextFrame(); + + expect(cellFocusSpy.calledOnce).to.be.true; + const e = cellFocusSpy.firstCall.args[0]; + expect(e.detail.context).to.be.deep.equal(expectedContext); + }); + + it('should not dispatch cell-focused event on scroll', async () => { + grid.scrollToIndex(Infinity); + + flushDataProvider(); + await nextFrame(); + + expect(cellFocusSpy.called).to.be.false; + }); + + it('should not dispatch an additional cell-focused event when navigating in body', async () => { + // Keyboard navigate to the last row cell + ctrlEnd(); + // Keyboard navigate back to the first row cell + ctrlHome(); + + flushDataProvider(); + await nextFrame(); + + expect(cellFocusSpy.calledOnce).to.be.true; + const e = cellFocusSpy.firstCall.args[0]; + expect(e.detail.context.item).to.be.deep.equal({ name: 'name-0' }); + }); + + it('should not dispatch an additional cell-focused event when navigating to head', async () => { + // Keyboard navigate to the last row cell + ctrlEnd(); + // Keyboard navigate to header + shiftTab(); + + flushDataProvider(); + await nextFrame(); + + expect(cellFocusSpy.calledOnce).to.be.true; + const e = cellFocusSpy.firstCall.args[0]; + expect(e.detail.context.section).to.be.equal('header'); + }); + + it('should not dispatch an additional cell-focused event when navigating back from head', async () => { + // Scroll half way down to get grid in loading state + grid.scrollToIndex(500); + down(); + + // Keyboard navigate to header + shiftTab(); + flushDataProvider(); + // Keyboard navigate back to body + tab(); + cellFocusSpy.resetHistory(); + // Scroll to bottom + grid.scrollToIndex(Infinity); + + flushDataProvider(); + await nextFrame(); + expect(cellFocusSpy.called).to.be.false; + }); + + it('should not dispatch a cell-focus event when grid has no focus', () => { + // Keyboard navigate to the last row cell + ctrlEnd(); + // Blur grid + focusable = fixtureSync(''); + focusable.focus(); + + cellFocusSpy.resetHistory(); + flushDataProvider(); + + expect(cellFocusSpy.called).to.be.false; + }); +});