diff --git a/dev/html/public/animate-layout/shared-element-spa-repeat.html b/dev/html/public/animate-layout/shared-element-spa-repeat.html new file mode 100644 index 0000000000..174961d279 --- /dev/null +++ b/dev/html/public/animate-layout/shared-element-spa-repeat.html @@ -0,0 +1,181 @@ + + +
+ + + + + + + + + + + diff --git a/packages/framer-motion/cypress/fixtures/animate-layout-tests.json b/packages/framer-motion/cypress/fixtures/animate-layout-tests.json index 35a130ed29..b95a4ebe73 100644 --- a/packages/framer-motion/cypress/fixtures/animate-layout-tests.json +++ b/packages/framer-motion/cypress/fixtures/animate-layout-tests.json @@ -1 +1 @@ -["app-store-a-b-a.html","basic-position-change.html","interrupt-animation.html","modal-open-after-animate.html","modal-open-close-interrupt.html","modal-open-close-open-interrupt.html","modal-open-close-open.html","modal-open-close.html","modal-open-opacity.html","modal-open.html","repeat-animation.html","scale-correction.html","scope-with-data-layout.html","shared-element-a-ab-a.html","shared-element-a-b-a-replace.html","shared-element-a-b-a-reuse.html","shared-element-basic.html","shared-element-configured.html","shared-element-crossfade.html","shared-element-nested-children-bottom.html","shared-element-nested-children.html","shared-element-no-crossfade.html","shared-multiple-elements.html"] \ No newline at end of file +["app-store-a-b-a.html","basic-position-change.html","interrupt-animation.html","modal-open-after-animate.html","modal-open-close-interrupt.html","modal-open-close-open-interrupt.html","modal-open-close-open.html","modal-open-close.html","modal-open-opacity.html","modal-open.html","repeat-animation.html","scale-correction.html","scope-with-data-layout.html","shared-element-a-ab-a.html","shared-element-a-b-a-replace.html","shared-element-a-b-a-reuse.html","shared-element-basic.html","shared-element-configured.html","shared-element-crossfade.html","shared-element-nested-children-bottom.html","shared-element-nested-children.html","shared-element-no-crossfade.html","shared-multiple-elements.html","shared-element-spa-repeat.html"] diff --git a/packages/motion-dom/src/projection/shared/stack.ts b/packages/motion-dom/src/projection/shared/stack.ts index b391c990fc..3711794a2b 100644 --- a/packages/motion-dom/src/projection/shared/stack.ts +++ b/packages/motion-dom/src/projection/shared/stack.ts @@ -1,131 +1,120 @@ import { addUniqueItem, removeItem } from "motion-utils" import { IProjectionNode } from "../node/types" +interface DOMInstance { + isConnected?: boolean + scheduleRender?: (immediate: boolean) => void +} + export class NodeStack { lead?: IProjectionNode prevLead?: IProjectionNode members: IProjectionNode[] = [] add(node: IProjectionNode) { + const valid = this.members.filter((m) => this.isValid(m)) + + if (valid.length !== this.members.length) { + this.members = valid + if (this.lead && !valid.includes(this.lead)) { + this.lead = valid[valid.length - 1] + } + if (this.prevLead && !valid.includes(this.prevLead)) { + this.prevLead = undefined + } + } + addUniqueItem(this.members, node) node.scheduleRender() } remove(node: IProjectionNode) { removeItem(this.members, node) - if (node === this.prevLead) { - this.prevLead = undefined - } - if (node === this.lead) { - const prevLead = this.members[this.members.length - 1] - if (prevLead) { - this.promote(prevLead) - } + + if (node === this.prevLead) this.prevLead = undefined + if (node === this.lead && this.members.length) { + this.promote(this.members[this.members.length - 1]) } } relegate(node: IProjectionNode): boolean { - const indexOfNode = this.members.findIndex((member) => node === member) - if (indexOfNode === 0) return false - - /** - * Find the next projection node that is present - */ - let prevLead: IProjectionNode | undefined - for (let i = indexOfNode; i >= 0; i--) { - const member = this.members[i] - if (member.isPresent !== false) { - prevLead = member - break - } - } + const idx = this.members.indexOf(node) + if (idx <= 0) return false - if (prevLead) { - this.promote(prevLead) - return true - } else { - return false + for (let i = idx; i >= 0; i--) { + if (this.members[i].isPresent !== false) { + this.promote(this.members[i]) + return true + } } + return false } promote(node: IProjectionNode, preserveFollowOpacity?: boolean) { - const prevLead = this.lead - - if (node === prevLead) return + const prev = this.lead + if (node === prev) return - this.prevLead = prevLead + const canResume = prev && this.isValid(prev) + this.prevLead = canResume ? prev : undefined this.lead = node - node.show() - if (prevLead) { - prevLead.instance && prevLead.scheduleRender() + if (prev && canResume) { + const prevInstance = prev.instance as DOMInstance + const isConnected = prevInstance?.isConnected !== false + + if (!prev.snapshot && isConnected) { + prev.updateSnapshot() + } + + if (isConnected) prev.scheduleRender() node.scheduleRender() - /** - * If both the new and previous lead have the same defined layoutDependency, - * skip the shared layout animation. This allows components with layoutId - * to opt-out of animations when their layoutDependency hasn't changed, - * even when the component unmounts and remounts in a different location. - */ - const prevDep = prevLead.options.layoutDependency - const nextDep = node.options.layoutDependency - const dependencyMatches = + const { layoutDependency: prevDep } = prev.options + const { layoutDependency: nextDep } = node.options + + if ( prevDep !== undefined && nextDep !== undefined && prevDep === nextDep + ) + return - if (!dependencyMatches) { - node.resumeFrom = prevLead - - if (preserveFollowOpacity) { - node.resumeFrom.preserveOpacity = true - } - - if (prevLead.snapshot) { - node.snapshot = prevLead.snapshot - node.snapshot.latestValues = - prevLead.animationValues || prevLead.latestValues - } + node.resumeFrom = prev + if (preserveFollowOpacity) prev.preserveOpacity = true - if (node.root && node.root.isUpdating) { - node.isLayoutDirty = true - } + if (prev.snapshot) { + node.snapshot = prev.snapshot + node.snapshot.latestValues = + prev.animationValues || prev.latestValues } - const { crossfade } = node.options - if (crossfade === false) { - prevLead.hide() - } + if (node.root?.isUpdating) node.isLayoutDirty = true + if (node.options.crossfade === false) prev.hide() } } exitAnimationComplete() { - this.members.forEach((node) => { - const { options, resumingFrom } = node - - options.onExitComplete && options.onExitComplete() - - if (resumingFrom) { - resumingFrom.options.onExitComplete && - resumingFrom.options.onExitComplete() - } + this.members.forEach((n) => { + n.options.onExitComplete?.() + n.resumingFrom?.options.onExitComplete?.() }) } scheduleRender() { - this.members.forEach((node) => { - node.instance && node.scheduleRender(false) + this.members.forEach((n) => { + const inst = n.instance as DOMInstance + inst?.scheduleRender?.(false) }) } - /** - * Clear any leads that have been removed this render to prevent them from being - * used in future animations and to prevent memory leaks - */ removeLeadSnapshot() { - if (this.lead && this.lead.snapshot) { - this.lead.snapshot = undefined - } + this.lead?.snapshot && (this.lead.snapshot = undefined) + } + + private isValid(n: IProjectionNode): boolean { + const inst = n.instance as DOMInstance + const isConnected = inst?.isConnected !== false + return isConnected || n.isPresent === false || !!n.snapshot } }