LearnNewsExamplesServices
Frontmatter
id18134
titleA dismissed rail reveal animates an empty slot: the pane is detached on the transition frame
stateClosed
labels
bugaitesting
assigneesneo-opus-grace
createdAtSep 2, 2026, 9:11 PM
updatedAtSep 2, 2026, 10:49 PM
githubUrlhttps://github.com/neomjs/neo/issues/18134
authorneo-opus-grace
commentsCount1
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtSep 2, 2026, 10:49 PM

A dismissed rail reveal animates an empty slot: the pane is detached on the transition frame

neo-opus-grace
neo-opus-grace commented on Sep 2, 2026, 9:11 PM

Context

Reported by the operator on the Workstation, 2026-09-02 ~19:00Z, after pulling dev and rebuilding themes: a dismissed rail reveal still disappears at once. Their second reading is the one that located it — "ESC key also hides instantly => the transition starts, and a couple of frames later (way too early) => removeDom".

So the exit motion from #18116 / PR #18118 is not broken: the -leaving class lands and the keyframes run. Something else empties the panel while they run.

The Problem

The two-phase hide defers the overlay's hidden class. It never deferred the content's removal.

Measured on dev@ec6856b804 with a throwaway probe on the dock-rail-origin fixture — reveal, let the entry settle, press Escape, read while -leaving is on the node:

before dismiss  slot children : 1
during leave    slot children : 0        ← the pane is already gone
during leave    slot animation: ["neo-dock-reveal-to-left"]
during leave    root animation: ["neo-dock-reveal-fade-out"]

An empty slot slides out. The root fades, the slot travels back to its edge, and there is nothing inside either of them — which is indistinguishable from an instant hide to anyone watching, and exactly what the operator saw.

Why the merged witness passes. DockRailRevealExit.spec.mjs asserts the overlay is -leaving and not -hidden, that display is not none, that pointer-events is none, and that the root and slot carry the expected animation names. Every one of those is true of an empty slot. The spec reads the slot's animation; it never reads the slot's contents. Reviewed and approved by me on PR #18118 — the gap is in the assertion set, not in the implementation it witnesses.

The Architectural Reality

  • Neo.dashboard.dock.interaction.Rail#onRevealStateChange (:648) runs on every machine transition and calls syncRevealOverlay().
  • syncRevealOverlay (:802) resolves railItem from machine.revealedItemId — null once dismissed — pushes the snapshot onto the overlay, and then calls me.syncRevealPane(railItem) with that null.
  • syncRevealPane (:925): nextId = railItem?.dockItemId ?? null; currentId !== nextId, so it reaches slot.remove(child, …) and adds nothing back. Synchronous, on the transition frame.
  • Neo.dashboard.dock.interaction.RevealOverlay#syncRevealMotion meanwhile takes the visible → hidden boundary into beginRevealLeave(), which keeps the overlay mounted until the root's animationend (completeRevealLeave) or the MotionSignal.FAIL_SAFE_MS backstop.

The two mechanisms are unaware of each other: the overlay owns "when does the DOM go", the rail owns "when does the pane go", and only the first learned about the exit.

The Fix

The pane detach has to wait for the same seam the hidden class already waits for.

  1. Defer the detach to the end of the leave. The overlay already has the terminal — completeRevealLeave() — and already knows it is leaving (revealLeaving). The rail's syncRevealPane(null) should not run while a leave is in flight; it should run when the leave settles (or when the overlay hides without one, the unmounted path).
  2. Keep the shape one-way. A re-entry during a leave cancels it (cancelRevealLeave) and may retarget to a different item — so a deferred detach must not fire after the overlay has re-entered, and must not detach the newly revealed pane. The generation/revealShownItemId bookkeeping added for the retarget (#18117) is the existing precedent for distinguishing those cases.
  3. The engine seam, not a rail timer. No setTimeout matching the token duration: that reintroduces the two-clocks problem #18117 explicitly avoided. The signal is the overlay's own end event.

Acceptance Criteria

  • AC-1 While the overlay carries -leaving, the pane slot still contains its pane. Red-first on dev: the probe above returns 0 children today, 1 after. Component witness on the rail fixture.
  • AC-2 The pane is detached once the leave completes — the slot is empty (or reparented back to its cache) only after -hidden lands, and the pane instance is not destroyed by the deferral.
  • AC-3 A re-entry during a leave keeps its pane: dismiss, re-open the same item mid-exit, and the slot holds that pane throughout with no detach afterwards.
  • AC-4 A retarget during a leave detaches the outgoing pane and mounts the incoming one exactly once: dismiss, click a different rail item mid-exit, and the slot ends holding the new pane.
  • AC-5 The existing exit arm is tightened so it cannot pass on an empty slot again — it asserts slot contents alongside the animation names it already reads.
  • AC-6 DockRailRevealExit, DockRailRevealRetarget, DockRailRevealOrigin, DockRailRevealIframe, unit/dashboard and component/dashboard stay green.
  • AC-7 (e2e, operator-reported path) An e2e arm on the Workstation's own rails proves a running exit animation and a mounted pane for its duration, for both dismissal routes the operator hit: Escape and focus-leave. This is the arm the component fixture could not supply — the report came from the served app, and the component witness was green throughout.

Out of Scope

  • The exit motion itself (#18116 / PR #18118) — the keyframes and the two-phase class flip are correct and stay.
  • The retarget slide (#18117 / PR #18120) — its swap generations are unaffected; AC-4 only pins that a retarget still detaches once.
  • T2's true sliding cross-fade (an outgoing layer that keeps the previous pane mounted after the incoming one arrives) — a different feature, still a design leaf.

Avoided Traps

  • Asserting the animation instead of the thing being animated. This is the defect that produced the report: the witness read the slot's animationName and its display, all of which an empty slot satisfies. Any new arm must read what is inside the animating element, not only that it animates.
  • A timer in the rail. Matching the exit duration with a setTimeout would put the paint and the detach on two clocks — the exact drift #18117 removed for the keyframes. The overlay's end event is the one signal.
  • Deferring unconditionally. A dismissal with no DOM (the unmounted path) settles synchronously and must still detach; a deferral that waits for an end event that never comes would leak the pane into a hidden overlay.
  • Reading the fixture as the app. The component fixture and the Workstation disagreed here — the fixture was green while the served app was visibly wrong — which is why AC-7 exists rather than another component arm alone.

Related

  • #18116 / PR #18118 — the exit motion this completes; its witness is the one AC-5 tightens.
  • #18117 / PR #18120 — the retarget slide, whose generation bookkeeping is the precedent for AC-3/AC-4.
  • #18074 / PR #18079 — the entry motion.
  • #13158 — the dock-layouts epic.

Live latest-open sweep: read the latest 20 open issues at 2026-09-02T19:10:35Z; nearest neighbours are #18121 (a cross-window NL spec's own instability) and #18027 (maximize FLIP flash) — different surfaces, neither this. Closed-queue sweep for a prior reveal-exit/pane-detach decline: none. A2A: no claim on the reveal exit since PR #18118 merged.

Origin Session ID: fb58e855-74b2-4367-9f83-5877f151f024

Retrieval Hint: "rail reveal exit animates an empty slot, syncRevealPane detaches on the transition frame, pane removed before animationend"

tobiu referenced in commit 1096dc2 - "fix(dock): a dismissed reveal keeps its pane until the exit ends (#18134) (#18139) on Sep 2, 2026, 10:49 PM
tobiu closed this issue on Sep 2, 2026, 10:49 PM