Description
🙋 Feature Request
I could also consider this a bug report, but its fix might require a breaking change, so I'm reporting this as a feature request instead.
I'd like for a better way to control how focus is restored when tabbing in an out of a FocusScope. Due to the nature of how Portals work, it's impossible to accomplish this effectively without updating its API.
🤔 Expected Behavior
When a FocusScope is created, its primary responsibility is to ensure that attempts to move focus in and out of the scope are handled correctly. When focus is contained, I expect that Tab
will wrap focus to the beginning and Shift+Tab
will wrap to the end. However, when focus isn't contained, I expect that focus will return to a logical position in the virtual DOM. Since the FocusScope has no way to know where in the DOM this is without additional context, it should rely on the native tab ordering.
In the case where the FocusScope's location in the virtual DOM is vastly different from the actual DOM (such as with Portals), I expect to pass an additional hint to indicate where focus should go when tabbing in and out of the scope. In 99% of cases, this will just be the trigger element responsible for opening the portal, so a sensible default value for this hint is to just use the nodeToRestore
(which is the existing behavior when restoreFocus
is true).
😯 Current Behavior
The existing FocusScope implementation has special handling to make moving focus in and out of portals more seamless. When containment is disabled, it uses the following approach:
- when
restoreFocus
is enabled, thenodeToRestore
anchors the focus scope to the tab order- this means that when focus leaves the FocusScope due to being unmounted, focus is returned to the
nodeToRestore
- additionally, if focus leaves the FocusScope due to
Tab
orShift+Tab
inputs, focus is restored relative tonodeToRestore
1. So even when the FocusScope is positioned far away from its trigger, tabbing out of it causes focus to move
- this means that when focus leaves the FocusScope due to being unmounted, focus is returned to the
- when
restoreFocus
is false, all tab handling is disabled- attempts to move focus out of the FocusScope use native tabbing behavior. When the FocusScope is rendered inside of a Portal, focus typically moves far away from the component's DOM context.
💁 Possible Solution
I drafted up an initial idea here: #2416 (comment)
🔦 Context
I have a popup component that supports rendering itself in a portal. It implements a non-modal disclosure pattern where focus is restored to the trigger when closed, but it is allowed to stay open when focusing or interacting outside of the popup. Users should be able to freely tab in or out of the popup, and when they do so, focus is moved to a position that corresponds to its position on the page rather than the element that opened it.
💻 Examples
🧢 Your Company/Team
🎁 Tracking Ticket (optional)
Footnotes
-
The existing behavior also questionably makes the
nodeToRestore
inaccessible viaShift+Tab
. If I have a non-modal popup rendered below a button, I'd expect thatShift+Tab
would move focus out of the popup and back onto the button. Instead, the existing behavior attempts to move focus to an often non-existent element situated before the button in the DOM ↩