Fix swipe hijacking h-scroll; drag bottom sheets down to dismiss
- onSwipe: before claiming a horizontal page-swipe, walk up from the touch target for an overflow-x auto/scroll ancestor that can still scroll that way (tab strip, moves list, coverage grid) and bail if found — sliding those no longer flips to the next Pokémon. - segThumb: retry placement until the seg is laid out, and observe it for resizes, so the sliding pill is positioned on first paint instead of only after the first click. Dropped the :has() selector for a JS-applied .seg--thumbed class. - New lib/sheet-drag.js: drag the game sheet / Pokémon picker down by its header to dismiss (past ~110px or a flick), else it springs back. Header gets touch-action:none + a grab cursor. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
This commit is contained in:
parent
17e8e30f9b
commit
5bce0f93f3
@ -7,6 +7,7 @@ import {
|
||||
} from '../data/pokedex-resolver.js';
|
||||
import { settings } from '../store/settings.js';
|
||||
import { gameColors } from '../data/game-colors.js';
|
||||
import { dragToClose } from '../lib/sheet-drag.js';
|
||||
|
||||
let openInstance = null;
|
||||
|
||||
@ -134,20 +135,18 @@ export async function openGameSheet() {
|
||||
}
|
||||
}
|
||||
|
||||
panel.append(
|
||||
const head = el(
|
||||
'div',
|
||||
{ class: 'gsheet__head' },
|
||||
el('h2', {}, 'Choose your game'),
|
||||
el(
|
||||
'div',
|
||||
{ class: 'gsheet__head' },
|
||||
el('h2', {}, 'Choose your game'),
|
||||
el(
|
||||
'button',
|
||||
{ class: 'gsheet__close', type: 'button', 'aria-label': 'Close', onclick: close },
|
||||
'✕',
|
||||
),
|
||||
'button',
|
||||
{ class: 'gsheet__close', type: 'button', 'aria-label': 'Close', onclick: close },
|
||||
'✕',
|
||||
),
|
||||
subdex,
|
||||
list,
|
||||
);
|
||||
dragToClose(panel, head, close);
|
||||
panel.append(head, subdex, list);
|
||||
backdrop.append(panel);
|
||||
document.body.append(backdrop);
|
||||
document.addEventListener('keydown', onKey);
|
||||
|
||||
@ -3,6 +3,7 @@ import { loadSnapshot } from '../data/snapshot.js';
|
||||
import { settings } from '../store/settings.js';
|
||||
import { Sprite } from './Sprite.js';
|
||||
import { TypeChip } from './TypeChip.js';
|
||||
import { dragToClose } from '../lib/sheet-drag.js';
|
||||
|
||||
let openInstance = null;
|
||||
|
||||
@ -83,16 +84,14 @@ export async function openPokemonPicker(onPick, { closeAfterPick = false } = {})
|
||||
}
|
||||
}
|
||||
|
||||
panel.append(
|
||||
el(
|
||||
'div',
|
||||
{ class: 'gsheet__head' },
|
||||
el('h2', {}, 'Add a Pokémon'),
|
||||
el('button', { class: 'gsheet__close', type: 'button', 'aria-label': 'Close', onclick: close }, '✕'),
|
||||
),
|
||||
input,
|
||||
results,
|
||||
const head = el(
|
||||
'div',
|
||||
{ class: 'gsheet__head' },
|
||||
el('h2', {}, 'Add a Pokémon'),
|
||||
el('button', { class: 'gsheet__close', type: 'button', 'aria-label': 'Close', onclick: close }, '✕'),
|
||||
);
|
||||
dragToClose(panel, head, close);
|
||||
panel.append(head, input, results);
|
||||
backdrop.append(panel);
|
||||
document.body.append(backdrop);
|
||||
document.addEventListener('keydown', onKey);
|
||||
|
||||
@ -30,19 +30,28 @@ export function countUp(node, to, { from = 0, duration = 550, decimals = 0, pref
|
||||
* reduced-motion users just get an instant jump.
|
||||
*/
|
||||
export function segThumb(seg, _count, active = 0) {
|
||||
seg.classList.add('seg--thumbed');
|
||||
const thumb = document.createElement('div');
|
||||
thumb.className = 'seg__thumb';
|
||||
seg.prepend(thumb);
|
||||
let current = active;
|
||||
const place = (i) => {
|
||||
const place = (i, tries = 0) => {
|
||||
current = i;
|
||||
const b = seg.querySelectorAll('.seg__btn')[i];
|
||||
if (!b || !b.offsetWidth) return;
|
||||
thumb.style.width = `${b.offsetWidth}px`;
|
||||
thumb.style.transform = `translateX(${b.offsetLeft - 3}px)`;
|
||||
if (b && b.offsetWidth) {
|
||||
thumb.style.width = `${b.offsetWidth}px`;
|
||||
thumb.style.transform = `translateX(${b.offsetLeft - 3}px)`;
|
||||
} else if (tries < 20) {
|
||||
// Not laid out yet (hidden tab, webfont pending) — try again.
|
||||
requestAnimationFrame(() => place(i, tries + 1));
|
||||
}
|
||||
};
|
||||
requestAnimationFrame(() => place(active));
|
||||
if (document.fonts?.ready) document.fonts.ready.then(() => place(current));
|
||||
if (typeof ResizeObserver !== 'undefined') {
|
||||
const ro = new ResizeObserver(() => place(current));
|
||||
ro.observe(seg);
|
||||
}
|
||||
return place;
|
||||
}
|
||||
|
||||
|
||||
43
src/lib/sheet-drag.js
Normal file
43
src/lib/sheet-drag.js
Normal file
@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Drag a bottom sheet down by its handle to dismiss it. `panel` is the
|
||||
* `.gsheet`, `handle` the grab area (its header), `close` the dismiss fn.
|
||||
* Past ~110px (or a quick flick) it closes; otherwise it springs back.
|
||||
*/
|
||||
export function dragToClose(panel, handle, close) {
|
||||
let y0 = 0;
|
||||
let dy = 0;
|
||||
let t0 = 0;
|
||||
let dragging = false;
|
||||
|
||||
const start = (e) => {
|
||||
if (e.touches.length !== 1) return;
|
||||
y0 = e.touches[0].clientY;
|
||||
dy = 0;
|
||||
t0 = Date.now();
|
||||
dragging = true;
|
||||
panel.style.transition = 'none';
|
||||
};
|
||||
|
||||
const move = (e) => {
|
||||
if (!dragging) return;
|
||||
dy = e.touches[0].clientY - y0;
|
||||
// Rubber-band an upward pull; follow a downward one.
|
||||
const shown = dy < 0 ? dy * 0.25 : dy;
|
||||
panel.style.transform = `translateY(${shown}px)`;
|
||||
if (dy > 0) e.preventDefault();
|
||||
};
|
||||
|
||||
const end = () => {
|
||||
if (!dragging) return;
|
||||
dragging = false;
|
||||
panel.style.transition = '';
|
||||
panel.style.transform = '';
|
||||
const flick = dy > 40 && Date.now() - t0 < 300;
|
||||
if (dy > 110 || flick) close();
|
||||
};
|
||||
|
||||
handle.addEventListener('touchstart', start, { passive: true });
|
||||
handle.addEventListener('touchmove', move, { passive: false });
|
||||
handle.addEventListener('touchend', end, { passive: true });
|
||||
handle.addEventListener('touchcancel', end, { passive: true });
|
||||
}
|
||||
@ -14,6 +14,24 @@ export function onSwipe(target, { onLeft, onRight, threshold = 60 } = {}) {
|
||||
let t0 = 0;
|
||||
let active = false;
|
||||
let claimed = false;
|
||||
let startNode = null;
|
||||
|
||||
// Would a horizontal drag from `node` scroll a nested element instead of
|
||||
// the page? (moves list, tab strip, coverage grid…) `dir` < 0 means the
|
||||
// finger is moving left — that scrolls such an element to the right.
|
||||
const scrollsInside = (node, dir) => {
|
||||
for (let n = node; n && n !== document.body && n !== target.parentElement; n = n.parentElement) {
|
||||
if (n.scrollWidth - n.clientWidth > 2) {
|
||||
const ox = getComputedStyle(n).overflowX;
|
||||
if (ox === 'auto' || ox === 'scroll') {
|
||||
const max = n.scrollWidth - n.clientWidth;
|
||||
if (dir < 0 && n.scrollLeft < max - 1) return true;
|
||||
if (dir > 0 && n.scrollLeft > 1) return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const start = (e) => {
|
||||
if (e.touches.length !== 1) {
|
||||
@ -25,6 +43,7 @@ export function onSwipe(target, { onLeft, onRight, threshold = 60 } = {}) {
|
||||
t0 = Date.now();
|
||||
active = true;
|
||||
claimed = false;
|
||||
startNode = e.target;
|
||||
};
|
||||
|
||||
const move = (e) => {
|
||||
@ -43,8 +62,15 @@ export function onSwipe(target, { onLeft, onRight, threshold = 60 } = {}) {
|
||||
active = false;
|
||||
return;
|
||||
}
|
||||
// Only claim a horizontal page-swipe once the drag is unambiguous.
|
||||
if (adx > 20 && adx > ady * 2) claimed = true;
|
||||
// Only claim a horizontal page-swipe once the drag is unambiguous —
|
||||
// and not when it should scroll something nested instead.
|
||||
if (adx > 20 && adx > ady * 2) {
|
||||
if (scrollsInside(startNode, dx)) {
|
||||
active = false;
|
||||
return;
|
||||
}
|
||||
claimed = true;
|
||||
}
|
||||
}
|
||||
if (claimed) e.preventDefault();
|
||||
};
|
||||
|
||||
@ -1725,6 +1725,11 @@
|
||||
justify-content: space-between;
|
||||
padding: 16px 0 8px;
|
||||
background: var(--surface);
|
||||
cursor: grab;
|
||||
touch-action: none;
|
||||
}
|
||||
.gsheet__head:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
.gsheet__head::before {
|
||||
content: "";
|
||||
@ -3347,7 +3352,7 @@
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
.seg:has(.seg__thumb) .seg__btn.is-active {
|
||||
.seg--thumbed .seg__btn.is-active {
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user