0.1.0 • Published 5 months ago

@cky__/lrud v0.1.0

Weekly downloads
-
License
MIT
Repository
github
Last release
5 months ago

lrud

Travis build status npm version Code coverage

A React library for managing focus in LRUD applications.

Motivation

The native focus system that ships with browsers is one-dimensional: users may only move forward and back. Some applications (typically those that use remote controls or video game controllers) require a two dimensional focus system.

Because of this, it is up to the application to manage its own focus state. That's where this library comes in: it makes working with two dimensional focus seamless.

Installation

Install using npm:

npm install @please/lrud

or yarn:

yarn add @please/lrud

This library has the following peer dependencies:

Updates

  • feature: add config: enableFocusByHover, default Value: true, Focus when the mouse overed
  • fix: mouse clicked a node that is not the focused node
  • feature: add Interval between two clicks

Table of Contents

Guides

Basic Setup

Render the FocusRoot high up in your application's component tree.

import { FocusRoot } from '@please/lrud';

export default function App() {
  return (
    <FocusRoot>
      <AppContents />
    </FocusRoot>
  );
}

You may then use FocusNode components to create a focusable elements on the page.

import { FocusNode } from '@please/lrud';

export default function Profile() {
  return <FocusNode className="profile">Profile</FocusNode>;
}

This library automatically moves the focus between the FocusNodes as the user inputs LRUD commands on their keyboard or remote control.

This behavior can be configured through the props of the FocusNode component. To learn more about those props, refer to the API documentation below.

Getting Started

The recommended way to familiarize yourself with this library is to begin by looking at the examples. The examples do a great job at demonstrating the kinds of interfaces you can create with this library using little code.

Once you've checked out a few examples you should be in a better position to read through these API docs!

FAQ

What is LRUD?

LRUD is an acronym that stands for left-right-up-down, and it refers to the directional buttons typically found on remotes. In LRUD systems, input devices usually also have some kind of "submit" button, and, less commonly, a back button.

API Reference

This section of the documentation describes the library's named exports.

<FocusRoot />

Serves as the root node of a new focus hierarchy. There should only ever be one FocusRoot in each application.

All props are optional.

PropTypeDefault valueDescription
orientationstring'horizontal'Whether the children of the root node are arranged horizontally or vertically.
wrappingbooleanfalseSet to true for the navigation to wrap when the user reaches the start or end of the root's children.
pointerEventsbooleanfalseSet to true to enable pointer events. Read the guide.
import { FocusRoot } from '@please/lrud';

export default function App() {
  return (
    <FocusRoot orientation="vertical">
      <AppContents />
    </FocusRoot>
  );
}

<FocusNode />

A Component that represents a focusable node in your application.

All props are optional. Example usage appears beneath the props table.

