react-sunbeam
TypeScript icon, indicating that this package has built-in type declarations

1.0.5 • Public • Published
Sunbeam Logo

react-sunbeam

Spatial navigation and focus management system for React apps

Test Status Netlify Status npm version

Installation

npm install react-sunbeam

or

yarn add react-sunbeam

Usage

// app.js
import React, { useCallback, useEffect } from "react"
import { Focusable, SunbeamProvider, FocusManager, useSunbeam } from "react-sunbeam"
import { FocusableCard } from "./FocusableCard"
 
function App() {
    const { setFocus, moveFocusLeft, moveFocusRight, moveFocusUp, moveFocusDown } = useSunbeam()
 
    const onKeyDown = useCallback(
        (event) => {
            if (!(event instanceof KeyboardEvent)) return
            switch (event.key) {
                case "ArrowRight":
                    moveFocusRight()
                    return
                case "ArrowLeft":
                    moveFocusLeft()
                    return
                case "ArrowUp":
                    moveFocusUp()
                    return
                case "ArrowDown":
                    moveFocusDown()
                    return
            }
        },
        [focusManager]
    )
 
    useEffect(() => {
        document.addEventListener("keydown", onKeyDown)
        return () => document.removeEventListener("keydown", onKeyDown)
    }, [onKeyDown])
 
    return (
        <div>
            <FocusableCard focusKey="card-1" />
            <Focusable focusKey="item1">
                {({ focused }) => <div>{focused ? "I am focused" : "I am not focused"}</div>}
            </Focusable>
            <Focusable focusKey="menuContainer">
                <div>
                    <Focusable focusKey="menuItem1">
                        {({ focused }) => (
                            <div style={{ backgroundColor: focused ? "salmon" : "deepskyblue" }}>
                                You can nest Focusables
                            </div>
                        )}
                    </Focusable>
                    <Focusable focusKey="menuItem2">
                        {({ focused }) => (
                            <div style={{ backgroundColor: focused ? "salmon" : "deepskyblue" }}>
                                In this case Sunbeam will try to find the best candidate for the focus within the common
                                Focusable parent first
                            </div>
                        )}
                    </Focusable>
                </div>
            </Focusable>
            <Focusable focusKey="item2">
                {({ focused, path }) => (
                    <div style={{ textDecoration: focused ? "underline" : "none" }} onClick={() => setFocus(path)}>
                        You can also programmatically change focus by using `setFocus` API
                    </div>
                )}
            </Focusable>
        </div>
    )
}
 
const focusManager = new FocusManager({
    initialFocusPath: ["menuContainer", "menuItem2"],
})
 
render(
    <SunbeamProvider focusManager={focusManager}>
        <App />
    </SunbeamProvider>,
    document.getElementById("app")
)
 
// FocusableCard.js
import React, { useRef } from "react"
import { useFocusable } from "react-sunbeam"
 
export function FocusableCard({ focusKey }) {
    const elementRef = useRef(null)
    const { focused } = useFocusable({ focusKey, elementRef })
 
    return (
        <div ref={elementRef} style={{ border: focused ? "1px solid salmon" : "1px solid transparent" }}>
            Card
        </div>
    )
}

API

FocusManager

FocusManager is responsible for maintaining the tree of focusable nodes and updating the currently focused node. Usually, your app will only have a single instance of it which you pass to a <SunbeamProvider> component

moveLeft() / moveRight() / moveUp() / moveDown()

These are the most important methods of FocusManager. They move focus to the nearest focusable node in the corresponding direction. You can call these methods in response to any events you want: key presses, game controller button presses, scroll events, etc.

setFocus(path: string[]): void

Immediately makes focused the focusable node with the given path.

Example

import { render } from "react-dom"
import { SunbeamProvider, FocusManager } from "react-sunbeam"
import { App } from "./App"
 
const focusManager = new FocusManager({
    initialFocusPath: ["menuContainer", "menuItem1"],
})
 
// Use arrow key presses to control focus.
document.addEventListener("keydown", (event) => {
    switch (event.key) {
        case "ArrowRight":
            focusManager.moveRight()
            return
        case "ArrowLeft":
            focusManager.moveLeft()
            return
        case "ArrowUp":
            focusManager.moveUp()
            return
        case "ArrowDown":
            focusManager.moveDown()
            return
        case "Backspace":
            focusManager.setFocus(["carousel", "item-1"])
            return
    }
})
 
render(
    <SunbeamProvider focusManager={focusManager}>
        <App />
    </SunbeamProvider>,
    document.getElementById("app")
)

SunbeamProvider

Focusable

useSunbeam(): { setFocus(focusPath: readonly string[]): void; moveFocusRight(): void; moveFocusLeft(): void; moveFocusUp(): void; moveFocusDown(): void; }

This hook provides access to some public methods of FocusManager inside the React components. It expects <SunbeamProvider> to be present in the tree, otherwise it returns no-op versions of the methods

useFocusable(options: Options): { focused: boolean; path: string[] }

This hook makes the enclosing component focusable. It can only be used for the "leaf" focusables so the component that uses it cannot have other focusable children. If you need the latter behaviour use <Focusable> instead.

Options

type Options = {
    // Ref object pointing to a DOM Element or any other object that has 'getBoundingClientRect(): ClientRect' method
    elementRef: React.RefObject<{ getBoundingClientRect(): ClientRect }>
    // If set to false the node is ignored in focus management process. Default: true
    focusable?: boolean
    focusKey?: string
    // If set prevents the node from losing focus when navigating in the given directions
    lock?: Direction | Direction[]
    onKeyPress?: (event: KeyboardEvent) => void
    onFocus?: (event: { focusablePath: readonly string[]; getBoundingClientRect: () => ClientRect }) => void
    onBlur?: (event: { focusablePath: readonly string[]; getBoundingClientRect: () => ClientRect }) => void
}

Example

import React from "react"
import { useFocusable } from "react-sunbeam"
 
export function FocusableButton({ children }) {
    const ref = React.useRef<HTMLButtonElement>(null)
    const { focused } = useFocusable({
        elementRef: ref,
        onKeyPress(event) {
            if (event.key === "Enter") {
                event.stopPropagation()
                alert("Click!")
            }
        },
    })
 
    return (
        <button style={{ border: focused ? "2px solid black" : "2px solid transparent" }} ref={ref}>
            {children}
        </button>
    )
}

KeyPressManager

TODO

Readme

Keywords

none

Package Sidebar

Install

npm i react-sunbeam

Weekly Downloads

33

Version

1.0.5

License

MIT

Unpacked Size

861 kB

Total Files

188

Last publish

Collaborators

  • vovacodes