Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/quiet-dialogs-return.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@youversion/platform-react-ui': patch
---

Fix focus restoration for opt-in Shadow DOM dialogs by preserving the original opener across a close/reopen during exit, while ignoring disconnected targets and targets moved out of their captured root.
19 changes: 10 additions & 9 deletions docs/adr/0007-prototype-shadow-dom-style-isolation.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,20 +142,21 @@ roots are a CSS boundary, not a security boundary.

## Nested and concurrent overlays

YPE-5355 exercised nested and concurrent overlays through the production
`ShadowRootHost` seam. Both nesting directions preserve topmost-only Escape
dismissal and restore focus into the remaining parent overlay. Either
YPE-5355 exercised nested and concurrent overlays through the real shared
`ShadowRootHost` implementation. Both nesting directions preserve topmost-only
Escape dismissal and restore focus into the remaining parent overlay. Either
nested-overlay teardown order also works with the current architecture. Separate
component shadow roots own distinct portal containers, but do not isolate Radix
outside interaction: opening a peer popover dismisses the existing peer across
the same or separate roots. A verse action popover does not restore final focus
after its nested dialog and then the popover close, and rapid dialog close/reopen
also loses final focus restoration.
the same or separate roots. Nested dialog-to-popover dismissal restores focus in
order, and final dismissal returns to the original outside control. A dialog
that closes and reopens during retained exit presence also preserves and restores
its original opener; disconnected targets and targets moved out of their captured
root (into the light DOM, another shadow root, or another document) are ignored.

These observations do not select or design production overlay coordination.
YPE-5356 owns deciding whether and how to support concurrent peers and exact
final focus restoration. The detailed Chromium evidence and remaining
validation live in the rollout plan.
YPE-5356 owns deciding whether and how to support concurrent peers. The detailed
Chromium evidence and remaining validation live in the rollout plan.

Radix's development-only relationship checks can also emit warnings for valid
IDs inside a shadow root because those checks query the document rather than
Expand Down
26 changes: 13 additions & 13 deletions docs/shadow-dom-isolation-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ This is a working plan, not approval for package-wide rollout.

## Current scope

- `YouVersionAuthButton` creates its Shadow DOM boundary automatically.
- `YouVersionAuthButton` is the only component automatically isolated by the
current prototype.
- `BibleVersionPicker` validates shadow-local inline and native top-layer
floating content through opt-in stories.
- The shared Dialog and Popover primitives support opt-in shadow-local portals.
Expand All @@ -22,19 +23,19 @@ This is a working plan, not approval for package-wide rollout.
- The internal `SignInDialog` is validated only through an opt-in
`ShadowRootHost` story.
- Nested and concurrent overlays within and across component shadow roots were
exercised through the production `ShadowRootHost` seam (YPE-5355). The Shadow
DOM ADR records the architectural boundary; the results below record the
supported contract and peer-dismissal and final focus-restoration gaps. Runtime
changes remain YPE-5356.
exercised through the real shared `ShadowRootHost` implementation (YPE-5355).
The Shadow DOM ADR records the architectural boundary; the results below record
the supported contract and the remaining peer-dismissal gap. Broader component
rollout and peer-overlay coordination remain with YPE-5356.

## Nested and concurrent overlay evidence

| Scenario | Result | Evidence and limitation |
| --- | --- | --- |
| Verse action popover opens the highlights permission dialog | Partially supported | Both render in the shadow-local top layer. The dialog receives focus and the wrapper is inert. A first Escape closes only the dialog and restores focus inside the popover; a second closes the popover. Chromium spike observation found no final focus restoration. Automated evidence covers the stable dismissal, containment, and teardown contract without requiring that known gap to remain. |
| Verse action popover opens the highlights permission dialog | Supported | Both render in the shadow-local top layer. The dialog receives focus and the wrapper is inert. A first Escape closes only the dialog and restores focus inside the popover; a second closes the popover and restores the original outside control. |
| Dialog contains a popover | Supported | The popover is interactive and receives focus. Escape closes it first and restores its trigger while the dialog remains modal. A second Escape closes the dialog and restores its opener. |
| Two independent popovers in the same or separate roots | Unsupported as concurrent peers | Chromium spike observation found that opening a popover dismisses an existing peer through Radix outside interaction, including when the peers use separate component shadow roots. Automated evidence covers the stable contract that the new peer receives focus, remains non-modal, tears down cleanly, and that separate roots remain usable after primary teardown. |
| Rapid dialog close/reopen during exit | Unsupported | The first dialog remains connected in its closed state when it is reopened. The reopened dialog receives focus and the wrapper remains inert. Chromium spike observation found that final dismissal loses the opener; automated evidence requires safe modal release without locking in that focus loss. |
| Rapid dialog close/reopen during exit | Supported | The first dialog remains connected in its closed state when it is reopened. The reopened dialog receives focus and the wrapper remains inert. Final dismissal releases the modal state and restores the original opener. Disconnected targets and targets moved out of their captured root—including into the light DOM, another shadow root, or another document—are ignored. |

