fix(ui,preview): panels chase the keyboard by frame, and the preview is finally mobile
The panel measured itself once, a fixed delay into the keyboard's animation, so it landed against a viewport that was still moving - and in a sidedock drawer no resize event fires at all, so it never moved. It now re-places every frame until its own position holds still, which needs neither a duration to guess nor an event to wait for. Its input taking focus is another trigger: that is what opens the keyboard in the first place. The preview had no viewport meta, so a 390px window still laid out at 980px: every "mobile" screenshot was a desktop one at a phone's aspect, and coarse-pointer rules could not apply. Which is why the tap targets looked fine here and were not.
This commit is contained in:
1 parent
b70331f7bd
commit
f7ccd21d7b
2 files changed
+39
-10
No files matched your search
@@ -2,6 +2,9 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
|
<!-- Without this a phone-sized window still lays out at 980px, so a
|
||||||
|
"mobile" screenshot is not one and coarse-pointer rules never apply. -->
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>beaver-calendar preview</title>
|
<title>beaver-calendar preview</title>
|
||||||
<link rel="stylesheet" href="/app.css" />
|
<link rel="stylesheet" href="/app.css" />
|
||||||
<link rel="stylesheet" href="/theme.css" />
|
<link rel="stylesheet" href="/theme.css" />
|
||||||
|
|||||||
@@ -27,8 +27,9 @@
|
|||||||
|
|
||||||
const view = useView();
|
const view = useView();
|
||||||
|
|
||||||
// iOS animates the keyboard in; the viewport only tells the truth after.
|
// The keyboard animates both ways: rather than guess how long that takes,
|
||||||
const KEYBOARD_SETTLE_MS = 350;
|
// the panel re-places every frame until its position holds still.
|
||||||
|
const STEADY_FRAMES = 4;
|
||||||
|
|
||||||
let panel = $state<HTMLElement | null>(null);
|
let panel = $state<HTMLElement | null>(null);
|
||||||
let at = $state({ top: 0, left: 0 });
|
let at = $state({ top: 0, left: 0 });
|
||||||
@@ -74,14 +75,38 @@
|
|||||||
if (!node.contains(event.target as Node)) onclose();
|
if (!node.contains(event.target as Node)) onclose();
|
||||||
};
|
};
|
||||||
const reflow = (): void => measure();
|
const reflow = (): void => measure();
|
||||||
|
/**
|
||||||
|
* Re-place every frame until the position holds still. The keyboard
|
||||||
|
* animates both ways, and in a sidedock nothing fires an event at all -
|
||||||
|
* a frame loop needs neither a duration to guess nor an event to wait for.
|
||||||
|
*/
|
||||||
|
let frame: number | null = null;
|
||||||
|
const settle = (): void => {
|
||||||
|
if (frame !== null) return;
|
||||||
|
let last = Number.NaN;
|
||||||
|
let steady = 0;
|
||||||
|
const tick = (): void => {
|
||||||
|
measure();
|
||||||
|
steady = at.top === last ? steady + 1 : 0;
|
||||||
|
last = at.top;
|
||||||
|
if (steady >= STEADY_FRAMES) {
|
||||||
|
frame = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
frame = win.requestAnimationFrame(tick);
|
||||||
|
};
|
||||||
|
frame = win.requestAnimationFrame(tick);
|
||||||
|
};
|
||||||
document.addEventListener("pointerdown", outside, true);
|
document.addEventListener("pointerdown", outside, true);
|
||||||
window.addEventListener("resize", reflow);
|
window.addEventListener("resize", reflow);
|
||||||
// The keyboard changes the visual viewport without resizing the window,
|
// The keyboard changes the visual viewport without resizing the window.
|
||||||
// and its animation takes a moment to settle.
|
|
||||||
const vv = win.visualViewport;
|
const vv = win.visualViewport;
|
||||||
vv?.addEventListener("resize", reflow);
|
vv?.addEventListener("resize", settle);
|
||||||
vv?.addEventListener("scroll", reflow);
|
vv?.addEventListener("scroll", settle);
|
||||||
const settle = win.setTimeout(reflow, KEYBOARD_SETTLE_MS);
|
// The panel's own input takes focus a beat before the keyboard is up, and
|
||||||
|
// swapping fields re-opens it.
|
||||||
|
node.addEventListener("focusin", settle);
|
||||||
|
settle();
|
||||||
// A panel whose body grew (an option list opened) re-places itself. Only
|
// A panel whose body grew (an option list opened) re-places itself. Only
|
||||||
// the content height is watched: measure() sets max-height, so observing
|
// the content height is watched: measure() sets max-height, so observing
|
||||||
// the panel itself would feed its own change back in.
|
// the panel itself would feed its own change back in.
|
||||||
@@ -97,9 +122,10 @@
|
|||||||
destroy() {
|
destroy() {
|
||||||
document.removeEventListener("pointerdown", outside, true);
|
document.removeEventListener("pointerdown", outside, true);
|
||||||
window.removeEventListener("resize", reflow);
|
window.removeEventListener("resize", reflow);
|
||||||
vv?.removeEventListener("resize", reflow);
|
vv?.removeEventListener("resize", settle);
|
||||||
vv?.removeEventListener("scroll", reflow);
|
vv?.removeEventListener("scroll", settle);
|
||||||
win.clearTimeout(settle);
|
node.removeEventListener("focusin", settle);
|
||||||
|
if (frame !== null) win.cancelAnimationFrame(frame);
|
||||||
watcher?.disconnect();
|
watcher?.disconnect();
|
||||||
win.removeEventListener("keydown", keys, true);
|
win.removeEventListener("keydown", keys, true);
|
||||||
panel = null;
|
panel = null;
|
||||||
|
|||||||
Reference in new issue
Block a user