Skip to content

Support passing a reference to FocusScope for controlling its tab order #2421

Closed
@kherock

Description

@kherock

🙋 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, the nodeToRestore 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 or Shift+Tab inputs, focus is restored relative to nodeToRestore1. So even when the FocusScope is positioned far away from its trigger, tabbing out of it causes focus to move
  • 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

  1. The existing behavior also questionably makes the nodeToRestore inaccessible via Shift+Tab. If I have a non-modal popup rendered below a button, I'd expect that Shift+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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions