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-arrow-navigation

v1.0.1

Published

A react library for managing navigation with arrow keys

Downloads

35

Readme

react-arrow-navigation

A react library for managing navigation with arrow keys

Install

npm install --save react-arrow-navigation

Usage

Mount ArrowNavigation in your app, and register components that receive navigation state with the useArrowNavigation hook. It takes two arguments: an x and y navigation index.

import React from 'react'
import { ArrowNavigation, useArrowNavigation } from 'react-arrow-navigation'

const NavigationChild = ({ xIndex, yIndex }) => {
    const { selected, active } = useArrowNavigation(xIndex, yIndex)
    return (
        <div className={selected && active ? 'child selected' : 'child'}>
            {`Index [${xIndex}, ${yIndex}]`}
        </div>
    )
}

const MyApp = () => (
    <ArrowNavigation className="nav">
        <NavigationChild xIndex={0} yIndex={0} />
        <NavigationChild xIndex={0} yIndex={1} />
        <NavigationChild xIndex={0} yIndex={2} />
    </ArrowNavigation>
)

Live example here

useArrowNavigation returns two values that represent the navigation state:

  • selected: whether this index is currently selected
  • active: whether this ArrowNavigation component is currently focused

This gives you flexibilty when implementing navigable components. For example:

  • A button group: you might want the button to stay in a selected state even when the button group is not focused
  • A drop down menu: the menu items would only need to be in a selected state when the dropdown is focused/open

The select callback

You may also want to be able to update the navigation state when a component is clicked. For example:

  1. A user navigates through a button group with the arrow keys
  2. Then they click on a button in the group
  3. Then they continue navigating with the arrow keys

When this happens you want the navigation index to be updated on click, so when they use the arrow keys again the index is correct.

To achieve this we can use the select callback provided by useArrowNavigation:

const ButtonGroupButton = ({ xIndex }) => {
    const { selected, select } = useArrowNavigation(xIndex, 0)
    return (
        <div
            className={selected ? 'bg-button selected' : 'bg-button'}
            onClick={() => {
                // Click handler logic goes here
                select() // Then call the `select` callback
            }}
        >
            {`Option ${xIndex + 1}`}
        </div>
    )
}

Live example here

ArrowNavigation focus state

To toggle the active state, ArrowNavigation returns a containing <div>, and listens to the onFocus, and onBlur events. It updates active to be true when the <div> is focused, and false when it is not. It also switches active to false when the Escape key is pressed.

Additional props passed to ArrowNavigation are spread on to the <div>. This includes support for a ref prop, implemented with React.forwardRef.

If you want to opt-out and manage the active state yourself, use BaseArrowNavigation. Its active state is determined by its active prop. It does not insert a containing <div>.

Managing the focus state of navigable components

Sometimes you will want navigable components to be focused when they are selected. There are behaviors built into browsers you might want to leverage (onClick being fired when the user hits the Enter key), and it's also good for acessibility: screen readers rely on the focus state.

To enable this there is a hook: useArrowNavigationWithFocusState. It returns an additional value: focusProps, which is spread onto the navigable component. focusProps is comprised of tabIndex, onClick, and ref. In more complex cases you may want to access these props directly: e.g. you need to do something else in the click handler.

Here is a dropdown menu implemented with it:

import React, { useState, useRef, useEffect } from 'react'
import { ArrowNavigation, useArrowNavigationWithFocusState } from 'react-arrow-navigation'

const DropdownMenuItem = ({ index, label, closeMenu }) => {
    const {
        focusProps: { ref, tabIndex, onClick },
    } = useArrowNavigationWithFocusState(0, index)

    return (
        <button
            className="menu-item"
            ref={ref}
            tabIndex={tabIndex}
            onClick={() => {
                onClick()
                alert(`Clicked: "${label}"`)
                closeMenu()
            }}
        >
            {label}
        </button>
    )
}

