Files
julian.kiedaischandClaude Opus 5.5 8c4e5049f6 feat(editor): handles for a finger on documentation items, and a grip that moves a text
The focused rectangle or text shows 44 px handles on its focus band: corners,
and the middles of long enough sides; on an item smaller than two targets only
the bottom right one, outside its corner. A text gets a grip above its top
left corner, since its middle is the field typed into. The three-dot button
moves above the top right corner; grip and button step aside at the surface's
edge. A tap on a handle is one on what lies under it, a long press opens the
menu, a second finger zooms. WebKit hands a tap's end to the handle despite
the capture, so the handle passes its events on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 22:04:40 +00:00

167 lines
5.5 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// A focused documentation item's handles for a finger: which it shows at
// which size on the screen (each boundary on both sides), and where its grip
// and three-dot button go — clear of each other and of the surface's edge.
import { describe, expect, it } from "vitest";
import {
ABOVE,
gripPlace,
handlePlace,
HANDLES,
menuPlace,
shownHandles,
TARGET,
} from "../src/canvas/handles.js";
const SURFACE = { width: 1000, height: 800 };
const ALL = HANDLES.map(({ name }) => name);
const CORNERS = ["top-left", "top-right", "bottom-right", "bottom-left"];
describe("the handles shown", () => {
it("shows all eight on an item three targets wide and high", () => {
expect(shownHandles({ width: 3 * TARGET, height: 3 * TARGET })).toEqual(
ALL,
);
});
it("shows a side's middle only from three targets of its length", () => {
const narrow = shownHandles({
width: 3 * TARGET - 0.5,
height: 3 * TARGET,
});
expect(narrow).not.toContain("top");
expect(narrow).not.toContain("bottom");
expect(narrow).toEqual(
expect.arrayContaining(["left", "right", ...CORNERS]),
);
const low = shownHandles({ width: 3 * TARGET, height: 3 * TARGET - 0.5 });
expect(low).toEqual(expect.arrayContaining(["top", "bottom", ...CORNERS]));
expect(low).not.toContain("left");
});
it("shows the four corners from two targets, below that only the bottom right one, so that the middle still moves it", () => {
expect(shownHandles({ width: 2 * TARGET, height: 2 * TARGET })).toEqual([
"top-left",
"top-right",
"bottom-right",
"bottom-left",
]);
expect(shownHandles({ width: 2 * TARGET - 0.5, height: 400 })).toEqual([
"bottom-right",
]);
expect(shownHandles({ width: 400, height: 2 * TARGET - 0.5 })).toEqual([
"bottom-right",
]);
});
it("puts a target on its corner or edge, and on an item too small for that outside its corner, reaching in by 6, the dot on the corner", () => {
const big = { x: 100, y: 100, width: 2 * TARGET, height: 2 * TARGET };
expect(handlePlace("bottom-right", big)).toEqual({
centre: { x: 100 + 2 * TARGET, y: 100 + 2 * TARGET },
dot: { x: 0, y: 0 },
});
expect(handlePlace("top", { ...big, width: 3 * TARGET })).toEqual({
centre: { x: 100 + 1.5 * TARGET, y: 100 },
dot: { x: 0, y: 0 },
});
const small = { x: 100, y: 100, width: 20, height: 20 };
const { centre, dot } = handlePlace("bottom-right", small);
expect(centre.x - TARGET / 2).toBe(120 - 6);
expect(centre.y - TARGET / 2).toBe(120 - 6);
expect([centre.x + dot.x, centre.y + dot.y]).toEqual([120, 120]);
});
it("names each handle's edges as its place says", () => {
for (const { at, edges } of HANDLES) {
expect(edges).toEqual({
left: at.x === 0,
right: at.x === 1,
top: at.y === 0,
bottom: at.y === 1,
});
}
});
});
describe("the grip and the three-dot button", () => {
const frame = { x: 300, y: 300, width: 200, height: 100 };
it("stand above the top corners, the button right of a narrow text’s grip", () => {
const grip = gripPlace(frame, SURFACE);
expect(grip).toEqual({ x: 300, y: 300 - ABOVE });
expect(menuPlace(frame, SURFACE, grip)).toEqual({ x: 500, y: 300 - ABOVE });
expect(menuPlace(frame, SURFACE, null)).toEqual({ x: 500, y: 300 - ABOVE });
const narrow = { ...frame, width: 20 };
expect(menuPlace(narrow, SURFACE, gripPlace(narrow, SURFACE))).toEqual({
x: 300 + TARGET + 4,
y: 300 - ABOVE,
});
});
it("move aside where the surface ends above: the grip to the left, the button to the right, each exactly at the boundary", () => {
const room = TARGET / 2 + ABOVE;
expect(gripPlace({ ...frame, y: room }, SURFACE)).toEqual({
x: 300,
y: TARGET / 2,
});
expect(gripPlace({ ...frame, y: room - 1 }, SURFACE)).toEqual({
x: 300 - ABOVE,
y: room - 1,
});
expect(menuPlace({ ...frame, y: room }, SURFACE, null)).toEqual({
x: 500,
y: TARGET / 2,
});
expect(menuPlace({ ...frame, y: room - 1 }, SURFACE, null)).toEqual({
x: 500 + ABOVE,
y: room - 1,
});
});
it("go below where there is no room above nor beside", () => {
const cornered = { x: 30, y: 10, width: SURFACE.width - 60, height: 200 };
expect(gripPlace(cornered, SURFACE)).toEqual({
x: 30,
y: 10 + 200 + ABOVE,
});
expect(menuPlace(cornered, SURFACE, null)).toEqual({
x: SURFACE.width - 30,
y: 10 + 200 + ABOVE,
});
});
it("keep the button off the grip where both would go below", () => {
const cornered = { x: 30, y: 10, width: 30, height: 200 };
const grip = gripPlace(cornered, SURFACE);
const button = menuPlace(cornered, SURFACE, grip);
expect(
Math.abs(button.x - grip.x) >= TARGET ||
Math.abs(button.y - grip.y) >= TARGET,
).toBe(true);
});
it("go not below where the surface ends there too", () => {
const tall = {
x: 30,
y: 10,
width: 100,
height: 300 - 10 - ABOVE - TARGET / 2 + 1,
};
expect(gripPlace(tall, { width: 1000, height: 300 })).toEqual({
x: 30,
y: 10 - ABOVE,
});
expect(
gripPlace(
{ ...tall, height: tall.height - 1 },
{ width: 1000, height: 300 },
),
).toEqual({ x: 30, y: 300 - TARGET / 2 });
});
it("keep to the first place where nothing fits", () => {
const tiny = { width: 30, height: 30 };
expect(gripPlace(frame, tiny)).toEqual({ x: 300, y: 300 - ABOVE });
});
});