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

103 lines
5.9 KiB
Markdown

# Nested dismissal isolation — 2026-09-26
Status: controlled follow-up to [boundary recovery](BoundaryRecoveryExperiment.md),
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](../experiments/boundary-recovery/isolation.mjs) bundles the unchanged
fixture with four variants, three fresh-page trials each, in fixed order. The
[48-trial receipt](../experiments/boundary-recovery/isolation-results.json) 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](../experiments/boundary-recovery/isolation-provenance.json)
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](../candidates/FocusReturn.md): 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](NestedOwnershipRouting.md) 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.