Fix AnimatePresence: apply object-form initial on re-entry#3662
Fix AnimatePresence: apply object-form initial on re-entry#3662lennondotw wants to merge 1 commit intomotiondivision:mainfrom
Conversation
Greptile SummaryThis PR fixes a bug in Key changes:
Minor concerns:
Confidence Score: 5/5
Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[ExitAnimationFeature.update] --> B{presenceContext exists?}
B -- No --> Z[return]
B -- Yes --> C{isPresent changed?}
C -- No --> Z
C -- Yes --> D{isPresent && prevIsPresent === false\ni.e. re-entry?}
D -- No --> E[setActive exit = !isPresent\nthen attach onExitComplete handler]
D -- Yes --> F{isExitComplete?}
F -- No --> G[setActive exit = false\ncancel exit animation]
F -- Yes --> H[read initial & custom from props]
H --> I{"typeof initial === 'string'\n|| typeof initial === 'object'\n★ NEW: object-form now included"}
I -- No --> K[skip jump step]
I -- Yes --> J[resolveVariant node, initial, custom]
J --> L{resolved truthy?}
L -- No --> K
L -- Yes --> M["destructure: transition, transitionEnd, ...target\nfor each key → node.getValue(key)?.jump(value)"]
M --> N[animationState.reset]
K --> N
N --> O[animationState.animateChanges]
O --> P[isExitComplete = false]
Reviews (1): Last reviewed commit: "Fix AnimatePresence: apply object-form i..." | Re-trigger Greptile |
| const { initial, custom } = this.node.getProps() | ||
|
|
||
| if (typeof initial === "string") { | ||
| if (typeof initial === "string" || typeof initial === "object") { |
There was a problem hiding this comment.
typeof null === "object" edge case
In JavaScript, typeof null === "object" is true, so this condition also matches null. While the if (resolved) guard on line 34 would protect against a null initial prop (since resolveVariantFromProps returns its input unchanged for non-function, non-string values), and TypeScript types prevent null from being passed at compile time, it's worth being precise.
Similarly, typeof [] === "object" is true, so array variant labels (e.g. initial={["hidden", "visible"]}) will now enter the block. resolveVariant will return the array as-is (truthy), the destructuring will produce numeric-keyed target props ({ 0: "hidden", 1: "visible" }), and this.node.getValue("0") will return undefined, making the jump a silent no-op. Functionally harmless, but not the intended path.
A more defensive condition:
| if (typeof initial === "string" || typeof initial === "object") { | |
| if (typeof initial === "string" || (initial !== null && !Array.isArray(initial) && typeof initial === "object")) { |
| // With fix: opacity should jump to 0.5 (object-form initial), then animate to 1 | ||
| // The first value in opacityChanges after re-entry should be 0.5 | ||
| // Without fix: opacity stays at 0 or goes straight to 1 without jumping to 0.5 | ||
| expect(opacityChanges).toContain(0.5) |
There was a problem hiding this comment.
Assertion weaker than described comment
The inline comment says "The first value in opacityChanges after re-entry should be 0.5", but the assertion only checks that 0.5 appears somewhere in the array — not that it's the first change. If the animation somehow fires values in a different order, the test would still pass.
To match the stated intent and prevent regressions where the value is eventually reached but not as the reset step, consider:
| // With fix: opacity should jump to 0.5 (object-form initial), then animate to 1 | |
| // The first value in opacityChanges after re-entry should be 0.5 | |
| // Without fix: opacity stays at 0 or goes straight to 1 without jumping to 0.5 | |
| expect(opacityChanges).toContain(0.5) | |
| // With fix: opacity should jump to 0.5 (object-form initial), then animate to 1 | |
| // Without fix: opacity stays at 0 or goes straight to 1 without jumping to 0.5 | |
| expect(opacityChanges[0]).toBe(0.5) | |
| expect(opacityChanges).toContain(1) |
|
/rerun |
|
Hi @mattgperry, friendly ping — this is a small one-line fix for object-form |
The re-entry logic added in 6a8d3ab only handles string variant labels. Object-form initial values (e.g., initial={{ opacity: 0.5 }}) are skipped, so the component animates from the exit end value instead of jumping to the specified initial. resolveVariant already supports both strings and objects, so extending the type guard is all that's needed.
5b837e3 to
3497306
Compare
|
Rebased on latest This PR now applies cleanly on top of the current code — just extends the type guard on line 28 of @mattgperry would appreciate a look when you get a chance — it's a small extension to the fix you landed in 6a8d3ab. |
Summary
When a child re-enters
AnimatePresenceafter its exit animation completed, object-forminitialvalues (e.g.,initial={{ opacity: 0.5 }}) are not applied. The component animates from the exit end value instead of jumping to the initial value first.Root cause
The re-entry logic added in 6a8d3ab and simplified in 3da9d09 only handles string variant labels:
Object-form initial values are skipped, so
resolveVariantis never called for them.Fix
Extend the condition to also handle object-form initial values:
resolveVariantalready supports both string and object forms — it returns objects as-is. ThenullandArray.isArrayguards handle the JStypeof null === "object"quirk and array variant labels respectively.Test plan