Accessibility

The Grid and its built-in components are accessible by screen readers and provide WAI-ARIA support.

Section 508

The Grid is compliant with the Section 508 requirements.

Common Keyboard Navigation Scenarios

We allow full control over the Grid keyboard navigation, as this lets the developer to choose which of the keyboard or keyboard and mouse combinations will be needed for the application.

We will show how some of the most common cases can be implemented using the KendoReact Grid.

Single Row Navigation

The following example showcase how to add a row selection using the arrows keys.

import React from 'react';
import ReactDOM from 'react-dom';
import { Grid, GridColumn as Column } from '@progress/kendo-react-grid';

import products from './products.json';
import { Keys } from './keys.js';

class App extends React.Component {
  state = {
    data: products,
    selectedItem: {}
  }

  handleRowClick = (props) => {
    this.setState({ selectedItem: props.dataItem });
  }

  handleKeyDown = (e) => {
    let index = products.findIndex((item) => item.ProductID === this.state.selectedItem.ProductID);
    if (e.keyCode === Keys.up && index > 0) {
      this.setState({ selectedItem: products[index - 1] }); // This is the item that has to be passed to the form
    } else if (e.keyCode === Keys.down && index < products.length - 1) {
      this.setState({ selectedItem: products[index + 1] }); // This is the item that has to be passed to the form
    }
  }

  render() {
    return (
      <div onKeyDown={this.handleKeyDown}>
        <Grid
          style={{ height: '300px' }}
          data={this.state.data.map(
            (item) => ({ ...item, selected: item.ProductID === this.state.selectedItem.ProductID }))
          }
          selectedField="selected"
          onRowClick={this.handleRowClick}
        >
          <Column field="ProductName" title="Product Name" width="300px" />
          <Column field="UnitsInStock" title="Units In Stock" />
          <Column field="UnitsOnOrder" title="Units On Order" />
          <Column field="ReorderLevel" title="Reorder Level" />
        </Grid>
      </div >
    );
  }
}

ReactDOM.render(
  <App />,
  document.querySelector('my-app')
);

Multiple Selection

The next example demonstrates how to achieve multiple selection only via the keyboard or via the keyboard and the mouse.

import React from "react";
import ReactDOM from "react-dom";
import { Grid, GridColumn as Column } from "@progress/kendo-react-grid";

import products from "./products.json";
import { Keys } from './keys.js';

class App extends React.Component {
    lastSelectedIndex = 0;
    direction = null;
    state = {
        data: products.map(dataItem => Object.assign({ selected: false }, dataItem))
    };

    rowClick = event => {
        let last = this.lastSelectedIndex;
        const data = [...this.state.data];
        const current = data.findIndex(
            dataItem => dataItem === event.dataItem
        );

        if (!event.nativeEvent.shiftKey) {
            this.lastSelectedIndex = last = current;
        }

        if (!event.nativeEvent.ctrlKey) {
            data.forEach(item => (item.selected = false));
        }
        const select = !event.dataItem.selected;
        for (let i = Math.min(last, current); i <= Math.max(last, current); i++) {
            data[i].selected = select;
        }
        this.setState({ data });
    };

    handleKeyDown = e => {
        let index = this.lastSelectedIndex;
        const direction = this.direction;
        if (e.nativeEvent.shiftKey) {
            if (e.keyCode === Keys.up && index > 0) {
                this.direction = "up";
                this.reverseSelected(index + (direction === "down" ? 0 : -1));
            } else if (e.keyCode === Keys.down && index < products.length - 1) {
                this.direction = "down";
                this.reverseSelected(index + (direction === "up" ? 0 : 1));
            }
        } else {
            if (e.keyCode === Keys.up && index > 0) {
                this.direction = "up";
                this.selectItem(index - 1, true);
            } else if (e.keyCode === Keys.down && index < products.length - 1) {
                this.direction = "down";
                this.selectItem(index + 1, true);
            }
        }
    };

    reverseSelected = (index) => {
        const data = [...this.state.data];
        data[index].selected = !data[index].selected;
        this.lastSelectedIndex = index;
        this.setState({ data });
    }

    selectItem = (index, value) => {
        const data = this.state.data.map((item) => ({ ...item, selected: false }));
        data[index].selected = value;
        this.lastSelectedIndex = index;
        this.setState({ data });
    }

    render() {
        return (
            <div onKeyDown={this.handleKeyDown}>
                <Grid
                    data={this.state.data}
                    style={{ height: "400px" }}
                    selectedField="selected"
                    onRowClick={this.rowClick}
                >
                    <Column field="ProductName" title="Product Name" width="300px" />
                    <Column field="UnitsInStock" title="Units In Stock" />
                    <Column field="UnitsOnOrder" title="Units On Order" />
                    <Column field="ReorderLevel" title="Reorder Level" />
                </Grid>
            </div>
        );
    }
}
ReactDOM.render(<App />, document.querySelector("my-app"));

Row Editing

The following example showcase how to implement row editing which can be operated only via the keyboard using the Enter, Esc and Tab keys.

