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

Adds basic aria roles and grid navigation #2187

Merged
merged 5 commits into from
Aug 2, 2019
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
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 });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@chandlerprall This doesn't show in our autodocs and I didn't know a good way to get this in.


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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO for a future PR

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