PropTypeDefault valueDescription
propsFromNodefunctionA function you can supply to compute additional props to apply to the element. The function is passed one argument, the focus node.
classNamestringA class name to apply to this element.
focusedClassstring"isFocused"A class name that is applied when this element is focused.
focusedLeafClassstring"isFocusedLeaf"A class name that is applied this element is a focused leaf.
activeClassstring"isActive"A class name that is applied this element is active.
disabledClassstring"focusDisabled"A class name that is applied this element is disabled.
elementTypestring|elementType'div'The React element type to render. For instance, "img" or motion.div.
focusIdstring{unique_id}A unique identifier for this node. Specify this yourself for debugging purposes, or when you will need to manually set focus to the node.
orientationstring'horizontal'Whether the children of this node are arranged horizontally or vertically. Pass "vertical" for vertical lists.
wrappingbooleanfalseSet to true for the navigation to wrap when the user reaches the start or end of the children list. For grids this sets wrapping in both directions.
wrapGridHorizontalbooleanfalseSet to true for horizontal navigation in grids to wrap.
wrapGridVerticalbooleanfalseSet to true for vertical navigation in grids to wrap.
disabledbooleanfalseThis node will not receive focus when true.
isGridbooleanfalsePass true to make this a grid.
isTrapbooleanfalsePass true to make this a focus trap.
forgetTrapFocusHierarchybooleanfalsePass true and, if this node is a trap, it will not restore their previous focus hierarchy when becoming focused again.
onMountAssignFocusTostringA focus ID of a nested child to default focus to when this node mounts.
defaultFocusChildnumber|functionThe child index that should receive focus when focus is assigned to this focus node. Does not work with grids. We strongly recommend using useCallback when using the function form to avoid infinite render loops.
defaultFocusColumnnumber0The column index that should receive focus when focus is assigned to this focus node. Applies to grids only.
defaultFocusRownumber0The row index that should receive focus when focus is assigned to this focus node. Applies to grids only.
isExitingbooleanPass true to signal that this node is animating out. Useful for certain kinds of exit transitions.
onFocusedfunctionA function that is called when the node receives focus. Passed one argument, an FocusEvent.
onBlurredfunctionA function that is called when the node loses focus. Passed one argument, an FocusEvent.
onKeyfunctionA function that is called when the user presses any TV remote key while this element has focus. Passed one argument, an LRUDEvent.
onArrowfunctionA function that is called when the user presses a directional button. Passed one argument, an LRUDEvent.
onLeftfunctionA function that is called when the user presses the left button. Passed one argument, an LRUDEvent.
onUpfunctionA function that is called when the user presses the up button. Passed one argument, an LRUDEvent.
onDownfunctionA function that is called when the user presses the down button. Passed one argument, an LRUDEvent.
onRightfunctionA function that is called when the user presses the right button. Passed one argument, an LRUDEvent.
onSelectedfunctionA function that is called when the user pressed the select button. Passed one argument, an LRUDEvent.
onBackfunctionA function that is called when the user presses the back button. Passed one argument, an LRUDEvent.
onMovefunctionA function that is called when the focused child index of this node changes. Only called for nodes with children that are not grids. Passed one argument, a MoveEvent.
onGridMovefunctionA function that is called when the focused child index of this node changes. Only called for grids. Passed one argument, a GridMoveEvent.
childrenReact Node(s)Children of the Focus Node.
...restanyAll other props are applied to the underlying DOM node.
import { FocusNode } from '@please/lrud';

export default function Profile() {
  return (
    <FocusNode
      elementType="button"
      className="profileBtn"
      onSelected={({ node }) => {
        console.log('The user just selected this profile', node);
      }}>
      Profile
    </FocusNode>
  );
}

useFocusNodeById( focusId )

A Hook that returns the focus node with ID focusId. If the node does not exist, then null will be returned instead.

import { useFocusNodeById } from '@please/lrud';

export default function MyComponent() {
  const navFocusNode = useFocusNodeById('nav');

  console.log('Is the nav focused?', navFocusNode?.isFocused);
}

useLeafFocusedNode()

A Hook that returns the currently-in-focus leaf node.

import { useLeafFocusedNode } from '@please/lrud';

export default function MyComponent() {
  const currentFocusedNode = useLeafFocusedNode();

  console.log('Currently focused node', currentFocusedNode);
}

useActiveNode()

A Hook that returns the active focus node, or null if no node is active. As a reminder, the active node is whatever node was selected last (similar to how interactive elements in the DOM become active after being clicked).

import { useActiveNode } from '@please/lrud';

export default function MyComponent() {
  const activeNode = useActiveNode();

  console.log('The active node:', activeNode);
}

useSetFocus()

A Hook that returns the setFocus function, which allows you to imperatively set the focus.

This can be used to:

  • override the default navigation behavior of the library
  • focus modals or traps
  • exit traps
import { useSetFocus } from '@please/lrud';

export default function MyComponent() {
  const setFocus = useSetFocus();

  useEffect(() => {
    setFocus('nav');
  }, []);
}

useNodeEvents( focusId, events )

A Hook that allows you to tap into a focus nodes' focus lifecycle events. Use this hook when you need to respond to the focus lifecycle for a node that is not in your current component.

import { useNodeEvents } from '@please/lrud';

export default function MyComponent() {
  useNodeEvents('nav', {
    focus(navNode) {
      console.log('The nav node is focused', navNode);
    }

    blur(navNode) {
      console.log('The nav node is no longer focused', navNode);
    }
  });
}

Each callback receives a single argument, the focus node.

The available event keys are:

Event keyCalled when
focusthe focus node receives focus.
blurthe focus node loses focus.
activethe focus node becomes active.
inactivethe focus node is no longer active.
disabledthe focus node is set as disabled.
enabledthe focus node is enabled.

