diff options
| author | Adam Soltys <[email protected]> | 2026-09-03 21:27:29 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-09-03 21:27:29 -0700 |
| commit | ebdb209ff4480ce88ee764c36b28b1b7d5890eed (patch) | |
| tree | 0dbca7006f30da27c8015e938e129382f9a1f41c /src | |
| parent | 3b65462549269e3895345919b51164e017bcd13d (diff) | |
| download | coinosv3-ebdb209ff4480ce88ee764c36b28b1b7d5890eed.tar.gz coinosv3-ebdb209ff4480ce88ee764c36b28b1b7d5890eed.zip | |
The carousel stops snapping back mid-throw: on release the mandatory snap-type was restored on a fixed 400ms timer, which during rapid back-and-forth fired while the next glide was only partway — re-applying mandatory snap then makes the browser snap to the nearest point, the card you started from, yanking it back. Snap now restores on the scrollend of that specific glide, guarded by a per-drag token so a stale restore from an earlier throw can't fire during a later one (timer kept only as a fallback for the no-scroll case)
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
Diffstat (limited to 'src')
| -rw-r--r-- | src/app.js | 17 |
1 files changed, 16 insertions, 1 deletions
@@ -3446,6 +3446,12 @@ function balanceCard() { if (e.target.closest && e.target.closest('button, a, input')) return; e.preventDefault(); // no text selection while pulling const el = e.currentTarget; + // A new grab invalidates any pending snap-restore from the previous + // release: bumping the token makes a stale restore a no-op, so it can + // never re-apply mandatory snap mid-glide and yank the card back to + // where it started. Kill its timer too so they don't pile up. + el._dragGen = (el._dragGen || 0) + 1; + clearTimeout(el._snapT); const x0 = e.clientX, s0 = el.scrollLeft; const kids0 = [...el.children]; // the card the drag started on — a flick throws relative to it @@ -3493,7 +3499,16 @@ function balanceCard() { if (Math.abs(vel) > 0.25) best = Math.max(0, Math.min(kids.length - 1, fromIdx + (vel < 0 ? 1 : -1))); const k = kids[best]; if (k) el.scrollTo({ left: k.offsetLeft + k.offsetWidth / 2 - el.clientWidth / 2, behavior: 'smooth' }); - setTimeout(() => { el.style.scrollSnapType = ''; }, 400); + // Restore mandatory snap only once THIS glide has come to rest, and + // only if no newer drag has taken over — a fixed timer used to fire + // mid-glide and snap the card back to its origin. scrollend marks + // the true end; the timer is just a fallback for when the target is + // already reached (no scroll, no scrollend). + const gen = el._dragGen; + const restore = () => { if (el._dragGen === gen && !el._dragging) el.style.scrollSnapType = ''; }; + el.addEventListener('scrollend', restore, { once: true }); + clearTimeout(el._snapT); + el._snapT = setTimeout(restore, 700); // the glide's own scroll events reschedule this; a release already // at rest on the target needs the switch to fire regardless clearTimeout(el._settle); |
