npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

react-grid-navigator

v1.2.26

Published

A keyboard based focus navigation engine for React

Downloads

18

Readme

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