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:
@@ -3,7 +3,7 @@
|
||||
// the sidebar's rectangle dragged onto the surface (its middle under the
|
||||
// pointer), moved, resized to Java's smallest size, coloured from Java's
|
||||
// list; the sidebar's text tapped into the view, typed into, made large and
|
||||
// italic; Java's paint in the documentation mode and in the design; Delete;
|
||||
// italic; its handles by mouse; Java's paint in the documentation mode and in the design; Delete;
|
||||
// a simulation started ends the mode; „Exportieren“ (F4.8c): the image
|
||||
// saved under the project's name, cut and painted as Java's, and Java's
|
||||
// message when saving fails; and a phone.
|
||||
@@ -11,7 +11,7 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { expect, test, type Page } from '@playwright/test';
|
||||
import { JAVA_LABEL_FONT } from '../src/canvas/layout.js';
|
||||
import { documentUpdates, drag, drawing, openEditor, serveEditor } from './editorPage.js';
|
||||
import { documentUpdates, drag, drawing, openEditor, scaleOf, serveEditor } from './editorPage.js';
|
||||
|
||||
let server: { readonly base: string; readonly close: () => Promise<void> };
|
||||
|
||||
@@ -122,6 +122,23 @@ test('makes, moves, resizes, colours and types documentation as Java does, and p
|
||||
})
|
||||
.toEqual([18, true, false]);
|
||||
await expect(field).toHaveCSS('font-style', 'italic');
|
||||
// Its handles take the mouse too, even where one jump of it lands over the field typed into: the corner follows.
|
||||
const sized = (await docOf(page, text.id))!;
|
||||
const scale = await scaleOf(page);
|
||||
const cornerHandle = (await page.locator('[data-handle="bottom-right"]').boundingBox())!;
|
||||
const from = { x: cornerHandle.x + cornerHandle.width / 2, y: cornerHandle.y + cornerHandle.height / 2 };
|
||||
const fieldBox = (await field.boundingBox())!;
|
||||
const into = { x: fieldBox.x + fieldBox.width - 4, y: fieldBox.y + fieldBox.height - 4 };
|
||||
await page.mouse.move(from.x, from.y);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(into.x, into.y);
|
||||
await page.mouse.up();
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const item = (await docOf(page, text.id))!;
|
||||
return [item.width, item.height].map(Math.round);
|
||||
})
|
||||
.toEqual([Math.round(Math.max(88, sized.width + (into.x - from.x) / scale)), Math.round(Math.max(20, sized.height + (into.y - from.y) / scale))]);
|
||||
|
||||
// Escape ends the focus: the text drawn, with Java's grey border in the documentation mode.
|
||||
await field.press('Escape');
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
// target at least 44 px, nothing beyond the screen's edge.
|
||||
|
||||
import { expect, test, type Page } from '@playwright/test';
|
||||
import { drawing, onDevice, openEditor, serveEditor } from './editorPage.js';
|
||||
import { drawing, onDevice, openEditor, scaleOf, serveEditor } from './editorPage.js';
|
||||
import { fingers, type Fingers, type Point } from './fingers.js';
|
||||
|
||||
let server: { readonly base: string; readonly close: () => Promise<void> };
|
||||
@@ -172,6 +172,103 @@ test('puts a device or an item down where it is when a second finger comes or th
|
||||
await expect.poll(async () => (await drawing(page)).docs.find((item) => item.id === rectangle.id)!.x).not.toBe(rectangle.x);
|
||||
});
|
||||
|
||||
test("resizes a rectangle of the documentation by its handles, and moves and resizes a text by its grip and handles, with a finger", async ({ page }) => {
|
||||
await openEditor(page, server.base, 'nat_with_mapped_port.fls');
|
||||
const finger = await fingers(page);
|
||||
await page.locator('button[name="documentationMode"]').tap();
|
||||
const docOf = async (id: string) => (await drawing(page)).docs.find((item) => item.id === id)!;
|
||||
const frameOf = async (id: string) => {
|
||||
const item = await docOf(id);
|
||||
return [item.x, item.y, item.width, item.height].map(Math.round);
|
||||
};
|
||||
const handle = (name: string) => page.locator(`[data-handle="${name}"]`);
|
||||
const scale = await scaleOf(page);
|
||||
|
||||
// A rectangle tapped: its handles, each a finger's target, the three-dot button clear of them.
|
||||
const rectangle = (await drawing(page)).docs.find((item) => item.kind === 'rectangle')!;
|
||||
await finger.tap(await centreOf(page, `g[data-doc="${rectangle.id}"] .doc-rectangle`));
|
||||
await expect(page.locator(`g[data-doc="${rectangle.id}"]`)).toHaveAttribute('data-focused', 'true');
|
||||
const names = await page.locator('[data-handle]').evaluateAll((all) => all.map((each) => each.getAttribute('data-handle')));
|
||||
expect(names).toEqual(expect.arrayContaining(['top-left', 'top-right', 'bottom-right', 'bottom-left']));
|
||||
for (const box of await page.locator('[data-handle]').evaluateAll((all) => all.map((each) => each.getBoundingClientRect().toJSON() as DOMRect))) expect([box.width, box.height]).toEqual([44, 44]);
|
||||
const menu = (await page.locator('.device-menu-button').boundingBox())!;
|
||||
for (const box of await page.locator('[data-handle]').evaluateAll((all) => all.map((each) => each.getBoundingClientRect().toJSON() as DOMRect))) {
|
||||
expect(box.y >= menu.y + menu.height || box.y + box.height <= menu.y || box.x >= menu.x + menu.width || box.x + box.width <= menu.x).toBe(true);
|
||||
}
|
||||
// The bottom right corner dragged 60 right and 40 down: wider and higher by as much in the drawing, the top left kept.
|
||||
const [x, y, width, height] = await frameOf(rectangle.id);
|
||||
const bottomRight = await centreOf(page, '[data-handle="bottom-right"]');
|
||||
await finger.drag(bottomRight, { x: bottomRight.x + 60, y: bottomRight.y + 40 });
|
||||
await expect.poll(() => frameOf(rectangle.id)).toEqual([x, y, Math.round(width! + 60 / scale), Math.round(height! + 40 / scale)]);
|
||||
// The top left one 30 left: the right edge kept.
|
||||
const wider = await frameOf(rectangle.id);
|
||||
const topLeft = await centreOf(page, '[data-handle="top-left"]');
|
||||
await finger.drag(topLeft, { x: topLeft.x - 30, y: topLeft.y });
|
||||
await expect.poll(async () => (await frameOf(rectangle.id))[0]).toBe(Math.round(wider[0]! - 30 / scale));
|
||||
const [left, , across] = await frameOf(rectangle.id);
|
||||
expect(Math.abs(left! + across! - (wider[0]! + wider[2]!))).toBeLessThanOrEqual(1);
|
||||
|
||||
// A long press on a handle opens the rectangle's menu, as on the rectangle.
|
||||
await finger.press(await centreOf(page, '[data-handle="top-left"]'));
|
||||
await expect(page.getByRole('menu').getByRole('group', { name: 'Farbe' })).toBeVisible();
|
||||
await page.keyboard.press('Escape');
|
||||
await expect(page.getByRole('menu')).toHaveCount(0);
|
||||
|
||||
// The text in its top edge lies partly under a handle: tapped there, the text is chosen and typed into, not the rectangle.
|
||||
const below = (await drawing(page)).docs.find((item) => item.kind === 'text' && item.text.startsWith('Hosts'))!;
|
||||
const words = (await page.locator(`g[data-doc="${below.id}"] .doc-text`).boundingBox())!;
|
||||
const covering = (await page.locator('[data-handle]').evaluateAll((all) => all.map((each) => each.getBoundingClientRect().toJSON() as DOMRect)))
|
||||
.map((box) => ({ left: Math.max(box.x, words.x), top: Math.max(box.y, words.y), right: Math.min(box.x + box.width, words.x + words.width), bottom: Math.min(box.y + box.height, words.y + words.height) }))
|
||||
.find((both) => both.right - both.left > 4 && both.bottom - both.top > 4)!;
|
||||
expect(covering).toBeDefined();
|
||||
await finger.tap({ x: (covering.left + covering.right) / 2, y: (covering.top + covering.bottom) / 2 });
|
||||
await expect(page.getByRole('textbox', { name: 'Text' })).toBeVisible();
|
||||
await expect(page.locator(`g[data-doc="${below.id}"]`)).toHaveAttribute('data-focused', 'true');
|
||||
|
||||
// A text with room around it, tapped: a grip above its top left corner moves it — its size kept.
|
||||
await page.getByRole('textbox', { name: 'Text' }).press('Escape');
|
||||
const text = (await drawing(page)).docs.find((item) => item.kind === 'text' && item.text.startsWith('Traffic'))!;
|
||||
await finger.tap(await centreOf(page, `g[data-doc="${text.id}"] .doc-text`));
|
||||
await expect(page.getByRole('textbox', { name: 'Text' })).toBeVisible();
|
||||
await expect(handle('move')).toBeVisible();
|
||||
// The handles sit on the blue band around the text (where Java takes hold of it), not on its words.
|
||||
const bandBox = (await page.locator(`g[data-doc="${text.id}"] .doc-focus`).boundingBox())!;
|
||||
const cornerAt = await centreOf(page, '[data-handle="bottom-right"]');
|
||||
expect(Math.abs(cornerAt.x - (bandBox.x + bandBox.width + scale))).toBeLessThanOrEqual(1.5);
|
||||
expect(Math.abs(cornerAt.y - (bandBox.y + bandBox.height + scale))).toBeLessThanOrEqual(1.5);
|
||||
// Every point of a handle's target takes the finger, also where it lies over the field typed into.
|
||||
const hidden = await page.locator('[data-handle]').evaluateAll((all) =>
|
||||
all.flatMap((each) => {
|
||||
const box = each.getBoundingClientRect();
|
||||
const points = [
|
||||
[box.left + 3, box.top + 3],
|
||||
[box.right - 3, box.top + 3],
|
||||
[box.left + 3, box.bottom - 3],
|
||||
[box.right - 3, box.bottom - 3],
|
||||
];
|
||||
return points.filter(([x, y]) => document.elementFromPoint(x!, y!)?.closest('[data-handle]') !== each).map(() => each.getAttribute('data-handle'));
|
||||
}),
|
||||
);
|
||||
expect(hidden).toEqual([]);
|
||||
const before = await frameOf(text.id);
|
||||
const grip = await centreOf(page, '[data-handle="move"]');
|
||||
await finger.drag(grip, { x: grip.x + 50, y: grip.y + 30 });
|
||||
await expect.poll(() => frameOf(text.id)).toEqual([Math.round(before[0]! + 50 / scale), Math.round(before[1]! + 30 / scale), before[2], before[3]]);
|
||||
// Its bottom right handle: wider and higher; what it says is unchanged.
|
||||
const moved = await frameOf(text.id);
|
||||
const corner = await centreOf(page, '[data-handle="bottom-right"]');
|
||||
await finger.drag(corner, { x: corner.x + 40, y: corner.y + 20 });
|
||||
await expect.poll(() => frameOf(text.id)).toEqual([moved[0], moved[1], Math.round(moved[2]! + 40 / scale), Math.round(moved[3]! + 20 / scale)]);
|
||||
expect((await docOf(text.id)).text).toBe(text.text);
|
||||
|
||||
// Two fingers, the second on a handle: they zoom, and the text keeps its size.
|
||||
const sized = await frameOf(text.id);
|
||||
const onHandle = await centreOf(page, '[data-handle="bottom-right"]');
|
||||
await finger.pinch({ x: onHandle.x - 40, y: onHandle.y }, 40, 120);
|
||||
await expect.poll(() => scaleOf(page)).toBeGreaterThan(scale * 1.5);
|
||||
expect(await frameOf(text.id)).toEqual(sized);
|
||||
});
|
||||
|
||||
test('reaches every action without a right click: a long press, the three-dot button, the documentation', async ({ page }) => {
|
||||
await openEditor(page, server.base, 'zwei_rechnernetze_komplett.fls');
|
||||
const finger = await fingers(page);
|
||||
@@ -220,7 +317,7 @@ test('gives every target 44 px at least and keeps the editor within the screen',
|
||||
.map(({ name, box }) => `${name} ${Math.round(box.width)}×${Math.round(box.height)}`),
|
||||
);
|
||||
const targets =
|
||||
'.editor-toolbar button, [data-palette], [data-docu], nav button, [data-zoom] ~ button, .zoom-bar button, .device-menu-button, aside[aria-label="Einstellungen"] button, aside[aria-label="Einstellungen"] [role="tab"]';
|
||||
'.editor-toolbar button, [data-palette], [data-docu], nav button, [data-zoom] ~ button, .zoom-bar button, .device-menu-button, [data-handle], aside[aria-label="Einstellungen"] button, aside[aria-label="Einstellungen"] [role="tab"]';
|
||||
expect(await fits()).toBe(true);
|
||||
expect(await small(targets)).toEqual([]);
|
||||
// With a device chosen: its sheet and its three-dot button.
|
||||
|
||||
Reference in New Issue
Block a user