fix(view,panel): the composer clears the navbar and the keyboard, panel opens on the master
The inset only knew the desktop status bar, so on a phone the composer sat under the navbar - which paints over a leaf instead of shortening it - and the software keyboard hid it outright. Every bottom bar is measured now, and the keyboard is read off `visualViewport`: it shrinks the visible area without resizing the layout, so no workspace event fires and the panel had no way to know. Focus re-measures once iOS has finished animating the keys in. The panel asks the shell to fall back to the latest master, so opening it on a phone lands in the day's thread rather than on a prompt. The preview fixtures anchor at midday instead of the wall clock: spread over hours, a run just after midnight pushed half of them into yesterday and emptied the "Today" group the switcher smoke checks.
This commit is contained in:
@@ -8,7 +8,12 @@ import type {
|
|||||||
|
|
||||||
const MINUTE = 60_000;
|
const MINUTE = 60_000;
|
||||||
const HOUR = 60 * MINUTE;
|
const HOUR = 60 * MINUTE;
|
||||||
const now = Date.now();
|
// Anchored inside the day rather than at the wall clock: the fixtures are
|
||||||
|
// spread over hours, and a run just after midnight would push half of them
|
||||||
|
// into yesterday and empty the "Today" group the rail is checked against.
|
||||||
|
const wall = new Date();
|
||||||
|
const now =
|
||||||
|
wall.getHours() < 12 ? new Date(wall).setHours(12, 0, 0, 0) : wall.getTime();
|
||||||
const ago = (ms: number) => new Date(now - ms).toISOString();
|
const ago = (ms: number) => new Date(now - ms).toISOString();
|
||||||
|
|
||||||
export const MASTER = "c1master0000";
|
export const MASTER = "c1master0000";
|
||||||
|
|||||||
@@ -58,6 +58,7 @@
|
|||||||
<PanelShell
|
<PanelShell
|
||||||
{client}
|
{client}
|
||||||
companion={state.companion}
|
companion={state.companion}
|
||||||
|
fallbackToMaster
|
||||||
{index}
|
{index}
|
||||||
onOpenFile={(path) => host.openNote?.(`мета/бобер/${path}`)}
|
onOpenFile={(path) => host.openNote?.(`мета/бобер/${path}`)}
|
||||||
showFollow
|
showFollow
|
||||||
|
|||||||
+52
-9
@@ -7,6 +7,13 @@ import App from "./ui/app.svelte";
|
|||||||
|
|
||||||
export const VIEW_TYPE_PANEL = "beaver-panel";
|
export const VIEW_TYPE_PANEL = "beaver-panel";
|
||||||
|
|
||||||
|
// Bars that paint over a leaf instead of shortening it. On a phone the navbar
|
||||||
|
// sits under every view, including the one filling the screen.
|
||||||
|
const BOTTOM_BARS = [".status-bar", ".mobile-navbar", ".mobile-toolbar"];
|
||||||
|
|
||||||
|
// iOS animates the keyboard in; the viewport is only honest once it is done.
|
||||||
|
const KEYBOARD_SETTLE_MS = 350;
|
||||||
|
|
||||||
interface SavedState {
|
interface SavedState {
|
||||||
follow?: unknown;
|
follow?: unknown;
|
||||||
selected?: unknown;
|
selected?: unknown;
|
||||||
@@ -79,6 +86,23 @@ export class PanelView extends ItemView {
|
|||||||
this.registerEvent(
|
this.registerEvent(
|
||||||
this.app.workspace.on("layout-change", () => this.inset())
|
this.app.workspace.on("layout-change", () => this.inset())
|
||||||
);
|
);
|
||||||
|
// The keyboard resizes the visual viewport only: no workspace event
|
||||||
|
// fires, so the composer would stay behind it without these two.
|
||||||
|
const vv = window.visualViewport;
|
||||||
|
if (vv) {
|
||||||
|
const onViewport = () => this.inset();
|
||||||
|
vv.addEventListener("resize", onViewport);
|
||||||
|
vv.addEventListener("scroll", onViewport);
|
||||||
|
this.register(() => {
|
||||||
|
vv.removeEventListener("resize", onViewport);
|
||||||
|
vv.removeEventListener("scroll", onViewport);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Focusing the composer opens the keyboard a beat before the viewport
|
||||||
|
// settles; re-measure once it has.
|
||||||
|
this.registerDomEvent(this.contentEl, "focusin", () => {
|
||||||
|
window.setTimeout(() => this.inset(), KEYBOARD_SETTLE_MS);
|
||||||
|
});
|
||||||
this.follow(this.app.workspace.getActiveFile());
|
this.follow(this.app.workspace.getActiveFile());
|
||||||
this.plugin.views.add(this);
|
this.plugin.views.add(this);
|
||||||
this.mountApp();
|
this.mountApp();
|
||||||
@@ -86,18 +110,37 @@ export class PanelView extends ItemView {
|
|||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Obsidian's status bar floats over the bottom-right of the workspace; a
|
// Whatever floats over the bottom of this leaf, measured rather than
|
||||||
// leaf that reaches down there gets padding so the composer stays clear.
|
// guessed: the desktop status bar, the phone's navbar (which the workspace
|
||||||
|
// does not subtract from a leaf's height), and the software keyboard, which
|
||||||
|
// shrinks the visual viewport without resizing the layout at all.
|
||||||
private inset(): void {
|
private inset(): void {
|
||||||
const bar = document.querySelector<HTMLElement>(".status-bar");
|
|
||||||
const box = this.contentEl.getBoundingClientRect();
|
const box = this.contentEl.getBoundingClientRect();
|
||||||
let overlap = 0;
|
if (box.height <= 0) {
|
||||||
if (bar && box.height > 0) {
|
return;
|
||||||
const b = bar.getBoundingClientRect();
|
|
||||||
const crosses = b.left < box.right && b.right > box.left;
|
|
||||||
overlap = crosses ? Math.max(0, box.bottom - b.top) : 0;
|
|
||||||
}
|
}
|
||||||
this.contentEl.style.setProperty("--beaver-inset-bottom", `${overlap}px`);
|
let overlap = 0;
|
||||||
|
for (const bar of BOTTOM_BARS) {
|
||||||
|
const node = document.querySelector<HTMLElement>(bar);
|
||||||
|
if (!node || node.offsetParent === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const b = node.getBoundingClientRect();
|
||||||
|
if (b.height > 0 && b.left < box.right && b.right > box.left) {
|
||||||
|
overlap = Math.max(overlap, box.bottom - b.top);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const vv = window.visualViewport;
|
||||||
|
if (vv) {
|
||||||
|
// The keyboard eats the bottom of the window; anything of this leaf
|
||||||
|
// below that line is unreachable until it goes away.
|
||||||
|
const floor = vv.offsetTop + vv.height;
|
||||||
|
overlap = Math.max(overlap, box.bottom - floor);
|
||||||
|
}
|
||||||
|
this.contentEl.style.setProperty(
|
||||||
|
"--beaver-inset-bottom",
|
||||||
|
`${Math.max(0, Math.round(overlap))}px`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async onClose(): Promise<void> {
|
async onClose(): Promise<void> {
|
||||||
|
|||||||
@@ -191,6 +191,23 @@ check(
|
|||||||
text().includes("Xiaomi Mi Blaster")
|
text().includes("Xiaomi Mi Blaster")
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Nothing picked - the panel just opened on a phone - lands on the master
|
||||||
|
// rather than on a prompt to pick something.
|
||||||
|
state.selected = null;
|
||||||
|
flushSync();
|
||||||
|
await settle();
|
||||||
|
flushSync();
|
||||||
|
check(
|
||||||
|
"with nothing picked it falls back to the master",
|
||||||
|
state.selected === MASTER,
|
||||||
|
String(state.selected)
|
||||||
|
);
|
||||||
|
state.selected = BRANCH_BLASTER;
|
||||||
|
flushSync();
|
||||||
|
await settle();
|
||||||
|
flushSync();
|
||||||
|
check("an explicit pick still wins", state.selected === BRANCH_BLASTER);
|
||||||
|
|
||||||
const composer = target.querySelector("textarea");
|
const composer = target.querySelector("textarea");
|
||||||
check("composer is there", composer !== null);
|
check("composer is there", composer !== null);
|
||||||
if (composer) {
|
if (composer) {
|
||||||
|
|||||||
Reference in New Issue
Block a user