Coder Social home page Coder Social logo

noriginmedia / react-spatial-navigation Goto Github PK

View Code? Open in Web Editor NEW
226.0 17.0 64.0 7.56 MB

DEPRECATED. HOC-based Spatial Navigation. NEW Hooks version is available here: https://github.com/NoriginMedia/norigin-spatial-navigation

License: MIT License

HTML 0.32% JavaScript 99.68%
react spatial-navigation hoc recompose

react-spatial-navigation's People

Contributors

asgvard avatar danieledilucido avatar enrico-bardelli avatar guilleccc avatar jakubkubista avatar petrumo avatar predikament avatar salvan13 avatar shirakaba avatar smishra25 avatar stuartflanagan avatar yarikleto avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

react-spatial-navigation's Issues

Error when Server Side Rendering

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.

Screenshots
Screen Shot 2021-06-26 at 10 00 27 pm

Additional context
This issue was fixed

How should I handle focus when opening/closing a pop-up?

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?

Performance/Crash issue when load multiple rails and hold arrow keys to navigate

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.

Sometimes navigation doesn't work after go back to previous screen

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:

  1. Go to next screen
  2. Go back
  3. Element is focused but the remote control doesn't work

Fire `onClick` when enter pressed of `button` like `onPress` example with `TouchableOpacity`

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);

How to restrict sets of focusables based on screen/route?

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.

a vulnerability CVE-2020-15168 is introduced in @noriginmedia/react-spatial-navigation

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

  1. in react-dnd, version 7.4.1 ➔ 7.4.2, remove recompose via commit
  2. in @nivo/legends, version 0.67.0 ➔ 0.68.0, remove recompose via commit

Are there any efforts planned that would remediate this vulnerability or migrate recompose?

Thanks
; )

Allow focus / onClick of native browser elements

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.

Focuses the second item in the list, after navigating from the fixed menu

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:

  1. Navigate to the menu from the first item in the list
  2. Click on 'right'
  3. Second element of the list is focused

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

Navigation jump

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

Focusable elements not recognised when layout is position absolute

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.

Working example for Tizen TV

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;

Create React hook (e.g `useFocusable`) that compliments the existing HOC.

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.

[Web] Upon arrow key press, any currently-selected items should be deselected

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:

  1. Click on any FocusableComponent. This will inadvertently cause its <TouchableOpacity> child to be selected (e.g. in the same way as if you'd highlighted a text field).
  2. Press the arrow keys to move focus to a different FocusableComponent.
  3. Press Enter.

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.

Focus gets lost in case of collision

In case of items/layout collision, focus gets lost
Usually noticeable when using absolute positioning with expandable containers

To Reproduce

  1. Edit the example, change the position for menu and carousels container to absolute.
  2. Expand menu width on focus
  3. focus will get lost (trapped within focus parent/scope)

@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;

Question RE: remembering focus state of screen

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.

Focusable items hidden are able to be accessed.

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.

Focus lost when Item Removed

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.

RTL is not supported

When I added <html style="direction: rtl"> to my application, navigation was not working correctly.
The first focused item on the lists was incorrect.

SIblings array is empty

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:

  1. Create a container of items that go well off screen
  2. Try to focus on the last items (the most off-screen)
  3. Unable to select last items
  4. See error

Expected behavior
To be able to focus on all focusable elements.

Screenshots
Screenshot 2021-01-15 09 07 40

Additional context
None

Typescript Support

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;
}

Optimization of `getNextFocusKey` method

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

Add sections/gorups/containers of focusable elements

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/

Select closest elements

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:

image

But when Im on number 6 for example, and press UP the focus goes to 1. When Im 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?

Track Child not recognizing children

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:

  1. Create two focusable parents in a row.
  2. Have one with static children that are focusable. Have the other change it's children based on input of the other parent.
  3. Then try to navigate between the two.
  4. See you are unable to navigate between the two.

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.
Capture

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.

Should components be focusable via setFocus() when focusable={false}?

There are two scenarios here:

  1. Directly focusing via setFocus() (a focus "teleport")
  2. Shifting focus via the directional keys (a focus "shift")

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:

  1. focusable={false} – cannot be focused by a focus shift, nor by a focus teleport.
  2. 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.

The third parameter to `addEventListener`

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?

Focus jumps on wrong component

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.

Create onFocusOut

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).

[Question] Behaviour on React Native (in contrast to React Native Web)

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?

[Bug] onBecameFocused always returns 0 for all dimensions on native platforms

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:

  • iOS: reports the scrollView's previously-measured -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.
  • Android: reports the focused item's currently-measured 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).

[Question] Are keydown events queued while spatial navigation is paused?

I'm developing for Web in this case. Here's the situation:

  1. I have a video player screen;
  2. After ten seconds, the UI controls disappear and stop obstructing the full-screen video. I pause spatial navigation now that there are no focusable components on screen;
  3. I receive a 'left' button press, and so I reveal the UI controls again;
  4. I unpause spatial navigation as soon as the UI controls have reappeared.

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?

Need to add setFocusByDirection("left", "right", "top" or "bottom")

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
}

A Working React Native Example

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.

A way to find the currently focused item.

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?

Exporting focusableComponents object from SpatialNavigation

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.

video

[Question]Can I use this package in a ReactJS project?

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!

CSS gap:0 breaks navigation

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

Components wrapped by withFocusable cannot use setFocus during componentDidMount.

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.

Error when trying to assign ref

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.

Overlapped elements are not focusable

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:

  1. Find which side (bottom, top, right, left) is the closest one to the overlapping node.
  2. Find the closest node to the center of the first element where they overlapping.
  3. Split the first element to the 4 or 9 smaller parts with the same size. Check which size is mostly overlapped by the second element.

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.

Optimization for very slow TV

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.

SSR Support

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.

Custom keyMap via setKeyMap does not work

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.

Focus between components

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;

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.