aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/features/messages.js22
-rw-r--r--tools/pager-tap-slop-test.js24
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');