From 223560c8da04f79dc6314fd88cdc463d2f205bcc Mon Sep 17 00:00:00 2001 From: h Date: Sat, 5 Sep 2026 01:15:11 +0200 Subject: [PATCH] fix(view): follow the keyboard through the navbar hiding behind it Same two misses as the calendar's: the four-frame watch stopped before Obsidian hid its navbar - a second move of the ground, after the keyboard's own - and a trigger that arrived while a watch was running was dropped rather than restarting it. The watch now needs twelve steady frames, restarts on every trigger, and listens to Obsidian's Capacitor keyboard events too. --- src/view.ts | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/src/view.ts b/src/view.ts index 7cefbf7..7bb6237 100644 --- a/src/view.ts +++ b/src/view.ts @@ -13,9 +13,18 @@ export const VIEW_TYPE_PANEL = "beaver-panel"; // sits under every view, including the one filling the screen. const BOTTOM_BARS = [".status-bar", ".mobile-navbar", ".mobile-toolbar"]; -// The keyboard and the navbar both animate. Rather than guess how long that -// takes, the inset is re-measured every frame until the reading holds still. -const STEADY_FRAMES = 4; +// The keyboard and the navbar both animate, and Obsidian hides the navbar +// *after* the keyboard is up: the inset is re-measured every frame until the +// reading has held still long enough to outlast both moves. +const STEADY_FRAMES = 12; + +/** Obsidian's Capacitor keyboard events on mobile; absent on the desktop. */ +const KEYBOARD_EVENTS = [ + "keyboardWillShow", + "keyboardDidShow", + "keyboardWillHide", + "keyboardDidHide", +] as const; interface SavedState { follow?: unknown; @@ -106,6 +115,14 @@ export class PanelView extends ItemView { // out the navbar comes back, so the leaf has to be re-measured after. this.registerDomEvent(this.contentEl, "focusin", () => this.settle()); this.registerDomEvent(this.contentEl, "focusout", () => this.settle()); + // Obsidian's own keyboard events (Capacitor, mobile only) fire before the + // animation and again as the navbar returns, which visualViewport alone + // does not always report. + for (const name of KEYBOARD_EVENTS) { + const onKeyboard = () => this.settle(); + window.addEventListener(name, onKeyboard); + this.register(() => window.removeEventListener(name, onKeyboard)); + } this.follow(this.app.workspace.getActiveFile()); this.plugin.views.add(this); this.mountApp(); @@ -121,11 +138,13 @@ export class PanelView extends ItemView { * which is the case in a sidedock drawer. */ private settle(): void { - if (this.settling !== null) { - return; - } let last = Number.NaN; let steady = 0; + // A fresh trigger restarts the watch rather than being dropped: the navbar + // coming back is a second move, after the first one has settled. + if (this.settling !== null) { + cancelAnimationFrame(this.settling); + } const tick = () => { const floor = this.inset(); steady = floor === last ? steady + 1 : 0;