Skip to content

Commit

Permalink
fix: make sure each grid section has a tabbable element (#2998) (#3026)
Browse files Browse the repository at this point in the history
  • Loading branch information
vaadin-bot committed Nov 11, 2021
1 parent 9f83d15 commit 72ef486
Show file tree
Hide file tree
Showing 14 changed files with 372 additions and 36 deletions.
43 changes: 28 additions & 15 deletions packages/grid/src/vaadin-grid-keyboard-navigation-mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,9 @@ export const KeyboardNavigationMixin = (superClass) =>

/** @private */
get __rowFocusMode() {
return this.__isRow(this._itemsFocusable);
return (
this.__isRow(this._itemsFocusable) || this.__isRow(this._headerFocusable) || this.__isRow(this._footerFocusable)
);
}

set __rowFocusMode(value) {
Expand Down Expand Up @@ -561,7 +563,10 @@ export const KeyboardNavigationMixin = (superClass) =>

index += step;
while (index >= 0 && index <= tabOrder.length - 1) {
const rowElement = this.__rowFocusMode ? tabOrder[index] : tabOrder[index].parentNode;
let rowElement = tabOrder[index];
if (rowElement && !this.__rowFocusMode) {
rowElement = tabOrder[index].parentNode;
}

if (!rowElement || rowElement.hidden) {
index += step;
Expand Down Expand Up @@ -592,9 +597,9 @@ export const KeyboardNavigationMixin = (superClass) =>
// assigned with a new index since last focus, probably because of
// scrolling. Focus the row for the stored focused item index instead.
const columnIndex = Array.from(targetRow.children).indexOf(this._itemsFocusable);
const focusedItemRow = Array.from(this.$.items.children).filter(
(row) => row.index === this._focusedItemIndex
)[0];
const focusedItemRow = Array.from(this.$.items.children).find(
(row) => !row.hidden && row.index === this._focusedItemIndex
);
if (focusedItemRow) {
itemsFocusTarget = focusedItemRow.children[columnIndex];
}
Expand Down Expand Up @@ -778,19 +783,27 @@ export const KeyboardNavigationMixin = (superClass) =>

/** @protected */
_resetKeyboardNavigation() {
if (!this.__isValidFocusable(this._headerFocusable) && this.$.header.firstElementChild) {
this._headerFocusable = Array.from(this.$.header.firstElementChild.children).filter((el) => !el.hidden)[0];
}
// Header / footer
['header', 'footer'].forEach((section) => {
if (!this.__isValidFocusable(this[`_${section}Focusable`])) {
const firstVisibleRow = [...this.$[section].children].find((row) => row.offsetHeight);
const firstVisibleCell = firstVisibleRow ? [...firstVisibleRow.children].find((cell) => !cell.hidden) : null;
if (firstVisibleRow && firstVisibleCell) {
this[`_${section}Focusable`] = this.__rowFocusMode ? firstVisibleRow : firstVisibleCell;
}
}
});

// Body
if (!this.__isValidFocusable(this._itemsFocusable) && this.$.items.firstElementChild) {
const firstVisibleIndexRow = this.__getFirstVisibleItem();
if (firstVisibleIndexRow) {
this._itemsFocusable = Array.from(firstVisibleIndexRow.children).filter((el) => !el.hidden)[0];
}
}
const firstVisibleRow = this.__getFirstVisibleItem();
const firstVisibleCell = firstVisibleRow ? [...firstVisibleRow.children].find((cell) => !cell.hidden) : null;

if (!this.__isValidFocusable(this._footerFocusable) && this.$.footer.firstElementChild) {
this._footerFocusable = Array.from(this.$.footer.firstElementChild.children).filter((el) => !el.hidden)[0];
if (firstVisibleCell && firstVisibleRow) {
// Reset memoized column
delete this._focusedColumnOrder;
this._itemsFocusable = this.__rowFocusMode ? firstVisibleRow : firstVisibleCell;
}
}
}

Expand Down
6 changes: 6 additions & 0 deletions packages/grid/src/vaadin-grid.js
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,9 @@ class Grid extends ElementMixin(

// If the focused cell's parent row got hidden by the size change, focus the corresponding new cell
cellCoordinates && cell.parentElement.hidden && this.__focusBodyCell(cellCoordinates);

// Make sure the body has a tabbable element
this._resetKeyboardNavigation();
}
}

Expand Down Expand Up @@ -791,6 +794,9 @@ class Grid extends ElementMixin(
if (row.hidden !== !visibleRowCells.length) {
row.hidden = !visibleRowCells.length;
}

// Make sure the section has a tabbable element
this._resetKeyboardNavigation();
}

/** @private */
Expand Down
169 changes: 168 additions & 1 deletion packages/grid/test/keyboard-navigation-row-focus.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import {
fixtureSync,
keyDownOn,
listenOnce,
nextFrame,
nextRender,
up as mouseUp
} from '@vaadin/testing-helpers';
import '../src/all-imports.js';
import { getCellContent } from './helpers.js';
import { flushGrid, getCellContent } from './helpers.js';

let grid, header, footer, body;

Expand Down Expand Up @@ -110,6 +111,10 @@ function getTabbableElements(root) {
return root.querySelectorAll('[tabindex]:not([tabindex="-1"])');
}

function getTabbableRows(root) {
return root.querySelectorAll('tr[tabindex]:not([hidden]):not([tabindex="-1"])');
}

function hierarchicalDataProvider({ parentItem }, callback) {
// Let's use a count lower than pageSize so we can ignore page + pageSize for now
const itemsOnEachLevel = 5;
Expand Down Expand Up @@ -383,3 +388,165 @@ describe('keyboard navigation - row focus', () => {
});
});
});

