DataGrid Keyboard Navigation

By Infinite Table Adminยท

Using your keyboard to navigate around an app is crucial to moving fast and being productive.

With version 0.3.6 Infinite Table added keyboard navigation to your favorite React DataGrid component.

By default, navigation is enabled for table cells - that means, as soon as the user clicks a cell, it becomes active and from that point on-wards, the user can use arrow keys, page up/down and home/end keys to navigate.

Check out our documentation for keyboard navigation to see more demos and a complete reference guide.

Note

Pro tip: when in cell navigation mode, you can use the Shift key to navigate horizontally in combination with page up/down and home/end keys.

In the example below, click a table cell and then use arrow keys to see keyboard navigation in action

Keyboard navigation is enabled by default

Click any cell in the grid and start navigating around using arrow keys.

View Mode
Fork
import {
  InfiniteTable,
  DataSource,
  DataSourceData,
} from '@infinite-table/infinite-react';
import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react';
import * as React from 'react';

type Developer = {
  id: number;
  firstName: string;
  lastName: string;
  country: string;
  city: string;
  currency: string;
  preferredLanguage: string;
  stack: string;
  canDesign: 'yes' | 'no';
  hobby: string;
  salary: number;
  age: number;
};

const dataSource: DataSourceData<Developer> = () => {
  return fetch(
    'https://infinite-table.com/.netlify/functions/json-server' +
      `/developers1k-sql?`
  )
    .then((r) => r.json())
    .then((data: Developer[]) => data);
};

const columns: InfiniteTablePropColumns<Developer> = {
  preferredLanguage: { field: 'preferredLanguage' },
  country: { field: 'country' },
  salary: {
    field: 'salary',
    type: 'number',
  },
  age: { field: 'age' },
  canDesign: { field: 'canDesign' },
  firstName: { field: 'firstName' },
  stack: { field: 'stack' },
  id: { field: 'id' },
  hobby: { field: 'hobby' },
  city: { field: 'city' },
  currency: { field: 'currency' },
};

export default function KeyboardNavigationForCells() {
  return (
    <>
      <DataSource<Developer>
        primaryKey="id"
        data={dataSource}>
        <InfiniteTable<Developer>
          // keyboardNavigation="cell" is the default, so no need to specify it
          columns={columns}
        />
      </DataSource>
    </>
  );
}

Another nice feature of keyboard navigation for cells is that you can specify a default active cell - you do so by using defaultActiveCell=[2,0] - meaning the cell on row 2 and column 0 should be active initially.

Default cell selection

In this example, the cell at position [1, 1] (so second row and second column) is selected by default.

View Mode
Fork
import {
  InfiniteTable,
  DataSource,
  DataSourceData,
} from '@infinite-table/infinite-react';
import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react';
import * as React from 'react';

type Developer = {
  id: number;
  firstName: string;
  lastName: string;
  country: string;
  city: string;
  currency: string;
  preferredLanguage: string;
  stack: string;
  canDesign: 'yes' | 'no';
  hobby: string;
  salary: number;
  age: number;
};

const dataSource: DataSourceData<Developer> = () => {
  return fetch(
    'https://infinite-table.com/.netlify/functions/json-server' + `/developers100-sql`
  )
    .then((r) => r.json())
    .then((data: Developer[]) => data);
};

const columns: InfiniteTablePropColumns<Developer> = {
  preferredLanguage: { field: 'preferredLanguage' },
  country: { field: 'country' },
  salary: {
    field: 'salary',
    type: 'number',
  },
  age: { field: 'age' },
  canDesign: { field: 'canDesign' },
  firstName: { field: 'firstName' },
  stack: { field: 'stack' },
  id: { field: 'id' },
  hobby: { field: 'hobby' },
  city: { field: 'city' },
  currency: { field: 'currency' },
};

export default function KeyboardNavigationForCells() {
  return (
    <>
      <DataSource<Developer>
        primaryKey="id"
        data={dataSource}>
        <InfiniteTable<Developer>
          defaultActiveCellIndex={[1, 1]}
          columns={columns}
        />
      </DataSource>
    </>
  );
}

Besides cell navigation, row navigation is also available. Switch to row navigation mode by specifying keyboardNavigation="row" - the rest is similar: user clicks a row, which becomes the active row. Using arrow keys, page up/down and home/end works as expected.

Having a default row set as active is also possible, via defaultActiveRowIndex={2} - this means the row at index 2 should be initially rendered as active.

Keyboard navigation for rows with default selection

