Keyboard Navigation for Cells

By default, keyboard navigation for cells is enabled in Infinite Table. When a cell is clicked, it shows a highlight that indicates it is the currently active cell. From that point onwards, the user can use the keyboard to navigate the table cells.

Click on a cell in the table and use the arrow keys to navigate around.

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' },
};

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

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>
    </>
  );
}

Note

  • Use ArrowUp and ArrowDown to navigate to the previous and next cells vertically.
  • Use ArrowLeft and ArrowRight to navigate to the previous and next cells horizontally.

  • Use PageUp and PageDown to navigate the cells vertically by pages (a page is considered equal to the visible row count).
  • Use Shift+PageUp and Shift+PageDown to navigate the cells horizontally by pages (a page is considered equal to the visible column count).

  • Use Home and End to navigate vertically to the cell above (that’s on the first row) and the cell below (that’s on the last row),
  • Use Shift+Home and Shift+End to navigate horizontally to the first and respectively last cell in the current row.

Keyboard navigation is controlled by the keyboardNavigation prop, which can be either "cell", "row" or false. Navigating table cells is the default behavior.

Using a default active cell

You can also specify an initial active cell, by using defaultActiveCellIndex=[2,4]. This tells the table that there should be a default active cell, namely the one at index 2,4 (row 2, so third row; column 4, so fifth column).

Note

The active cell should be an array of length 2, where the first number is the index of the row and the second number is the index of the column (both are zero-based).

This example starts with cell [2,0] already active.

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' },
};

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

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

Listening to active cell changes

You can easily listen to changes in the cell navigation by using the onActiveCellIndexChange callback.

Note

When you use controlled activeCellIndex, make sure to use onActiveCellIndexChange to update the prop value, as otherwise the component will not update on navigation

This example starts with cell [2,0] already active and uses onActiveCellIndexChange to update activeCellIndex.

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' },
};

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

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

Theming

There are a number of ways to customize 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.

Note

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.

There are other CSS variables as well, that give you fined-tuned control over both the border and background color for the active cell, if you don’t want to use the above three variables to propagate the same color across both border and background.

  • --infinite-active-cell-background - the background color. If you use this, you need to set opacity yourself.
  • --infinite-active-cell-border - border configuration (eg:2px solid magenta). If you use this, it will not be propagated to the background color.
Theming active cell highlight

Use the color picker to configured the desired color for the active cell highlight

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 KeyboardNavigationForCells() {
  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>
    </>
  );
}