Separate `ShadowRootHost` instances use different portal containers and tear
those containers down independently. This provides lifecycle isolation, not
Expand All @@ -55,12 +56,12 @@ open.
| Portal lifecycle | Unit and browser coverage exercise lazy creation, exit-animation retention, cleanup, immediate reopen behavior, and the direct-Radix `VerseActionPopover` consumer. | Validated for shared primitives and the known bypass | Repeat the consumer audit when adding another direct overlay primitive. |
| Dialog relationships | Chromium resolves title and description relationships inside the component tree. | Validated in Chromium | Verify announcements with real assistive technology. |
| Dialog keyboard containment | Browser coverage exercises initial focus, programmatic escape redirection, forward and reverse traversal, radio-group collapsing, negative `tabindex`, and wraparound. | Validated in Chromium | Expand the browser and assistive-technology matrix. |
| Dialog modal lifetime | Coverage verifies inert background content while open and through staggered Content and Overlay exit animations. YPE-5355 also exercises both unmount orders for overlapping popover and dialog exits. | Validated for order-independent teardown | YPE-5356 owns peer concurrency across component roots and final focus-restoration gaps. Expand browser and assistive-technology coverage. |
| Dialog modal lifetime | Coverage verifies inert background content while open and through staggered Content and Overlay exit animations. YPE-5355 also exercises both unmount orders for overlapping popover and dialog exits. | Validated for order-independent teardown | YPE-5356 owns peer concurrency across component roots. Expand browser and assistive-technology coverage. |
| Dialog dismissal and restoration | Coverage exercises Escape, backdrop click, full-viewport hit testing, overlay-only focus, and restoration after both modal nodes unmount. | Validated in Chromium | Verify real screen-reader and cross-browser behavior. |
| Consumer form participation | Chromium coverage verifies that a light-DOM form does not own or serialize a native control inside an SDK shadow root. | Unsupported across tree scopes | Use an explicit component contract if a rollout target requires outer-form participation. |
| Consumer labels and ARIA ID references | Chromium coverage verifies that external native labels, `aria-labelledby`, and `aria-describedby` relationships do not resolve to controls inside the root. | Unsupported across tree scopes | Keep relationships in one tree scope; verify real assistive technology separately. |
| Consumer events, refs, and automation | Coverage verifies native retargeting, the auth button's React handler and forwarded ref, open-root queries, and effect-driven attachment timing. | Supported with documented constraints | Repeat for each public component selected for rollout. |
| Nested shadow roots | Coverage verifies basic rendering, recursive queries, and event retargeting at each boundary. | Supported for the validated basics | Nested and concurrent overlay ownership remains with YPE-5355. |
| Nested shadow roots | Coverage verifies basic rendering, recursive queries, and event retargeting at each boundary. | Supported for the validated basics | Peer-overlay coordination remains with YPE-5356; expand nested-overlay browser and assistive-technology validation during rollout. |
| Realistic same-page usage | YPE-5437 mounts, removes, and re-adds a 12-component mix in Normal and Strict Mode. Chromium coverage verifies exact host counts, rendered scripture content, and one shared stylesheet object across roots and remounts. A production-build comparison found a small warm-run mount-cost difference on one machine. | No shared-host blocker found | Repeat user-visible performance and compatibility checks for each component selected for rollout. |

## Direct overlay inventory
Expand All @@ -85,10 +86,9 @@ decision. YPE-5356 owns whether and how to implement that coordination.
- Apply [ADR 0007's client-only SSR and hydration contract](adr/0007-prototype-shadow-dom-style-isolation.md#ssr-and-hydration-contract)
per rollout component. YPE-5356 decides whether its first-paint, layout, and
no-JavaScript limitations are acceptable for that component.
- Resolve the YPE-5355 peer-dismissal and final focus-restoration gaps before
shipping nested and concurrent overlays (YPE-5356). The decision must consider
trigger-time peer dismissal as well as overlay order and restore targets;
ADR 0007 records the gaps but does not select a coordination design.
- Resolve the YPE-5355 peer-dismissal gap before shipping concurrent peer
overlays (YPE-5356). The decision must consider trigger-time peer dismissal
and overlay order; ADR 0007 records the gap but does not select a coordination design.
Cross-browser and assistive-technology coverage still remain.
- Keep the YPE-5400 custom-property contract and compiled-stylesheet prevention
guard green as component styles change. The audit below closes the known
Expand Down
Loading
Loading