react-grid-navigator
TypeScript icon, indicating that this package has built-in type declarations

1.2.26 • Public • Published

react-grid-navigator

A small library for keyboard focus-based navigation of a UI

Installation

Use the package manager npm to install.

npm install react-grid-navigator

Adding keyboard listeners

Firstly we need to hook keyboard events into the FocusEngine

import { FocusEngine } from "react-grid-navigator";

document.addEventListener("keydown", (event) => {
  event.preventDefault();
  switch (event.code) {
    case "ArrowUp":
      FocusEngine.onArrowUp();
      break;
    case "ArrowDown":
      FocusEngine.onArrowDown();
      break;
    case "ArrowLeft":
      FocusEngine.onArrowLeft();
      break;
    case "ArrowRight":
      FocusEngine.onArrowRight();
      break;
    case "Enter":
      FocusEngine.onEnter();
      break;
    default:
      break;
  }
});

Setting up context provider

This package uses context for state propegation. For this to work, you need to wrap the root element of your app in GridContext

import React from "react";

import RootComponent from './src/root'

import { GridContext } from 'react-grid-navigator'

export default function App () {

    return <GridContext>
    <RootComponent>
    </GridContext>

}

Setting up the grid

Example

First we need to define our grid layout by providing a nested array of rows and columns. This grid will mirror the layout of your UI.

Start by defining cell names on the grid. Each cell will have focusable indexes inside and focus will switch between the cells when a cell's limit is reached.

import { FocusEngine } from "react-grid-navigator";

FocusEngine.setGrid(
  [
    ["sidenav", "contentTop", "sidebar"],
    ["sidenav", "content", "sidebar"],
  ],
  "content" // <- starting cell
);

With the above grid, when the last focusable element of cell content is reached on the x-axis, focus will switch to the first element in cell sidebar.

Likewise, if the last focusable element of cell content is reached on the y axis, focus will be switched to the first element of the contentTop cell.

NOTE : The grid that you specify will determine what cells are navigatable to from relative cells

This is an example of where you'll be able to navigate from content to sidebar, but not from contentTop to sidebar.

import { FocusEngine } from "react-grid-navigator";

FocusEngine.setGrid(
  [
    ["sidenav", "contentTop"],
    ["sidenav", "content", "sidebar"],
  ],
  "content"
);

Defining a grid is best done before all UI components are mounted. So do this in a top-level component's componentDidMount method after binding listeners.

You can also pass a default coordinate for the starting cell by adding an additional argument to setGrid in the format of [x: number, y:number] which will be the default coordinate when the grid is set :

import { FocusEngine } from "react-grid-navigator";

FocusEngine.setGrid(
  [
    ["sidenav", "contentTop", "sidebar"],
    ["sidenav", "content", "sidebar"],
  ],
  "content",
  [0, 1] // <- Starting coordinate for cell `content`
);

Defining focusable components

Example1

FocusProvider is used to wrap elements that need to have a focusable state. All elements that need to be focusable need to be direct children of FocusProvider.

FocusProvider passes a focused prop into children if they should render in a focused state.

FocusProvider takes a cell param which indicates which cell in the grid the FocusProvider belongs to. The cell props needs to match the cell name defined in the above grid.

Direct children of FocusProvider each require a focusIndex prop which consists of an x and y coordinate. These coordinates are used to determine the position of the elements relative to each other.

Direct children of FocusProvider can also optionally take a focusAction props which binds to a function that gets fired when the enter key is pressed while that element has focus.

