Skip to content

fix(ui): restore shadow dialog focus after rapid reopen (YPE-5889) - #414

Open
abharms wants to merge 12 commits into
journey-to-the-shadow-domfrom
ype-5889-shadow-overlay-runtime
Open

abharms wants to merge 12 commits into
journey-to-the-shadow-domfrom
ype-5889-shadow-overlay-runtime

Conversation

@abharms

@abharms abharms commented Sep 16, 2026

Copy link
Copy Markdown
Collaborator

Summary

Targets journey-to-the-shadow-dom, which contains the existing Shadow DOM overlay seam.

This is pre-rollout work. The browser stories exercise the real shared overlay implementations, but no released production component currently uses this Shadow DOM overlay path.

  • preserve the original opener when a controlled shadow dialog closes and reopens during retained exit presence
  • restore final focus only when the target remains connected in its captured root, which must still be the owning shadow root or owning document
  • discard stale focus targets when a consumer prevents close autofocus
  • add direct Chromium evidence for rapid reopen, nested teardown, cross-root, cross-document, and disconnected targets
  • reconcile the Shadow DOM ADR and rollout plan, and add a UI patch changeset

Requirement evidence

Jira acceptance area Evidence
Rapid close/reopen regression The Chromium story closes the dialog, waits until Content unmounts while the Overlay remains present and focused, reopens it, dismisses it again, and asserts final focus returns to the original Run control.
Valid final restoration useShadowDialogFocus retains the first captured opener through the interrupted exit; ShadowRootHost restores it after modal release.
Invalid targets ignored Restoration now requires a connected target that remains in its captured root, which must still be the owning shadow root or owning document. Chromium evidence covers moving the opener into the light DOM, another shadow root, or another document, and removing it from the document.
Nested overlays remain correct The existing popover-to-dialog story exercising the shared implementation now asserts intermediate focus inside the popover and final focus on the original outside control. The inverse dialog-to-popover story remains green.
Consumer-prevented autofocus A focused unit regression proves that preventing one close autofocus clears the stored opener so a later dialog session captures and restores its own opener.
Documentation and release impact ADR 0007 and the rollout plan now record supported final restoration and retain peer concurrency/cross-browser limits. A UI patch changeset is included.

Review scope

Required outcomes:

  • restore the original valid opener after rapid close/reopen during retained exit presence
  • ignore disconnected and cross-root restoration targets
  • preserve nested popover/dialog and light-DOM behavior
  • cover the behavior with focused unit and Chromium evidence

Permitted support work:

  • the smallest focus-lifecycle changes in the existing dialog and shadow-host seams
  • focused Storybook fixtures and unit regressions
  • directly corresponding ADR/rollout documentation and a patch changeset

Non-goals:

  • an overlay manager, ownership stack, or peer-concurrency runtime
  • public API, dependency, or broad Radix behavior changes
  • unrelated cleanup or automatic component rollout
  • exhaustive cross-browser or assistive-technology validation

Review only against YPE-5889 and this stated scope. Treat correctness defects, regressions, explicit repository-rule violations, and unmet required outcomes as blocking. Treat broader hardening, optional refactors, and out-of-scope improvements as follow-up work.

Verification

  • focused Chromium Shadow DOM overlay story file — 9/9 passed
  • focused dialog unit tests — 5/5 passed
  • pnpm --filter @youversion/platform-react-ui typecheck — passed
  • pnpm lint — passed
  • pnpm turbo build --force --filter=@youversion/platform-react-ui — passed
  • pnpm test — passed on clean rerun; all six workspace tasks green, UI 547/547
  • pnpm exec prettier --check ... and git diff --check — passed

The first concurrent full-suite run hit the existing five-second timeout in bible-reader-controlled.test.tsx. The unchanged timed-out test passed 33/33 in isolation, and the complete suite then passed on rerun.

Compatibility and limitations

  • Light-DOM/document-portaled dialogs continue through the unchanged Radix-owned focus path.
  • Public exports, props, types, and dependencies are unchanged.
  • Peer overlay concurrency remains outside this ticket and is still documented as rollout work.
  • Chromium provides the direct browser evidence; cross-browser and assistive-technology validation remain follow-up gates.

Jira

RetriggerConfidence Score: 5/5

The PR appears safe to merge.

Summary

The PR preserves the original focus target when a Shadow DOM dialog reopens during retained exit presence and validates that target before final restoration.

  • Adds owner-bound scheduling, reclamation, and cancellation of deferred focus restoration.
  • Rejects disconnected, cross-document, and moved cross-root targets.
  • Adds focused unit and Chromium story coverage and updates the Shadow DOM documentation and changeset.

Diagram

sequenceDiagram
    participant O as Original opener
    participant D as Shadow dialog
    participant H as ShadowRootHost
    O->>D: Open dialog
    D->>D: Capture opener and root
    D->>H: Schedule restoration on close
    Note over D,H: Overlay remains during exit
    D->>H: Reclaim restoration on rapid reopen
    D->>H: Schedule restoration on final close
    H->>H: Release final modal presence
    H->>H: Validate connection, document, and root
    H->>O: Restore focus when valid
