tachyon-tv-nav
TypeScript icon, indicating that this package has built-in type declarations

32.0.0 • Public • Published

Tachyon TV Nav

Provides 2D UI navigation support for arbitrary user keypress-based input.

Features:

  • Compatible with React Concurrent mode
  • Arrow key support out of the box
  • Highly composable
  • Virtualization support
  • Minimizes expensive sub-tree re-renders with subscription based focus listeners

Testing / Demo

This package has a corresponding package-example. It is extremely handy for developing against as well to test any changes.

Setting Up

import { NavigationRoot } from 'tachyon-tv-nav';

function App() {
  // Without defining a custom navigation key mapping, the package will
  // listen on arrow key codes and trigger navigation events accordingly.
  const customKeyMap = { [KeyValue.Escape]: window.history.back // ... };

  return (
    <NavigationRoot customKeyMap={customKeyMap} focusSeed={someDynamicPageRelatedValue}>
      {/* app */}
    </NavigationRoot>
  );
}

Basic Use

Creating Navigation Elements

The useFocus hook is used to determine when an element is focused within a navigation area. An optional refFocusHandler function can be used to call ".focus()" on any component that supports the interface.

import type { FC } from 'react';
import { refFocusHandler, useFocus } from 'tachyon-tv-nav';

export const Card: FC<{ focusIndex: number }> = ({ focusIndex }) => {
  const { focused } = useFocus(focusIndex);
  return <div ref={refFocusHandler(focused)}>{/* ... */}</div>;
};

Imperatively Taking Focus

Focus can be imperatively taken by a registered navigation element using takeFocus:

import type { FC } from 'react';
import { refFocusHandler, useFocus } from 'tachyon-tv-nav';

export const Card: FC<{ focusIndex: number }> = ({ focusIndex }) => {
  const { focused, takeFocus } = useFocus(focusIndex);
  return <div ref={refFocusHandler(focused)}>{/* ... */}</div>;
};

Creating Navigation Areas

A navigation area is a grouping of focusable children that are logically related based on a common navigation direction or paradigm between them.

There are 4 types of navigation areas:

  • NodeNav: for a single focusable child
  • HorizontalNav: for a horizontal line of focusable children
  • VerticalNav: for a vertical line of focusable children
  • GridNav: for a grid (with fixed width and height based on number of children) of focusable children

Horizontal and Vertical Nav

Use HorizontalNav or VerticalNav to create a navigation area in their respective orientations:

import type { FC } from 'react';
import { HorizontalNav } from 'tachyon-tv-nav';
import { Card } from './Card';

/**
 * Left / right arrow key presses navigate between the elements in the navigation area.
 */
export const ListFeature: FC<{ focusIndex: number }> = ({ focusIndex }) => (
  <HorizontalNav focusIndex={focusIndex} elementCount={2}>
    <Card focusIndex={0} />
    <Card focusIndex={1} />
  </HorizontalNav>
);

GridNav

import type { FC } from 'react';
import { GridNav } from 'tachyon-tv-nav';
import { Card } from './Card';

/**
 * Left/right/up/down arrow key presses are all used to navigate between the elements in the navigation area.
 * It is up to you to ensure that the Grid is laid out correctly.
 */
export const GridFeature: FC<{ focusIndex: number }> = ({ focusIndex }) => (
  <GridNav focusIndex={focusIndex} elementsPerRow={2} elementCount={40}>
    {/* First row */}
    <Card focusIndex={0} />
    <Card focusIndex={1} />
    {/* Second row */}
    <Card focusIndex={3} />
    <Card focusIndex={4} />
    {/* ... */}
  </GridNav>
);

Composing Areas

All navigation areas can be composed together to build complex layouts. Focus will transition between areas when the current area does not have another element in the direction the user is trying to navigate assuming that there is another area in that direction with elements. See the package example for more inspiration.

import type { FC } from 'react';
import { HorizontalNav, VerticalNav } from 'tachyon-tv-nav';

export const PageLayout: FC<{ focusIndex: number }> = ({ focusIndex }) => (
  <VerticalNav focusIndex={focusIndex} elementCount={2} >
    <HorizontalNav focusIndex={0} elementCount={...}>{/* ... */}</HorizontalNav>
    <HorizontalNav focusIndex={1} elementCount={...}>{/* ... */}</HorizontalNav>
  </VerticalNav>
);

Advanced Use

Preceding and Precluding The Default Input Handler Behaviors

Each nav area exposes onDown, onLeft, onRight, and onUp which takes a callback that will be invoked when the area is focused and the intent is triggered (E.G. pressing right when the area is focused will trigger onRight).

If the callback returns true then the normal nav behavior will be precluded.

Note: Stable functions should be used to avoid expensive re-renders when possible.

import type { FC } from 'react';
import { HorizontalNav } from 'tachyon-tv-nav';
import { FeatureRequiringSpecialActions } from './FeatureRequiringSpecialActions';

const onLeft = () => {
  // some special actions
  // stop propagation
  return true;
};

const onRight = () => {
  // some special actions
  // propagate to the normal nav handler (move focus right)
  return;
};

export const GridFeature: FC<{ focusIndex: number }> = ({ focusIndex }) => (
  <HorizontalNav
    focusIndex={focusIndex}
    elementCount={1}
    onLeft={onLeft}
    onRight={onRight}
  >
    <FeatureRequiringSpecialActions focusIndex={0} />
  </HorizontalNav>
);

Listening On An Area's Child Focus Index

Sometimes it's useful to know the index of a parent navigation area's current focused child (like when you want to manage child virtualization). We provide useAreaChildFocusIndex for this purpose:

import { useAreaChildFocusIndex } from 'tachyon-tv-nav';

function useCustomNavBehavior() {
  const childFocusIndex = useAreaChildFocusIndex();
  // do something interesting with the value
}

Readme

Keywords

none

Package Sidebar

Install

npm i tachyon-tv-nav

Weekly Downloads

3

Version

32.0.0

License

UNLICENSED

Unpacked Size

82.7 kB

Total Files

57

Last publish

Collaborators

  • frgjdhskxjogu