import React from "react";
import ReactDOM from "react-dom";
import { Grid, GridColumn as Column } from "@progress/kendo-react-grid";

import data from "./products.json";
import { Keys } from './keys.js';

const products = data.map((p, i) => ({ ...p, index: i }));

class App extends React.Component {
    esc = false;
    state = {
        data: products,
        selectedItem: products[0],
        editItem: {}
    };
    table = undefined;

    handleKeyDown = e => {
        if (e.keyCode === Keys.esc) {
            this.setState({ editItem: {} });
            this.table && this.table.focus();
        }

        if (e.target.nodeName === "TABLE") {
            if (e.keyCode === Keys.enter) {
                this.setState({ editItem: this.state.selectedItem });
            }
            if (e.keyCode === Keys.up || e.keyCode == Keys.dows) {
                e.preventDefault();
                // Ensure that the selected index in not outside of the dataset range
                const min = Math.min(this.state.selectedItem.index - 39 + e.keyCode, products.length - 1);
                const newSelectedIndex = Math.max(min, 0);
                this.setState({
                    selectedItem: products[newSelectedIndex]
                });
            }
        }
    };

    handleItemChange = event => {
        const data = this.state.data.map(item => {
            if (item.ProductID === event.dataItem.ProductID) {
                item[event.field] = event.value;
            }
            return item;
        });
        this.setState({ data: data });
    };

    rowRender = (trElement, rowProps) => {
        const trRef = e => {
            if (!e) {
                return;
            }
            if (rowProps.dataItem.ProductID === this.state.selectedItem.ProductID) {
                // Keep the scroll position sync with the selected row
                e.scrollIntoView({ block: "center", inline: "nearest" });
            }
        };
        return (
            <tr
                {...trElement.props}
                ref={trRef}
                onClick={e => this.setState({ selectedItem: rowProps.dataItem })}
                onDoubleClick={e => this.setState({ editItem: rowProps.dataItem })}
            >
                {trElement.props.children}
            </tr>
        );
    };

    render() {
        return (
            <div onKeyDown={this.handleKeyDown}>
                <Grid
                    rowRender={this.rowRender}
                    ref={e => {
                        if (e) {
                            e.vs.table.accessKey = "w";
                            this.table = e.vs.table;
                        }
                    }}
                    style={{ height: "300px" }}
                    data={this.state.data.map(item => ({
                        ...item,
                        selected: item.ProductID === this.state.selectedItem.ProductID,
                        inEdit: item.ProductID === this.state.editItem.ProductID
                    }))}
                    selectedField="selected"
                    editField="inEdit"
                    onItemChange={this.handleItemChange}
                >
                    <Column field="ProductName" title="Product Name" width="300px" />
                    <Column field="UnitsInStock" title="Units In Stock" editor="numeric" />
                    <Column field="UnitsOnOrder" title="Units On Order" editor="numeric" />
                    <Column field="ReorderLevel" title="Reorder Level" editor="numeric" />
                </Grid>
                <hr />
                <p>
                    (use Alt(or [Control] [Alt] for Mac)+<code>w</code> to focus the Grid, <code></code> and{" "}
                    <code></code> to navigate, <code>Enter</code> to edit a record and{" "}
                    <code>Esc</code> to stop editing)
                </p>
            </div>
        );
    }
}

ReactDOM.render(<App />, document.querySelector("my-app"));

In-cell Editing with Tab

This example showcase how to handle the tab key to provide faster editing experience with the keyboard.

import React from 'react';
import ReactDOM from 'react-dom';
import { Grid, GridColumn, GridToolbar } from '@progress/kendo-react-grid';
import { sampleProducts } from './sample-products.jsx';
import { Keys } from './keys.js';

const cloneProduct = (product) => ({ ...product });

const columns = [
    { field: 'ProductID', title: 'ID', editable: false },
    { field: 'ProductName', title: 'Product Name', editable: true },
    { field: 'UnitsInStock', title: 'Units In Stock', editable: true, editor: 'numeric' },
    { field: 'FirstOrderedOn', title: 'First Ordered', editable: true, editor: 'date', format: "{0:d}" }
]