In this example, keyboard navigation for rows is enabled, with row at index 2 being active by default.

View Mode
Fork
import {
  InfiniteTable,
  DataSource,
  DataSourceData,
} from '@infinite-table/infinite-react';
import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react';
import * as React from 'react';

type Developer = {
  id: number;
  firstName: string;
  lastName: string;
  country: string;
  city: string;
  currency: string;
  preferredLanguage: string;
  stack: string;
  canDesign: 'yes' | 'no';
  hobby: string;
  salary: number;
  age: number;
};

const dataSource: DataSourceData<Developer> = () => {
  return fetch(
    'https://infinite-table.com/.netlify/functions/json-server' + `/developers1k-sql`
  )
    .then((r) => r.json())
    .then((data: Developer[]) => data);
};
const columns: InfiniteTablePropColumns<Developer> = {
  preferredLanguage: { field: 'preferredLanguage' },
  country: { field: 'country' },
  salary: {
    field: 'salary',
    type: 'number',
  },
  age: { field: 'age' },
  canDesign: { field: 'canDesign' },
  firstName: { field: 'firstName' },
  stack: { field: 'stack' },
  id: { field: 'id' },
  hobby: { field: 'hobby' },
  city: { field: 'city' },
  currency: { field: 'currency' },
};

export default function KeyboardNavigationForRows() {
  return (
    <>
      <DataSource<Developer>
        primaryKey="id"
        data={dataSource}>
        <InfiniteTable<Developer>
          columns={columns}
          keyboardNavigation="row"
          defaultActiveRowIndex={2}
        />
      </DataSource>
    </>
  );
}

Controlling active row/cell

Both cell and row navigation can be used as React uncontrolled and controlled behaviors.

In the controlled version, you have to use onActiveCellIndexChange (or onActiveRowIndexChange) to respond to navigation changes and update the corresponding index.

The example below demoes controlled cell navigation - initially starting with no active cell, and it updates the active cell as a result to user changes. This means you as a developer are responsible for updating the value when needed, as you no longer wish to leave this update to happen internally in the table. This makes controlled behavior excellent for advanced use-cases when you want to implement custom navigation logic.

Controlled cell navigation
View Mode
Fork
import {
  InfiniteTable,
  DataSource,
  DataSourceData,
} from '@infinite-table/infinite-react';
import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react';
import * as React from 'react';

type Developer = {
  id: number;
  firstName: string;
  lastName: string;
  country: string;
  city: string;
  currency: string;
  preferredLanguage: string;
  stack: string;
  canDesign: 'yes' | 'no';
  hobby: string;
  salary: number;
  age: number;
};

const dataSource: DataSourceData<Developer> = () => {
  return fetch(
    'https://infinite-table.com/.netlify/functions/json-server' + `/developers1k-sql`
  )
    .then((r) => r.json())
    .then((data: Developer[]) => data);
};

const columns: InfiniteTablePropColumns<Developer> = {
  preferredLanguage: { field: 'preferredLanguage' },
  country: { field: 'country' },
  salary: {
    field: 'salary',
    type: 'number',
  },
  age: { field: 'age' },
  canDesign: { field: 'canDesign' },
  firstName: { field: 'firstName' },
  stack: { field: 'stack' },
  id: { field: 'id' },
  hobby: { field: 'hobby' },
  city: { field: 'city' },
  currency: { field: 'currency' },
};

export default function KeyboardNavigationForCells() {
  const [activeCellIndex, setActiveCellIndex] =
    React.useState<[number, number] | null>(null);
  return (
    <>
      <div
        style={{
          color: 'var(--infinite-cell-color)',
        }}>
        Current active cell:{' '}
        {activeCellIndex ? (
          <>
            {activeCellIndex?.[0]}, {activeCellIndex?.[1]}
          </>
        ) : (
          'none'
        )}
        .
        <div>
          <button onClick={() => setActiveCellIndex(null)}>
            clear active cell
          </button>
        </div>
      </div>
      <DataSource<Developer>
        primaryKey="id"
        data={dataSource}>
        <InfiniteTable<Developer>
          activeCellIndex={activeCellIndex}
          onActiveCellIndexChange={setActiveCellIndex}
          columns={columns}
        />
      </DataSource>
    </>
  );
}

Turning off keyboard navigation

Disabling keyboard navigation is done by specifying keyboardNavigation=false - this ensures the user can no longer interact with the table rows or cells via the keyboard.

Theming

There are a number of ways to customise the appearance of the element that highlights the active cell.

