The panel had an inset for this; the calendar never did. Its root filled the leaf, and Obsidian's phone navbar floats over a leaf rather than shortening it, so the scroll container ran to the bottom of the screen and the last row of the list sat under the bar - unreachable. `lib/inset.ts` is the same pure measurement the panel plugin grew, with the same tests: the deeper of navbar and keyboard, never their sum, and a bar that sits beside the view covers nothing of it. The view re-measures by frame until it holds still and writes `--bcal-inset-bottom`. `viewport()` subtracts the bar too, so anchored panels and the quick look are placed against what is actually reachable instead of ending under it.
136 lines
4.5 KiB
TypeScript
136 lines
4.5 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { onScreen, place, roomFor, viewport } from "../src/lib/place";
|
|
import type { Box } from "../src/lib/place";
|
|
|
|
const VIEW = { width: 1280, height: 800 };
|
|
const PANEL = { width: 320, height: 200 };
|
|
const row = (over: Partial<Box> = {}): Box => ({
|
|
top: 300,
|
|
left: 400,
|
|
width: 500,
|
|
height: 40,
|
|
...over,
|
|
});
|
|
|
|
describe("onScreen", () => {
|
|
it("skips the copy that is hidden and has no box", () => {
|
|
const hidden = row({ top: 0, left: 0, width: 0, height: 0 });
|
|
const shown = row();
|
|
expect(onScreen([hidden, shown])).toBe(shown);
|
|
});
|
|
|
|
it("takes the first one when several are on screen", () => {
|
|
const first = row();
|
|
expect(onScreen([first, row({ top: 500 })])).toBe(first);
|
|
});
|
|
|
|
it("says nothing when every copy is hidden", () => {
|
|
expect(onScreen([row({ width: 0, height: 0 })])).toBeNull();
|
|
expect(onScreen([])).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("place", () => {
|
|
it("sits just under the anchor", () => {
|
|
expect(place(row(), PANEL, VIEW)).toEqual({ top: 346, left: 400 });
|
|
});
|
|
|
|
it("flips above when there is no room below", () => {
|
|
expect(place(row({ top: 700 }), PANEL, VIEW)).toEqual({ top: 494, left: 400 });
|
|
});
|
|
|
|
it("stays inside the window when there is room neither way", () => {
|
|
const tall = { width: 320, height: 780 };
|
|
const spot = place(row({ top: 400 }), tall, VIEW);
|
|
expect(spot.top).toBeGreaterThanOrEqual(10);
|
|
expect(spot.top + tall.height).toBeLessThanOrEqual(VIEW.height);
|
|
});
|
|
|
|
it("pulls back from the right edge", () => {
|
|
expect(place(row({ left: 1200 }), PANEL, VIEW).left).toBe(950);
|
|
});
|
|
|
|
it("never goes off the left edge", () => {
|
|
expect(place(row({ left: -40 }), PANEL, VIEW).left).toBe(10);
|
|
});
|
|
|
|
it("centres rather than hiding in the corner with nothing to anchor to", () => {
|
|
expect(place(null, PANEL, VIEW)).toEqual({ top: 300, left: 480 });
|
|
});
|
|
});
|
|
|
|
// A phone's keyboard shrinks the visual viewport and leaves the window alone.
|
|
describe("place against a keyboard", () => {
|
|
const SHRUNK = { width: 390, height: 400, top: 0, left: 0 };
|
|
|
|
it("keeps the panel above the keyboard", () => {
|
|
const spot = place(row({ top: 380, left: 10, width: 370 }), PANEL, SHRUNK);
|
|
expect(spot.top + PANEL.height).toBeLessThanOrEqual(SHRUNK.height);
|
|
});
|
|
|
|
it("measures from where the visible area starts", () => {
|
|
const scrolled = { width: 390, height: 400, top: 120, left: 0 };
|
|
const spot = place(null, PANEL, scrolled);
|
|
expect(spot.top).toBeGreaterThanOrEqual(scrolled.top);
|
|
expect(spot.top + PANEL.height).toBeLessThanOrEqual(
|
|
scrolled.top + scrolled.height
|
|
);
|
|
});
|
|
|
|
it("comes back into view when the anchor itself is under the keyboard", () => {
|
|
// The row the panel hangs off is at 650, the keyboard leaves 380: neither
|
|
// above nor below the anchor is reachable, so the visible area wins.
|
|
const spot = place(row({ top: 650, height: 40 }), PANEL, SHRUNK);
|
|
expect(spot.top).toBeGreaterThanOrEqual(10);
|
|
expect(spot.top + PANEL.height).toBeLessThanOrEqual(SHRUNK.height);
|
|
});
|
|
});
|
|
|
|
describe("roomFor", () => {
|
|
it("leaves the panel less height than the visible area", () => {
|
|
expect(roomFor({ width: 390, height: 400 })).toBeLessThan(400);
|
|
});
|
|
|
|
it("never collapses to nothing when the keyboard takes almost everything", () => {
|
|
expect(roomFor({ width: 390, height: 60 })).toBeGreaterThan(100);
|
|
});
|
|
});
|
|
|
|
// Obsidian's phone navbar is not part of the visual viewport - it floats over
|
|
// it - so a panel sized to the viewport still ends up underneath it.
|
|
describe("viewport minus the floating navbar", () => {
|
|
function fakeWin(navbarTop: number | null, height = 780): Window {
|
|
const bar = {
|
|
getBoundingClientRect: () => ({
|
|
top: navbarTop ?? 0,
|
|
bottom: height,
|
|
height: height - (navbarTop ?? 0),
|
|
left: 0,
|
|
right: 390,
|
|
}),
|
|
offsetParent: {},
|
|
};
|
|
return {
|
|
innerWidth: 390,
|
|
innerHeight: height,
|
|
visualViewport: { width: 390, height, offsetTop: 0, offsetLeft: 0 },
|
|
document: {
|
|
querySelector: (selector: string) =>
|
|
selector === ".mobile-navbar" && navbarTop !== null ? bar : null,
|
|
},
|
|
} as unknown as Window;
|
|
}
|
|
|
|
it("takes the navbar off the bottom", () => {
|
|
expect(viewport(fakeWin(724)).height).toBe(724);
|
|
});
|
|
|
|
it("leaves the height alone with no navbar in the way", () => {
|
|
expect(viewport(fakeWin(null)).height).toBe(780);
|
|
});
|
|
|
|
it("still leaves a usable strip if a bar somehow covers everything", () => {
|
|
expect(viewport(fakeWin(0)).height).toBeGreaterThanOrEqual(120);
|
|
});
|
|
});
|