facebook / facebook/astryx

Dialog captures the wrong trigger when focusable content mounts on open

Open
#5,637 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
13.1k
Forks
1.1k
Avg merge
1d 14h
Merged PRs (30d)
669

Description

## Version

`@astryxdesign/core@0.5.0`

## Problem

A controlled `Dialog` can restore focus to `` instead of its opener when its content is mounted only while `isOpen` is true.

`DialogHeader` runs a child `useEffect` that focuses its heading on mount. React runs that child effect before the parent `Dialog` effect that assigns `triggerElementRef.current = document.activeElement`. The dialog therefore captures its own heading (or another newly mounted autofocus child), not the external trigger. If the closed state removes/sanitizes that content, the captured node is detached before close restoration.

`DialogHeader` also focuses its heading when mounted under a closed Dialog because its `shouldAutoFocus` check only excludes inline mode, not `isOpen === false`.

## Minimal shape

```tsx
setOpen(true)}>Open

{open ? (
} content={...} />
) : null}

```

1. Focus and activate `Open`.
2. Close with Escape or `onOpenChange(false)`.
3. Focus lands on `body`, not `Open`.

This shape is useful when a closed dialog must not retain a sensitive payload in the DOM.

## Expected

The Dialog captures the external active element before descendant mount/autofocus effects, and restores that element after the controlled `true -> false` transition. `DialogHeader` should not focus while the parent Dialog is closed.

Potential directions: capture the rising-edge trigger before passive descendant effects, expose `isOpen` in `DialogContext`, and make the header title the Dialog's supported `data-autofocus` target rather than running an unconditional mount effect.

Contributor guide

Open the contributing guide

Research direction

Start with the controlled Dialog and DialogHeader focus and effect behavior described in the issue, then reproduce the minimal conditional-mounting shape with the Escape close path. Verify that the external opener is captured before descendant autofocus effects, restored after the true-to-false transition, and that a closed DialogHeader does not focus; the issue's potential directions identify the relevant context and autofocus areas to investigate.

Written by the indexing model from the issue text.

Assessment

Tech stack
react, typescript
Domain
accessibility, frontend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.