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

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

Closed EwanRoycroft closed 2 years ago

EwanRoycroft commented 3 years ago

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?

asgvard commented 3 years ago

Hi, sorry for the late reply. We have a number of similar use cases where we need to steal focus on the popups (or when using the stack navigation where you can have multiple pages stacked at the same time). Usually when the popup appears - it steals focus. Then whenever the popup closes - there is always some "parent" component that is aware of the popup state, and when the popup hides - that parent does the job of restoring the focus where it should be. As per your example here, the App component knows when the popup is shown and hidden, so it can decide that when the popup is hidden (via usePrevious hook) - it needs to focus let's say "page1" by its focus key. The basic rule is to have the popup to steal focus on itself when it's shown, but the parent that is aware of the popup state to decide what to focus when the popup is hidden. Hope it helps :)

asgvard commented 3 years ago

As for the "lastFocusedChild" logic - it only applies when you focus some parent component. It will try to focus the last focused child within that focusable tree. However it doesn't automatically happen when something loses focus. This you have to handle yourself.

asgvard commented 2 years ago

Closing due to inactivity