import { FocusProvider } from 'react-grid-navigator'

 <Content>
            <ContentWrapper>
              <SideNav>
                <FocusProvider cell={"sidenav"}>
                  <SidenavItem
                    focusAction={this.openModal}
                    focusIndex={[0, 0]}
                  />
                  <SidenavItem focusIndex={[0, 1]} />
                  <SidenavItem focusIndex={[0, 2]} />
                  <SidenavItem focusIndex={[0, 3]} />
                  <SidenavItem focusIndex={[0, 4]} />
                  <SidenavItem focusIndex={[0, 5]} />
                </FocusProvider>
              </SideNav>
              <ContentInner>
                <ContentTopbar>
                  <FocusProvider cell={"contentTop"}>
                    <TopbarItem focusIndex={[0, 0]} />
                    <TopbarItem focusIndex={[1, 0]} />
                  </FocusProvider>
                </ContentTopbar>
                <CardContainer>
                  <FocusProvider cell={"content"}>
                    <ContentCard focusIndex={[0, 0]} />
                    <ContentCard focusIndex={[1, 0]} />
                    <ContentCard focusIndex={[2, 0]} />
                  </FocusProvider>
                </CardContainer>
                <CardContainer>
                  <FocusProvider cell={"content"}>
                    <ContentCard focusIndex={[0, 1]} />
                    <ContentCard focusIndex={[1, 1]} />
                    <ContentCard focusIndex={[2, 1]} />
                    <ContentCard focusIndex={[3, 1]} />
                  </FocusProvider>
                </CardContainer>
                <CardContainer>
                  <FocusProvider cell={"content"}>
                    <ContentCard focusIndex={[0, 2]} />
                    <ContentCard focusIndex={[1, 2]} />
                  </FocusProvider>
                </CardContainer>
              </ContentInner>
            </ContentWrapper>
          </Content>
          <Sidebar>
            <FocusProvider cell={"sidebar"}>
              <SidebarItem focusIndex={[0, 0]} />
              <SidebarItem focusIndex={[0, 1]} />
              <SidebarItem focusIndex={[0, 2]} />
              <SidebarItem focusIndex={[0, 3]} />
              <SidebarItem focusIndex={[0, 4]} />
              <SidebarItem focusIndex={[0, 5]} />
              <SidebarItem focusIndex={[0, 6]} />
              <SidebarItem focusIndex={[0, 7]} />
            </FocusProvider>
          </Sidebar>

Note that multiple FocusProviders can belong to the same cell. If FocusProviders belong to the same cell, their inner focusable elements are all part of the same inner-grid, which means that their coordinates need to correspond with their positions relative to the focusable elements in other FocusProviders belonging to the same cell.

Useful methods

FocusProvider.setGrid(grid, initialFocusedCell)

FocusProvider.setGrid can be called at any time to provide a new grid. This is useful for when conditionally rendered content is used for modals and such. Here is an example for use with a modal:

import { FocusEngine } from "react-grid-navigator";

setDefaultGrid = () => {
  // <- this is called on component mount to setup full page grid
  FocusEngine.setGrid(
    [
      ["sidenav", "contentTop", "sidebar"],
      ["sidenav", "content", "sidebar"],
    ],
    "content"
  );
};

setModalGrid = () => {
  // <- this is called when modal opens to set a new grid for the modal
  FocusEngine.setGrid([["modalTop"], ["modalContent"]], "modalTop");
};

openModal = () => {
  // <- gets bound to the `focusAction` of the element we need to trigger the modal
  this.setState({ modalOpen: true });
  this.setModalGrid();
};
closeModal = () => {
  // <- gets bound to the `focusAction` of the element we need to close the modal
  this.setState({ modalOpen: false });
  this.setDefaultGrid();
};

Above we can see that we switch from the full page grid to a modal grid which consists of just 1 column with 2 rows.

Here is what the modal would look like:

{
  modalOpen && (
    <ModalContainer>
      <Modal>
        <ModalTopbar>
          <FocusProvider cell={"modalTop"}>
            <ModalExit focusAction={this.closeModal} focusIndex={[0, 0]} />
          </FocusProvider>
        </ModalTopbar>
        <ModalContent>
          <FocusProvider cell={"modalContent"}>
            <ModalAction focusIndex={[0, 0]} />
            <ModalAction focusIndex={[1, 0]} />
          </FocusProvider>
        </ModalContent>
      </Modal>
    </ModalContainer>
  );
}

From this example we can se that the modalTop cell contains the close modal button, and the modalContent cell contains the modal actions.

FocusEngine.addCellFocusEvent(cellName, function)

This method is fired when a specific cell receives focus. This is useful for cases where we want to trigger things like a sidebar opening

FocusEngine.overrideIndex(coords)

This method will override the current coordinates that the navigator is set to. Coordinates to be provided in [x, y] format.

FocusEngine.addCellBlurEvent(cellName, function)

This method is fired when a specific cell loses focus

FocusEngine.addCellIndexChangeEvent(cellName, function(newCoords))

This method is fired when focus changes inside a specific cell. On focus change, the specified function will be called with a param containing the new coordinates. The param passed into this function looks like:

{
  nX: 1, // new x coordinate
  nY: 0, // new y coordinate
  direction: "x" // direction of change - "x" | "-x" | "y" | "-y"
}

Contributing

Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.

License

MIT

Package Sidebar

Install

npm i react-grid-navigator

Weekly Downloads

59

Version

1.2.26

License

MIT

Unpacked Size

115 kB

Total Files

16

Last publish

Collaborators

  • rasbrits