|
| 1 | +/** |
| 2 | + * Wraps a synchronous function call with scroll position preservation. |
| 3 | + * SolidJS fine-grained DOM updates complete synchronously within fn(), |
| 4 | + * so a single synchronous scrollTo is sufficient — no rAF needed. |
| 5 | + */ |
| 6 | +export function withScrollLock(fn: () => void): void { |
| 7 | + const y = window.scrollY; |
| 8 | + try { |
| 9 | + fn(); |
| 10 | + } finally { |
| 11 | + window.scrollTo(0, y); |
| 12 | + } |
| 13 | +} |
| 14 | + |
| 15 | +/** |
| 16 | + * FLIP animation for reorderable lists. Records positions of elements |
| 17 | + * matching `[data-repo-group]` before fn(), then animates them to their |
| 18 | + * new positions after the DOM update. |
| 19 | + * |
| 20 | + * Consistent with TrackedTab's FLIP: 200ms ease-in-out, respects |
| 21 | + * prefers-reduced-motion (falls back to withScrollLock). |
| 22 | + */ |
| 23 | +export function withFlipAnimation(fn: () => void): void { |
| 24 | + if (typeof window === "undefined") { fn(); return; } |
| 25 | + |
| 26 | + // Reduced motion: fall back to instant scroll lock |
| 27 | + if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) { |
| 28 | + withScrollLock(fn); |
| 29 | + return; |
| 30 | + } |
| 31 | + |
| 32 | + // First: record positions of all repo group wrappers |
| 33 | + const items = document.querySelectorAll<HTMLElement>("[data-repo-group]"); |
| 34 | + const before = new Map<string, DOMRect>(); |
| 35 | + for (const el of items) { |
| 36 | + const key = el.dataset.repoGroup!; |
| 37 | + before.set(key, el.getBoundingClientRect()); |
| 38 | + } |
| 39 | + |
| 40 | + // Execute state change (SolidJS updates DOM synchronously) |
| 41 | + const scrollY = window.scrollY; |
| 42 | + fn(); |
| 43 | + |
| 44 | + // Last, Invert, Play — reads before writes to avoid layout thrash. |
| 45 | + // All getBoundingClientRect calls happen before scrollTo so the browser |
| 46 | + // doesn't force a synchronous layout recalculation between them. |
| 47 | + requestAnimationFrame(() => { |
| 48 | + const afterItems = document.querySelectorAll<HTMLElement>("[data-repo-group]"); |
| 49 | + const scrollDrift = window.scrollY - scrollY; |
| 50 | + const deltas: { el: HTMLElement; dy: number }[] = []; |
| 51 | + for (const el of afterItems) { |
| 52 | + const key = el.dataset.repoGroup!; |
| 53 | + const old = before.get(key); |
| 54 | + if (!old) continue; |
| 55 | + const now = el.getBoundingClientRect(); |
| 56 | + // Adjust for scroll drift: old.top was measured at scrollY, |
| 57 | + // now.top is measured at the current (possibly drifted) scroll position. |
| 58 | + const dy = old.top - now.top - scrollDrift; |
| 59 | + if (Math.abs(dy) < 1) continue; |
| 60 | + deltas.push({ el, dy }); |
| 61 | + } |
| 62 | + window.scrollTo(0, scrollY); |
| 63 | + for (const { el, dy } of deltas) { |
| 64 | + el.animate( |
| 65 | + [{ transform: `translateY(${dy}px)` }, { transform: "translateY(0)" }], |
| 66 | + { duration: 200, easing: "ease-in-out" }, |
| 67 | + ); |
| 68 | + } |
| 69 | + }); |
| 70 | +} |
0 commit comments