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

grid-navigator

v1.3.4

Published

no-fuss accessible keyboard navigation handling to a grid component

Downloads

8

Readme

Grid Navigator

A no-fuss accessible keyboard navigation handling for a flexible grid component. This is not a spreadsheet-like grid or table, but provides keyboard navigation appropriate for one. If you have a grid (as in a CSS grid) that the user will want to navigate between items, this provides a simple way to provide keyboard navigation following W3C recommendations.

  • Provides "standard" W3C keyboard navigation
  • Simple integration with any App, including React; it requires a single delegation of keydown events.
  • Responsive. Handles row size changes automatically
  • Dynamic. Handles dynamic lists of elements efficiently
  • Rigorously tested
  • Customizable

For our users' sake, it's convenient and recommended for data grids to have keyboard navigation. The W3 recommends certain behaviors, but these aren't built in to HTML-- nor are they easily obtainable. You have to write Javascript and switch statements. Once you start writing this, you realize the code gets complicated-- or at least tedious. With any sort of responsive layout, moving the the "next row" is a dynamic calculation, not just an addition, as is finding the first or last element of the row.

This package handles all that with a simple TypeScript package.

Demo

A proper demo will come, but in the meantime, see https://amp-what.com

Steps to integrate

Node.js CI

To add keyboard navigation to a grid of elements, images or tiles, here are the steps:

1. Include the package in your project, eg.

npm add --save grid-navigator

(Yarn and Typescript also work.)

2. Create an "elements provider".

Your code must provide a function to tell the Grid Navigator which elements the user is navigating. This is specified as a function that returns a NodeList of elements. (There's no performance penalty if this list of elements never changes.) Here is a simple example function:

const elementsProvider = () => document.getElementsByTagName('li')

This will be called lazily when needed.

3. Create a "select callback".

You are also responsible for reflecting the focus, or "select" status, to the user. This is done by implementing a callback function called selectCallback. The W3C recommends using tabIndex values of '-1' and '0' to keep track of this, but it's up to you. This callback function will receive two arguments: the element, and a boolean about whether the element is being selected or not. As the user moves from one element to the next, this will function will be called twice-- once for leaving the old element, and once for entering the new one:

const selectCallback = (e, selectNow) => {
  if (selectNow)     // entering/focusing
    e.tabindex = 0
  else               // leaving/blur
    e.tabindex = -1
}

Typically you will use some accompanying CSS, such as:

[role="grid"] [tabindex="0"]:focus {
  outline: #005a9c;
  outline-style: dotted;
  outline-width: 3px;
}

4. Create a navigator:

To stitch it all together, you need a GridNavigator object, fully configured.

import { GridNavigator } from 'grid-navigator'

const myNav = new GridNavigator({ elementProvider, selectCallback })

5. Call the handler

The final required step is to delegate keyboard events to the GridNavigator. From the keydown handler of your grid container, you need to add a keydown handler:

const myNav = new GridNavigator({ elementProvider, selectCallback })
const grid  = document.getElementById('grid')

grid.addEventHandler('keydown', (e) => {
  if (myNav.onKeyDown(e)) return // `onKeyDown` returns TRUE if it handles the event 

  // do you normal stuff
...
})

This keydown handler will look for applicable keys, and if present, call the selectCallback appropriately. It returns true if it handles the keyboard event, and false otherwise.

6. Handle dynamic lists (Optional)

The code uses the elementProvider to determine the elements being navigated. Once they are calculated, it assumes they are stable unless you tell it otherwise. To indicate that elements have changed, call myNav.markStale(), and GridNavigator will call elementProvider (along with other appropriate callbacks). Note, this calculation is done lazily, only during navigation, which means there is minimal overhead for a heavily dynamic grid.

7. Determine row sizes (Optional)

By default, rows widths are determine by looking at the position of elements on the page, and uses a simple heuristic to see where columns line up. This works well for responsive layouts, where the row width is determined within the CSS, and not readily available to the Javascript. If you have this information available in JS, or just want to calculate it yourself, provide a function columnCountCalculator: (elems: NodeListOf<E>) => number. It will be called whenever a layout change has been detected.

React Component

Here's a sketch of a hook:

const useGridNavigator = (gridRef, selectCallback) => {
  const [gridNav, setGridNav] = useState()
  useEffect(() => {
    if (gridRef.current) {
      const elementProvider = () => gridRef.current.getElementsByTagName('li')
      setGridNav(new GridNavigator({ elementProvider, selectCallback }))
      gridRef.current.addEventHandler('keydown', (e) => {
        if (gridNav.onKeyDown(e)) e.stopImmediatePropagation()
      })
    }
  }, [gridRef])
  
  useEffect(() => gridNav?.markStale())
}
...
const selectCallback = ...
useGridNavigator(myGridRef, selectCallback)

Key Bindings

Out of the box, the default key bindings are those suggested by the w3 documentation. These are specified within grid-navigator using a Javascript object, interpreted as a map:

export const DEFAULT_STANDARD: KeyToMoveOpMap = {
  'ArrowLeft':   'prev',
  'ArrowRight':  'next',
  'ArrowUp':     'up',
  'ArrowDown':   'down',
  'PageDown':    'pageDown',
  'PageUp':      'pageUp',
  'Home':        'startOfLine',
  'End':         'endOfLine',
  'Ctrl + Home': 'first',
  'Ctrl + End':  'last',
}

Supplementary Keyboard Map

You can amend this map with any of your own.

For convenience, a few supplementary key bindings are provided:

  • VI-ish
  • EMACS-ish
  • NUMPAD, for navigation using a numeric keypad. This is outside the standard recommendation, but convenient for some users.

To use any of these, import them and pass them in to the GridNavigator constructor:

import { KeyMaps } from 'grid-navigator'

myKeyMaps = [...KeyMaps.DEFAULT_STANDARD, ...KeyMaps.VI, ...KeyMaps.EMACS]

const myNav = new GridNavigator({
                                  keyMap: myKeyMaps,
                                  elementProvider,
                                  selectCallback
                                })

Overriding

The grid navigator accepts a keyMap property to so that you can control in fine detail how keystrokes are handled. As described above, this is a Javascript object used as a map from a key value to a "moveOp".

License

Contact me for licensing or consulting.

Development

See package.json.

To release, yarn publish

Credits

Copyright (c) 2021–2023 Andrew J. Peterson, NDP Software. All Rights Reserved.