+
+
beaver-calendar preview
diff --git a/src/ui/anchored-panel.svelte b/src/ui/anchored-panel.svelte
index 22506b7..0a8ab0a 100644
--- a/src/ui/anchored-panel.svelte
+++ b/src/ui/anchored-panel.svelte
@@ -27,8 +27,9 @@
const view = useView();
- // iOS animates the keyboard in; the viewport only tells the truth after.
- const KEYBOARD_SETTLE_MS = 350;
+ // The keyboard animates both ways: rather than guess how long that takes,
+ // the panel re-places every frame until its position holds still.
+ const STEADY_FRAMES = 4;
let panel = $state(null);
let at = $state({ top: 0, left: 0 });
@@ -74,14 +75,38 @@
if (!node.contains(event.target as Node)) onclose();
};
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);
window.addEventListener("resize", reflow);
- // The keyboard changes the visual viewport without resizing the window,
- // and its animation takes a moment to settle.
+ // The keyboard changes the visual viewport without resizing the window.
const vv = win.visualViewport;
- vv?.addEventListener("resize", reflow);
- vv?.addEventListener("scroll", reflow);
- const settle = win.setTimeout(reflow, KEYBOARD_SETTLE_MS);
+ vv?.addEventListener("resize", settle);
+ vv?.addEventListener("scroll", settle);
+ // 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
// the content height is watched: measure() sets max-height, so observing
// the panel itself would feed its own change back in.
@@ -97,9 +122,10 @@
destroy() {
document.removeEventListener("pointerdown", outside, true);
window.removeEventListener("resize", reflow);
- vv?.removeEventListener("resize", reflow);
- vv?.removeEventListener("scroll", reflow);
- win.clearTimeout(settle);
+ vv?.removeEventListener("resize", settle);
+ vv?.removeEventListener("scroll", settle);
+ node.removeEventListener("focusin", settle);
+ if (frame !== null) win.cancelAnimationFrame(frame);
watcher?.disconnect();
win.removeEventListener("keydown", keys, true);
panel = null;