diff --git a/packages/grid-pro/src/vaadin-grid-pro-inline-editing-mixin.js b/packages/grid-pro/src/vaadin-grid-pro-inline-editing-mixin.js index d813d8c68f4..db509563286 100644 --- a/packages/grid-pro/src/vaadin-grid-pro-inline-editing-mixin.js +++ b/packages/grid-pro/src/vaadin-grid-pro-inline-editing-mixin.js @@ -188,7 +188,7 @@ export const InlineEditingMixin = (superClass) => _disabledChanged(disabled, oldDisabled) { super._disabledChanged(disabled, oldDisabled); - if (disabled && this.__edited) { + if (disabled) { this._stopEdit(true); } } @@ -330,6 +330,9 @@ export const InlineEditingMixin = (superClass) => if (this.__edited && !this.__edited.cell.isConnected) { this._stopEdit(true, false); } + if (this.__pendingEdit && !this.__pendingEdit.cell.isConnected) { + this.__pendingEdit = null; + } } /** @private */ @@ -357,10 +360,22 @@ export const InlineEditingMixin = (superClass) => _startEdit(cell, column) { const isCellEditable = this._isCellEditable(cell); + // A new edit request supersedes any previously deferred one. + this.__pendingEdit = null; + // TODO: remove `_editingDisabled` after Flow counterpart is updated. if (this.disabled || this._editingDisabled || !isCellEditable) { return; } + + // A loading row has no item yet, so the editor can't be rendered against + // it. Defer the edit and resume it from `__updateRow` once the item has + // loaded. This can happen when committing an edit triggers clearCache() + // and the next cell's row is still loading. + if (cell.__parentRow.hasAttribute('loading')) { + this.__pendingEdit = { cell, column }; + return; + } // Cancel debouncer enqueued on focusout this._cancelStopEdit(); @@ -391,6 +406,9 @@ export const InlineEditingMixin = (superClass) => * @protected */ _stopEdit(shouldCancel, shouldRestoreFocus) { + // Stopping supersedes a deferred edit start. + this.__pendingEdit = null; + if (!this.__edited) { return; } @@ -552,6 +570,16 @@ export const InlineEditingMixin = (superClass) => } } super.__updateRow(row, item); + + // Resume an edit that was deferred while the row was loading, now that + // the row has an item again. + if (this.__pendingEdit && item) { + const { cell, column } = this.__pendingEdit; + if (cell.__parentRow === row && this._isCellEditable(cell)) { + this.__pendingEdit = null; + this._startEdit(cell, column); + } + } } /** diff --git a/packages/grid-pro/test/keyboard-navigation.test.js b/packages/grid-pro/test/keyboard-navigation.test.js index 61f0c442c54..01e6537b30f 100644 --- a/packages/grid-pro/test/keyboard-navigation.test.js +++ b/packages/grid-pro/test/keyboard-navigation.test.js @@ -440,7 +440,8 @@ describe('keyboard navigation', () => { // Normal behavior should call it once or twice at most expect(stopEditSpy.callCount).to.be.lessThan(5); - // Verify the second cell is now in edit mode + // The next cell's row is loading after clearCache, so the edit is + // deferred and resumes once the item loads const secondCell = getContainerCell(grid.$.items, 0, 1); expect(getCellEditor(secondCell)).to.be.ok; }); diff --git a/packages/grid/src/vaadin-grid-mixin.js b/packages/grid/src/vaadin-grid-mixin.js index af362d034b5..ab202ab4f20 100644 --- a/packages/grid/src/vaadin-grid-mixin.js +++ b/packages/grid/src/vaadin-grid-mixin.js @@ -773,18 +773,18 @@ export const GridMixin = (superClass) => * @private */ __updateRow(row) { + row._item = this.__getRowItem(row); + this.__a11yUpdateRowRowindex(row); this.__updateRowOrderParts(row); - const item = this.__getRowItem(row); - if (item) { + if (row._item) { this.__updateRowLoading(row, false); } else { this.__updateRowLoading(row, true); return; } - row._item = item; const model = this.__getRowModel(row); this._toggleDetailsCell(row, model.detailsOpened); diff --git a/packages/grid/test/activate-events.test.js b/packages/grid/test/activate-events.test.js index 897398bad92..90280d9a5cc 100644 --- a/packages/grid/test/activate-events.test.js +++ b/packages/grid/test/activate-events.test.js @@ -41,6 +41,16 @@ describe('activate events', () => { expect(e.detail.model.item).to.be.ok; }); + it('should fire with undefined item on loading cell click', () => { + grid.dataProvider = () => {}; + getBodyCellContent(grid, 0, 0).click(); + expect(spy).to.be.calledOnce; + + const e = spy.firstCall.args[0]; + expect(e.detail.model.index).to.equal(0); + expect(e.detail.model.item).to.be.undefined; + }); + it('should fire on cell Space', async () => { await sendKeys({ press: 'Space' }); expect(spy).to.be.calledOnce; @@ -91,6 +101,17 @@ describe('activate events', () => { expect(e.detail.model.item).to.be.ok; }); + it('should fire with undefined item on loading row Space', async () => { + grid.dataProvider = () => {}; + await sendKeys({ press: 'ArrowLeft' }); + await sendKeys({ press: 'Space' }); + expect(spy).to.be.calledOnce; + + const e = spy.firstCall.args[0]; + expect(e.detail.model.index).to.equal(0); + expect(e.detail.model.item).to.be.undefined; + }); + it('should not fire on cell Space', async () => { await sendKeys({ press: 'ArrowRight' }); await sendKeys({ press: 'Space' }); diff --git a/packages/grid/test/scroll-to-index.test.js b/packages/grid/test/scroll-to-index.test.js index 66e8e548a6c..e1d1834ab60 100644 --- a/packages/grid/test/scroll-to-index.test.js +++ b/packages/grid/test/scroll-to-index.test.js @@ -299,7 +299,7 @@ describe('scroll to index', () => { } function getFirstVisibleItemId() { - return getFirstVisibleItem(grid)._item.name; + return getFirstVisibleItem(grid)._item?.name; } beforeEach(async () => {