react-arrow-navigation v1.0.1
react-arrow-navigation
A react library for managing navigation with arrow keys
Install
npm install --save react-arrow-navigationUsage
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>
)useArrowNavigation returns two values that represent the navigation state:
selected: whether this index is currently selectedactive: whether thisArrowNavigationcomponent 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:
- A user navigates through a button group with the arrow keys
- Then they click on a button in the group
- 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>
)
}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']} />
)useArrowNavigationWithFocusState has to interact with the focus state of ArrowNavigation, so it is not compatible with BaseArrowNavigation.
Other things to be aware of
useArrowNavigationretrieves the navigation state fromArrowNavigationusing 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.NodeinitialIndex:[number, number](optional)An index to be selected when
ArrowNavigationis first focusedmode:'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:
'roundTheWorld'(this is the default)
'continuous'
'bounded'
reInitOnDeactivate:true | false(optional)Resets the indexes when
ArrowNavigationdeactivates...divPropsAll other props passed to
ArrowNavigationare passed onto thedivit returns. This includes support for therefprop.
useArrowNavigation(x: number, y: number)
The useArrowNavigation hook takes two arguments: an x and y navigation index.
Returned values:
selected:true | falseWhether this index is currently selected
active:true | falseWhether the navigation component (
ArrowNavigationorBaseArrowNavigation) is active. Active means it is responding to keypresses.select:() => voidA 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, andselectare the same as foruseArrowNavigationfocusProps:{ ref, tabIndex, onClick }focusPropsshould be spread onto the navigable component. They will:- Set the
tabIndexto0if it is selected and-1otherwise - Focus the component when its index is selected, and
activeistrue - 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` }}- Set the
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, andreInitOnDeactivateare the same as forArrowNavigationactive:true | falseWhether the component should update the selected index in response to keypresses
License
MIT © Jack Aldridge