Skip to content

Commit

Permalink
fix: defer grid cell-focus event with lazy data provider (#6618) (#6626)
Browse files Browse the repository at this point in the history
  • Loading branch information
tomivirkki authored Oct 10, 2023
1 parent 32f8f27 commit 30a0c1f
Show file tree
Hide file tree
Showing 3 changed files with 149 additions and 2 deletions.
1 change: 1 addition & 0 deletions packages/grid/src/vaadin-grid-data-provider-mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,7 @@ export const DataProviderMixin = (superClass) =>
});

this.__scrollToPendingIndex();
this.__dispatchPendingBodyCellFocus();
});

if (!this._cache.isLoading()) {
Expand Down
17 changes: 15 additions & 2 deletions packages/grid/src/vaadin-grid-keyboard-navigation-mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -832,9 +832,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) {
Expand All @@ -848,6 +851,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.
Expand Down
133 changes: 133 additions & 0 deletions packages/grid/test/keyboard-navigation.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down Expand Up @@ -2345,3 +2349,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(`
<vaadin-grid>
<vaadin-grid-column path="name"></vaadin-grid-column>
</vaadin-grid>
`);
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('<input>');
focusable.focus();

cellFocusSpy.resetHistory();
flushDataProvider();

expect(cellFocusSpy.called).to.be.false;
});
});

0 comments on commit 30a0c1f

Please sign in to comment.