frontend-patterns/research/reviews/NestedDismissalIsolation.md
tegwick 29ccad7002 Test lifecycle-aware Escape ownership routing without settling delays
Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a0cffe-206f-7323-89d3-b20ca4cf4bf9
2026-09-26 22:58:07 +02:00

5.9 KiB

Nested dismissal isolation — 2026-09-26

Status: controlled follow-up to boundary recovery, Codex editorial analysis. Same pinned React/Radix fixture, now compared across Chromium 151.0.7922.34 and Firefox 155.0, development and production builds. No package upgrade, deployment, model call or accessibility certification.

Design and provenance

isolation.mjs bundles the unchanged fixture with four variants, three fresh-page trials each, in fixed order. The 48-trial receipt retains successes, failures, focus snapshots, open/close transitions and layer event traces. It hashes runner, fixture, lockfile, inspected dependency source and every bundle. Additional provenance records Firefox build/executable hash and the initial sandbox launch failure.

  • raw: unmodified dependencies, no layer tracing, immediate browser Escape after focus arrives and a before-event snapshot is captured.
  • traced: in-memory instrumentation logs layer registration, listener changes and Escape handling; no intended change to vendor control flow.
  • ready: same instrumentation; additionally waits until only the inner layer's Escape listener is installed. This uses private instrumentation, not a public API.
  • guarded: traced build adds a current-top-layer check at event handling time; stale listeners return without dismissing. This is experimental modified behavior.

All variants retain keyboard Enter/Tab/Escape interaction and assert inner return followed by outer return. No fixed settling sleep or hidden retry. Extra snapshots, tracing and readiness observation can change scheduling. Results are exploratory counts, not failure rates. Fixed ordering and three trials do not establish browser or build superiority. Production mode changes React build selection, not the fixture into a production application. The historical eight-case runner/results are unchanged.

Results

Pass means both expected return targets were reached in order.

Browser Build raw traced ready guarded
Chromium development 2/3 2/3 3/3 3/3
Chromium production 3/3 1/3 3/3 2/3
Firefox development 3/3 3/3 3/3 3/3
Firefox production 3/3 3/3 3/3 3/3

43/48 obligations met. Keep conditions separate: a readiness wait changes the precondition, and the guard changes the implementation. An unmodified production failure was not observed in these three Chromium trials; failures with tracing show the timing problem is not restricted to a development bundle, but instrumentation may influence whether its window is hit. Firefox's passes do not prove immunity.

Captured mechanism

Chromium/development/traced trial 1 records this order:

  1. Outer Escape listener is installed.
  2. Inner layer is registered; focus reaches its close button.
  3. Escape invokes the outer listener while the registered layer stack already ends in the inner layer.
  4. The outer listener is removed and the inner listener installed afterward.
  5. Outer-close transition removes both dialogs; focus returns to the outer invoker.

This directly locates a stale Escape-owner interval in the instrumented fixture. The shipped dismissable-layer module selects its listener using render-time isHighestLayer and effect registration; event-time handling does not recheck the current layer stack. Source hash matches the earlier inspected package. This explains the captured wrong-owner dismissal; it is not a universal diagnosis of all focus bugs or a claim about every Radix version/application.

Chromium/production/guarded trial 1 rejects the stale outer handler while focus is inside the inner dialog. The inner listener is installed later. Both dialogs remain open: the guard preserves the outer task but loses this Escape action. A check that prevents a wrong transition is insufficient when it cannot deliver the valid action.

The readiness variant passes 12/12 by waiting for observed ownership before Escape. It supports distinguishing focus arrival from event-owner readiness. It does not establish a product solution: users cannot query this private trace, and a product must define how input during the ownership transition is accepted, routed or deferred.

Pattern disposition and next work

Refine Focus Return: test both which task may close and where focus returns, including input during activation and teardown. Neither visibility nor focus alone proves the dismissal handler belongs to the active task. Avoid arbitrary sleeps and avoid treating ignored input as successful recovery. Keep hypothesis maturity; this is still one synthetic implementation family.

No new pattern is needed yet. The broader question is whether temporal ownership belongs in the containing modal/interaction contract. A production repair would need stable ownership/routing plus tests for opening, nesting, removal and teardown; this study has not supplied or deployed such a repair. No upstream issue was sent.

WP-0003 T01 retains a bounded residual: evaluate a lifecycle-safe ownership strategy without arbitrary delay and verify ordinary/nested dismissal in both browsers before recommending it. Feature/import versus release-boundary research and navigation checks remain open; this focused investigation does not complete those tasks or M2.

Routing follow-up

Experimental routing now supplies a mechanism that passes the tested ordinary/nested/cancel/removal/reopen/teardown cases without a settling delay. It changes the owner-selection architecture, not just the stale handler guard. Broader lifecycle coverage is still required for a recommendation; this does not overwrite the failed isolation or guard results above.