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

use-rove

v2.1.0

Published

Customize roving tabindex behavior using a React hook

Downloads

8

Readme

use-rove

Customize roving tabindex behavior using a React hook.

Install

npm i use-rove

Usage

Assuming items is an array of objects (props) where each item has a unique key property.

import React from 'react';
import { useRove } from 'use-rove';

function MyList(props) {
  const { items = [] } = props || {};
  const keys = items.map(item => item.key);
  const getTargetProps = useRove(keys);
  return (
    <ul>
      { items.map({ key, ...item } =>
        <li { ...getTargetProps(key) } { ...item } />)
      }
    </ul>
  );
}

useRove(keys, options)

The hook requires the first argument (keys) to be an array of unique values that are expected to represent each item to be focused in tab order. This can be the id of each item or the index in loose usage.

// Not recommended, use unique id values instead.
const keys = items.map((_, index) => index);
const getTargetProps = useRove(keys);

The hook uses this list of keys to determine which item in the collection to focus.

getTargetProps(key)

This is the result of the hook; a function expected to be run for each unique key when rendering each item in the collection to be focused. The function will return the following props:

const {
  key, // Applies the given key to the element.
  ref, // Stores element reference to use .focus().
  onClick, // Click/tap event callback; setting this item as document.activeElement.
  onKeyDown, // Keyboard event callback; setting the next item as document.activeElement.
} = getTargetProps(key);

Merging props

You can also provide the component props into the argument instead as long as the key prop is included. The props used within the hook will be merged with the ones incoming. In other words, event listeners will fire and refs will be set. The only prop that will not be honored is tabIndex since this hook needs to control this.

function Item(props) {
  // props.key will need to exist here otherwise the return will just be the given props.

  return (
    <li { ...getTargetProps(props) }/>
  )
}
  • The hook will handle Home and End keys; jumping to the beginning and end of the collection respectedly.
  • The hook will jump to the child based on the starting character of the text content or aria-label of each child.

disabled items

Use the following pattern to handle disabled items.

function Listbox(props) {
  // Assume items is an array of props to build buttons, each with a unique key.
  const { items } = props;
  // Filter the list of items, looking for non-disabled keys.
  const activeKeys = items.filter(({ disabled }) => !disabled).map(({ key }) => key);
  // Setup the hook.
  const getTargetProps = useRove(activeKeys);
  // Render items.
  return items.map(item => <button { ...( item.disabled ? { tabIndex: -1, ...item } : getTargetProps(item) ) }/> );
}

The hook does not check if an item is disabled when setting the next item. It is merely selecting the next key in the given list of keys from the initial setup. Setting an item as disabled is fundamentally changing the state of the available items and will require a new list.

"visual" focus

In some cases, you might not want to move the DOM focus when traversing the children. Setting focus: false will avoid shifting the DOM focus. However, the tabIndex attribute will still cycle accordingly so you may target the element with a focused style.

ul [tabindex=0] {
  /* https://css-tricks.com/copy-the-browsers-native-focus-styles/ */
  outline: 5px auto Highlight;
  outline: 5px auto -webkit-focus-ring-color;
}

Options

These are options you can pass as an object to the second argument of useRove().

| Option | Value | Description | | ------ | ----- | ----------- | | start | String | A key (found in the list of keys) to set as the first item to receive focus on tab. Defaults to the first key in keys. | | loop | Boolean | Determines if the arrow keys can loop around past the ends of the list. Defaults to false. | | rtl | Boolean | Determines if the user is expecting to control the focus in a right-to-left language. This will flip the horizontal arrow keys. Defaults to false |. | orientation | 'horizontal' 'vertical' 'both' | Determines which keyboard arrow keys to trigger next focus. Defaults to 'both' | | focus | Boolean | Calls .focus() on the DOM element ref. Defaults to true |

Example

const getTargetProps = useRove(keys, {
  start: 'my-selected-item', // Start at the item with the key 'my-selected-item'.
  loop: true, // Do not stop at the ends when using arrow keys.
  rtl: false, // Use left-to-right navigational pattern.
  orientation: 'horizontal', // Do not listen for up or down arrow keys.
  focus: false, // Will not call `.focus()`, useful for showing a visual appearance of focus.
});

Prior art

The concept of "roving tabindex" is explained on web.dev by Rob Dodson. Some experiences require navigation to be performed by using the arrow keys instead of the tab key.

Much of this package is modeled after features from roving-ux-react except it was missing some options. The most important is setting the starting item. Many experiences will have a selected item on load which should be the item that receives first focus in some roving scenarios. I've also taken some features embedded within the original hook (like right-to-left support) and allowed them as options as well.

The source code is not a direct copy of the original. As one example, the way the next item to be focused is determined is unique to this implementation. However, the test suite is nearly identical (except coverage for the new options) and kudos to Dangoo for the testing approach of roving-ux-react.

I've also noticed that some other roving tabindex solutions avoided the parent container altogether by listening for onKeyDown on each item. I found this to be a good solution so the hook didn't need to control both the parent and the children. Also many solutions could not handle roving tabindex with a dynamic list (such as one found within a search result panel). This hook is also designed to handle this case as well.