Library for making page navigation (using keyboard or TV Remote Control in STB/ Smart TV apps) React way
- Lightweight (8 kb minified, 3 kb gzipped)
- No dependencies (except for React itself)
This project is under development and is still experimental
In STB/ Smart TV apps navigation is performed using TV remote control. In traditional approach, navigation logic is implemented outside 'View' layer and is tightly coupled with UI layout, which doesn't fit with React principles of reusable and independent components.
The idea is to make components' navigation logic encapsulated within themselves and independent of the layout the component is placed into.
react-navtree
was designed to help make navigation React way - declarative and reusable.
Live examples. See examples
directory for source code.
npm install --save react-navtree
- Initialize
NavTree
instance which will serve as an entry point for keypress events
// At the bootstrap phase (before rendering the app):
const navTree = new NavTree()
// this code depends on the platform you're developing for
window.document.addEventListener('keydown', (e) => {
let key
switch (e.keyCode) {
case 40:
key = 'down'; break
case 38:
key = 'up'; break
case 37:
key = 'left'; break
case 39:
key = 'right'; break
case 27:
key = 'enter'; break
default:
}
if (key) {
e.preventDefault()
navTree.resolve(key)
}
}, false)
- Make use of
<Nav>
component
class Header extends React.PureComponent {
constructor (props) {
super(props)
this.state = { tab: 'tab-foo' }
}
render () {
return <Nav className='header'>
<Nav
onNav={path => { if (path) this.setState({ tab: path[0] }) }}
func={/* focus open tab first */ (key, navTree, focusedNode) => !navTree.focusedNode ? this.state.tab : navDynamic(key, navTree, focusedNode)}
className='tabs'
>
<Nav navId='tab-foo' className={this.state.tab === 'tab-foo' ? 'selected' : ''}>Foo</Nav>
<Nav navId='tab-qux' className={this.state.tab === 'tab-qux' ? 'selected' : ''}>Qux</Nav>
</Nav>
{ this.state.tab === 'tab-foo' && <Nav className='tab-content' >
<Nav className='nav-block inline'>Foo</Nav>
<Nav className='nav-block inline'>Bar</Nav>
<Nav className='nav-block inline'>Baz</Nav>
</Nav> }
{ this.state.tab === 'tab-qux' && <Nav className='tab-content' >
<Nav className='nav-block'>Qux</Nav>
<Nav className='nav-block'>Quux</Nav>
</Nav> }
</Nav>
}
}
class Table extends React.PureComponent {
constructor (props) {
super(props)
this.state = { focusedPath: false }
}
render () {
let {children, navId, cols} = this.props
return <Nav onNav={path => { this.setState({ focusedPath: path }) }} navId={navId} className='nav-block'>
<div className='caption'>{ this.state.focusedPath && 'Focused: ' + this.state.focusedPath.join(' -> ') }</div>
<div className={'tbl col' + cols}>
{ children.map((child, i) => <div key={i}><Nav navId={`row ${Math.ceil((i + 1) / cols)} cell ${(i % cols) + 1}`} className='nav-block'>{child}</Nav></div>) }
</div>
</Nav>
}
}
class Body extends React.PureComponent {
render () {
return (<div>
<Table cols={3}>
<div>A</div>
<div>B</div>
<div>C</div>
<div>D</div>
<div>E</div>
<div>F</div>
<div>G</div>
<div>H</div>
<div>J</div>
</Table>
<Table cols={2}>
<Table navId='table 1' cols={2}>
<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
</Table>
<Table navId='table 2' cols={2}>
<div>5</div>
<div>6</div>
<div>7</div>
<div>8</div>
</Table>
</Table>
</div>)
}
}
class Footer extends React.PureComponent {
render () {
return (
<Nav className='footer'>
<Nav className='nav-block inline'>Foo</Nav>
<Nav className='nav-block inline'>Bar</Nav>
<Nav className='nav-block inline'>Baz</Nav>
</Nav>
)
}
}
export default function Layout () {
return <Nav>
<Header />
<Body />
<Footer />
</Nav>
}
ReactDOM.render(<Layout tree={navTree} />)
The project consists of 2 main classes:
NavTree
- Navigation tree<Nav>
- React component
NavTree
represents a tree structure reflecting React's node hierarchy of <Nav>
components.
It is accomplished by binding <Nav>
components to the navigation tree at the mounting phase.
When <Nav>
component is mounted it creates a new branch in the tree.
This branch in its turn will serve as a parent tree to children Nav
components through Reacts' context
mechanism.
Likewise, when <Nav>
component is unmounted, the branch gets removed from the tree thus keeping the tree in sync with React.
Navigation is performed by using NavTree.focus(path)
or NavTree.resolve(event)
functions.
NavTree.focus(path)
is used for imperative navigation.
NavTree.resolve(event)
is used for resolving an event (keypress) to a node that should be focused next.
Navigation resolving is a process of finding a node that should be focused next when an event occurred.
Each node in the navigation tree must have so-called "resolve function" that is responsible for finding a node that should receive focus according to an event and previously focused node.
* By default, navDynamic
is used if the resolve function is not provided.
- Resolving Up: Starting at the deepest focused node, the event is propagated up the tree until it is resolved.
- Resolving Down: Starting at the resolved node in phase #1, the event is propagated down the tree as long as it is resolved.
- Focus: The destination node (the last one in phase #2) gets focused.
* event is resolved if the node's "resolve function" returns a child node ID or NULL (itself)
* if a node gets focused, all of his ascendants get focused as well
When an event (keypress) occurs, first, the currently focused <Nav>
component gets control of the process.
The resolve function (passed as "func" property) gets called to determine if the component can resolve the event.
If so, the component stays focused and the event is propagated down to a child component returned by the resolve function. (If the resolve function returns NULL, the component stays focused, but the focused child (if any) will lose focus)
If not, the event is propagated up to the parent component. The component will lose focus if the event is eventually resolved to a different component.
Property | Type | Default | Description |
---|---|---|---|
func |
function (event: any, node: NavTree, previouslyFocusedNode: NavTree) => string | null | false | navDynamic | Navigation resolve function. |
tree |
NavTree | obtained from context | Parent navigation tree. Should be set only on root component. |
navId |
string | ID used for the bound NavTree node. Must be unique within parent tree's direct nodes. If omitted, a numerical ID will be generated. |
|
defaultFocused |
boolean | false | If set to true , the component will get focused after it's been mounted |
onNav |
function (path: Array<string> | false ) => void | Navigation event callback | |
component |
string | 'div' | Wrapper element |
focusedClass |
string | 'nav-focused' | CSS-class applied to focused component |
... |
All other properties are passed to the component node. |
Method | Arguments | Return value | Description |
---|---|---|---|
focus(path) |
path: string | Array<string> | undefined | Focuses a node specified by path argument. If omitted, the tree instance itself gets focused. |
|
resolve(event) |
event: any | Resolves the event so that the appropriate node gets focused. | |
addNode(id) |
id: string | undefined | NavTree | Creates a new branch in the tree. If id is omitted, a numerical ID will be generated.id must be unique within the direct nodes of the tree. |
removeNode(id) |
id: string | Removes the node from the tree | |
getNode(path) |
string | Array<string> | NavTree | null | Returns descendant node instance |
getFocusedPath() |
Array<string> | Returns descendant focused node's path |
Property | Type | Description |
---|---|---|
nodes |
Object{ id: Navtree } | List of child nodes |
nodesId |
Array<string> | Array of child nodes ID arranged in the order the nodes have been added |
parent |
Navtree | null | Parent node |
focusedNode |
string | null | ID of focused child node |