Closed redgoalsuk closed 4 years ago
Hi,
From the first look I can see that you call initNavigation from many components, while this should be done once at the root of the app. Please try to change this and see if it helps. Also make sure that initial “setFocus” is called when the components are mounted.
Hi
Brilliant thanks! Yes that was the error I think along with having focusable={false} on Link6.js, im posting the working code below if anyone needs it.
I have a followup question regarding focus on items and how to execute actions when focus is obtained on the TouchableOpacity, im currently doing this below but it never executes when the TouchableOpacity item has focus
render() {
// console.log('Menu item rendered: ', this.props.realFocusKey);
return (<TouchableOpacity style={[styles.topNavmenuItem, this.props.focused ? styles.TopNavfocused : null]} onFocus={() => console.log("hasfocus")} ><Link id={this.props.itemId} to={this.props.linkTo} >{this.props.itemText}</Link></TouchableOpacity>);
}
Is there someway of doing this? My aim is to use the TopNav.js menu items to redirect to different pages when each item has focus rather then keypress to achieve the same thing?
Working example of the issue reported in the original post:
App.js
import React, { Component, useState } from 'react';
import './App.css';
import PrivateRoute from './PrivateRoute';
import { HashRouter as Router, Route, Link, Switch } from 'react-router-dom';
import TopNav from './components/TopNav';
import Link1 from './pages/Link1';
import Link2 from './pages/Link2';
import Link3 from './pages/Link3';
import Link4 from './pages/Link4';
import Link5 from './pages/Link5';
import Link6 from './pages/Link6';
import {initNavigation, setKeyMap, withFocusable} from '@noriginmedia/react-spatial-navigation';
initNavigation({
debug: false,
visualDebug: false
})
function App(props) {
const Auth = JSON.parse(localStorage.getItem('authData'));
const isAuthenticated = (Auth && Auth.sessionId != '') ? true : true;
return (
<Router >
<div className="App">
{isAuthenticated && (
<header className="App-header">
<TopNav />
</header>
)}
<div>
<Switch>
<Route path="/" exact component={Link1} />
<Route path="/Link2" component={Link2} />
<Route path="/Link3" component={Link3} />
<Route path="/Link4" component={Link4} />
<Route path="/Link5" component={Link5} />
<Route path="/Link6" component={Link6} />
</Switch>
</div>
</div>
</Router>
);
}
export default App;
TopNav.js
import React from 'react'
import "core-js/stable";
import "regenerator-runtime/runtime";
import PropTypes from 'prop-types';
import {View, Text, StyleSheet, TouchableOpacity, ScrollView} from 'react-native';
import {initNavigation, setKeyMap, withFocusable} from '@noriginmedia/react-spatial-navigation';
import { Link, withRouter } from 'react-router-dom';
const styles = StyleSheet.create({
menu2: {
maxWidth: 900,
flexDirection: 'row'
},
menuFocused2: {
backgroundColor: '#546e84'
},
topNavmenuItem: {
alignItems: 'center',
padding: 14,
//textDecoration: 'none',
backgroundColor: '#333',
flexDirection: 'row'
},
TopNavfocused: {
backgroundColor: '#ccc'
}
});
const KEY_ENTER = 'enter';
const RETURN_KEY = 8;
class TopMenuItem extends React.Component {
constructor(props) {
super(props);
}
componentDidMount() {
}
render() {
// console.log('Menu item rendered: ', this.props.realFocusKey);
return (<TouchableOpacity style={[styles.topNavmenuItem, this.props.focused ? styles.TopNavfocused : null]}><Link id={this.props.itemId} to={this.props.linkTo} >{this.props.itemText}</Link></TouchableOpacity>);
}
}
TopMenuItem.propTypes = {
focused: PropTypes.bool.isRequired,
// realFocusKey: PropTypes.string.isRequired
};
const TopNavMenuItemFocusable = withFocusable()(TopMenuItem);
class TopNavMenu extends React.PureComponent {
constructor(props) {
super(props);
this.onPressKey = this.onPressKey.bind(this);
}
componentDidMount() {
document.getElementById('Link1').addEventListener('focus', this.handleFocus);
}
componentWillUnmount() {
window.removeEventListener('keydown', this.onPressKey);
}
onPressKey(event) {
if (event.keyCode === RETURN_KEY) {
this.props.setFocus();
}
}
render() {
// console.log('Menu rendered: ', this.props.realFocusKey);
return (<View style={[styles.menu2, this.props.hasFocusedChild ? styles.menuFocused2 : null]}>
<TopNavMenuItemFocusable focusKey={'TOPMENU-1'} itemText='Link1' linkTo='' itemId="Link1" />
<TopNavMenuItemFocusable focusKey={'TOPMENU-2'} itemText='Link2' linkTo='Link2' itemId="Link2" />
<TopNavMenuItemFocusable focusKey={'TOPMENU-3'} itemText='Link3' linkTo='Link3' itemId="Link3" />
<TopNavMenuItemFocusable focusKey={'TOPMENU-4'} itemText='Link4' linkTo='Link4' itemId="Link4" />
<TopNavMenuItemFocusable focusKey={'TOPMENU-5'} itemText='Link5' linkTo='Link5' itemId="Link5" />
<TopNavMenuItemFocusable focusKey={'TOPMENU-6'} itemText='Link6' linkTo='Link6' itemId="Link6" />
</View>);
}
}
TopNavMenu.propTypes = {
hasFocusedChild: PropTypes.bool.isRequired
};
const TopNavMenuFocusable = withFocusable({
trackChildren: true
})(TopNavMenu);
class TopNavSpatial extends React.PureComponent {
constructor(props) {
super(props);
}
componentDidMount() {
this.props.setFocus();
}
render() {
return (<View>
<TopNavMenuFocusable focusable={true} history={this.props.history} />
</View>
);
}
}
TopNavSpatial.propTypes = {
setFocus: PropTypes.func.isRequired,
hasFocusedChild: PropTypes.bool.isRequired,
history: PropTypes.shape({
push: PropTypes.func.isRequired
}).isRequired
};
const TopNav = withFocusable({
trackChildren: true
})(TopNavSpatial);
export default withRouter(TopNav);
Link6.js
/* eslint-disable react/no-multi-comp */
import React from 'react';
import PropTypes from 'prop-types';
import shuffle from 'lodash/shuffle';
import throttle from 'lodash/throttle';
import "core-js/stable";
import "regenerator-runtime/runtime";
import {View, Text, StyleSheet, TouchableOpacity, ScrollView} from 'react-native';
import {setKeyMap, withFocusable} from '@noriginmedia/react-spatial-navigation';
const KEY_ENTER = 'enter';
const styles = StyleSheet.create({
wrapper: {
flex: 1,
maxHeight: 400,
maxWidth: 800,
backgroundColor: '#333333',
flexDirection: 'row'
},
content: {
flex: 1
},
menu: {
maxWidth: 60,
flex: 1,
alignItems: 'center',
justifyContent: 'space-around'
},
menuFocused: {
backgroundColor: '#546e84'
},
menuItem: {
width: 50,
height: 50,
backgroundColor: '#f8f258'
},
activeWrapper: {
flex: 1,
alignItems: 'center',
justifyContent: 'center'
},
activeProgram: {
width: 160,
height: 120
},
activeProgramTitle: {
padding: 20,
color: 'white'
},
programWrapper: {
padding: 10,
alignItems: 'center'
},
program: {
height: 100,
width: 100
},
programTitle: {
color: 'white'
},
categoryWrapper: {
padding: 20
},
categoryTitle: {
color: 'white'
},
categoriesWrapper: {
flex: 1
},
focusedBorder: {
borderWidth: 6,
borderColor: 'red',
backgroundColor: 'white'
}
});
const categories = shuffle([{
title: 'Featured'
}, {
title: 'Cool'
}, {
title: 'Decent'
}]);
const programs = shuffle([{
title: 'Program 1',
color: '#337fdd'
}, {
title: 'Program 2',
color: '#dd4558'
}, {
title: 'Program 3',
color: '#7ddd6a'
}, {
title: 'Program 4',
color: '#dddd4d'
}, {
title: 'Program 5',
color: '#8299dd'
}, {
title: 'Program 6',
color: '#edab83'
}, {
title: 'Program 7',
color: '#60ed9e'
}, {
title: 'Program 8',
color: '#d15fb6'
}, {
title: 'Program 9',
color: '#c0ee33'
}]);
const RETURN_KEY = 8;
/* eslint-disable react/prefer-stateless-function */
class MenuItem extends React.PureComponent {
render() {
// console.log('Menu item rendered: ', this.props.realFocusKey);
return (<TouchableOpacity style={[styles.menuItem, this.props.focused ? styles.focusedBorder : null]} />);
}
}
MenuItem.propTypes = {
focused: PropTypes.bool.isRequired
// realFocusKey: PropTypes.string.isRequired
};
const MenuItemFocusable = withFocusable()(MenuItem);
class Menu extends React.PureComponent {
constructor(props) {
super(props);
this.onPressKey = this.onPressKey.bind(this);
}
componentDidMount() {
window.addEventListener('keydown', this.onPressKey);
}
componentWillUnmount() {
window.removeEventListener('keydown', this.onPressKey);
}
onPressKey(event) {
if (event.keyCode === RETURN_KEY) {
this.props.setFocus();
}
}
render() {
// console.log('Menu rendered: ', this.props.realFocusKey);
return (<View style={[styles.menu, this.props.hasFocusedChild ? styles.menuFocused : null]}>
<MenuItemFocusable focusKey={'MENU-1'} />
<MenuItemFocusable focusKey={'MENU-2'} />
<MenuItemFocusable focusKey={'MENU-3'} />
<MenuItemFocusable focusKey={'MENU-4'} />
<MenuItemFocusable focusKey={'MENU-5'} />
<MenuItemFocusable focusKey={'MENU-6'} />
</View>);
}
}
Menu.propTypes = {
setFocus: PropTypes.func.isRequired,
hasFocusedChild: PropTypes.bool.isRequired
// realFocusKey: PropTypes.string.isRequired
};
const MenuFocusable = withFocusable({
trackChildren: true
})(Menu);
class Content extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
currentProgram: null
};
this.onProgramPress = this.onProgramPress.bind(this);
}
onProgramPress(programProps, {pressedKeys} = {}) {
if (pressedKeys && pressedKeys[KEY_ENTER] > 1) {
return;
}
this.setState({
currentProgram: programProps
});
}
render() {
// console.log('content rendered: ', this.props.realFocusKey);
return (<View style={styles.content}>
<Active program={this.state.currentProgram} />
<CategoriesFocusable
focusKey={'CATEGORIES'}
onProgramPress={this.onProgramPress}
/>
</View>);
}
}
Content.propTypes = {
// realFocusKey: PropTypes.string.isRequired
};
const ContentFocusable = withFocusable()(Content);
class Active extends React.PureComponent {
render() {
const {program} = this.props;
const style = {
backgroundColor: program ? program.color : 'grey'
};
return (<View style={styles.activeWrapper}>
<View style={[style, styles.activeProgram]} />
<Text style={styles.activeProgramTitle}>
{program ? program.title : 'No Program'}
</Text>
</View>);
}
}
Active.propTypes = {
program: PropTypes.shape({
title: PropTypes.string.isRequired,
color: PropTypes.string.isRequired
})
};
Active.defaultProps = {
program: null
};
class Program extends React.PureComponent {
render() {
// console.log('Program rendered: ', this.props.realFocusKey);
const {color, onPress, focused, title} = this.props;
const style = {
backgroundColor: color
};
return (<TouchableOpacity
onPress={onPress}
style={styles.programWrapper}
>
<View style={[style, styles.program, focused ? styles.focusedBorder : null]} />
<Text style={styles.programTitle}>
{title}
</Text>
</TouchableOpacity>);
}
}
Program.propTypes = {
title: PropTypes.string.isRequired,
color: PropTypes.string.isRequired,
onPress: PropTypes.func.isRequired,
focused: PropTypes.bool.isRequired
// realFocusKey: PropTypes.string.isRequired
};
const ProgramFocusable = withFocusable()(Program);
class Category extends React.PureComponent {
constructor(props) {
super(props);
this.scrollRef = null;
this.onProgramFocused = this.onProgramFocused.bind(this);
this.onProgramArrowPress = this.onProgramArrowPress.bind(this);
}
onProgramFocused({x}) {
this.scrollRef.scrollTo({
x
});
}
onProgramArrowPress(direction, {categoryIndex, programIndex}) {
if (direction === 'right' && programIndex === programs.length - 1 && categoryIndex < categories.length - 1) {
this.props.setFocus(`CATEGORY-${categoryIndex + 1}`);
return false;
}
return true;
}
render() {
// console.log('Category rendered: ', this.props.realFocusKey);
return (<View style={styles.categoryWrapper}>
<Text style={styles.categoryTitle}>
{this.props.title}
</Text>
<ScrollView
horizontal
ref={(reference) => {
if (reference) {
this.scrollRef = reference;
}
}}
>
{programs.map((program, index) => ((<ProgramFocusable
{...program}
focusKey={`PROGRAM-${this.props.realFocusKey}-${index}`}
onPress={() => this.props.onProgramPress(program)}
onEnterPress={this.props.onProgramPress}
key={program.title}
onBecameFocused={this.onProgramFocused}
onArrowPress={this.onProgramArrowPress}
programIndex={index}
categoryIndex={this.props.categoryIndex}
/>)))}
</ScrollView>
</View>);
}
}
Category.propTypes = {
title: PropTypes.string.isRequired,
onProgramPress: PropTypes.func.isRequired,
realFocusKey: PropTypes.string.isRequired,
categoryIndex: PropTypes.number.isRequired,
setFocus: PropTypes.func.isRequired
};
const CategoryFocusable = withFocusable()(Category);
class Categories extends React.PureComponent {
constructor(props) {
super(props);
this.scrollRef = null;
this.onCategoryFocused = this.onCategoryFocused.bind(this);
}
onCategoryFocused({y}) {
this.scrollRef.scrollTo({
y
});
}
render() {
// console.log('Categories rendered: ', this.props.realFocusKey);
return (<ScrollView
ref={(reference) => {
if (reference) {
this.scrollRef = reference;
}
}}
style={styles.categoriesWrapper}
>
{categories.map((category, index) => (<CategoryFocusable
focusKey={`CATEGORY-${index}`}
{...category}
onProgramPress={this.props.onProgramPress}
key={category.title}
onBecameFocused={this.onCategoryFocused}
categoryIndex={index}
// preferredChildFocusKey={`PROGRAM-CATEGORY-${index}-${programs.length - 1}`}
/>))}
</ScrollView>);
}
}
Categories.propTypes = {
onProgramPress: PropTypes.func.isRequired,
realFocusKey: PropTypes.string.isRequired
};
const CategoriesFocusable = withFocusable()(Categories);
class Spatial extends React.PureComponent {
constructor(props) {
super(props);
this.onWheel = this.onWheel.bind(this);
this.throttledWheelHandler = throttle(this.throttledWheelHandler.bind(this), 500, {trailing: false});
}
componentDidMount() {
window.addEventListener('wheel', this.onWheel, {passive: false});
}
componentWillUnmount() {
window.removeEventListener('wheel', this.onWheel);
}
onWheel(event) {
event.preventDefault();
this.throttledWheelHandler(event);
}
throttledWheelHandler(event) {
event.preventDefault();
const {deltaY, deltaX} = event;
const {navigateByDirection} = this.props;
if (deltaY > 1) {
navigateByDirection('down');
} else if (deltaY < 0) {
navigateByDirection('up');
} else if (deltaX > 1) {
navigateByDirection('right');
} else if (deltaX < 1) {
navigateByDirection('left');
}
}
render() {
return (<View style={styles.wrapper}>
<MenuFocusable
focusKey={'MENU'}
/>
<ContentFocusable
focusKey={'CONTENT'}
/>
</View>);
}
}
Spatial.propTypes = {
navigateByDirection: PropTypes.func.isRequired
};
const SpatialFocusable = withFocusable()(Spatial);
class Link6Spatial extends React.PureComponent {
constructor(props) {
super(props);
}
componentDidMount() {
this.props.setFocus();
}
render() {
return (<View>
<SpatialFocusable focusable={true} />
</View>);
}
}
Link6Spatial.propTypes = {
setFocus: PropTypes.func.isRequired,
hasFocusedChild: PropTypes.bool.isRequired
};
const Link6 = withFocusable({
trackChildren: true
})(Link6Spatial);
export default Link6;
Glad it worked :)
In case you want to do something when item is focused, you can use onBecameFocused
callback prop that is automatically exposed from the component that is wrapped into withFocusable
:
https://github.com/NoriginMedia/react-spatial-navigation#using-props-on-focusable-components
So for example in your TopNav
component you can listen to onBecameFocusable
on each of the menu items and trigger page navigation.
onFocus
callback on TouchableOpacity is more like a native browser "onfocus" event, which is not the same as the "internal" focus from this library.
nice that worked thanks! but just one small bug, when im on Link5.js and I press right to goto Link6.js then I press down to goto the content on Link6.js then press up to go back to the menu it jumps to Link5.js instead of setting focus to Link6, its like when going from Link5 to Link6 it does not set focus to Link6 like it ignored that action?
return (<View style={[styles.menu2, this.props.hasFocusedChild ? styles.menuFocused2 : null]}>
<TopNavMenuItemFocusable focusKey={'TOPMENU-1'} itemText='Link1' linkTo='' itemId="Link1" onBecameFocused={() => this.props.history.push("/") } />
<TopNavMenuItemFocusable focusKey={'TOPMENU-2'} itemText='Link2' linkTo='Link2' itemId="Link2" onBecameFocused={() => this.props.history.push("/Link2") } />
<TopNavMenuItemFocusable focusKey={'TOPMENU-3'} itemText='Link3' linkTo='Link3' itemId="Link3" onBecameFocused={() => this.props.history.push("/Link3") } />
<TopNavMenuItemFocusable focusKey={'TOPMENU-4'} itemText='Link4' linkTo='Link4' itemId="Link4" onBecameFocused={() => this.props.history.push("/Link4") } />
<TopNavMenuItemFocusable focusKey={'TOPMENU-5'} itemText='Link5' linkTo='Link5' itemId="Link5" onBecameFocused={() => this.props.history.push("/Link5") } />
<TopNavMenuItemFocusable focusKey={'TOPMENU-6'} itemText='Link6' linkTo='Link6' itemId="Link6" onBecameFocused={() => this.props.history.push("/Link6") } />
</View>);
Im getting the hang of this :) I removed the setFocus inside Link6.js and that now keeps focus on the menu item Link6 when going from the menu to the content and then back again.
Closing as resolved
Hi
I have been trying to get this working for a few hours now but I cannot seem to work it out. I have a simple Tizen TV app with a navigation at the top and then content below, focus needs to be seamless between the navigation at the top and the content below - ie you move from the top nav to the content by pressing up/down on the remote. The only thing which works right now is the focus is on the top nav and I can move left and right but pressing down to focus on the content below does not work.
App.js
TopNav.js
Link6.js