Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions dev/react/src/tests/animate-presence-accordion-rapid.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { AnimatePresence, motion } from "framer-motion"
import { useState } from "react"

const items = [
{ id: "a", title: "Item A" },
{ id: "b", title: "Item B" },
{ id: "c", title: "Item C" },
]

function AccordionItem({ id, title }: { id: string; title: string }) {
const [open, setOpen] = useState(false)

return (
<div>
<button
className="trigger"
data-id={id}
onClick={() => setOpen((o) => !o)}
>
{title}
</button>
<AnimatePresence initial={false}>
{open && (
<motion.section
key="content"
data-panel={id}
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{
type: "tween",
ease: "linear",
duration: 0.5,
}}
style={{ overflow: "hidden" }}
>
<div
data-content={id}
style={{ padding: 20, height: 100 }}
>
Content for {title}
</div>
</motion.section>
)}
</AnimatePresence>
</div>
)
}

export const App = () => {
return (
<div id="accordion" style={{ width: 400 }}>
{items.map((item) => (
<AccordionItem key={item.id} id={item.id} title={item.title} />
))}
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
describe("AnimatePresence accordion rapid click (#2674)", () => {
it("Toggling open mid-exit leaves the panel visible at full height", () => {
cy.visit("?test=animate-presence-accordion-rapid")
.wait(200)
.get('[data-panel="a"]')
.should("not.exist")
// Open
.get('[data-id="a"]')
.trigger("click", { force: true })
// Wait for full open animation
.wait(800)
// Close
.get('[data-id="a"]')
.trigger("click", { force: true })
// Mid-exit (250ms into 500ms exit), re-open
.wait(250)
.get('[data-id="a"]')
.trigger("click", { force: true })
// Wait for animation to settle
.wait(1500)
// Panel should exist at full height
.get('[data-panel="a"]')
.should("exist")
.then(($el: any) => {
const height = parseFloat(
getComputedStyle($el[0] as HTMLElement).height
)
const opacity = parseFloat(
getComputedStyle($el[0] as HTMLElement).opacity
)
expect(height).to.be.greaterThan(100)
expect(opacity).to.equal(1)
})
})

it("Toggling closed mid-enter removes the panel", () => {
cy.visit("?test=animate-presence-accordion-rapid")
.wait(200)
// Open
.get('[data-id="a"]')
.trigger("click", { force: true })
// Mid-enter (250ms into 500ms enter), close
.wait(250)
.get('[data-id="a"]')
.trigger("click", { force: true })
// Wait for exit to finish
.wait(1500)
.get('[data-panel="a"]')
.should("not.exist")
})

it("Rapidly toggling many times ends in correct state (open)", () => {
cy.visit("?test=animate-presence-accordion-rapid")
.wait(200)
// 5 rapid clicks → ends open
.get('[data-id="a"]')
.trigger("click", { force: true })
.wait(50)
.get('[data-id="a"]')
.trigger("click", { force: true })
.wait(50)
.get('[data-id="a"]')
.trigger("click", { force: true })
.wait(50)
.get('[data-id="a"]')
.trigger("click", { force: true })
.wait(50)
.get('[data-id="a"]')
.trigger("click", { force: true })
.wait(2000)
.get('[data-panel="a"]')
.should("exist")
.then(($el: any) => {
const height = parseFloat(
getComputedStyle($el[0] as HTMLElement).height
)
const opacity = parseFloat(
getComputedStyle($el[0] as HTMLElement).opacity
)
expect(height).to.be.greaterThan(100)
expect(opacity).to.equal(1)
})
})
})