useFocusHierarchy()

A Hook that returns an array representing the focus hierarchy, which are the nodes that are currently focused. Each entry in the array is a focus node.

import { useFocusHierarchy } from '@please/lrud';

export default function MyComponent() {
  const focusHierarchy = useFocusHierarchy();

  console.log(focusHierarchy);
  // => [
  //   { focusId: 'root', ... },
  //   { focusId: 'homePage', ... },
  //   { focusId: 'mainNav', ... },
  // ]
}

useProcessKey()

A Hook that allows you to imperatively trigger LRUD key presses.

import { useProcessKey } from '@please/lrud';

function MyComponent() {
  const processKey = useProcessKey();

  useEffect(() => {
    // Imperatively trigger a down key press
    processKey.down();

    // Imperatively trigger a back key press
    processKey.select();

    // ...same, for the back button.
    processKey.back();
  }, []);
}

The full API is as follows.

interface ProcessKey {
  select: () => void;
  back: () => void;
  down: () => void;
  left: () => void;
  right: () => void;
  up: () => void;
}

useFocusStoreDangerously()

⚠️ Heads up! The FocusStore is an internal API. We strongly discourage you from accessing properties or calling methods on the FocusStore directly!

A Hook that returns the FocusStore. Typically, you should not need to use this hook.

One use-case for this hook is attaching the focusStore to the window when developing, which can be useful for debugging purposes.

import { useFocusStoreDangerously } from '@please/lrud';

export default function MyComponent() {
  const focusStore = useFocusStoreDangerously();

  useEffect(() => {
    if (process.env.NODE_ENV !== 'production') {
      window.focusStore = focusStore;
    }
  }, []);
}

Interfaces

These are the objects you will encounter when using this library.

FocusNode

A focus node. Each <FocusNode/> React component creates one of these.

PropertyTypeDescription
elRefrefA ref containing the HTML element for this node.
focusIdstringA unique identifier for this node.
childrenArray\<string>An array of focus IDs representing the children of this node.
focusedChildIndexnumber|nullThe index of the focused child of this node, if there is one.
prevFocusedChildIndexnumber|nullThe index of the previously-focused child of this node, if there was one.
isFocusedbooleantrue when this node is focused.
isFocusedLeafbooleanWhether or not this node is the leaf of the focus hierarchy.
activebooleantrue this node is active.
disabledbooleantrue when this node is disabled.
isExitingbooleanSet to true to indicate that the node will be animating out. Useful for certain exit animations.
wrappingbooleantrue when the navigation at the end of the node will wrap around to the other side.
wrapGridVerticalbooleantrue when grid rows will wrap.
wrapGridHorizontalbooleantrue when grid columns will wrap.
isRootbooleantrue this is the root node.
trapbooleantrue when this node is a focus trap.
forgetTrapFocusHierarchybooleanSet to false and a focus trap will restore its previous hierarchy upon becoming re-focused.
parentIdstring | nullThe focus ID of the parent node. null for the root node.
orientationstringA string representing the orientation of the node (either "horizontal" or "vertical")
navigationStylestringOne of 'first-child' or 'grid'
nodeNavigationItemstringHow this node is used in the navigation algorithm. Possible values are 'default', 'grid-container', 'grid-row', 'grid-item'
defaultFocusColumnnumberThe column index that should receive focus when focus is assigned to this focus node. Applies to grids only.
defaultFocusRownumberThe row index that should receive focus when focus is assigned to this focus node. Applies to grids only.
_gridColumnIndexnumber | nullThe focused column index of a grid.
_gridRowIndexnumber | nullThe focused row index of a grid.
_focusTrapPreviousHierarchyArray\<string>The previous focus hierarchy of a trap.

LRUDEvent

An object that is passed to you in the LRUD-related callbacks of a FocusNode component:

  • onKey
  • onArrow
  • onLeft
  • onRight
  • onUp
  • onDown
  • onSelected
  • onBack
PropertyTypeDescription
keystringA string representing the key that was pressed. One of "left", "right", "up", "down", "select", or "back".
isArrowbooleanWhether or not this key is an arrow.
nodeFocusNodeThe current FocusNode that received this event as the event propagates up the focus hierarchy. Analagous to event.currentTarget
targetNodeFocusNodeThe leaf FocusNode from which this event propagated. Analagous to event.target
preventDefaultfunctionCall this to stop the default behavior of the event. Commonly used to override the navigation behavior
stopPropagationfunctionCall this to stop the propagation of the event.