const DropdownMenu = ({ label, itemLabels }) => {
    const [open, setOpen] = useState(false)
    const navRef = useRef()

    useEffect(() => {
        if (open) {
            navRef.current && navRef.current.focus()
        }
    }, [open])

    return (
        <div>
            <button
                className="dropdown-button"
                onClick={() => {
                    setOpen(!open)
                    navRef.current && navRef.current.focus()
                }}
            >
                {open ? 'Close the menu' : 'Open the menu'}
            </button>
            {open && (
                <ArrowNavigation className="menu" ref={navRef} initialIndex={[0, 0]}>
                    {itemLabels.map((itemLabel, index) => (
                        <DropdownMenuItem
                            index={index}
                            label={itemLabel}
                            closeMenu={() => setOpen(false)}
                            key={index}
                        />
                    ))}
                </ArrowNavigation>
            )}
        </div>
    )
}

const MyApp = () => (
    <DropdownMenu itemLabels={['Navigate through', 'The menu items', 'With arrow keys']} />
)

Live example here

useArrowNavigationWithFocusState has to interact with the focus state of ArrowNavigation, so it is not compatible with BaseArrowNavigation.

Other things to be aware of

  • useArrowNavigation retrieves the navigation state from ArrowNavigation using the context API, so navigable components can be arbitrarly nested
  • The navigation indexes can have holes. For example: if the y index for each navigable component is 0, and the x indexes are 0, 2, 3, and 4, it will navigate from 0 to 2 when you hit the right arrow key. This can be useful when you need to dynamically pull a navigable component from the navigation index, e.g. a menu item that is currently disabled.

API

ArrowNavigation

ArrowNavigation manages the selection state of the navigation indexes, and its active state based on if it is focused.

Props:

  • children: React.Node

  • initialIndex: [number, number] (optional)

    An index to be selected when ArrowNavigation is first focused

  • mode: 'roundTheWorld' | 'continuous' | 'bounded' (optional)

    The edge mode of the navigation: what happens when a user goes over the edges of the x and y indexes. The options are:

    1. 'roundTheWorld' (this is the default)

      round the world edge mode diagram

    2. 'continuous'

      continuous edge mode diagram

    3. 'bounded'

      bounded edge mode diagram

  • reInitOnDeactivate: true | false (optional)

    Resets the indexes when ArrowNavigation deactivates

  • ...divProps

    All other props passed to ArrowNavigation are passed onto the div it returns. This includes support for the ref prop.

useArrowNavigation(x: number, y: number)

The useArrowNavigation hook takes two arguments: an x and y navigation index.

Returned values:

  • selected: true | false

    Whether this index is currently selected

  • active: true | false

    Whether the navigation component (ArrowNavigation or BaseArrowNavigation) is active. Active means it is responding to keypresses.

  • select: () => void

    A callback that updates the selected index to this one

useArrowNavigationWithFocusState(x: number, y: number)

The useArrowNavigation hook takes two arguments: an x and y navigation index. It is not compatible with BaseArrowNavigation.

Returned values:

  • selected, active, and select are the same as for useArrowNavigation

  • focusProps: { ref, tabIndex, onClick }

    focusProps should be spread onto the navigable component. They will:

    • Set the tabIndex to 0 if it is selected and -1 otherwise
    • Focus the component when its index is selected, and active is true
    • Set the selected index to this one on click

    In complex cases you may want to access these props directly, e.g. if you need to do another thing in the component's click handler:

        onClick={() => {
            // Click handler logic goes here
            onClick() // Then call the onClick callback from `focusProps`
        }}

BaseArrowNavigation

BaseArrowNavigation works in a similar way to ArrowNavigation, except it does not return a containing div, and does not manage it's active state. This is now passed in with a prop.

Props:

  • children, initialIndex, mode, and reInitOnDeactivate are the same as for ArrowNavigation

  • active: true | false

    Whether the component should update the selected index in response to keypresses

License

MIT © Jack Aldridge