describe('keyboard navigation on column groups - row focus', () => {
beforeEach(async () => {
grid = fixtureSync(`
<vaadin-grid>
<vaadin-grid-column-group header="main group header">
<vaadin-grid-column-group header="sub group header">
<vaadin-grid-column header="column header"></vaadin-grid-column>
</vaadin-grid-column-group>
</vaadin-grid-column-group>
</vaadin-grid>
`);
grid.items = ['foo', 'bar'];
grid.querySelector('vaadin-grid-column').renderer = (root, _, model) => (root.textContent = model.item);
flushGrid(grid);

await nextRender(grid);

// Make the grid enter row focus mode initially
focusFirstHeaderCell();
left();

await nextRender(grid);
});

describe('updating tabbable rows', () => {
describe('updating tabbable rows - header', () => {
let header;
let mainGroup;
let subGroup;
let column;

beforeEach(() => {
header = grid.$.header;
mainGroup = grid.querySelector('vaadin-grid-column-group');
subGroup = mainGroup.querySelector('vaadin-grid-column-group');
column = subGroup.querySelector('vaadin-grid-column');
});

it('should update tabbable header row on header row hide', async () => {
const initialTabbableHeaderRow = getTabbableRows(header)[0];

// Hide the first header row
mainGroup.header = null;
await nextFrame();

const tabbableHeaderRow = getTabbableRows(header)[0];
expect(tabbableHeaderRow.offsetHeight).not.to.equal(0);
expect(tabbableHeaderRow).not.to.equal(initialTabbableHeaderRow);
});

it('should have no tabbable header rows when header is hidden', async () => {
// Hide all header rows
mainGroup.header = null;
subGroup.header = null;
column.header = null;
await nextFrame();

expect(getTabbableRows(header)).to.be.empty;
});

it('should update tabbable header row on header row unhide', async () => {
// Hide all header rows
mainGroup.header = null;
subGroup.header = null;
column.header = null;
await nextFrame();

column.header = 'column';
await nextFrame();

const tabbableHeaderRow = getTabbableRows(header)[0];
expect(tabbableHeaderRow.offsetHeight).not.to.equal(0);
});
});

describe('updating tabbable rows - body', () => {
let body;

beforeEach(() => {
body = grid.$.items;
});

it('should update tabbable body row on body row hide', async () => {
// Focus the second body row / make it tabbable
tabToBody();
down();

// Hide the second body row
grid.items = [grid.items[0]];

await nextFrame();

// Expect the tabbable body row to be on the first row
const tabbableBodyRow = getTabbableRows(body)[0];
expect(tabbableBodyRow.index).to.equal(0);
expect(tabbableBodyRow.offsetHeight).not.to.equal(0);
});

it('should have no tabbable body row when there are no rows', async () => {
// Remove all body rows
grid.items = [];

const tabbableBodyRow = getTabbableRows(body)[0];
expect(tabbableBodyRow).not.to.be.ok;
});

it('should update tabbable body row on body row unhide', async () => {
// Remove all body rows
grid.items = [];
await nextFrame();

grid.items = ['foo', 'bar'];
await nextFrame();

const tabbableBodyRow = getTabbableRows(body)[0];
expect(tabbableBodyRow.index).to.equal(0);
expect(tabbableBodyRow.offsetHeight).not.to.equal(0);
});
});
});
});

['header', 'footer', 'body'].forEach((section) => {
describe(`empty grid - row focus - ${section}`, () => {
beforeEach(async () => {
grid = fixtureSync(`<vaadin-grid>
<vaadin-grid-column></vaadin-grid-column>
</vaadin-grid>`);
const column = grid.firstElementChild;

if (section === 'header') {
column.header = 'header';
} else if (section === 'footer') {
column.footerRenderer = (root) => (root.textContent = 'footer');
} else if (section === 'body') {
grid.items = ['foo'];
}

flushGrid(grid);
});

it(`should enter row focus mode - ${section}`, () => {
getTabbableElements(grid.$.table)[0].focus();
left();

const tabbableRow = getTabbableRows(grid.shadowRoot)[0];
expect(tabbableRow).to.be.ok;
});

it(`should return to cell focus mode - ${section}`, () => {
getTabbableElements(grid.$.table)[0].focus();
left();

const tabbableRow = getTabbableRows(grid.shadowRoot)[0];
right();

const tabbableCell = getTabbableElements(tabbableRow)[0];
expect(tabbableCell.parentElement).to.equal(tabbableRow);
});
});
});
Loading

0 comments on commit 72ef486

Please sign in to comment.