Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: make sure each grid section has a tabbable element (#2998) (CP: 22.0) #3026

Merged
merged 1 commit into from
Nov 11, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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