Skip to content

Commit fef4c31

Browse files
vaadin-botvursen
andauthored
fix: scroll to correct focused row on keyboard navigation (#8003) (#8010)
Co-authored-by: Sergey Vinogradov <mr.vursen@gmail.com>
1 parent 809b306 commit fef4c31

4 files changed

Lines changed: 105 additions & 53 deletions

File tree

packages/grid-pro/src/vaadin-grid-pro-inline-editing-mixin.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -508,7 +508,7 @@ export const InlineEditingMixin = (superClass) =>
508508
if (!this.singleCellEdit && nextCell !== cell) {
509509
this._startEdit(nextCell, nextColumn);
510510
} else {
511-
this._ensureScrolledToIndex(nextIndex);
511+
this.__ensureFlatIndexInViewport(nextIndex);
512512
nextCell.focus();
513513
}
514514
}

packages/grid/src/vaadin-grid-keyboard-navigation-mixin.js

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -275,10 +275,10 @@ export const KeyboardNavigationMixin = (superClass) =>
275275
}
276276

277277
/** @private */
278-
_ensureScrolledToIndex(index) {
278+
__ensureFlatIndexInViewport(index) {
279279
const targetRowInDom = [...this.$.items.children].find((child) => child.index === index);
280280
if (!targetRowInDom) {
281-
this.scrollToIndex(index);
281+
this._scrollToFlatIndex(index);
282282
} else {
283283
this.__scrollIntoViewport(index);
284284
}
@@ -499,7 +499,7 @@ export const KeyboardNavigationMixin = (superClass) =>
499499
}
500500

501501
// Ensure correct vertical scroll position, destination row is visible
502-
this._ensureScrolledToIndex(dstRowIndex);
502+
this.__ensureFlatIndexInViewport(dstRowIndex);
503503

504504
// When scrolling with repeated keydown, sometimes FocusEvent listeners
505505
// are too late to update _focusedItemIndex. Ensure next keydown
@@ -729,23 +729,14 @@ export const KeyboardNavigationMixin = (superClass) =>
729729
// The focus is about to exit the grid to the bottom.
730730
this.$.focusexit.focus();
731731
} else if (focusTarget === this._itemsFocusable) {
732-
let itemsFocusTarget = focusTarget;
733-
const targetRow = isRow(focusTarget) ? focusTarget : focusTarget.parentNode;
734-
this._ensureScrolledToIndex(this._focusedItemIndex);
735-
if (targetRow.index !== this._focusedItemIndex && isCell(focusTarget)) {
736-
// The target row, which is about to be focused next, has been
737-
// assigned with a new index since last focus, probably because of
738-
// scrolling. Focus the row for the stored focused item index instead.
739-
const columnIndex = Array.from(targetRow.children).indexOf(this._itemsFocusable);
740-
const focusedItemRow = Array.from(this.$.items.children).find(
741-
(row) => !row.hidden && row.index === this._focusedItemIndex,
742-
);
743-
if (focusedItemRow) {
744-
itemsFocusTarget = focusedItemRow.children[columnIndex];
745-
}
746-
}
732+
this.__ensureFlatIndexInViewport(this._focusedItemIndex);
733+
734+
// Ensure the correct element is set as focusable after scrolling.
735+
// The virtualizer may use a different element to render the item.
736+
this.__updateItemsFocusable();
737+
747738
e.preventDefault();
748-
itemsFocusTarget.focus();
739+
this._itemsFocusable.focus();
749740
} else {
750741
e.preventDefault();
751742
focusTarget.focus();

packages/grid/test/keyboard-navigation-row-focus.common.js

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -99,19 +99,17 @@ function getTabbableRows(root) {
9999
return root.querySelectorAll('tr[tabindex]:not([hidden]):not([tabindex="-1"])');
100100
}
101101

102-
function hierarchicalDataProvider({ parentItem }, callback) {
103-
// Let's use a count lower than pageSize so we can ignore page + pageSize for now
104-
const itemsOnEachLevel = 5;
105-
106-
const items = [...Array(itemsOnEachLevel)].map((_, i) => {
102+
function hierarchicalDataProvider({ parentItem, page, pageSize }, callback) {
103+
const items = Array.from({ length: 100 }, (_, i) => {
107104
return {
108105
name: `${parentItem ? `${parentItem.name}-` : ''}${i}`,
109106
// Let's only have child items on every second item
110107
children: i % 2 === 0,
111108
};
112109
});
113110

114-
callback(items, itemsOnEachLevel);
111+
const offset = page * pageSize;
112+
callback(items.slice(offset, offset + pageSize), items.length);
115113
}
116114

117115
describe('keyboard navigation - row focus', () => {
@@ -145,6 +143,39 @@ describe('keyboard navigation - row focus', () => {
145143
await nextRender(grid);
146144
});
147145

146+
describe('scrolling and navigating', () => {
147+
it('should scroll focused nested row into view on arrow key', () => {
148+
focusItem(0);
149+
// Expand first row
150+
right();
151+
// Focus first nested row
152+
down();
153+
// Simulate real scrolling to get the virtualizer to render
154+
// the focused item in a different element.
155+
grid.$.table.scrollTop = grid.$.table.scrollHeight / 2;
156+
flushGrid(grid);
157+
down();
158+
expect(getFocusedRowIndex(grid)).to.equal(2);
159+
});
160+
161+
it('should scroll focused nested row into view on Tab', () => {
162+
focusItem(0);
163+
// Expand first row
164+
right();
165+
// Focus first nested row
166+
down();
167+
// Move focus to header
168+
shiftTab();
169+
// Simulate real scrolling to get the virtualizer to render
170+
// the focused item in a different element.
171+
grid.$.table.scrollTop = grid.$.table.scrollHeight / 2;
172+
flushGrid(grid);
173+
// Move focus back to items
174+
tab();
175+
expect(getFocusedRowIndex(grid)).to.equal(1);
176+
});
177+
});
178+
148179
describe('navigating with tab', () => {
149180
it('should have single tabbable row in every section', () => {
150181
const tabbableElements = getTabbableElements(grid.shadowRoot);
@@ -339,7 +370,7 @@ describe('keyboard navigation - row focus', () => {
339370

340371
end();
341372

342-
expect(getFocusedRowIndex(grid)).to.equal(4);
373+
expect(getFocusedRowIndex(grid)).to.equal(99);
343374
});
344375
});
345376
});

packages/grid/test/keyboard-navigation.common.js

Lines changed: 56 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1305,43 +1305,73 @@ describe('keyboard navigation', () => {
13051305
up();
13061306
expect(grid.$.table.scrollTop).to.equal(scrollTop);
13071307
});
1308+
});
13081309

1309-
describe('rotating focus indicator prevention', () => {
1310-
it('should hide navigation mode when a focused row goes off screen', () => {
1311-
focusItem(0);
1312-
right();
1310+
describe('scrolling and navigating', () => {
1311+
beforeEach(() => {
1312+
grid.items = undefined;
1313+
grid.size = 200;
1314+
grid.dataProvider = infiniteDataProvider;
1315+
flushGrid(grid);
1316+
});
13131317

1314-
expect(grid.hasAttribute('navigating')).to.be.true;
1318+
it('should hide navigation mode when a focused row goes off screen', () => {
1319+
focusItem(0);
1320+
right();
13151321

1316-
grid.scrollToIndex(100);
1317-
flushGrid(grid);
1322+
expect(grid.hasAttribute('navigating')).to.be.true;
13181323

1319-
expect(grid.hasAttribute('navigating')).to.be.false;
1320-
});
1324+
grid.scrollToIndex(100);
1325+
flushGrid(grid);
13211326

1322-
it('should reveal navigation mode when a focused row is back on screen', () => {
1323-
focusItem(0);
1324-
right();
1325-
grid.scrollToIndex(100);
1326-
flushGrid(grid);
1327+
expect(grid.hasAttribute('navigating')).to.be.false;
1328+
});
13271329

1328-
grid.scrollToIndex(0);
1329-
flushGrid(grid);
1330+
it('should reveal navigation mode when a focused row is back on screen', () => {
1331+
focusItem(0);
1332+
right();
1333+
grid.scrollToIndex(100);
1334+
flushGrid(grid);
13301335

1331-
expect(grid.hasAttribute('navigating')).to.be.true;
1332-
});
1336+
grid.scrollToIndex(0);
1337+
flushGrid(grid);
13331338

1334-
it('should not hide navigation mode if a header cell is focused', () => {
1335-
tabToHeader();
1336-
right();
1339+
expect(grid.hasAttribute('navigating')).to.be.true;
1340+
});
13371341

1338-
expect(grid.hasAttribute('navigating')).to.be.true;
1342+
it('should not hide navigation mode if a header cell is focused', () => {
1343+
tabToHeader();
1344+
right();
13391345

1340-
grid.scrollToIndex(100);
1341-
flushGrid(grid);
1346+
expect(grid.hasAttribute('navigating')).to.be.true;
13421347

1343-
expect(grid.hasAttribute('navigating')).to.be.true;
1344-
});
1348+
grid.scrollToIndex(100);
1349+
flushGrid(grid);
1350+
1351+
expect(grid.hasAttribute('navigating')).to.be.true;
1352+
});
1353+
1354+
it('should scroll focused row into view on arrow key', () => {
1355+
focusItem(0);
1356+
// Simulate real scrolling to get the virtualizer to render
1357+
// the focused item in a different element.
1358+
grid.$.table.scrollTop = grid.$.table.scrollHeight / 2;
1359+
flushGrid(grid);
1360+
down();
1361+
expect(getFocusedRowIndex(grid)).to.equal(1);
1362+
expect(getFocusedCellIndex(grid)).to.equal(0);
1363+
});
1364+
1365+
it('should scroll focused row into view on Tab', () => {
1366+
focusItem(0);
1367+
tabToHeader();
1368+
// Simulate real scrolling to get the virtualizer to render
1369+
// the focused item in a different element.
1370+
grid.$.table.scrollTop = grid.$.table.scrollHeight / 2;
1371+
flushGrid(grid);
1372+
tab();
1373+
expect(getFocusedRowIndex(grid)).to.equal(0);
1374+
expect(getFocusedCellIndex(grid)).to.equal(0);
13451375
});
13461376
});
13471377
});

0 commit comments

Comments
 (0)