fix(dom): wait for element connection before starting enter animation - #71
Merged
Conversation
forkSlotEnter forks the enter animation fiber via Effect.forkIn(slotScope) from inside addSlot. Effect's scheduler can hand that fiber control on the next microtask, before the outer synchronous render flow has finished appending the wrapper's ancestor chain to the document. When that happens, onBeforeEnter runs against a disconnected node — getComputedStyle returns empty strings, browsers won't compute or transition styles against detached nodes, and the enter transition never fires. The animation stalls to the timeout. Hydration didn't surface this: it walks pre-existing DOM so every node was already connected when the fiber ran. Client-mode re-mounts (e.g. router nav-back) broke it, but only when the animated block sat inside another wrapper — its ancestor had to be appended AFTER the fork ran. Reproduced with a top-level `when` toggle wrapping two levels of `$.div` around an `animated` — element.isConnected was false at onBeforeEnter on the second mount. Yield microtasks at the start of the animation fiber's body until the element is connected, up to 3 attempts. The outer flow completes within 1-2 microtasks in practice; the bound keeps tests that yield an animated element without ever appending it (e.g. the existing "gates on an AnimationGroup" test) from hanging. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Deploying effex with
|
| Latest commit: |
ec2ad34
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://4164b0a6.effex.pages.dev |
| Branch Preview URL: | https://fix-enter-anim-detached-elem.effex.pages.dev |
Deploying effex-api with
|
| Latest commit: |
ec2ad34
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://c611c9cb.effex-api.pages.dev |
| Branch Preview URL: | https://fix-enter-anim-detached-elem.effex-api.pages.dev |
2 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes intro animations stalling on client-mode re-mounts (e.g. router nav-back) when the animated block is nested inside other wrapper elements. Directly diagnosed from a user report —
onBeforeEnter'sgetComputedStylesnapshot returned all-empty values on re-mount, which only happens when the element is disconnected from the document.Root cause
forkSlotEnterforks the enter animation fiber viaEffect.forkIn(slotScope)from insideaddSlot. Effect's scheduler can hand that fiber control on the next microtask, but for a nestedanimatedblock the outer render flow is still building the ancestor chain bottom-up in memory:animated'saddSlotcreates its wrapper div, applies enterFrom classes, inserts it into its owncontainerElement(adisplay:contentsdiv).forkSlotEnterforks the animation fiber → queued for the next microtask.Headline's div → HomePage's div → into the Outlet's slot container, and finally into the actual document at the top.Steps 2 and 3 race. If the forked fiber gets control before step 3 finishes,
onBeforeEnterfires against a wrapper whose grandparents aren't in the document yet.getComputedStylereturns empty strings on disconnected nodes, and browsers won't compute or transition styles against them — so the enter transition never fires and the animation stalls to the timeout.Why hydration didn't surface this: the hydration walker only ever sees DOM that was emitted by SSR and already exists in the document. Every element was connected when the fiber ran, so the race never triggered a visible bug. Client-mode re-mounts (nav-back,
whentoggle) were the first path where the tree got built in memory and only inserted at the top level.Reproduction
User's
onBeforeEnterdiagnostic dumped on nav-back:Class list correctly has enterFrom →
applyPreInsertEnterFromfired. But every computed-style field is""— the tell for a disconnected node. Confirmed reproduction is the timing race, not a missing enterFrom.Fix
At the start of the animation fiber's body (after gate but before
runEnterAnimation), yield microtasks untilelement.isConnectedis true, bounded at 3 attempts. One or two microtasks is enough to let the outer synchronous flow commit the tree in practice; the bound keeps tests that never insert their animated result (e.g. the existinggates on an AnimationGrouptest) from spinning.Doesn't use
requestAnimationFrame— that would add ~16ms latency per animation and broke a timing-sensitive existing test. Microtask polling is essentially free when the element is connected.Tests
New regression in
Control.test.tsunderanimated:whentoggle wrapping two$.divlevels around ananimatedblock.document.bodysoisConnectedpropagates.element.isConnected === trueatonBeforeEnteron the initial mount AND the toggle-back.Fails before the fix on the second
onBeforeEnter(isConnectedreturns false); passes after.tsc --noEmitclean.Changeset:
@effex/dompatch.🤖 Generated with Claude Code