tachyon-tv-nav
v32.0.0
Published
TV Navigation Helpers
Downloads
2
Readme
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 childHorizontalNav
: for a horizontal line of focusable childrenVerticalNav
: for a vertical line of focusable childrenGridNav
: 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
}