Skip to content

feat: restore the focus when a root becomes active again - #229

Open
pujarajeshraju-bit wants to merge 1 commit into
bamlab:mainfrom
pujarajeshraju-bit:feature/restore-focus-on-activate
Open

pujarajeshraju-bit wants to merge 1 commit into
bamlab:mainfrom
pujarajeshraju-bit:feature/restore-focus-on-activate

Conversation

@pujarajeshraju-bit

@pujarajeshraju-bit pujarajeshraju-bit commented Oct 21, 2026 •

Copy link
Copy Markdown

The problem

In a multi-screen TV app, every screen usually has its own SpatialNavigationRoot, and only the active one is enabled (isActive). When the user navigates to another screen and comes back, the focus should be on the element they left, not on the first element of the screen.

Today it is not, for two reasons:

  1. When the content of a screen is re-rendered, the focused node is unregistered. LRUD forgets where the focus was (currentFocusNode = undefined), and another element takes it. This is the "focus jumps after screen navigation" reported in Focus jumps after screen navigation when using SpatialNavigationRoot — breaks without it due to VirtualizedList dependency #195.
  2. Because the select event is sent to the focused element (LRUD.handleKeyEvent runs onSelect on the current focus node, and does nothing when there is none), the "select" button press is sent to the wrong element, or lost entirely. This is the "it is not possible to select the focusable element for the 2nd time when we return to the previous screen" reported in It is not possible to select the focusable element for the 2nd time when we return to the previous screen. #174.

The proposal

An opt-in restoreFocus prop on the SpatialNavigationRoot:

<SpatialNavigationRoot isActive={activeScreen === 'home'} restoreFocus>
  {/* ... */}
</SpatialNavigationRoot>

When the root is deactivated - because isActive became false, or because the spatial navigation was locked by a modal - the root remembers where the focus was. When the root becomes active again, the focus is given back to that element.

It works for the modal case too: the focus of the screen behind the modal is restored when the modal is closed.

Why the position in the tree, and not the id

The ids of the nodes are generated when they are mounted (lodash.uniqueid), so they change every time the content of a screen is re-rendered. The position of the node in the tree is stable, so the path of the focused node (the list of the indexes of its children, from the root of the tree down to the node) is what is remembered.

This has the same constraint as the one described in the pitfalls about conditional rendering: the order of the nodes has to be stable. When it is not, the focus is restored to the element that is at that place in the tree, and if there is no focusable element there, the focus is left where it is.

The default behaviour is unchanged: restoreFocus is false by default.

Tests

4 new tests in packages/lib/src/spatial-navigation/components/tests/SpatialNavigationFocusRestore.test.tsx:

Two of them fail without the change.

yarn test (lint, types, core tests and example tests) passes.

Where it comes from

This was implemented for CookCue, a React + Vite cooking app for Fire TV (Amazon Developer Hackathon 2026). The app is a web app running in a WebView, and it has the exact same constraint: the focus has to come back where the user left it when they navigate back to a screen.

In a multi-screen app, each screen usually has its own SpatialNavigationRoot
and only the active one is enabled. When the user navigates to another screen
and comes back, the focus should be on the element they left, and not on the
first element of the screen.

Today the focus is lost when the content of a screen is re-rendered: the
focused node is unregistered, LRUD forgets where the focus was, and another
element takes it. Because the select event is sent to the focused element,
the "select" button is then sent to the wrong element (or lost), which is
the behaviour reported in bamlab#174 and bamlab#195.

This adds an opt-in `restoreFocus` prop to the SpatialNavigationRoot. When the
root is deactivated (because `isActive` became false, or because the spatial
navigation was locked by a modal), the root remembers where the focus was.
When the root becomes active again, the focus is given back to that element.

The position of the node in the tree is used to find the element back, and not
its id: the ids are generated when a node is mounted, so they change when the
content of the screen is re-rendered.

The default behaviour is unchanged: the prop is false by default.

Closes bamlab#174
Refs bamlab#195

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant