diff options
| -rw-r--r-- | src/features/messages.js | 22 | ||||
| -rw-r--r-- | tools/pager-tap-slop-test.js | 24 |
2 files changed, 38 insertions, 8 deletions
diff --git a/src/features/messages.js b/src/features/messages.js index a7dc54c..6dbb4fd 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -12442,7 +12442,7 @@ export function messagesFeature(ctx) { box.style.scrollSnapType = 'none'; const done = () => { box._glide = 0; box.style.scrollSnapType = ''; }; if (Math.abs(dist) < 1) { done(); return; } - const ms = Math.max(90, Math.min(220, Math.abs(dist) / Math.max(2.5, Math.abs(vel) * 1.5))); + const ms = Math.max(80, Math.min(170, Math.abs(dist) / Math.max(3.5, Math.abs(vel) * 1.5))); const t0 = performance.now(); const step = (now) => { const k = Math.min(1, (now - t0) / ms), e = 1 - Math.pow(1 - k, 3); @@ -12451,6 +12451,13 @@ export function messagesFeature(ctx) { }; box._glide = requestAnimationFrame(step); } + // Where a slide (or post) sits in its scroller — measured, not counted in + // screens: a post is 100dvh, a hair off the scroller's height, and n + // screens down landed past it for the snap to yank back. + const pagerAt = (box, el, y) => { + const b = box.getBoundingClientRect(), r = el.getBoundingClientRect(); + return y ? box.scrollTop + r.top - b.top : box.scrollLeft + r.left - b.left; + }; const pagerHold = (box) => { if (box && box._glide) { cancelAnimationFrame(box._glide); box._glide = 0; } }; function buildPager(want) { @@ -12503,10 +12510,11 @@ export function messagesFeature(ctx) { // let go of a drag: on to the next (or the one before), or back function settle(d, step, vel = 0) { const box = d.box, y = d.axis === 'y'; - const size = (y ? box.clientHeight : box.clientWidth) || 1; - const last = Math.max(0, Math.round(((y ? box.scrollHeight : box.scrollWidth) - size) / size)); - const to = Math.min(last, Math.max(0, Math.round(d.start / size) + step)); - pagerGlide(box, d.axis, to * size, vel); + const stops = [...box.children].filter((c) => c.matches(y ? '.mp-group, .mp-tail' : '.mp-slide')).map((c) => pagerAt(box, c, y)); + if (!stops.length) return; + // the stop it set off from, then one along (or back) + const from = stops.reduce((bi, p, i) => (Math.abs(p - d.start) < Math.abs(stops[bi] - d.start) ? i : bi), 0); + pagerGlide(box, d.axis, stops[Math.min(stops.length - 1, Math.max(0, from + step))], vel); } el.addEventListener('touchend', (e) => { const d = touch; @@ -12734,13 +12742,13 @@ export function messagesFeature(ctx) { e.preventDefault(); const from = pager.onTail || !here ? pager.tail : here.closest('.mp-group'); const to = from && (v > 0 ? from.nextElementSibling : from.previousElementSibling); - if (to) pagerGlide(pager.scroller, 'y', to.offsetTop); + if (to) pagerGlide(pager.scroller, 'y', pagerAt(pager.scroller, to, true)); return; } if (hz) { e.preventDefault(); const to = !pager.onTail && here && (hz > 0 ? here.nextElementSibling : here.previousElementSibling); - if (to) pagerGlide(here.parentElement, 'x', to.offsetLeft); + if (to) pagerGlide(here.parentElement, 'x', pagerAt(here.parentElement, to, false)); return; } if (e.key === ' ' && pager.cur) { diff --git a/tools/pager-tap-slop-test.js b/tools/pager-tap-slop-test.js index b0bd339..bcc790c 100644 --- a/tools/pager-tap-slop-test.js +++ b/tools/pager-tap-slop-test.js @@ -132,7 +132,29 @@ try { await page.evaluate(() => { window.__landed = new Promise((res) => { const t0 = performance.now(); const look = () => { const s = [...document.querySelectorAll('.mp-slide')].find((x) => { const r = x.getBoundingClientRect(); return Math.abs(r.top) < 1 && Math.abs(r.left) < 1; }); if (s && !s.getAttribute('data-k').endsWith('/tall.webm')) res(Math.round(performance.now() - t0)); else if (performance.now() - t0 > 2000) res(-1); else requestAnimationFrame(look); }; requestAnimationFrame(look); }); }); await page.touchscreen.touchEnd(); const ms = await page.evaluate(() => window.__landed); - check('a swipe lands on the next post within 300ms', ms > 0 && ms <= 300, ms + 'ms'); + check('a swipe lands on the next post within 250ms', ms > 0 && ms <= 250, ms + 'ms'); + + console.log('\n[no overshoot: a post not quite a screen tall, as 100dvh can be on a phone]'); + await page.addStyleTag({ content: '.mp-group, .mp-tail { height: calc(100dvh + 12px) !important; }' }); + await page.evaluate(() => { document.querySelector('.mp-scroll').scrollTop = 0; }); + await sleep(600); + // every frame's scroll, from the finger lifting until well after it lands + const glide = async () => { + await page.touchscreen.touchStart(195, 650); + for (let i = 1; i <= 8; i++) { await page.touchscreen.touchMove(195, 650 - 40 * i); await sleep(16); } + await page.evaluate(() => { const sc = document.querySelector('.mp-scroll'); window.__trace = []; const t0 = performance.now(); const rec = () => { window.__trace.push(sc.scrollTop); if (performance.now() - t0 < 900) requestAnimationFrame(rec); }; requestAnimationFrame(rec); }); + await page.touchscreen.touchEnd(); + await sleep(1100); + return page.evaluate(() => { const sc = document.querySelector('.mp-scroll'); const tops = [...sc.querySelectorAll('.mp-group, .mp-tail')].map((g) => g.getBoundingClientRect().top); return { trace: window.__trace, off: Math.min(...tops.map(Math.abs)) }; }); + }; + for (let n = 1; n <= 2; n++) { + const g = await glide(); + const end = g.trace[g.trace.length - 1]; + const past = Math.max(...g.trace.map((x) => x - end)); + const back = g.trace.some((x, i) => i && x < g.trace[i - 1] - 0.5); + check('swipe ' + n + ': straight on to the post, never past it and back', past <= 1 && !back, g.trace[0].toFixed(1) + '→' + end.toFixed(1) + ', past by ' + past.toFixed(1) + 'px' + (back ? ', moved back' : '')); + check('…resting square on it', g.off < 1, g.off.toFixed(2) + 'px off'); + } check('no page errors', errs.length === 0, errs.slice(0, 2).join(' | ')); } finally { await browser.close(); server.stop(true); } console.log(ok ? '\n✅ a little movement is still a tap' : '\n❌ failed'); |
