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>
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
// 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 });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user