| author | |
| committer | |
| log | f391b88c3754047c3c9434b76328699c083723b0 |
| tree | ae903ac7fc107e797562fd8610a980e1a91216fa |
| parent | 30e3649c3281fe88b3d8d125293f1bb58132a3c2 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: maria-rcks <254055478+maria-rcks@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>2 files changed, 226 insertions(+), 32 deletions(-)
apps/web/src/components/Sidebar.motion.test.ts+159-16| ... | @@ -281,10 +281,16 @@ describe("sidebar list motion", () => { | ... | @@ -281,10 +281,16 @@ describe("sidebar list motion", () => { |
| 281 | layout([b, fresh]); | 281 | layout([b, fresh]); |
| 282 | motion.update(true); | 282 | motion.update(true); |
| 283 | expect(a.animations[0]!.cancel).toHaveBeenCalledOnce(); | 283 | expect(a.animations[0]!.cancel).toHaveBeenCalledOnce(); |
| 284 | expect(fresh.animate).toHaveBeenLastCalledWith([{ opacity: 0 }, { opacity: 1 }], { | 284 | expect(fresh.animate).toHaveBeenLastCalledWith( |
| 285 | duration: 150, | 285 | [ |
| 286 | easing: "ease-out", | 286 | { opacity: 0, transform: "translateY(83px)" }, |
| 287 | }); | 287 | { opacity: 1, transform: "translateY(0px)" }, |
| 288 | ], | ||
| 289 | { | ||
| 290 | duration: 150, | ||
| 291 | easing: "ease-out", | ||
| 292 | }, | ||
| 293 | ); | ||
| 288 | const clone = a.clones[0]!; | 294 | const clone = a.clones[0]!; |
| 289 | expect(clone.style).toMatchObject({ | 295 | expect(clone.style).toMatchObject({ |
| 290 | position: "absolute", | 296 | position: "absolute", |
| ... | @@ -299,10 +305,16 @@ describe("sidebar list motion", () => { | ... | @@ -299,10 +305,16 @@ describe("sidebar list motion", () => { |
| 299 | expect(clone.attributes).toEqual([{ name: "aria-hidden", value: "true" }]); | 305 | expect(clone.attributes).toEqual([{ name: "aria-hidden", value: "true" }]); |
| 300 | expect(clone.children[0]!.attributes).toEqual([{ name: "data-state", value: "open" }]); | 306 | expect(clone.children[0]!.attributes).toEqual([{ name: "data-state", value: "open" }]); |
| 301 | expect(clone.children[1]!.attributes).toEqual(icon.attributes); | 307 | expect(clone.children[1]!.attributes).toEqual(icon.attributes); |
| 302 | expect(clone.animate).toHaveBeenCalledWith([{ opacity: 1 }, { opacity: 0 }], { | 308 | expect(clone.animate).toHaveBeenCalledWith( |
| 303 | duration: 150, | 309 | [ |
| 304 | easing: "ease-out", | 310 | { opacity: 1, transform: "translateY(0px)" }, |
| 305 | }); | 311 | { opacity: 0, transform: "translateY(-83px)" }, |
| 312 | ], | ||
| 313 | { | ||
| 314 | duration: 150, | ||
| 315 | easing: "ease-out", | ||
| 316 | }, | ||
| 317 | ); | ||
| 306 | expect(parent.children.includes(clone)).toBe(true); | 318 | expect(parent.children.includes(clone)).toBe(true); |
| 307 | motion.update(true); | 319 | motion.update(true); |
| 308 | expect(clone.animations).toHaveLength(1); | 320 | expect(clone.animations).toHaveLength(1); |
| ... | @@ -311,6 +323,106 @@ describe("sidebar list motion", () => { | ... | @@ -311,6 +323,106 @@ describe("sidebar list motion", () => { |
| 311 | expect(parent.children.includes(clone)).toBe(false); | 323 | expect(parent.children.includes(clone)).toBe(false); |
| 312 | }); | 324 | }); |
| 313 | 325 | ||
| 326 | it("rides entering rows on the shelf displacement so an opened shelf moves as one block", () => { | ||
| 327 | const a = new TestRow("a", 40); | ||
| 328 | const header = new TestRow("header", 32); | ||
| 329 | const x = new TestRow("x", 36); | ||
| 330 | const y = new TestRow("y", 36); | ||
| 331 | const z = new TestRow("z", 36); | ||
| 332 | const { motion, layout } = fixture([a, header, x]); | ||
| 333 | motion.update(true); | ||
| 334 | // The shelf is anchored below the list, so two revealed rows lift the | ||
| 335 | // header and its existing rows by the same 72px. | ||
| 336 | layout([a, header, x, y, z]); | ||
| 337 | for (const row of [header, x, y, z]) row.offsetTop -= 72; | ||
| 338 | motion.update(true); | ||
| 339 | expectMove(header, 72); | ||
| 340 | expectMove(x, 72); | ||
| 341 | expect(a.animate).not.toHaveBeenCalled(); | ||
| 342 | expect(y.animate).toHaveBeenLastCalledWith( | ||
| 343 | [ | ||
| 344 | { opacity: 0, transform: "translateY(72px)" }, | ||
| 345 | { opacity: 1, transform: "translateY(0px)" }, | ||
| 346 | ], | ||
| 347 | { duration: 150, easing: "ease-out" }, | ||
| 348 | ); | ||
| 349 | expect(z.animate).toHaveBeenLastCalledWith( | ||
| 350 | [ | ||
| 351 | { opacity: 0, transform: "translateY(72px)" }, | ||
| 352 | { opacity: 1, transform: "translateY(0px)" }, | ||
| 353 | ], | ||
| 354 | { duration: 150, easing: "ease-out" }, | ||
| 355 | ); | ||
| 356 | }); | ||
| 357 | |||
| 358 | it("rides new rows on a retained header's unfinished travel", () => { | ||
| 359 | const header = new TestRow("header", 32); | ||
| 360 | const above = new TestRow("above", 39); | ||
| 361 | const incoming = new TestRow("incoming", 36); | ||
| 362 | const { motion, layout } = fixture([header]); | ||
| 363 | motion.update(true); | ||
| 364 | layout([above, header]); | ||
| 365 | motion.update(true); | ||
| 366 | header.animations[0]!.progress = 0.25; | ||
| 367 | layout([above, header, incoming]); | ||
| 368 | motion.update(true); | ||
| 369 | expect(incoming.animate).toHaveBeenLastCalledWith( | ||
| 370 | [ | ||
| 371 | { opacity: 0, transform: "translateY(-30px)" }, | ||
| 372 | { opacity: 1, transform: "translateY(0px)" }, | ||
| 373 | ], | ||
| 374 | { duration: 150, easing: "ease-out" }, | ||
| 375 | ); | ||
| 376 | }); | ||
| 377 | |||
| 378 | it("cancels in-flight row travel when a bulk update skips animation", () => { | ||
| 379 | const a = new TestRow("a"); | ||
| 380 | const b = new TestRow("b"); | ||
| 381 | const { motion, layout } = fixture([a, b]); | ||
| 382 | motion.update(true); | ||
| 383 | layout([b, a]); | ||
| 384 | motion.update(true); | ||
| 385 | const first = a.animations[0]!; | ||
| 386 | first.progress = 0.4; | ||
| 387 | const incoming = Array.from({ length: 41 }, (_, index) => new TestRow(`new-${index}`)); | ||
| 388 | layout(incoming); | ||
| 389 | motion.update(true); | ||
| 390 | expect(first.cancel).toHaveBeenCalledOnce(); | ||
| 391 | expect(incoming.every((row) => row.animate.mock.calls.length === 0)).toBe(true); | ||
| 392 | }); | ||
| 393 | |||
| 394 | it("keeps in-flight shelf entry travel when the shelf closes mid-animation", () => { | ||
| 395 | const a = new TestRow("a", 40); | ||
| 396 | const header = new TestRow("header", 32); | ||
| 397 | const x = new TestRow("x", 36); | ||
| 398 | const y = new TestRow("y", 36); | ||
| 399 | const { motion, layout } = fixture([a, header, x]); | ||
| 400 | motion.update(true); | ||
| 401 | layout([a, header, x, y]); | ||
| 402 | for (const row of [header, x, y]) row.offsetTop -= 36; | ||
| 403 | motion.update(true); | ||
| 404 | const entry = y.animations[0]!; | ||
| 405 | header.animations[0]!.progress = 0.25; | ||
| 406 | x.animations[0]!.progress = 0.25; | ||
| 407 | entry.progress = 0.25; | ||
| 408 | layout([a, header, x]); | ||
| 409 | motion.update(true); | ||
| 410 | expect(entry.cancel).toHaveBeenCalledOnce(); | ||
| 411 | const clone = y.clones[0]!; | ||
| 412 | // Remaining entry travel (36 * 0.75) is baked into the clone's box so the | ||
| 413 | // fade starts from the row's current visual top instead of jumping to 0. | ||
| 414 | expect(clone.style.top).toBe("110px"); | ||
| 415 | expect(clone.animate).toHaveBeenCalledWith( | ||
| 416 | [ | ||
| 417 | { opacity: 0.25, transform: "translateY(0px)" }, | ||
| 418 | { opacity: 0, transform: "translateY(9px)" }, | ||
| 419 | ], | ||
| 420 | { duration: 150, easing: "ease-out" }, | ||
| 421 | ); | ||
| 422 | expectMove(header, -9); | ||
| 423 | expectMove(x, -9); | ||
| 424 | }); | ||
| 425 | |||
| 314 | it("clears exit clones on pickup and does not fade the release commit", () => { | 426 | it("clears exit clones on pickup and does not fade the release commit", () => { |
| 315 | const [a, b, c] = [new TestRow("a"), new TestRow("b"), new TestRow("c")]; | 427 | const [a, b, c] = [new TestRow("a"), new TestRow("b"), new TestRow("c")]; |
| 316 | const { motion, layout, parent } = fixture([a, b]); | 428 | const { motion, layout, parent } = fixture([a, b]); |
| ... | @@ -329,10 +441,16 @@ describe("sidebar list motion", () => { | ... | @@ -329,10 +441,16 @@ describe("sidebar list motion", () => { |
| 329 | expect(c.animations).toHaveLength(0); | 441 | expect(c.animations).toHaveLength(0); |
| 330 | layout([c, a]); | 442 | layout([c, a]); |
| 331 | motion.update(true); | 443 | motion.update(true); |
| 332 | expect(a.animate).toHaveBeenCalledWith([{ opacity: 0 }, { opacity: 1 }], { | 444 | expect(a.animate).toHaveBeenCalledWith( |
| 333 | duration: 150, | 445 | [ |
| 334 | easing: "ease-out", | 446 | { opacity: 0, transform: "translateY(0px)" }, |
| 335 | }); | 447 | { opacity: 1, transform: "translateY(0px)" }, |
| 448 | ], | ||
| 449 | { | ||
| 450 | duration: 150, | ||
| 451 | easing: "ease-out", | ||
| 452 | }, | ||
| 453 | ); | ||
| 336 | motion.dispose(); | 454 | motion.dispose(); |
| 337 | expect(a.animations.at(-1)!.cancel).toHaveBeenCalledOnce(); | 455 | expect(a.animations.at(-1)!.cancel).toHaveBeenCalledOnce(); |
| 338 | }); | 456 | }); |
| ... | @@ -349,15 +467,40 @@ describe("sidebar list motion", () => { | ... | @@ -349,15 +467,40 @@ describe("sidebar list motion", () => { |
| 349 | motion.update(true); | 467 | motion.update(true); |
| 350 | expect(marker.clones).toHaveLength(0); | 468 | expect(marker.clones).toHaveLength(0); |
| 351 | const clone = a.clones[0]!; | 469 | const clone = a.clones[0]!; |
| 352 | expect(clone.animate).toHaveBeenCalledWith([{ opacity: 0.4 }, { opacity: 0 }], { | 470 | expect(clone.animate).toHaveBeenCalledWith( |
| 353 | duration: 150, | 471 | [ |
| 354 | easing: "ease-out", | 472 | { opacity: 0.4, transform: "translateY(0px)" }, |
| 355 | }); | 473 | { opacity: 0, transform: "translateY(40px)" }, |
| 474 | ], | ||
| 475 | { | ||
| 476 | duration: 150, | ||
| 477 | easing: "ease-out", | ||
| 478 | }, | ||
| 479 | ); | ||
| 356 | motion.update(false); | 480 | motion.update(false); |
| 357 | expect(parent.children).toEqual([]); | 481 | expect(parent.children).toEqual([]); |
| 358 | expect(clone.animations[0]!.cancel).toHaveBeenCalledOnce(); | 482 | expect(clone.animations[0]!.cancel).toHaveBeenCalledOnce(); |
| 359 | }); | 483 | }); |
| 360 | 484 | ||
| 485 | it("retargets an in-flight entry when a later layout shift moves the row", () => { | ||
| 486 | const a = new TestRow("a", 40); | ||
| 487 | const header = new TestRow("header", 32); | ||
| 488 | const x = new TestRow("x", 36); | ||
| 489 | const y = new TestRow("y", 36); | ||
| 490 | const { motion, layout } = fixture([a, header, x]); | ||
| 491 | motion.update(true); | ||
| 492 | layout([a, header, x, y]); | ||
| 493 | for (const row of [header, x, y]) row.offsetTop -= 36; | ||
| 494 | motion.update(true); | ||
| 495 | const entry = y.animations[0]!; | ||
| 496 | entry.progress = 0.25; | ||
| 497 | for (const row of [header, x, y]) row.offsetTop += 40; | ||
| 498 | motion.update(true); | ||
| 499 | expect(entry.cancel).not.toHaveBeenCalled(); | ||
| 500 | // Remaining 27px of the 36px entry plus the new 40px shift. | ||
| 501 | expectMove(y, -13); | ||
| 502 | }); | ||
| 503 | |||
| 361 | it("skips fades when a large list change would clone too many rows", () => { | 504 | it("skips fades when a large list change would clone too many rows", () => { |
| 362 | const rows = Array.from({ length: 41 }, (_, index) => new TestRow(`row-${index}`)); | 505 | const rows = Array.from({ length: 41 }, (_, index) => new TestRow(`row-${index}`)); |
| 363 | const { motion, layout, parent } = fixture(rows); | 506 | const { motion, layout, parent } = fixture(rows); |
apps/web/src/components/Sidebar.motion.ts+67-16| ... | @@ -1,4 +1,8 @@ | ... | @@ -1,4 +1,8 @@ |
| 1 | const motionTiming = { duration: 150, easing: "ease-out" }; | 1 | const motionTiming = { duration: 150, easing: "ease-out" }; |
| 2 | // Rows normally ride their displaced neighbour's travel. Absent a moving | ||
| 3 | // neighbour, a row still travels on its own, clamped so a tall card does not | ||
| 4 | // slide its full height. | ||
| 5 | const rowTravel = (height: number) => Math.min(height, 40); | ||
| 2 | // A project filter change or a bulk snooze swaps a large part of the list at | 6 | // A project filter change or a bulk snooze swaps a large part of the list at |
| 3 | // once. Fades are the expensive part: every removed row gets a deep clone and | 7 | // once. Fades are the expensive part: every removed row gets a deep clone and |
| 4 | // every clone and entering row gets its own animation, and the layout reads | 8 | // every clone and entering row gets its own animation, and the layout reads |
| ... | @@ -23,7 +27,7 @@ export function createSidebarListMotion(parent: HTMLUListElement) { | ... | @@ -23,7 +27,7 @@ export function createSidebarListMotion(parent: HTMLUListElement) { |
| 23 | "(prefers-reduced-motion: reduce)", | 27 | "(prefers-reduced-motion: reduce)", |
| 24 | ); | 28 | ); |
| 25 | const running = new Map<HTMLElement, { animation: Animation; offset: number }>(); | 29 | const running = new Map<HTMLElement, { animation: Animation; offset: number }>(); |
| 26 | const entering = new Map<HTMLElement, Animation>(); | 30 | const entering = new Map<HTMLElement, { animation: Animation; travel: number }>(); |
| 27 | const exiting = new Map<HTMLElement, Animation>(); | 31 | const exiting = new Map<HTMLElement, Animation>(); |
| 28 | // Visual tops at drag release, relative to the list, so the release | 32 | // Visual tops at drag release, relative to the list, so the release |
| 29 | // commit can glide every row from where dnd-kit left it into its slot. | 33 | // commit can glide every row from where dnd-kit left it into its slot. |
| ... | @@ -31,15 +35,19 @@ export function createSidebarListMotion(parent: HTMLUListElement) { | ... | @@ -31,15 +35,19 @@ export function createSidebarListMotion(parent: HTMLUListElement) { |
| 31 | 35 | ||
| 32 | const remainingOffset = (node: HTMLElement) => { | 36 | const remainingOffset = (node: HTMLElement) => { |
| 33 | const current = running.get(node); | 37 | const current = running.get(node); |
| 34 | return current ? current.offset * (1 - progress(current.animation)) : 0; | 38 | const run = current ? current.offset * (1 - progress(current.animation)) : 0; |
| 39 | const entry = entering.get(node); | ||
| 40 | const enter = entry ? entry.travel * (1 - progress(entry.animation)) : 0; | ||
| 41 | return run + enter; | ||
| 35 | }; | 42 | }; |
| 36 | const clearFades = () => { | 43 | const clearFades = () => { |
| 37 | for (const animation of [...entering.values(), ...exiting.values()]) animation.cancel(); | 44 | for (const entry of entering.values()) entry.animation.cancel(); |
| 45 | for (const animation of exiting.values()) animation.cancel(); | ||
| 38 | for (const node of exiting.keys()) node.remove(); | 46 | for (const node of exiting.keys()) node.remove(); |
| 39 | entering.clear(); | 47 | entering.clear(); |
| 40 | exiting.clear(); | 48 | exiting.clear(); |
| 41 | }; | 49 | }; |
| 42 | const fadeOut = (node: HTMLElement, position: RowPosition) => { | 50 | const fadeOut = (node: HTMLElement, position: RowPosition, travel: number) => { |
| 43 | if (position.height === 0) return; | 51 | if (position.height === 0) return; |
| 44 | // React owns the removed row; only a noninteractive copy stays for the fade. | 52 | // React owns the removed row; only a noninteractive copy stays for the fade. |
| 45 | const clone = node.cloneNode(true) as HTMLElement; | 53 | const clone = node.cloneNode(true) as HTMLElement; |
| ... | @@ -72,8 +80,12 @@ export function createSidebarListMotion(parent: HTMLUListElement) { | ... | @@ -72,8 +80,12 @@ export function createSidebarListMotion(parent: HTMLUListElement) { |
| 72 | }); | 80 | }); |
| 73 | parent.append(clone); | 81 | parent.append(clone); |
| 74 | const entry = entering.get(node); | 82 | const entry = entering.get(node); |
| 83 | const entryProgress = entry ? progress(entry.animation) : 1; | ||
| 75 | const animation = clone.animate( | 84 | const animation = clone.animate( |
| 76 | [{ opacity: entry ? progress(entry) : 1 }, { opacity: 0 }], | 85 | [ |
| 86 | { opacity: entryProgress, transform: "translateY(0px)" }, | ||
| 87 | { opacity: 0, transform: `translateY(${travel}px)` }, | ||
| 88 | ], | ||
| 77 | motionTiming, | 89 | motionTiming, |
| 78 | ); | 90 | ); |
| 79 | exiting.set(clone, animation); | 91 | exiting.set(clone, animation); |
| ... | @@ -99,7 +111,10 @@ export function createSidebarListMotion(parent: HTMLUListElement) { | ... | @@ -99,7 +111,10 @@ export function createSidebarListMotion(parent: HTMLUListElement) { |
| 99 | }; | 111 | }; |
| 100 | const move = (node: HTMLElement, offset: number) => { | 112 | const move = (node: HTMLElement, offset: number) => { |
| 101 | cancel(node); | 113 | cancel(node); |
| 102 | if (offset === 0) return; | 114 | const entry = entering.get(node); |
| 115 | // The newer transform supersedes entry travel; its original opacity keeps fading. | ||
| 116 | if (entry) entry.travel = 0; | ||
| 117 | if (offset === 0 && !entry) return; | ||
| 103 | const animation = node.animate( | 118 | const animation = node.animate( |
| 104 | [{ transform: `translateY(${offset}px)` }, { transform: "translateY(0px)" }], | 119 | [{ transform: `translateY(${offset}px)` }, { transform: "translateY(0px)" }], |
| 105 | motionTiming, | 120 | motionTiming, |
| ... | @@ -144,15 +159,42 @@ export function createSidebarListMotion(parent: HTMLUListElement) { | ... | @@ -144,15 +159,42 @@ export function createSidebarListMotion(parent: HTMLUListElement) { |
| 144 | positions !== null && | 159 | positions !== null && |
| 145 | !reducedMotion?.matches && | 160 | !reducedMotion?.matches && |
| 146 | fadeCount <= MAX_FADED_ROWS_PER_UPDATE; | 161 | fadeCount <= MAX_FADED_ROWS_PER_UPDATE; |
| 162 | const movedDelta = new Map<HTMLElement, number>(); | ||
| 163 | const nextOrder = [...next.keys()]; | ||
| 164 | const oldOrder = positions === null ? [] : [...positions.keys()]; | ||
| 165 | const ridingDelta = ( | ||
| 166 | order: readonly HTMLElement[], | ||
| 167 | index: number, | ||
| 168 | retained: (node: HTMLElement) => boolean, | ||
| 169 | ) => { | ||
| 170 | for (let cursor = index - 1; cursor >= 0; cursor--) { | ||
| 171 | const node = order[cursor]!; | ||
| 172 | const delta = movedDelta.get(node); | ||
| 173 | if (delta !== undefined) return delta; | ||
| 174 | if (retained(node)) return remainingOffset(node); | ||
| 175 | } | ||
| 176 | return undefined; | ||
| 177 | }; | ||
| 147 | if (!shouldAnimate) clearFades(); | 178 | if (!shouldAnimate) clearFades(); |
| 148 | else { | 179 | else { |
| 180 | // A shelf that opens above its collapsed anchor shifts every retained | ||
| 181 | // row by the same amount. Entering rows take that same displacement so | ||
| 182 | // the shelf arrives as one moving block instead of rows popping into | ||
| 183 | // their final slots; exiting rows leave by it. | ||
| 184 | for (const [node, position] of next) { | ||
| 185 | const previousTop = positions!.get(node)?.top; | ||
| 186 | if (previousTop === undefined || previousTop === position.top) continue; | ||
| 187 | movedDelta.set(node, previousTop + remainingOffset(node) - position.top); | ||
| 188 | } | ||
| 149 | for (const [node, position] of positions!) { | 189 | for (const [node, position] of positions!) { |
| 150 | if (!next.has(node)) fadeOut(node, position); | 190 | if (next.has(node)) continue; |
| 191 | const delta = ridingDelta(oldOrder, oldOrder.indexOf(node), (n) => next.has(n)); | ||
| 192 | fadeOut(node, position, delta === undefined ? rowTravel(position.height) : -delta); | ||
| 151 | } | 193 | } |
| 152 | } | 194 | } |
| 153 | for (const [node, animation] of entering) { | 195 | for (const [node, entry] of entering) { |
| 154 | if (!next.has(node)) { | 196 | if (!next.has(node)) { |
| 155 | animation.cancel(); | 197 | entry.animation.cancel(); |
| 156 | entering.delete(node); | 198 | entering.delete(node); |
| 157 | } | 199 | } |
| 158 | } | 200 | } |
| ... | @@ -160,26 +202,35 @@ export function createSidebarListMotion(parent: HTMLUListElement) { | ... | @@ -160,26 +202,35 @@ export function createSidebarListMotion(parent: HTMLUListElement) { |
| 160 | if (!shouldAnimate || !next.has(node)) cancel(node); | 202 | if (!shouldAnimate || !next.has(node)) cancel(node); |
| 161 | } | 203 | } |
| 162 | if (shouldAnimate) { | 204 | if (shouldAnimate) { |
| 163 | for (const [node, position] of next) { | 205 | for (const [index, node] of nextOrder.entries()) { |
| 164 | const previousTop = positions?.get(node)?.top; | 206 | const position = next.get(node)!; |
| 207 | const previousTop = positions!.get(node)?.top; | ||
| 165 | if (previousTop === undefined) { | 208 | if (previousTop === undefined) { |
| 166 | if (position.height > 0) { | 209 | if (position.height > 0) { |
| 167 | const animation = node.animate([{ opacity: 0 }, { opacity: 1 }], motionTiming); | 210 | const delta = ridingDelta(nextOrder, index, (n) => positions!.has(n)); |
| 168 | entering.set(node, animation); | 211 | const travel = delta === undefined ? -rowTravel(position.height) : delta; |
| 212 | const animation = node.animate( | ||
| 213 | [ | ||
| 214 | { opacity: 0, transform: `translateY(${travel}px)` }, | ||
| 215 | { opacity: 1, transform: "translateY(0px)" }, | ||
| 216 | ], | ||
| 217 | motionTiming, | ||
| 218 | ); | ||
| 219 | entering.set(node, { animation, travel }); | ||
| 169 | animation.addEventListener( | 220 | animation.addEventListener( |
| 170 | "finish", | 221 | "finish", |
| 171 | () => { | 222 | () => { |
| 172 | if (entering.get(node) === animation) entering.delete(node); | 223 | if (entering.get(node)?.animation === animation) entering.delete(node); |
| 173 | }, | 224 | }, |
| 174 | { once: true }, | 225 | { once: true }, |
| 175 | ); | 226 | ); |
| 176 | } | 227 | } |
| 177 | continue; | 228 | continue; |
| 178 | } | 229 | } |
| 179 | if (previousTop === position.top) continue; | 230 | const delta = movedDelta.get(node); |
| 180 | // Computed progress includes the effect's easing. Only our own | 231 | // Computed progress includes the effect's easing. Only our own |
| 181 | // translate is carried forward; dnd-kit's transforms are never read. | 232 | // translate is carried forward; dnd-kit's transforms are never read. |
| 182 | move(node, previousTop + remainingOffset(node) - position.top); | 233 | if (delta !== undefined) move(node, delta); |
| 183 | } | 234 | } |
| 184 | } | 235 | } |
| 185 | if (released !== null) { | 236 | if (released !== null) { |