noriginmedia / react-spatial-navigation Goto Github PK
View Code? Open in Web Editor NEWDEPRECATED. HOC-based Spatial Navigation. NEW Hooks version is available here: https://github.com/NoriginMedia/norigin-spatial-navigation
License: MIT License
DEPRECATED. HOC-based Spatial Navigation. NEW Hooks version is available here: https://github.com/NoriginMedia/norigin-spatial-navigation
License: MIT License
Describe the bug
Server Side Rendering and Static Builds fail with window is undefined
To Reproduce
Render Component that utilises withFocusable
on an environment without the window object
Expected behavior
All operations that require window
should not run and component should render on the server without issue.
Additional context
This issue was fixed
I'm creating a TV web-app with react-spatial-navigation and am running into some issues when opening pop-up dialogues.
The application has a number of different pages, managed by React Router, as well as some hidden background components. I want any of the components in the application to be able to open pop-up error dialogues on-top of all the other content.
I've created a simplified version of my application here: https://github.com/EwanRoycroft/rsn-test
There are two pages with navigable buttons. Selecting the top button will open up a pop-up dialogue in front of the other content. Clicking a button on the dialogue will close it.
So now my problem - when I open the dialogue, it will steal the focus from the background pages. However, when I close the dialogue, focus will not return to the content in the background, and the document loses focus completely.
The first thing I note is that spatialNavigation
has saved the button on the background page as the last child. My understanding is that, when the current element loses focus, it should return to the last child. So why doesn't it return to the background button when I close the pop-up?
16:47:48.835 spatialNavigation.js:739 setFocustargetFocusKey popupFocusable0
16:47:48.835 spatialNavigation.js:739 getNextFocusKeytargetFocusKey popupFocusable0
16:47:48.835 spatialNavigation.js:739 setFocusnewFocusKey popupFocusable0
16:47:48.835 spatialNavigation.js:739 saveLastFocusedChildKey containerA lastFocusedChildKey set focusableA0
16:47:48.836 spatialNavigation.js:739 saveLastFocusedChildKey containerA lastFocusedChildKey set focusableA0
Ignoring the last child mechanism, what if I were to manage the focus myself? So, as you can see from lines 31, 57, and 82, the pages/pop-up only steal focus on first mount. This is fine when the pop-up opens, because it steals the focus; but when the pop-up closes, the background buttons update but don't steal the focus back. So, I tried stealing the focus every time the pages mount/update: https://github.com/EwanRoycroft/rsn-test/tree/page-refocus. This time, when I open the pop-up, the pop-up steals the focus, then the background buttons update and steal the focus back. I then tried stealing focus every time the pop-up mounts/updates as well: https://github.com/EwanRoycroft/rsn-test/tree/all-refocus. This appears to work, but looking at the console you can see that the page/pop-up get stuck in a loop stealing focus from each other.
I then thought perhaps I should have the top-level App component alone manage the focus, seen as it knows when the pop-up is visible. This works for a simple application, but gets much more complicated when you introduce routes. The App would have to know what page is currently visible and which button on the page to navigate back to. This gets even more complicated when you apply this to my original application, which has more complexity and nested routes.
So - is there anything wrong with what I'm doing and is there any way I can make this work?
Describe the bug
When we load multiple rails in our application and hold the button (up or down arrow for example) to navigate between items on those rails navigation is very slow and imprecise. We tried to use throttle, but we lost some functionality related to item placement. Would there be any alternative to handle the event of holding a key to navigate between items?
For our app, an ideal behavior would be to treat the pressed key as if it were a key being pressed once and repeat that key again only once.
Sometimes the application crashes, especially on TVs with less hardware(<=2018 in our scenario).
To Reproduce
Hold a key to navigate between items
Expected behavior
A fluid navigation with less loss of performance.
Describe the bug
Sometimes navigation doesn't work after go back to previous screen. Element is focused, but the remote control doesn't work on this element. Only the back button works but I can't navigate through items or click enter.
To Reproduce
Steps to reproduce the behavior:
I have a button with active focus, but when another component on the page is rendered again with map I completely lose focus
To Reproduce
When looking at the basic example provided, onPress is triggered when using a ReactNative TouchableOpacity component as well as onEnterPress. This does not occur with a regular HTML button element with onClick. Is this possible to achieve the same behaviour with a button and trigger the click as well? Just wondering how the React Native onPress is being fired.
**Example: **
import {withFocusable} from '@noriginmedia/react-spatial-navigation';
const ComponentTouchableOpacity = ({focused}) => (
<TouchableOpacity
// This gets triggered
onPress={() => {
console.log('TouchableOpacity::onPress');
}}
/>
);
const FocusableComponent = withFocusable()(ComponentTouchableOpacity);
const ComponentButton = ({focused}) => (
<button
// This gets triggered
onClick={() => {
console.log('TouchableOpacity::onPress');
}}
/>);
const FocusableComponent = withFocusable()(ComponentButton);
Is your feature request related to a problem? Please describe.
I'm using react-navigation
together with React Native and React Native Web. It's working nicely, but poses some challenges for usage with react-spatial-navigation
.
In my app, I have a MainMenu screen with a selection of items to pick from. Upon picking an item, a modal will open, showing the VideoModal screen, which presents both a video and a set of related items (that are focusable).
I achieve this flow using a StackNavigator, which renders one screen at a time and provides transitions between screens. When a new screen is opened it is placed on top of the stack.
The problem is that StackNavigator does not unmount the previous screen when presenting the modal; it merely hides it. Thus, the focusables are all still mounted, and so if the user presses up/down/left/right, the focus may leave the bounds of the screen and select these hidden focusables. Instead, it should ignore those hidden modals belonging to a different route.
Describe the solution you'd like
I'd like an enabled
prop (defaults to true
) to include/exclude any focusable from the tree of 'available' focusables. I could toggled this to true
/false
based on whether the given navigation route is active (visible) or not.
Describe alternatives you've considered
In practice, I write my route names into the focusKey
(e.g. MainMenu/Carousel
and VideoModal/Carousel
) as if to compose a directory hierarchy. Routes could be excluded based on string-matching with the focusKey
, but this requires users of the library to be diligent with how they name their focus keys and may harm component re-use.
If a user presses any Arrow keys but there is no element focussed at the time.
We want to be able to return focus to the safest element on any given screen if Focus is lost!
Is there anyway to detect this scenario?
Hi, @asgvard, a vulnerability CVE-2020-15168 is introduced in @noriginmedia/react-spatial-navigation via:
● @noriginmedia/[email protected] ➔ [email protected] ➔ [email protected] ➔ [email protected] ➔ [email protected]
recompose is a legacy package. It has not been maintained for about 3 years, and is not likely to be updated.
Is it possible to migrate recompose to other package to remediate this vulnerability?
I noticed several migration records for recompose in other js repos, such as
Are there any efforts planned that would remediate this vulnerability or migrate recompose?
Thanks
; )
Is your feature request related to a problem? Please describe.
Have currently been working with focusing elements and using voice over on Tizen TV.
Voice over works well and as expected when focusing of aria labelled elements.
Describe the solution you'd like
Allow actual focus of an element composed withFocusable
to get native focus state from browser engine.
logIndex
property should not be incremented if the debug is not active
Describe the bug
On the left side a fixed vertical menu on the right a horizontal list.
If the menu has hasFocusedChild=true its width is increased.
When navigating to the right, the second list item is focused.
To Reproduce
Steps to reproduce the behavior:
Expected behavior
After right click the first element of the list should be focused
Screenshots
https://prnt.sc/1qac4h5
https://prnt.sc/1qac743
Additional context
Does not matter whether the menu overlaps or shifts
When I have plenty elements on the screen, if I press an arrow button, I.E. right, the focus jumps to the second element to the right of the current focus instead of the first element.
Is there any setting or configuration which could help to solve this issue?
Thanks
Marco
Describe the bug
Elements positioned absolutely do not seem to be discovered and are not focusable via smart navigation
To Reproduce
Layout a set of focusable elements with absolute position via CSS try to navigate from one absolute position element to the next.
Expected behavior
Smart Navigate moves to closest focusable item on navigation
Screenshots
If applicable, add screenshots to help explain your problem.
Additional context
Add any other context about the problem here.
Hi
I am trying to get the Testbed example running on a Tizen TV but I cannot seem to get it running at all, I see an error in the JS console "Uncaught TypeError: Object.values is not a function".
This is my App.js which is more or less the same as the example at https://raw.githubusercontent.com/NoriginMedia/react-spatial-navigation/master/src/App.js
Any help or advice regarding this?
/* 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 {View, Text, StyleSheet, TouchableOpacity, ScrollView} from 'react-native';
import {initNavigation, setKeyMap, withFocusable} from '@noriginmedia/react-spatial-navigation';
initNavigation({
debug: true,
visualDebug: true
})
// SpatialNavigation.setKeyMap(keyMap); -> Custom key map
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() {
this.props.setFocus();
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);
const App = () => (<View>
<SpatialFocusable focusable={false} />
</View>);
export default App;
Is your feature request related to a problem? Please describe.
Hooks were introduced 2 years ago in React 16.8. With class-based components decreasing in popularity compared to functional components, it would make sense to create a hook as an alternative to the withFocusable
HOC.
Describe the solution you'd like
The hook could work like this:
const { focused, setFocus, stealFocus } = useFocusable({
trackChildren: true,
forgetLastFocusedChild: true,
});
Additional context
This is highly desireable, since many projects are not using HOCs anymore. In our project, withFocusable
is the last remaining HOC.
Please let me know if you'd like help with this.
Describe the bug
I'm examining how mouse and keyboard behave together in a React Native Web App, with an interest in supporting desktop browsers (where either interaction mechanism might be used).
To Reproduce
Steps to reproduce the behaviour:
<TouchableOpacity>
child to be selected (e.g. in the same way as if you'd highlighted a text field).I'd expect the Enter-press to trigger onEnterPress
on the FocusableComponent; but in practice, react-spatial-navigation
performs no operation at all.
This is because the key event is absorbed by the TouchableOpacity that was clicked on earlier (as it became highlighted upon click). So instead the Enter-press results in onPress
firing on the TouchableOpacity, and I believe the event stops propagating from there.
Workaround
I found that calling HTMLElement.blur() works around the problem perfectly well for my needs.
const Item = ({
item,
onClick,
focused,
stealFocus,
}) => {
const { ViewFactory, touchableStyle, source } = item;
const refObj = React.createRef<TouchableOpacity>();
return (
<TouchableOpacity
ref={refObj}
onFocus={() => {
const currentRef = refObj.current;
/* React Native Components happen to implement blur() just like
* HTMLElement does, but it's safest to check anyway. Not sure
* whether there's any harm in calling it on Native as well as
* Web – haven't tested on tvOS and Android TV. */
if(currentRef && currentRef.blur){
currentRef.blur();
}
}}
onPress={() => {
stealFocus();
onClick();
}}
style={touchableStyle}
>
<ViewFactory focused={focused} source={source} />
</TouchableOpacity>
);
};
const ItemFocusable = withFocusable()(props => <Item {...props} />);
Additional context
Note that the visual debugger blocks clicks altogether, so cannot be used to reproduce this issue.
Also: Any Enter-press performed when a TouchableOpacity is highlighted does not propagate to a keydown
event listener, so I believe the browser is handling the event to some extent.
Discussion
Should this issue be left for consumers of the library to solve themselves (as I have done with my workaround), or is there any elegant way that the library could handle it? I can't think of any myself, but I'd be interested to hear your thoughts.
Either way, with this issue filing, devs will be able to see at least one way to work around it.
In case of items/layout collision, focus gets lost
Usually noticeable when using absolute positioning with expandable containers
To Reproduce
@asgvard
Here's an example
/* 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 {View, Text, StyleSheet, TouchableOpacity, ScrollView} from 'react-native';
import {initNavigation, setKeyMap, withFocusable} from '@noriginmedia/react-spatial-navigation';
initNavigation({
debug: false,
visualDebug: false
})
// SpatialNavigation.setKeyMap(keyMap); -> Custom key map
const KEY_ENTER = 'enter';
const styles = StyleSheet.create({
wrapper: {
height: '100vh',
width: '100vw',
backgroundColor: '#333333',
flexDirection: 'row'
},
content: {
width:'100vw',
position:'absolute',
left:60
},
menu: {
width: 60,
maxWidth:200,
alignItems: 'center',
justifyContent: 'space-around',
left:0,
top:0,
position:'absolute',
height:'100vh',
},
menuFocused: {
backgroundColor: '#546e84',
width:200,
zIndex:1
},
menuItem: {
width: 50,
height: 50,
backgroundColor: '#f8f258'
},
activeWrapper: {
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'
},
{
title: 'Program 10',
color: '#c04e43'
},
{
title: 'Program 11',
color: '#f05e46'
}
]);
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);
}
componentDidMount(){
this.props.setFocus();
}
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);
const App = () => (<View>
<SpatialFocusable focusable={false} />
</View>);
export default App;
I have a small application set up at the moment with some routing in place to navigate screens.
Just using react-router: 5.0.0
at the moment.
My question is on how I might retain state of focus between component rendering.
If I am on a category page with some rows like your example application and I navigate away from this screen does the inbuilt focus state persistence of child components still function?
I am asking as i am having trouble getting this to remember focus state of parents and child components when returning from another route.
Thanks for all the work you have put into this library.
Describe the bug
We currently have a bug where I am able to touch focusable events that are covered by the current screen i'm on using React Navigation
. We are using React Navigation to push new screens and when we do this we are running into issues with Focus events getting lost and focusing on items in the background of the current screen. Is there a solution to this?
Expected behavior
I would expect to only be able to focus on items on the current screen rather than HOC focusable components.
Any guidance / help would be greatly appreciated.
Describe the bug
Focus is lost when focused item is removed from the DOM
To Reproduce
Steps to reproduce the behavior:
Page loads focus is set to a previous item.
That item is removed from the DOM
Navigation control is lost
Expected behavior
Would think a navigation should re focus to the next available item.
Screenshots
If applicable, add screenshots to help explain your problem.
Additional context
Add any other context about the problem here.
In smartNavigate
reference points are calculated twice for all the siblings
When I added <html style="direction: rtl">
to my application, navigation was not working correctly.
The first focused item on the lists was incorrect.
Describe the bug
I have a vertical list of rectangles (dvd covers), 4 across and 4 down. The screen can display about 1.25 rows at a time. The first three rows are focusable, the last row is not. Once the focus reaches the bottom row, the siblings array is empty despite visualDebug recognizing the items.
To Reproduce
Steps to reproduce the behavior:
Expected behavior
To be able to focus on all focusable elements.
Additional context
None
Is your feature request related to a problem? Please describe.
Can't use your library with typescript
Describe the solution you'd like
Include typescript typings in project
Additional context
I wrote my own typings to make spatial-navigation work with typescript, they are not complete and not 100% true, so please consider adding them to a repository after modification.
declare module '@noriginmedia/react-spatial-navigation' {
import { FunctionComponent, ReactComponentElement } from 'react';
import { FocusableProps } from '@noriginmedia/react-spatial-navigation';
interface Layout {
width: number;
height: number;
x: number;
y: number;
top: number;
left: number;
node: HTMLElement;
}
interface FocusableProps {
focused: boolean;
hasFocusedChild: boolean;
setFocus(focus: string): void;
}
interface FocusableWrapperProps {
focusKey?: string;
focusable?: boolean;
onEnterPress?(event: unknown): void;
onBecameFocused?(layout: Layout): void;
children?: ReactNode;
}
interface WithFocusableOptions {
trackChildren?: boolean;
}
interface InitNavigationParams {
debug?: true;
visualDebug?: true;
}
export function initNavigation(params?: InitNavigationParams): void;
export function setKeyMap({ [string]: number }): void;
export function withFocusable(
WithFocusableOptions?: WithFocusableOptions
): <T>(
Component: FunctionComponent<T>
) => (
FC: Omit<T, keyof FocusableProps> & FocusableWrapperProps
) => ReactComponentElement;
}
This method recursively finds a focusable child without his own focusable children.
If the preferredChildFocusKey
and lastFocusedChildKey
are "disabled", we need to select at least something.
// getNextFocusKey method
/**
* Otherwise, trying to focus something by coordinates
*/
const sortedXChildren = sortBy(children, (child) => child.layout.left);
const sortedYChildren = sortBy(sortedXChildren, (child) => child.layout.top);
const {focusKey: childKey} = first(sortedYChildren);
this.log('getNextFocusKey', 'childKey will be focused', childKey);
return this.getNextFocusKey(childKey);
Here, we try to find a component closer to (x: 0, y: 0). It's O(2n)
const [{focusKey: childKey}] = sortBy(
children, ({layout}) => Math.abs(layout.left) + Math.abs(layout.top)
);
It's the same. O(n)
.
Or we can just pick a first child. It's very fast :)
const [{focusKey: childKey}] = children
Is your feature request related to a problem? Please describe.
We need to create a modal popup who goes on top of the other content, the focus should be bound in the modal and not go outside
Describe the solution you'd like
A way to keep the focus in a container, or disable other containers
Additional context
Another problem to keep in mind is when the focus is set async (after a delay or request) it can be lost by the current container/element
eg: this library is implementing sections, maybe it can be used as reference https://github.com/luke-chang/js-spatial-navigation/
Hello,
I have this code
`
const KeyOne = ({ value, focused }: any) => {
return (
<View style={focused ? styles.focused : {}}>
{value}
)
}
const KeyRow = ({ children, focused }: any) => {
return (
<View style={focused ? styles.focused : {}}>
<Text style={styles.keyRow}>{children}</Text>
</View>
)
}
const Container = ({ children, focused }: any) => {
return (
<div>{children}</div>
)
}
const FocusableKeyOne = withFocusable()(KeyOne);
const FocusableRow = withFocusable()(KeyRow);
const FocusableContainer = withFocusable()(Container);
return (
<FocusableContainer>
<FocusableRow focusKey={'ROW-1'}>
<FocusableKeyOne focusKey={'KEY-1'} value={"1"} />
<FocusableKeyOne focusKey={'KEY-2'} value={"2"} />
<FocusableKeyOne focusKey={'KEY-3'} value={"3"} />
</FocusableRow>
<FocusableRow focusKey={'ROW-2'}>
<FocusableKeyOne focusKey={'KEY-4'} value={"4"} />
<FocusableKeyOne focusKey={'KEY-5'} value={"5"} />
<FocusableKeyOne focusKey={'KEY-6'} value={"6"} />
</FocusableRow>
<FocusableRow focusKey={'ROW-3'}>
<FocusableKeyOne focusKey={'KEY-7'} value={"7"} />
<FocusableKeyOne focusKey={'KEY-8'} value={"8"} />
<FocusableKeyOne focusKey={'KEY-9'} value={"9"} />
</FocusableRow>
</FocusableContainer>
)
`
Its produces a small virtual keyboard like that:
But when Im on number 6 for example, and press UP the focus goes to 1. When I
m on number 8 for example and press UP the focus goes to 6.
I dont know what a reson due a framework dont get the closest value based on key pressed.
Someone can help me please?
I have two columns, both are focusable parents. The right parent has dynamically changing children. Both parents have trackChildren enabled, and the children have focusKey's set and are focusable. If i enable visual debug, they have names and green rectangles. But when navigating by arrow, it selects the children of the non-dynamic left column parent, but none of the children in the right column (dynamically changing children). I can see it selects the right column, but doesn't recognize it's children.
To Reproduce
Steps to reproduce the behavior:
Expected behavior
I expect the focus to transition between the parents without issue.
Screenshots
In this screenshot, I was navigating to the right from the letter F. It selected the right column parent (noted by the yellow cube). But it did not recognize it's children, which you can see by the lack of squares on the corners (which i circled). In practice, if I keep try to continue going right, it loops back to the letter A.
Additional context
I'm not 100% sure this is a bug, but I'm definitely stumped on why it's acting this way. Any help would be appreciated.
There are two scenarios here:
setFocus()
(a focus "teleport")At the minimum, components with focusable={false}
should not be focusable by shifting focus (which is the case right now).
It's less clear what the sensible behaviour should be for direct focus. As of 2.7.2
, focusable={false}
components can be focused by a direct call to setFocus()
.
From my view, if a component is marked as focusable={false}
, it should not be focusable under any circumstances. It probably has that property set because: it's off-screen or invisible; or needs to be left disabled due to a request being in-flight, or due to a privilege being inactive. There is a chance of the app entering an illegal state if an unexpected async action leads to a focusable={false}
component gaining focus via a focus teleport.
But I can also see that setFocus()
should reliably do its job of setting the focus (as advertised).
Is there perhaps a need for a distinction:
focusable={false}
– cannot be focused by a focus shift, nor by a focus teleport.navigable={false}
– cannot be focused by a focus shift, but can be focused by a focus teleport.Naming of course can be improved upon.
... Or an init-level option e.g. setFocusRespectsFocusableValue: true
.
An old browser (Firefox 5) throws an exception if addEventListener
doesn't have the third parameter (false
). I use the old Smart TV (Orsay)
MDN: https://developer.mozilla.org/ru/docs/Web/API/EventTarget/addEventListener
TODO:
I can add a new field to the init
(private of initNavigation
) method, like listenerOptions
(or useCapture
) and pass it to
init({
debug: debug = false,
visualDebug: visualDebug = false,
nativeMode: nativeMode = false,
throttle: throttle = 0,
listenerOptions = false
} = {}) {
// Rest code
}
window.addEventListener('keyup', this.keyUpEventListener, listenerOptions);
window.addEventListener('keydown', this.keyDownEventListener, listenerOptions);
What do you think about it?
Sometimes when navigating in a direction the focus jumps on an element in another direction.
It's not happening often, in our case only on Tizen when we keep pressed down/up keys in the homepage.
We investigated the problem and we found wrong layout.top
positions, causing wrong siblings detection in a direction ( the array siblings
here https://github.com/NoriginMedia/react-spatial-navigation/blob/master/src/spatialNavigation.js#L540 contains wrong elements )
The root cause is the setTimeout
in measureLayout
function
https://github.com/NoriginMedia/react-spatial-navigation/blame/master/src/measureLayout.js#L28
It seems some of the callbacks are executed before the actual sibling detection while some other are executed after.
We removed it to update the layouts synchronously and the problem disappeared.
If there isn't any reason to keep it I suggest to remove it.
Is your feature request related to a problem? Please describe.
The library react-spatial-navigation contains onBecameFocused (which works as JS focus - https://developer.mozilla.org/en-US/docs/Web/API/Element/focus_event), but not onFocusOut (oposite). Or maybe better case onFocused, which would returns true/false.
Describe the solution you'd like
MDN has this functionality like this:
https://developer.mozilla.org/en-US/docs/Web/API/Element/focusout_event
Describe alternatives you've considered
Currently we need to use a prop focused from child component A to send it to parent component B, if we want to use this focused property in another component C (included in component A).
In version 2.3.0
, I see in the changelog that you added support for non-Web environments. Thank you, this is a perfect solution for my use-case (which react-tv-navigation
falls short of), as I would love to deploy to as many platforms as possible.
Out of curiosity, how does react-spatial-navigation
work for mobile platforms? e.g. if a keyboard is attached, which is not unheard of on iPad, will spatial navigation still be useable?
Describe the bug
I'm trying to make my ScrollView scroll to the focused item (just like a Netflix-style carousel) whenever the focus updates (i.e. upon onBecameFocused
). It works flawlessly on the web (via React Native Web) but not on Android/iOS, wherein the x value is always reported as 0, rather than the focused item's currently-measured offsetLeft
.
To Reproduce
const scrollViewRef = React.createRef();
const SpatialItem = withFocusable()(({ item, ...props }) => <Text {...props}>{item.content}<Text/>);
const ScrollViewThatScrollsToFocusedItem = ({ items }) => (
<ScrollView ref={scrollViewRef} horizontal>
{items.map((item, index) => {
const itemFocusKey = `focusable-item-${item._key}`;
return (
<SpatialItem
item={item}
focusKey={itemFocusKey}
key={itemFocusKey}
onBecameFocused={
({ x, y, top, left, width, height }) => {
console.log(`[onBecameFocused] x: ${x}`);
// BUG: Logs incorrect value on native.
scrollViewRef.current.scrollTo({ x });
}
}
/>
);
})}
</ScrollView>
);
You can render <ScrollViewThatScrollsToFocusedItem>
as follows:
// Make several items in this format; I've included an array of one for brevity.
<ScrollViewThatScrollsToFocusedItem items={[{ _key: item1, content: "Item 1" }]} />
Expected behavior
The ScrollView should scroll along to the focused <SpatialItem>
child whenever the focused child updates.
Additional context
I believe the core of the problem is in measureLayout.js
. In the measureLayout()
function, the condition if (relativeNode) { }
is never satisfied for a native app, because the node.parentNode
API does not exist in React Native. Thus, the frame of the focusable is always reported as the default starting value (0
for all the values of the frame, e.g. x
and y
).
I've tried introducing the following else
block for that condition to fix things, but it's not really working:
else {
node.measure((fx, fy, width, height, px, py) => {
var frameRect = {
width: width,
height: height,
left: fx,
top: fy,
};
var pageRect = {
width: width,
height: height,
left: px,
top: py,
};
console.log('[measure] Component rect to frame is: ', frameRect);
console.log('[measure] Component rect to page is: ', pageRect);
/* Note: I'm not sure if this maths is accurate, but React Native doesn't expose
* any APIs closer to the DOM implementation, so I'll enter them as a proof-of-concept. */
var x = pageRect.left - frameRect.left;
var y = pageRect.top - frameRect.top;
callback(x, y, frameRect.width, frameRect.height, frameRect.left, frameRect.top);
});
}
This improves (but far from fixes) the behaviour as follows:
-1 *
scrollLeft
value. In other words, the measurement occurs too late for the onBecameFocused
callback; a stale value will always be received in the callback.offsetLeft
, minus one item's worth of extra offset, until you manually scroll the view back to the start (whereupon the maths becomes perfect), that I can't really explain.Disclaimer: it's a bit hard to characterise, so I may not be completely accurate in my above analysis of what the logic is behind the reported values for iOS and Android.
Basically, it's failing partially due to race conditions (due to both React Spatial Navigation's layout measurement process and React Native's asynchronous nature), partially due to Android/iOS differences, and largely because there are no equivalent measurement APIs to the DOM ones.
Could anyone more familiar with the layout measurement process please investigate this?
Related: #47 by @salvan13 (which improved the Web layout measurement behaviour, but had no impact on the native layout measurement behaviour).
I'm developing for Web in this case. Here's the situation:
However, the 'left' navigation action still took place. In order to stop it, I need to call keyEvent.preventDefault()
and keyEvent.stopPropagation()
while spatial navigation is paused. This behaviour looked to me like Spatial Navigation was queuing these key events and handling them upon unpause; does it do that, or am I probably just misunderstanding something in my own setup?
When I handle the wheel event, I need to move to the top or bottom (I won't use focus by keys).
We can use a smartNavigate
method of the SpatialNavigation
class.
onKeyEvent(keyCode) {
this.visualDebugger && this.visualDebugger.clear();
const direction = findKey(this.getKeyMap(), (code) => keyCode === code);
this.smartNavigate(direction); // We can use it
}
Is your feature request related to a problem? Please describe.
The existing example is for a web app, using window object even though it also imports from React Native. I managed to get it to run in a web browser, but I have not been able to run it on a Android emulator yet.
Describe the solution you'd like
A working example that we can run in a TV emulator
Describe alternatives you've considered
I tried to adapt the example to React Native, but I haven't gotten it to work
Additional context
Add any other context or screenshots about the feature request here.
Is your feature request related to a problem? Please describe.
A way to find the currently focused item.
This may already be available I am just not sure of the API to surface it sorry.
Describe the solution you'd like
SpatialNavigation.currentlyFocusedItem()
I am trying to find a way to get the currently focused item to return to after certain warnings or actions.
Is there a way to retrieve the current withFocusable
item that is in focus from the library?
I was trying to create navigation for my product. The case was to have two navigation elements highlighted (one of them focused). I needed to have access to all of navigation components to do that. However it was missing in my context. I came up with the solution to export focusableComponents from SpatialNavigation in index.js file. Please consider introducing this idea in your master branch or show me another way to achieve my goal.
The attached animation shows working solution with exported focusableComponents. As you can see element on left list is focused and the corresponding element on the right is highlighted.
Hi there,
first of all thanks for this package that seems very well maintained and complete!
I know that it might sound like a silly question, but can I use this package in a ReactJS project or it has to be a react-native-web one?
I'm trying to make a Samsung Tizen TV app and of course I wanted to use React or React Native for it, but the thing is I never used react-native for something besides iOS or Android.
I've managed to run a ReactJS app on my Samsung TV but I'm not sure how to do it with a react-native or react-native-web project
This is where I'm coming from: https://stackoverflow.com/questions/57773000/how-do-i-build-a-create-react-app-in-tizen
Thanks again!
Describe the bug
I created a CSS grid with a gap of "2vw 0" and react-spatial-navigation seems to have trouble deciding which cell it should navigate to. When navigating sideways, it will not focus on certain cells. The cell it will skip depends on which direction you're travelling in and the exact size of the grid. Changing the grid margin/padding or resizing the window will change the behaviour. This only seems to happen when the gap is exactly 0. Setting it to 1px negates the issue.
To Reproduce
I've created a simple example app here: https://github.com/EwanRoycroft/rsn-test/tree/grid
To reproduce, clone the repo and follow instructions to start the dev server. Once you have opened the app, try navigating from side-to-side and you should see it skipping cells. As I mentioned above, the behaviour is dependent on the size of the grid, so if you can't reproduce, try adjusting the margin of .page
or resize the window.
Examples
In this recording, see that RSN skips over the second cell when I navigate right. When I try to navigate back left, I cannot focus on the first column:
https://user-images.githubusercontent.com/2302279/128329581-7f48c43d-694a-408f-976f-9fccca6d3b4e.mov
In this recording, I have enlarged the window. RSN is no longer skipping over cells, but I cannot focus on the last column:
https://user-images.githubusercontent.com/2302279/128329614-6fe00128-4029-4cb1-bf1f-fe80e3d51c66.mov
Describe the bug
I have a class component, DismissButton, which needs to steal focus as soon as it mounts. The use-case is an error popup that appears and steals the spatial navigation focus to one of its buttons (such as "Dismiss" or "Retry"). Here is how I've written it:
import { withFocusable } from "@noriginmedia/react-spatial-navigation";
import * as React from "react";
import { Text, TouchableOpacity } from "react-native";
class DismissButton extends React.PureComponent {
ref = React.createRef();
componentDidMount(): void {
const { stealFocusOnAppearance, focusable, focusKey, setFocus } = this.props;
if (stealFocusOnAppearance && focusable) {
console.log(`[DismissButton.componentDidMount] stealing focus for: "${focusKey}"`);
setFocus(); // Fails because addFocusable(/* ... */) has not occurred yet
}
}
render() {
const { text = "Dismiss", focused, onClick } = this.props;
return (
<TouchableOpacity ref={this.ref} onPress={onClick}>
<Text style={{ backgroundColor: focused ? "yellow" : "white" }}>{text}</Text>
</TouchableOpacity>
);
}
}
const DismissButtonFocusable = withFocusable({})(DismissButton);
As you can see, DismissButton has its own componentDidMount()
method in which it calls this.props.stealFocus()
, but at this moment, it appears that DismissButton has not been registered in focusableComponents
. Registration (by addFocusable()
) happens only during the componentDidMount()
lifecycle method added to the DismissButtonFocusable HOC by the recompose
library.
To my understanding, DismissButton mounts before the HOC, and thus the order is:
DismissButton.componentDidMount() // In which we call setFocus()
DismissButtonFocusable.componentDidMount() // In which addFocusable() runs
If I wrap DismissButton.componentDidMount()
's call to setFocus()
in a timeout, then it succeeds.
For some reason, there is no such problem in the "Menu" class component in the repo's sample app, though:
https://github.com/NoriginMedia/react-spatial-navigation/blob/master/src/App.js#L145
I'm wondering if I'm missing something.
Hi, guys, brilliant work on this library. The only problem I've found so far is here:
Describe the bug
When I create myself a component, then wrap it with the focusable HOC, if I try to assign a ref it breaks...
To Reproduce
const Li = styled.li`
grid-column-end: span ${props => props.span};
border: 3px solid ${props => props.focused ? 'red': 'yellow'};
`
const GridItemFocusable = withFocusable()(Li);
...
constructor(props) {
super(props);
this.wrapperRef = React.createRef();
}
...
<GridItemFocusable
ref={this.wrapperRef}
onBecameFocused={this.onGridItemFocused}
{...this.props}
>
TypeError: Cannot read property 'scrollIntoView' of null
> 23 | this.wrapperRef.current.scrollIntoView()
Expected behavior
I expect to be able to use refs so I don't have to use a redundant span in this case like this:
<GridItemFocusable
onBecameFocused={this.onGridItemFocused}
{...this.props}
>
<span ref={this.wrapperRef}>
{this.props.children}
</span>
</GridItemFocusable>
Styled Components supports the ref, but it seems to break after the wrap with the HOC
Screenshots
If applicable, add screenshots to help explain your problem.
Additional context
Add any other context about the problem here.
Is your feature request related to a problem? Please describe.
Consider for example 3 elements (rectangles next to each other in the row). The first overlaps a bit the second and the third is not overlapped by the second. If you press right from the first one, then you will appears on the third one instead of the second one.
Describe the solution you'd like
Consider rectangle element. This element should includes some collision checker / condition, which checks if element is overlapping some element or is overlapped by the second element. Now we have possibilities how to detect where is the second element. Some examples:
Describe alternatives you've considered
If we don't won't to go with some overlapping solution we need to use some manual focusing techniques and that's not optimal.
We need to move updateAllLayouts
from setFocus
to another place. Because if a person doesn't use smart navigation (only focus by key) we don't need to update the layout of nodes.
Problem:
I get a 'window is not defined' error when I try to use in SSR application with razzle framework because virtualDebugger is using 'window' object without checking it is exist or not.
Even I got an error without using virtual debugger.
Solution:
Adding basic 'window' check for SSR support.
Describe the bug
Documentation implies that initialising the library and setting a key map should be done with the following property names from: https://github.com/NoriginMedia/react-spatial-navigation#initialization
// Optional
setKeyMap({
'left': 9001,
'up': 9002,
'right': 9003,
'down': 9004,
'enter': 9005
});
Using these name value pairs does not seem to work.
Expected behavior
Should be able to set a device specific key map for left, right, up, down & enter.
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
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';
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';
initNavigation({
debug: false,
visualDebug: false
})
const styles = StyleSheet.create({
menu2: {
maxWidth: 900,
flexDirection: 'row'
},
menuFocused2: {
backgroundColor: '#546e84'
},
topNavmenuItem: {
color: '#fff',
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 {
render() {
// console.log('Menu item rendered: ', this.props.realFocusKey);
return (<TouchableOpacity style={[styles.topNavmenuItem, this.props.focused ? styles.TopNavfocused : null]}><Link 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() {
this.props.setFocus();
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.menu2, this.props.hasFocusedChild ? styles.menuFocused2 : null]}>
<TopNavMenuItemFocusable focusKey={'TOPMENU-1'} itemText='Link1' linkTo='' />
<TopNavMenuItemFocusable focusKey={'TOPMENU-2'} itemText='Link2' linkTo='Link2' />
<TopNavMenuItemFocusable focusKey={'TOPMENU-3'} itemText='Link3' linkTo='Link3' />
<TopNavMenuItemFocusable focusKey={'TOPMENU-4'} itemText='Link4' linkTo='Link4' />
<TopNavMenuItemFocusable focusKey={'TOPMENU-5'} itemText='Link5' linkTo='Link5' />
<TopNavMenuItemFocusable focusKey={'TOPMENU-6'} itemText='Link6' linkTo='Link6' />
</View>);
}
}
TopNavMenu.propTypes = {
setFocus: PropTypes.func.isRequired,
hasFocusedChild: PropTypes.bool.isRequired
};
const TopNavMenuFocusable = withFocusable({
trackChildren: true
})(TopNavMenu);
class TopNavSpatial extends React.Component {
render() {
return (
<View style={styles.wrapper}>
<TopNavMenuFocusable
focusKey={'TOPMENU'}
/>
</View>
);
}
}
TopNavSpatial.propTypes = {
navigateByDirection: PropTypes.func.isRequired
};
const TopNavSpatialFocusable = withFocusable()(TopNavSpatial);
const TopNav = () => (<View>
<TopNavSpatialFocusable />
</View>);
export default 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 {initNavigation, setKeyMap, withFocusable} from '@noriginmedia/react-spatial-navigation';
initNavigation({
debug: false,
visualDebug: false
})
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() {
//this.props.setFocus();
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);
const Link6 = () => (<View>
<SpatialFocusable focusable={false} />
</View>);
export default Link6;
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.