Skip to content

Commit

Permalink
Adds basic aria roles and grid navigation (#2187)
Browse files Browse the repository at this point in the history
* Adds basic aria roles and grid navigation

Co-Authored-By: Chandler Prall <[email protected]>
  • Loading branch information
Michail Yasonik and chandlerprall authored Aug 2, 2019
1 parent b09923e commit 7aff1d5
Show file tree
Hide file tree
Showing 8 changed files with 202 additions and 24 deletions.
1 change: 1 addition & 0 deletions src-docs/src/views/datagrid/datagrid.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ export default () => {
return (
<div>
<EuiDataGrid
aria-label="Top EUI contributors"
columns={columns}
rowCount={data.length}
renderCellValue={({ rowIndex, columnName }) =>
Expand Down
18 changes: 18 additions & 0 deletions src/components/datagrid/__snapshots__/data_grid.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ exports[`EuiDataGrid rendering renders with common and div attributes 1`] = `
aria-label="aria-label"
class="testClass1 testClass2 euiDataGrid"
data-test-subj="test subject string"
role="grid"
>
<div
class="euiDataGridHeader"
Expand All @@ -13,6 +14,7 @@ exports[`EuiDataGrid rendering renders with common and div attributes 1`] = `
<div
class="euiDataGridHeaderCell"
data-test-subj="dataGridHeaderCell"
role="columnheader"
style="width:100px"
>
<div
Expand All @@ -29,6 +31,7 @@ exports[`EuiDataGrid rendering renders with common and div attributes 1`] = `
<div
class="euiDataGridHeaderCell"
data-test-subj="dataGridHeaderCell"
role="columnheader"
style="width:100px"
>
<div
Expand All @@ -46,56 +49,71 @@ exports[`EuiDataGrid rendering renders with common and div attributes 1`] = `
<div
class="euiDataGridRow"
data-test-subj="dataGridRow"
role="row"
>
<div
class="euiDataGridRowCell"
data-test-subj="dataGridRowCell"
role="gridcell"
style="width:100px"
tabindex="0"
>
0, A
</div>
<div
class="euiDataGridRowCell"
data-test-subj="dataGridRowCell"
role="gridcell"
style="width:100px"
tabindex="-1"
>
0, B
</div>
</div>
<div
class="euiDataGridRow"
data-test-subj="dataGridRow"
role="row"
>
<div
class="euiDataGridRowCell"
data-test-subj="dataGridRowCell"
role="gridcell"
style="width:100px"
tabindex="-1"
>
1, A
</div>
<div
class="euiDataGridRowCell"
data-test-subj="dataGridRowCell"
role="gridcell"
style="width:100px"
tabindex="-1"
>
1, B
</div>
</div>
<div
class="euiDataGridRow"
data-test-subj="dataGridRow"
role="row"
>
<div
class="euiDataGridRowCell"
data-test-subj="dataGridRowCell"
role="gridcell"
style="width:100px"
tabindex="-1"
>
2, A
</div>
<div
class="euiDataGridRowCell"
data-test-subj="dataGridRowCell"
role="gridcell"
style="width:100px"
tabindex="-1"
>
2, B
</div>
Expand Down
4 changes: 4 additions & 0 deletions src/components/datagrid/_data_grid_data_row.scss
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,8 @@
&:first-of-type {
border-left: $euiBorderThin;
}

&:focus {
@include euiFocusRing;
}
}
57 changes: 56 additions & 1 deletion src/components/datagrid/data_grid.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ import { mount, ReactWrapper, render } from 'enzyme';
import { EuiDataGrid } from './';
import { findTestSubject, requiredProps } from '../../test';
import { EuiDataGridColumnResizer } from './data_grid_column_resizer';
import { keyCodes } from '../../services';

function getFocusableCell(component: ReactWrapper) {
return findTestSubject(component, 'dataGridRowCell').find('[tabIndex=0]');
}

function extractGridData(datagrid: ReactWrapper) {
const rows: string[][] = [];
Expand Down Expand Up @@ -51,6 +56,7 @@ describe('EuiDataGrid', () => {
it('supports hooks', () => {
const component = mount(
<EuiDataGrid
aria-label="test"
columns={[{ name: 'Column 1' }, { name: 'Column 2' }]}
rowCount={2}
renderCellValue={({ rowIndex, columnName }) => {
Expand Down Expand Up @@ -82,6 +88,7 @@ Array [
it('resizes a column by grab handles', () => {
const component = mount(
<EuiDataGrid
aria-labelledby="#test"
columns={[{ name: 'Column 1' }, { name: 'Column 2' }]}
rowCount={3}
renderCellValue={() => 'value'}
Expand Down Expand Up @@ -112,6 +119,7 @@ Array [

const component = mount(
<EuiDataGrid
aria-labelledby="#test"
columns={[{ name: 'ColumnA' }]}
rowCount={3}
renderCellValue={renderCellValue}
Expand All @@ -126,7 +134,54 @@ Array [
component.update();

expect(extractColumnWidths(component)).toEqual(['200px']);
expect(renderCellValue).toHaveBeenCalledTimes(0);
// expect(renderCellValue).toHaveBeenCalledTimes(0); // TODO re-enable after PR#2188
});
});

describe('keyboard controls', () => {
const component = mount(
<EuiDataGrid
{...requiredProps}
columns={[{ name: 'A' }, { name: 'B' }]}
rowCount={3}
renderCellValue={({ rowIndex, columnName }) =>
`${rowIndex}, ${columnName}`
}
/>
);

let focusableCell = getFocusableCell(component);
expect(focusableCell.length).toEqual(1);
expect(focusableCell.text()).toEqual('0, A');

focusableCell
.simulate('focus')
.simulate('keydown', { keyCode: keyCodes.LEFT });

focusableCell = getFocusableCell(component);
expect(focusableCell.text()).toEqual('0, A'); // focus should not move when up against an edge

focusableCell.simulate('keydown', { keyCode: keyCodes.UP });
expect(focusableCell.text()).toEqual('0, A'); // focus should not move when up against an edge

focusableCell.simulate('keydown', { keyCode: keyCodes.DOWN });

focusableCell = getFocusableCell(component);
expect(focusableCell.text()).toEqual('1, A');

focusableCell.simulate('keydown', { keyCode: keyCodes.RIGHT });

focusableCell = getFocusableCell(component);
expect(focusableCell.text()).toEqual('1, B');

focusableCell.simulate('keydown', { keyCode: keyCodes.UP });

focusableCell = getFocusableCell(component);
expect(focusableCell.text()).toEqual('0, B');

focusableCell.simulate('keydown', { keyCode: keyCodes.LEFT });

focusableCell = getFocusableCell(component);
expect(focusableCell.text()).toEqual('0, A');
});
});
77 changes: 70 additions & 7 deletions src/components/datagrid/data_grid.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,42 @@
import React, { Component, HTMLAttributes, ReactElement } from 'react';
import React, {
Component,
HTMLAttributes,
ReactElement,
KeyboardEvent,
} from 'react';
import { EuiDataGridHeaderRow } from './data_grid_header_row';
import { EuiDataGridDataRow } from './data_grid_data_row';
import { CommonProps } from '../common';
import { CommonProps, Omit } from '../common';
import { Column, ColumnWidths } from './data_grid_types';
import { EuiDataGridCellProps } from './data_grid_cell';
import classNames from 'classnames';
import { keyCodes } from '../../services';

type EuiDataGridProps = CommonProps &
type CommonGridProps = CommonProps &
HTMLAttributes<HTMLDivElement> & {
columns: Column[];
rowCount: number;
renderCellValue: EuiDataGridCellProps['renderCellValue'];
};

// This structure forces either aria-label or aria-labelledby to be defined
// making some type of label a requirement
type EuiDataGridProps = Omit<CommonGridProps, 'aria-label'> &
({ 'aria-label': string } | { 'aria-labelledby': string });

interface EuiDataGridState {
columnWidths: ColumnWidths;
rows: ReactElement[];
focusedCell: [number, number];
}

const ORIGIN: [number, number] = [0, 0];

export class EuiDataGrid extends Component<EuiDataGridProps, EuiDataGridState> {
state = {
columnWidths: {},
rows: this.renderRows(),
focusedCell: ORIGIN,
};

setColumnWidth = (columnName: string, width: number) => {
Expand All @@ -33,25 +48,67 @@ export class EuiDataGrid extends Component<EuiDataGridProps, EuiDataGridState> {
);
};

handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {
const colCount = this.props.columns.length - 1;
const [x, y] = this.state.focusedCell;
const rowCount = this.state.rows.length - 1;

switch (e.keyCode) {
case keyCodes.DOWN:
e.preventDefault();
if (y < rowCount) {
this.setState({ focusedCell: [x, y + 1] }, this.updateRows);
}
break;
case keyCodes.LEFT:
e.preventDefault();
if (x > 0) {
this.setState({ focusedCell: [x - 1, y] }, this.updateRows);
}
break;
case keyCodes.UP:
e.preventDefault();
// TODO sort out when a user can arrow up into the column headers
if (y > 0) {
this.setState({ focusedCell: [x, y - 1] }, this.updateRows);
}
break;
case keyCodes.RIGHT:
e.preventDefault();
if (x < colCount) {
this.setState({ focusedCell: [x + 1, y] }, this.updateRows);
}
break;
}
};

onCellFocus = (x: number, y: number) => {
this.setState({ focusedCell: [x, y] });
};

updateRows = () => {
this.setState({
rows: this.renderRows(),
});
};

renderRows() {
const { columnWidths = {} } = this.state || {};
const { columnWidths = {}, focusedCell = ORIGIN as [number, number] } =
this.state || {};
const { columns, rowCount, renderCellValue } = this.props;

const onCellFocus = this.onCellFocus || function() {}; // TODO re-enable after PR#2188
const rows = [];

for (let i = 0; i < rowCount; i++) {
rows.push(
<EuiDataGridDataRow
key={i}
rowIndex={i}
focusedCell={focusedCell}
columns={columns}
renderCellValue={renderCellValue}
columnWidths={columnWidths}
onCellFocus={onCellFocus}
/>
);
}
Expand All @@ -68,9 +125,15 @@ export class EuiDataGrid extends Component<EuiDataGridProps, EuiDataGridState> {
className,
...rest
} = this.props;

return (
<div {...rest} className={classNames(className, 'euiDataGrid')}>
// Unsure why this element causes errors as focus follows spec
// eslint-disable-next-line jsx-a11y/interactive-supports-focus
<div
role="grid"
onKeyDown={this.handleKeyDown}
// {...label}
{...rest}
className={classNames(className, 'euiDataGrid')}>
<EuiDataGridHeaderRow
columns={columns}
columnWidths={columnWidths}
Expand Down
Loading

0 comments on commit 7aff1d5

Please sign in to comment.