MoveEvent

An object that is passed to you in the onMove callback of a FocusNode component.

PropertyTypeDescription
orientationstringThe orientation of the move. Either "horizontal" or "vertical".
directionstringThe direction of the move. Either "forward" or "back".
arrowstringThe arrow that was pressed. One of "up", "down", "left", or "right".
prevChildIndexnumberThe index of the previously-focused child FocusNode.
nextChildIndexnumberThe index of the child FocusNode that is now focused.
prevChildNodeFocusNode | nullThe previously-focused FocusNode.
nextChildNodeFocusNodeThe child FocusNode that is now focused.

GridMoveEvent

An object that is passed to you in the onGridMove callback of a FocusNode component that is a grid.

PropertyTypeDescription
orientationstringThe orientation of the move. Either "horizontal" or "vertical".
directionstringThe direction of the move. Either "forward" or "back".
arrowstringThe arrow that was pressed. One of "up", "down", "left", or "right".
prevRowIndexnumberThe index of the previously-focused row.
nextRowIndexnumberThe index of the newly-focused row.
prevColumnIndexnumberThe index of the previously-focused column.
nextColumnIndexnumberThe index of the newly-focused column.

FocusEvent

An object that is passed to you in the onFocused and onBlurred callbacks of a FocusNode component.

PropertyTypeDescription
focusNodeFocusNode|undefinedThe newly-focused leaf FocusNode.
blurNodeFocusNode|undefinedThe previously-focused leaf FocusNode.
currentNodeFocusNodeThe FocusNode that is receiving the event, as it traverses the focus hierarchy. Analogous to event.currentTarget.

FocusStore

⚠️ Heads up! This is an internal API. We strongly discourage you from accessing properties or calling methods on the FocusStore directly!

An object that represents the store that contains all of the state related to what is in focus. Typically, you should not need to interact with this object directly, but it is made available to you for advanced use cases that you may have.

PropertyTypeDescription
getStatefunctionReturns the current FocusState
createNodesfunctionCreates one or more focus nodes in the tree.
deleteNodefunctionDeletes a focus node from the tree.
setFocusfunctionImperatively assign focus to a particular focus node.
updateNodefunctionUpdate an existing node. Used to, for example, set a node as disabled.
processKeyobjectContains methods to trigger key presses.
handleArrowfunctionUpdates store state after arrow key presses.
handleSelectfunctionUpdates store state after select button presses.
configurePointerEventsfunctionEnable or disable pointer events. Receives one argument, a boolean.
destroyfunctionCall when disposing of the store. Cleans up event listeners.

FocusState

⚠️ Heads up! This is an internal API. We strongly discourage you from accessing this object directly in your application.

An object representing the state of the focus in the app.

PropertyTypeDescription
focusedNodeIdstringThe ID of the leaf node in the focus hierarchy.
focusHierarchyArrayAn array of node IDs representing the focus hierarchy.
activeNodeIdstring | nullThe ID of the active node, if there is one.
nodesObjectA mapping of all of the focus nodes that exist.
interactionModestringThe active interaction mode of the app. Either "lrud" or "pointer".
_hasPointerEventsEnabledbooleanA boolean used internally for managing the creation of nested nodes.
_hasPointerEventsEnabledbooleanWhether or not pointer events are currently enabled.
_updatingFocusIsLockedbooleanA boolean used internally for managing the creation of nested nodes.

Examples

This repository contains example projects showing common patterns when using this library. Each example is located in the ./examples folder.

Instructions for running the examples are found in the README file for each example.

Basic Examples

  • Basic Layout - Demonstrates using the orientation prop for vertical and horizontal lists
  • Wrapping - Includes usage of the wrapping prop
  • Grid - Shows how to build a grid of focus nodes
  • Disabled Focus Nodes - Shows how to disable focus nodes using the disabled prop
  • Focus Trap - Demonstrates how to create focus traps

Advanced Examples

Prior Art

0.1.0

5 months ago

0.0.26

5 months ago

0.0.25

11 months ago

0.0.24

12 months ago