class App extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            data: sampleProducts.map(cloneProduct),
            editField: undefined,
            changes: false
        };
    }
    rowRender = (trElement) => {
        const trProps = {
            ...trElement.props,
            onMouseDown: () => {
                this.preventExit = true;
                clearTimeout(this.preventExitTimeout);
                this.preventExitTimeout = setTimeout(() => { this.preventExit = undefined; });
            },
            onFocus: () => { clearTimeout(this.blurTimeout); }
        };
        return React.cloneElement(trElement, { ...trProps }, trElement.props.children);
    }

    cellRender = (tdElement, cellProps) => {
        const dataItem = cellProps.dataItem;
        const cellField = cellProps.field;
        const inEditField = this.state.editField;
        // Set in edit only the cell of the row that matches the field
        const additionalProps = cellField && cellField === inEditField && dataItem.inEdit === cellField ?
            {
                ref: (td) => {
                    const input = td && td.querySelector('input');
                    const activeElement = document.activeElement;

                    if (!input ||
                        !activeElement ||
                        input === activeElement ||
                        !activeElement.contains(input)) {
                        return;
                    }

                    if (input.type === 'checkbox') {
                        input.focus();
                    } else {
                        input.select();
                    }
                },
                onKeyDown: (e) => { this.handleKeyDown(dataItem, e); }
            } : {
                onClick: () => { this.enterEdit(dataItem, cellField); },
                onKeyDown: (e) => { this.handleKeyDown(dataItem, e); }
            };
        return React.cloneElement(tdElement, { ...tdElement.props, ...additionalProps }, tdElement.props.children);
    }

    handleKeyDown = (dataItem, e) => {
        if (e.keyCode === Keys.tab) {
            e.preventDefault();
            let currentEditFieldIndex = columns.findIndex(column =>  column.field === this.state.editField);
            let currentDataItemIndex = this.state.data.findIndex(item => item.ProductID === dataItem.ProductID);
            if (currentDataItemIndex === this.state.data.length - 1 && currentEditFieldIndex === columns.length - 1) {
                // Stop editing after editing on the last cell of the last row
                this.exitEdit();
                return;
            }
            // Go to the next row after editing a cell in the last column
            currentEditFieldIndex === columns.length - 1 ?
                this.enterEdit(this.state.data[currentDataItemIndex + 1], columns[1].field)
                : this.enterEdit(dataItem, columns[currentEditFieldIndex + 1].field)

        }
    }
    render() {
        return (
            <Grid
                style={{ height: '420px' }}
                data={this.state.data}
                rowHeight={50}
                onItemChange={this.itemChange}
                cellRender={this.cellRender}
                rowRender={this.rowRender}
                editField="inEdit"
            >
                <GridToolbar>
                    <button
                        title="Save Changes"
                        className="k-button"
                        onClick={this.saveChanges}
                        disabled={!this.state.changes}
                    >
                        Save Changes
                    </button>
                    <button
                        title="Cancel Changes"
                        className="k-button"
                        onClick={this.cancelChanges}
                        disabled={!this.state.changes}
                    >
                        Cancel Changes
                    </button>
                </GridToolbar>
                {columns.map((column, index) => {
                    return <GridColumn field={column.field} title={column.title} editable={column.editable} editor={column.editor} format={column.format} key={index} />
                })}
            </Grid>
        );
    }

    enterEdit = (dataItem, field) => {
        const data = this.state.data.map(item => ({
            ...item,
            inEdit: item.ProductID === dataItem.ProductID ? field : undefined
        })
        );
        this.setState({
            data,
            editField: field
        });
    }

    exitEdit = () => {
        const data = this.state.data.map(item => (
            { ...item, inEdit: undefined }
        ));

        this.setState({
            data,
            editField: undefined,
        });
    }

    saveChanges = () => {
        sampleProducts.splice(0, sampleProducts.length, ...this.state.data);
        this.setState({
            editField: undefined,
            changes: false
        });
    }

    cancelChanges = () => {
        this.setState({
            data: sampleProducts.map(cloneProduct),
            changes: false
        });
    }

    itemChange = (event) => {
        event.dataItem[event.field] = event.value;
        this.setState({
            changes: true
        });
    }

}

ReactDOM.render(
    <App />,
    document.querySelector('my-app')
);

Sorting

This example demonstrates how to focus the first column and sort different columns only using the keyboard.

import React from "react";
import ReactDOM from "react-dom";
import { Grid, GridColumn as Column } from "@progress/kendo-react-grid";

import { orderBy } from "@progress/kendo-data-query";

import products from "./products.json";

const headerCellRender = (cell, props) => {
    return (
        <a className="k-link" href="#" onClick={props.onClick} accessKey={props.title === "ID" ? "w" : null}> {/*Add the access key to the first column*/}
            {props.title}
            {props.children}
        </a>
    );
};

class App extends React.Component {
    state = {
        sort: [{ field: "ProductID", dir: "asc" }]
    };
    render() {
        return (
            <div>
                <Grid
                    style={{ height: "300px" }}
                    data={orderBy(products, this.state.sort)}
                    sortable
                    headerCellRender={headerCellRender}
                    sort={this.state.sort}
                    onSortChange={e => {
                        this.setState({
                            sort: e.sort
                        });
                    }}
                >
                    <Column field="ProductID" title="ID" />
                    <Column field="ProductName" title="Product Name" />
                    <Column field="UnitPrice" title="Unit Price" />
                </Grid>

                {/*Styles added here for visibility. Recommend to add them in the css file.*/}
                <style>{".k-grid th:focus-within {box-shadow: inset 0 0 0 2px rgba(0,0,0, 0.13) !important}"}</style>
                <hr />
                <p>
                    (use Alt(or [Control] [Alt] for Mac)+<code>w</code> to focus the first header cell,{" "}
                    <code>Enter</code> sorts the column, <code>Tab</code> and{" "}
                    <code>Shift + Tab</code> navigate between the header cells.)
                </p>
            </div>
        );
    }
}

ReactDOM.render(<App />, document.querySelector("my-app"));
 /