The easiest is to override those three CSS variables:

  • --infinite-active-cell-border-color--r - the red component of the border color
  • --infinite-active-cell-border-color--g - the green component of the border color
  • --infinite-active-cell-border-color--b - the blue component of the border color

The initial values for those are 77, 149 and215 respectively, so the border color is rgb(77, 149, 215). In addition, the background color of the active cell highlight element is set to the same color as the border color (computed based on the above r, g and b variables), but with an opacity of 0.25, configured via the --infinite-active-cell-background-alpha CSS variable.

When the table is not focused, the opacity for the background color is set to 0.1, which is the default value of the --infinite-active-cell-background-alpha--table-unfocused CSS variable.

To summarize, use:

  • --infinite-active-cell-border-color--r
  • --infinite-active-cell-border-color--g
  • --infinite-active-cell-border-color--b to control border and background color of the active cell highlight element.

See below a demo on how easy it is to customize the colors for the active element highlighter

Theming keyboard navigation
View Mode
Fork
import {
  InfiniteTable,
  DataSource,
  DataSourceData,
  debounce,
} from '@infinite-table/infinite-react';
import type { InfiniteTablePropColumns } from '@infinite-table/infinite-react';
import * as React from 'react';
import { useState } from 'react';
import { useMemo } from 'react';
import { HTMLProps } from 'react';
import { ChangeEvent } from 'react';

type Developer = {
  id: number;
  firstName: string;
  lastName: string;
  country: string;
  city: string;
  currency: string;
  preferredLanguage: string;
  stack: string;
  canDesign: 'yes' | 'no';
  hobby: string;
  salary: number;
  age: number;
};

const dataSource: DataSourceData<Developer> = () => {
  return fetch(
    'https://infinite-table.com/.netlify/functions/json-server' + `/developers1k-sql`
  )
    .then((r) => r.json())
    .then((data: Developer[]) => data);
};
const columns: InfiniteTablePropColumns<Developer> = {
  preferredLanguage: { field: 'preferredLanguage' },
  country: { field: 'country' },
  salary: {
    field: 'salary',
    type: 'number',
  },
  age: { field: 'age' },
  canDesign: { field: 'canDesign' },
  firstName: { field: 'firstName' },
  stack: { field: 'stack' },
  id: { field: 'id' },
  hobby: { field: 'hobby' },
  city: { field: 'city' },
  currency: { field: 'currency' },
};

const domProps = { style: { height: '90vh' } };

const rgb = {
  r: 77,
  g: 149,
  b: 215,
};
const defaultColor = `#${rgb.r.toString(
  16
)}${rgb.g.toString(16)}${rgb.b.toString(16)}`;

export default function KeyboardNavigationTheming() {
  const [color, setColor] = useState({
    ...rgb,
  });

  const domProps = useMemo(() => {
    return {
      style: {
        '--infinite-active-cell-border-color--r': color.r,
        '--infinite-active-cell-border-color--g': color.g,
        '--infinite-active-cell-border-color--b': color.b,
        // for the same of the example being more obvious,
        // make the opacity of the unfocused table same as the one used on focus
        '--infinite-active-cell-background-alpha--table-unfocused':
          '0.25', // but this defaults to 0.1
      },
    } as HTMLProps<HTMLDivElement>;
  }, [color]);

  const onChange = useMemo(() => {
    const onColorChange = (
      event: ChangeEvent<HTMLInputElement>
    ) => {
      const color = event.target.value;

      const r = parseInt(color.substr(1, 2), 16);
      const g = parseInt(color.substr(3, 2), 16);
      const b = parseInt(color.substr(5, 2), 16);

      setColor({
        r,
        g,
        b,
      });
    };
    return debounce(onColorChange, { wait: 200 });
  }, []);

  return (
    <>
      <div
        style={{
          color: 'var(--infinite-cell-color)',
        }}>
        Select color{' '}
        <input
          type="color"
          onChange={onChange}
          defaultValue={defaultColor}
        />
      </div>
      <DataSource<Developer>
        primaryKey="id"
        data={dataSource}>
        <InfiniteTable<Developer>
          defaultActiveCellIndex={[5, 0]}
          domProps={domProps}
          columns={columns}
        />
      </DataSource>
    </>
  );
}

Enjoy

Thanks for following us thus far - we appreciate feedback, so please to let us know if keyboard navigation is useful for you or how we could make it better.

Please follow us @get_infinite to keep up-to-date with news about the product. Thank you.