NoriginMedia / react-spatial-navigation

DEPRECATED. HOC-based Spatial Navigation. NEW Hooks version is available here: https://github.com/NoriginMedia/norigin-spatial-navigation
MIT License
226 stars 64 forks source link

Add sections/gorups/containers of focusable elements #54

Closed salvan13 closed 4 years ago

salvan13 commented 4 years ago

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/

asgvard commented 4 years ago

Would it be a solution to mark focusable component as “border” that will stop any navigation outside of it? I mean before propagating the navigation up to the parent we could check this boolean prop that will restrict the up-tree propagation?

shirakaba commented 4 years ago

I have the same use-case. Currently it's quite fiddly to solve.

I'm using React Navigation as my navigation library, so components can listen for didFocus and didBlur navigation events. Upon receiving either of these events, I set this.state.screenIsFocused accordingly, and grab focus to one of the focusables on my screen, e.g.:

navigation.addListener('didFocus', () => {
    this.setState(
        { screenIsFocused: true },
        () => {
            spatialNavigation.setFocus("Modal/Dismiss");
        }
    );
});

Additionally, for every single focusable on each of my screens, I couple the value of focusable to this.state.screenIsFocused (and also this.state.modalVisible, just in case).

<NavigatorBackButtonFocusable
    focusKey={"MainScreen/BackButton"}
    /* The `this.state.modalVisible` check is redundant, but I've added for peace of mind
     * in case React Navigation has any race conditions that I don't understand. */
    focusable={this.state.screenIsFocused && !this.state.modalVisible}
/>

Library-level solution

Border

Would it be a solution to mark focusable component as “border” that will stop any navigation outside of it?

@asgvard I think that would be a potential solution. Would a "border" focusable component itself be focusable?

There are edge-cases to consider if we think beyond up-down-left-right spatial navigation, though (if using this library as a cross-platform solution, where more interaction methods are possible):

  1. In a web app, does it safely handle the case of a user pressing tab to manually move the browser's highlight focus to another focusable element outside of the border?
  2. In a web app, what happens if a user clicks on another focusable element outside the border?
  3. In a mobile app, what happens if a user taps on another focusable element outside the border?

There is a valid argument though that these edge cases are out of scope of the library and that such cases need to be guarded against by the developer instead.

Namespacing

Another potential option would be to restrict navigation to within a certain namespace.

I'm currently namespacing each of my focusKeys (e.g. for <Sidebar> components on my "MainMenu" and "Synopsis" screens, I'd use the focus keys MainMenu/Sidebar and Synopsis/Sidebar; for multiple items in a carousel, I'd use MainMenu/Category-0/Item-0). So, for example, smart navigation would be unable to navigate from MainMenu to Synopsis.

This idea would need some deeper thought to cover all sorts of use-cases, however.