refactor(inset): the bottom-inset arithmetic becomes testable, and is tested
The measurement lived inside the view, where the only way to check it was to run Obsidian on a phone. It moves to a pure `insetFor(leaf, bars, visible)` against the numbers a phone reports - including the case the events hid: while the keyboard is up the navbar is behind it, so the deeper of the two is the inset and adding them would push the composer twice as far as it must go.
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* How much of a leaf's bottom is covered by something the operator cannot
|
||||
* move: Obsidian's desktop status bar, a phone's navbar (which paints over a
|
||||
* leaf rather than shortening it), and the software keyboard, which shrinks
|
||||
* the visual viewport while the layout keeps its full height.
|
||||
*
|
||||
* Pure on purpose - the view feeds it live rectangles, the tests feed it the
|
||||
* numbers a phone actually reports.
|
||||
*/
|
||||
|
||||
export interface Rect {
|
||||
bottom: number;
|
||||
height: number;
|
||||
left: number;
|
||||
right: number;
|
||||
top: number;
|
||||
}
|
||||
|
||||
export interface Visible {
|
||||
/** `visualViewport.height`: what is left once the keyboard is up. */
|
||||
height: number;
|
||||
/** `visualViewport.offsetTop`: where the visible area starts. */
|
||||
offsetTop: number;
|
||||
}
|
||||
|
||||
export function insetFor(
|
||||
leaf: Rect,
|
||||
bars: Rect[],
|
||||
visible: Visible | null
|
||||
): number {
|
||||
if (leaf.height <= 0) {
|
||||
return 0;
|
||||
}
|
||||
let overlap = 0;
|
||||
for (const bar of bars) {
|
||||
// A bar that does not sit over this leaf horizontally covers nothing of
|
||||
// it - the desktop status bar next to a left-hand sidedock, say.
|
||||
if (bar.height > 0 && bar.left < leaf.right && bar.right > leaf.left) {
|
||||
overlap = Math.max(overlap, leaf.bottom - bar.top);
|
||||
}
|
||||
}
|
||||
if (visible) {
|
||||
const floor = visible.offsetTop + visible.height;
|
||||
overlap = Math.max(overlap, leaf.bottom - floor);
|
||||
}
|
||||
return Math.max(0, Math.round(overlap));
|
||||
}
|
||||
+14
-20
@@ -1,6 +1,8 @@
|
||||
import type { ViewStateResult, WorkspaceLeaf } from "obsidian";
|
||||
import { ItemView, TFile } from "obsidian";
|
||||
import { mount, unmount } from "svelte";
|
||||
import type { Rect } from "./inset";
|
||||
import { insetFor } from "./inset";
|
||||
import type BeaverPlugin from "./main";
|
||||
import { PanelState } from "./state.svelte";
|
||||
import App from "./ui/app.svelte";
|
||||
@@ -144,34 +146,26 @@ export class PanelView extends ItemView {
|
||||
}
|
||||
|
||||
// Whatever floats over the bottom of this leaf, measured rather than
|
||||
// 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. Returns
|
||||
// the inset it wrote, so the settle loop can tell when it stops changing.
|
||||
// guessed. Returns the inset it wrote, so the settle loop can tell when the
|
||||
// reading stops changing; the arithmetic itself lives in ``inset.ts``.
|
||||
private inset(): number {
|
||||
const box = this.contentEl.getBoundingClientRect();
|
||||
if (box.height <= 0) {
|
||||
return Number.NaN;
|
||||
}
|
||||
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 bars: Rect[] = [];
|
||||
for (const selector of BOTTOM_BARS) {
|
||||
const node = document.querySelector<HTMLElement>(selector);
|
||||
if (node && node.offsetParent !== null) {
|
||||
bars.push(node.getBoundingClientRect());
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
const inset = Math.max(0, Math.round(overlap));
|
||||
const inset = insetFor(
|
||||
box,
|
||||
bars,
|
||||
vv ? { height: vv.height, offsetTop: vv.offsetTop } : null
|
||||
);
|
||||
this.contentEl.style.setProperty("--beaver-inset-bottom", `${inset}px`);
|
||||
return inset;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { Rect } from "../src/inset";
|
||||
import { insetFor } from "../src/inset";
|
||||
|
||||
const rect = (over: Partial<Rect> = {}): Rect => ({
|
||||
bottom: 780,
|
||||
height: 780,
|
||||
left: 0,
|
||||
right: 390,
|
||||
top: 0,
|
||||
...over,
|
||||
});
|
||||
|
||||
// A phone: 780 tall, a 56px navbar over the bottom, 340px of keyboard.
|
||||
const NAVBAR = rect({ bottom: 780, height: 56, top: 724 });
|
||||
|
||||
describe("insetFor", () => {
|
||||
it("clears nothing when the leaf stands alone", () => {
|
||||
expect(insetFor(rect(), [], null)).toBe(0);
|
||||
});
|
||||
|
||||
it("clears the navbar that paints over the leaf", () => {
|
||||
expect(insetFor(rect(), [NAVBAR], null)).toBe(56);
|
||||
});
|
||||
|
||||
it("clears the keyboard, which shortens no layout", () => {
|
||||
expect(insetFor(rect(), [], { height: 440, offsetTop: 0 })).toBe(340);
|
||||
});
|
||||
|
||||
it("takes the deeper of the two rather than their sum", () => {
|
||||
// While the keyboard is up the navbar is behind it: adding them would
|
||||
// push the composer twice as far as it has to go.
|
||||
expect(insetFor(rect(), [NAVBAR], { height: 440, offsetTop: 0 })).toBe(340);
|
||||
});
|
||||
|
||||
it("comes back to the navbar once the keyboard goes", () => {
|
||||
expect(insetFor(rect(), [NAVBAR], { height: 780, offsetTop: 0 })).toBe(56);
|
||||
});
|
||||
|
||||
it("ignores a bar that sits beside the leaf, not over it", () => {
|
||||
// The desktop status bar floats bottom-right; a left-hand sidedock never
|
||||
// reaches it.
|
||||
const sidedock = rect({ right: 300 });
|
||||
const statusBar = rect({
|
||||
bottom: 780,
|
||||
height: 20,
|
||||
left: 700,
|
||||
right: 980,
|
||||
top: 760,
|
||||
});
|
||||
expect(insetFor(sidedock, [statusBar], null)).toBe(0);
|
||||
});
|
||||
|
||||
it("measures from where the visible area starts, not from zero", () => {
|
||||
// iOS scrolls the page up under the keyboard instead of resizing it.
|
||||
expect(insetFor(rect(), [], { height: 440, offsetTop: 100 })).toBe(240);
|
||||
});
|
||||
|
||||
it("says nothing about a leaf with no height yet", () => {
|
||||
expect(insetFor(rect({ bottom: 0, height: 0 }), [NAVBAR], null)).toBe(0);
|
||||
});
|
||||
|
||||
it("never returns a negative inset", () => {
|
||||
const short = rect({ bottom: 300, height: 300 });
|
||||
expect(insetFor(short, [], { height: 780, offsetTop: 0 })).toBe(0);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user