Loading

Reviews (9) · Last reviewed commit: "Merge remote-tracking branch 'origin/jou..."

@changeset-bot

changeset-bot Bot commented Sep 16, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 6315896

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 4 packages
Name Type
@youversion/platform-react-ui Patch
vite-react Patch
@youversion/platform-core Patch
@youversion/platform-react-hooks Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 16, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-17T16:58:44.647337Z 6315896 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bb78417ce3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/ui/src/lib/shadow-root-host.tsx Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 542f73ae2a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

return;
}
if (!container || capturedRestoreFocusRef.current) return;
if (!container || capturedRestoreFocusRef.current || restoreFocusRef.current) return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge issue: Preserve the opener through overlay-only exit presence

When Content has a shorter exit animation than Overlay and the dialog is programmatically reopened after Content unmounts but before Overlay does, the first onCloseAutoFocus consumes and clears restoreFocusRef while modal presence is still retained by the Overlay. This guard then allows the reopen to capture the transient post-unmount focus (typically the document body/host) instead of the original opener, so the final close does not restore focus correctly. The original target needs to remain associated with the retained modal session until all modal presence ends, not merely until Content's close-autofocus fires.

Useful? React with 👍 / 👎.

@abharms
abharms marked this pull request as draft September 16, 2026 22:11
@abharms
abharms marked this pull request as ready for review September 17, 2026 13:30

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a305090ab2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +140 to +143
if (event.defaultPrevented) {
restoreFocusRef.current = null;
restoreFocusRootRef.current = null;
return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge issue: Cancel deferred focus when autofocus is prevented

When Content has already unmounted during a staggered exit, restoreFocusWhenModalReleased has copied the opener into ShadowRootHost's pending-focus slot. If the dialog reopens before Overlay unmounts and the consumer prevents the final onCloseAutoFocus, this branch clears only the hook-local refs; when modal presence finally ends, setModalPresent still focuses the previously queued opener, overriding the consumer's preventDefault(). The prevented path therefore also needs to cancel the host-owned deferred restoration for this dialog session.

Useful? React with 👍 / 👎.

@abharms
abharms requested a review from cameronapak September 17, 2026 14:54

@cameronapak cameronapak left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review

YPE-5889

Summary

Standards: 0 must-fix. Spec: 0 must-fix. Primary concern: none.

Review evidence
  • Scope: reviewed revision, Jira requirements, PR discussion, prior review threads, changed code, nearby lifecycle code, tests, ADR, and rollout plan.
  • Method: independently reviewed Standards, Spec, and Correctness; traced initial open, content-first exit, overlay-only presence, reopen, final close, prevented autofocus, invalid targets, nested overlays, cleanup, and light-DOM compatibility. Current Radix source and relevant Shadow DOM issues were checked for a maintained alternative; none supplies owner/root-aware deferred restoration, reclaim, and cancellation.
Behavior or check Method / command Result Evidence source
Dialog focus lifecycle Source trace through use-shadow-dialog-focus.ts, shadow-root-host.tsx, and Radix Dialog/FocusScope/Presence Required ownership and root invariants preserved Coordinator
Focused dialog regressions vitest run --project unit src/components/ui/dialog.test.tsx 5/5 passed Coordinator
Rapid close/reopen Chromium evidence Focused story run repeated three times Passed 3/3 Coordinator
UI type safety and build UI typecheck; turbo build --force --filter=@youversion/platform-react-ui Passed; 3 build tasks successful Coordinator
Full PR checks Integration Tests and current check suite 621/621 tests passed; all required checks green CI
  • Limits: one combined local story-file run hit a transient pre-existing timing assertion and an unrelated pre-existing layout assertion. The affected rapid-reopen story then passed three consecutive focused runs, and CI passed the full suite. Cross-browser and assistive-technology validation remain documented rollout follow-ups.
  • CI and bot review: all required checks passed; no unresolved current-HEAD bot finding remains.
  • Event: APPROVE.

Written by Code Reviewer bot on behalf of Cam.

[canRestoreFocus],
);

const reclaimFocusRestore = useCallback(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise: Binding deferred restoration to the dialog owner keeps this fix narrow while preserving the original opener through interrupted exit presence.

For Agents: owner-bound focus lifecycle

The schedule, reclaim, and cancel operations model the exact retained-presence lifecycle without introducing the out-of-scope overlay manager. Carrying the captured root also makes final validation reject disconnected and moved targets at the point of use.

Written by Code Reviewer bot on behalf of Cam.

@cameronapak

Copy link
Copy Markdown
Collaborator

I just want to say thank you for all the pre-reviews that you do before you send it out for review. It means a lot and makes reviewing easy

…ype-5889-shadow-overlay-runtime

# Conflicts:
#	packages/ui/src/test/mocks/handlers.ts
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.

2 participants