diff --git a/packages/grid/src/vaadin-grid-keyboard-navigation-mixin.js b/packages/grid/src/vaadin-grid-keyboard-navigation-mixin.js index 35c6d479678..b52036562dd 100644 --- a/packages/grid/src/vaadin-grid-keyboard-navigation-mixin.js +++ b/packages/grid/src/vaadin-grid-keyboard-navigation-mixin.js @@ -657,7 +657,41 @@ export const KeyboardNavigationMixin = (superClass) => } } - return tabOrder[index]; + let focusStepTarget = tabOrder[index]; + + // If the target focusable is tied to a column that is not visible, + // find the first visible column and update the target in order to + // prevent scrolling to the start of the row. + if (focusStepTarget && focusStepTarget._column && !this.__isColumnInViewport(focusStepTarget._column)) { + const firstVisibleColumn = this._getColumnsInOrder().find((column) => this.__isColumnInViewport(column)); + if (firstVisibleColumn) { + if (focusStepTarget === this._headerFocusable) { + focusStepTarget = firstVisibleColumn._headerCell; + } else if (focusStepTarget === this._itemsFocusable) { + const rowIndex = focusStepTarget._column._cells.indexOf(focusStepTarget); + focusStepTarget = firstVisibleColumn._cells[rowIndex]; + } else if (focusStepTarget === this._footerFocusable) { + focusStepTarget = firstVisibleColumn._footerCell; + } + } + } + + return focusStepTarget; + } + + /** + * Returns true if the given column is horizontally inside the viewport. + * @private + */ + __isColumnInViewport(column) { + if (column.frozen || column.frozenToEnd) { + // Assume frozen columns to always be inside the viewport + return true; + } + return ( + column._sizerCell.offsetLeft + column._sizerCell.offsetWidth >= this._scrollLeft && + column._sizerCell.offsetLeft <= this._scrollLeft + this.clientWidth + ); } /** @private */ diff --git a/packages/grid/src/vaadin-grid.js b/packages/grid/src/vaadin-grid.js index 4525fad294d..7d6e3abd186 100644 --- a/packages/grid/src/vaadin-grid.js +++ b/packages/grid/src/vaadin-grid.js @@ -816,6 +816,10 @@ class Grid extends ElementMixin( cell.setAttribute('part', 'cell body-cell'); row.appendChild(cell); + if (row === this.$.sizer) { + column._sizerCell = cell; + } + if (index === cols.length - 1 && this.rowDetailsRenderer) { // Add details cell as last cell to body rows this._detailsCells = this._detailsCells || []; diff --git a/packages/grid/test/keyboard-navigation.test.js b/packages/grid/test/keyboard-navigation.test.js index 7583ea3e0ee..68705cb490e 100644 --- a/packages/grid/test/keyboard-navigation.test.js +++ b/packages/grid/test/keyboard-navigation.test.js @@ -1124,6 +1124,34 @@ describe('keyboard navigation', () => { left(); expect(grid.$.table.scrollLeft).to.equal(0); }); + + it('should not scroll to the start when tabbed from header to body', () => { + tabToHeader(); + end(); + tab(); + expect(grid.$.table.scrollLeft).to.be.at.least(100); + }); + + it('should not scroll to the start when shift-tabbed from footer to body', () => { + shiftTabToFooter(); + end(); + shiftTab(); + expect(grid.$.table.scrollLeft).to.be.at.least(100); + }); + + it('should not scroll to the start when tabbed from body to footer', () => { + tabToBody(); + end(); + tab(); + expect(grid.$.table.scrollLeft).to.be.at.least(100); + }); + + it('should not scroll to the start when shift-tabbed from body to header', () => { + tabToBody(); + end(); + shiftTab(); + expect(grid.$.table.scrollLeft).to.be.at.least(100); + }); }); describe('vertical scrolling', () => {