feat(editor): a tap opens a device's menu in the simulation; undo and redo drawn
In the simulation a tap on a device chooses it and opens its menu where the finger was; the three-dot button is gone there, the long press stays. The context menu moves in from the surface's edge instead of being cut off. Undo and redo show drawn arrows: the characters took whatever font the device had and looked squashed on an iPad (the buttons measured 44 x 44 at every width; the size was not the cause). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -279,6 +279,7 @@ Swift-Pfade relativ zu `filius-ipad/ios/FiliusPad/TopologyEditor/` (Stand
|
||||
| `src/simulation/PropertiesSheet.tsx` (neu), `src/simulation/DropHold.tsx` (vorher `DropControl.tsx`), `src/simulation/useSimulation.ts` (`restart`), `src/Editor.tsx`, `src/sheet/sheet.css`, `src/desktop/desktop.css`, `src/i18n/*.json` (`properties.*`; `drop.list` entfällt), `browser/viewers.spec.ts`, `browser/shared.spec.ts`, `browser/fromF.spec.ts`, `tools/live/acceptance.ts` | Java `SimulationControl` | geändert (10.10.2026, auf Wunsch des Nutzers): im Aktionsmodus ein eigener Inspector „Eigenschaften“ (Knopf über der Fläche, Blatt neben ihr wie die Geräte-Einstellungen, auf dem Handy über dem ganzen Editor) statt Hinweis und Knöpfen in der Werkzeugleiste: wer die Simulation gestartet hat, „Simulation neu starten“ (für alle, nach einer Rückfrage; die Tabellen des Datenaustauschs verlieren ihre Auswahl), „Pakete verwerfen“ und darunter die verworfenen Pakete samt Anzahl. Wer nur zusieht, sieht Starter und Liste, ohne Neustart und Verwerfen |
|
||||
| `src/canvas/handles.ts` (neu), `src/canvas/Canvas.tsx` (Griffe, Durchtippen, Platz von „⋯“), `src/sheet/sheet.css` (`.doc-handle`), `src/i18n/*.json` (`docu.resize`, `docu.move`), `test/handles.test.ts`, `browser/touch.spec.ts`, `browser/documentation.spec.ts` | Java `JDocuElement` (Rand von 7 zum Greifen) | erweitert (10.10.2026, auf Wunsch des Nutzers): im Dokumentationsmodus zeigt das fokussierte Element Griffe für den Finger — Trefferfläche 44 px, Punkt in Javas Fokusblau auf dem Fokusband: an den Ecken, ab drei Zielen Länge auch an den Kantenmitten; auf einem Element unter zwei Zielen nur rechts unten, außen an der Ecke, damit seine Mitte verschiebt und das Menü öffnet. Ein Text hat über seiner linken oberen Ecke einen Verschiebe-Griff (seine Mitte ist das Eingabefeld). „⋯“ steht über der rechten oberen Ecke; Griff und „⋯“ weichen am Rand der Fläche aus. Ein Tippen auf einen Griff gilt dem, was darunter liegt; langes Drücken öffnet das Menü. Gemessen: Javas Rand war 10–14 Bildschirm-Pixel breit und unsichtbar. WebKit gibt das Ende eines Tippens dem Griff statt der Fläche, die die Berührung gefangen hat; der Griff reicht seine Ereignisse deshalb weiter. Javas Rand für die Maus bleibt. Abweichend von Java: dort keine Griffe |
|
||||
| `src/theme/tokens.css` (Fläche, `--doc-text-border`, `--java-*`, `color-scheme`), `src/desktop/desktop.css` (109 feste Farben → `--java-*`), `src/canvas/documentationPaint.ts` (`darkRectangleFill`, `darkRectangleBorder`, `darkTextColor`), `src/canvas/Canvas.tsx`, `src/design/DocumentationText.tsx`, `src/simulation/ModeBar.tsx`, `src/sheet/sheet.css`, `src/assets/modes/*_dunkel.png` (neu, aus Javas Bildern freigestellt: `tools/dark-pictures/make.py`), `test/documentationPaint.test.ts`, `browser/theme.spec.ts` (neu) | Java `JCablePanel`, `JDocuElement`, `GUIMainMenu` (Modusbilder), Swing-Metal in den Programmfenstern | geändert (10.10.2026, auf Wunsch des Nutzers): das dunkle Schema überall — die Fläche dunkel (#262b33, vorher bewusst hell #e9ecf1) mit heller Beschriftung, helleren Kabeln und einem feinen hellen Rand um Javas Symbole (sonst versinken die schwarzen Switches); Gliederungsflächen als Tönung ihrer Farbe, Texte zur hellen Schrift hin gemischt; die Modusknöpfe mit freigestellten Bildern, der gedrückte Knopf in der Akzentfarbe; `color-scheme: dark` für Kästchen, Regler und Auswahllisten; die Programmfenster des Desktops über Java-Look-Token, hell mit Javas Werten unverändert. Hell bleiben Javas Desktop-Hintergrund und die Webseite im simulierten Browser (Inhalt). Exportiertes Bild und Bericht malen weiter Javas Farben |
|
||||
| `src/canvas/Canvas.tsx` (Tippen in der Simulation, kein „⋯“ dort), `src/design/ContextMenu.tsx` (rückt vom Rand ein), `src/Editor.tsx` (`HistoryIcon`), `browser/capture.spec.ts`, `browser/simulation.spec.ts`, `browser/touch.spec.ts`, `browser/editorPage.ts` (`showFromMenu`) | Java `GUIKnotenItem` (Klick im Aktionsmodus), `GUIEvents.kontextMenueAktionsmodus` | geändert (11.10.2026, auf Wunsch des Nutzers): in der Simulation öffnet ein Tippen auf ein Gerät sofort sein Menü an der Stelle des Fingers (vorher markierte es nur, „⋯“ öffnete); „⋯“ gibt es dort nicht mehr, langes Drücken bleibt. Das Menü rückt vom Rand der Fläche so weit ein, dass es ganz zu sehen ist (vorher wurde es abgeschnitten). Rückgängig und Wiederholen zeigen gezeichnete Pfeile statt der Zeichen ↶ ↷: deren Schrift wählte jedes Gerät selbst, auf dem iPad wirkten sie gestaucht. Gemessen: die Knöpfe sind auf allen Breiten 44 × 44 px, die Bildschirmgröße war nicht die Ursache. Abweichend von Java: dort öffnet der Klick den Desktop oder die Tabelle direkt, das Menü kommt mit der rechten Maustaste |
|
||||
|
||||
## packages/server
|
||||
|
||||
|
||||
@@ -208,51 +208,70 @@ test("shows a card's frames as Java's data exchange window does, with their deta
|
||||
await expect(window).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('chooses a device with a tap in the simulation, its three-dot button as in the design, and opens nothing by itself', async ({ page }) => {
|
||||
test('opens a device\'s menu with a tap in the simulation, chooses it, and opens nothing else by itself', async ({ page }) => {
|
||||
await openPages(page);
|
||||
await start(page);
|
||||
const button = page.locator('.device-menu-button');
|
||||
const menu = page.getByRole('menu');
|
||||
const sheet = page.locator('aside[aria-label="Einstellungen"]');
|
||||
// A router: the button opens the same menu as the long press.
|
||||
// A router: the tap chooses it and opens the same menu as the long press, where the finger was; no three-dot button.
|
||||
const router = await onDevice(page, await idOf(page, 'Router'));
|
||||
await page.mouse.click(router.x, router.y);
|
||||
await expect(page.locator(`g.device[data-node="${await idOf(page, 'Router')}"]`)).toHaveAttribute('data-selected', 'true');
|
||||
await button.click();
|
||||
await expect(page.getByRole('menuitem')).toHaveText(['Datenaustausch anzeigen (192.168.0.1)', 'Datenaustausch anzeigen (10.0.0.1)']);
|
||||
const opened = (await menu.boundingBox())!;
|
||||
expect([opened.x, opened.y]).toEqual([router.x, router.y]);
|
||||
await expect(page.locator('.device-menu-button')).toHaveCount(0);
|
||||
await page.getByRole('menuitem', { name: 'Datenaustausch anzeigen (10.0.0.1)' }).click();
|
||||
await expect(page.locator('section[data-capture]').getByRole('tab', { selected: true })).toHaveText('Router - 10.0.0.1');
|
||||
await page.locator('section[data-capture] .simulation-sheet__close').click();
|
||||
// A computer: the tap opens no desktop (Java's click does), it only chooses; its settings stay closed, the design is locked.
|
||||
// A computer: the tap opens its menu, no desktop by itself (Java's click does); its settings stay closed, the design is locked.
|
||||
const browser = await idOf(page, 'Browser');
|
||||
const at = await onDevice(page, browser);
|
||||
await page.mouse.click(at.x, at.y);
|
||||
await expect(button).toBeVisible();
|
||||
await expect(page.getByRole('menuitem')).toHaveText(['Desktop anzeigen', 'Datenaustausch anzeigen (192.168.0.10)']);
|
||||
await expect(page.locator('section[data-desktop]')).toHaveCount(0);
|
||||
await expect(sheet).toHaveCount(0);
|
||||
await button.click();
|
||||
await expect(page.getByRole('menuitem')).toHaveText(['Desktop anzeigen', 'Datenaustausch anzeigen (192.168.0.10)']);
|
||||
// A tap on another device beside the open menu: that one's menu (the menu lies over what is below it on the right).
|
||||
await page.mouse.click(router.x, router.y);
|
||||
await expect(page.getByRole('menuitem')).toHaveText(['Datenaustausch anzeigen (192.168.0.1)', 'Datenaustausch anzeigen (10.0.0.1)']);
|
||||
await page.keyboard.press('Escape');
|
||||
// A switch: the tap opens no table either; its menu does.
|
||||
const lanSwitch = (await drawing(page)).nodes.find((node) => node.kind === 'networkSwitch')!.id;
|
||||
const hub = await onDevice(page, lanSwitch);
|
||||
await page.mouse.click(hub.x, hub.y);
|
||||
await expect(page.locator(`g.device[data-node="${lanSwitch}"]`)).toHaveAttribute('data-selected', 'true');
|
||||
await expect(page.locator('section[data-viewer]')).toHaveCount(0);
|
||||
await button.click();
|
||||
await expect(page.getByRole('menuitem')).toHaveText(['SAT Tabelle anzeigen']);
|
||||
await expect(page.locator('section[data-viewer]')).toHaveCount(0);
|
||||
await page.getByRole('menuitem', { name: 'SAT Tabelle anzeigen' }).click();
|
||||
await expect(page.locator(`section[data-viewer="${lanSwitch}"]`)).toBeVisible();
|
||||
await page.locator('.simulation-sheet__close').click();
|
||||
// A tap beside lets go.
|
||||
// A tap beside closes the menu and lets go.
|
||||
await page.mouse.click(at.x, at.y);
|
||||
await expect(button).toBeVisible();
|
||||
await expect(menu).toBeVisible();
|
||||
const surface = (await page.locator('svg.filius-canvas').boundingBox())!;
|
||||
await page.mouse.click(surface.x + surface.width - 30, surface.y + surface.height - 80);
|
||||
await expect(button).toHaveCount(0);
|
||||
// Back in the design nothing stays chosen.
|
||||
await expect(menu).toHaveCount(0);
|
||||
await expect(page.locator(`g.device[data-node="${browser}"]`)).not.toHaveAttribute('data-selected', 'true');
|
||||
// A device at the surface's edge: its menu moves in, whole on the surface.
|
||||
await page.mouse.click(at.x, at.y);
|
||||
await page.keyboard.press('Escape');
|
||||
const area = (await page.locator('.editor-area').boundingBox())!;
|
||||
// The wheel moves the surface (a drag on a device in the simulation moves nothing).
|
||||
await page.mouse.move(at.x, at.y);
|
||||
await page.mouse.wheel(-(area.x + area.width - 12 - at.x), -(area.y + area.height - 12 - at.y));
|
||||
await expect.poll(async () => (await onDevice(page, browser)).x).toBeGreaterThan(area.x + area.width - 40);
|
||||
const edge = await onDevice(page, browser);
|
||||
await page.mouse.click(edge.x, edge.y);
|
||||
const moved = (await menu.boundingBox())!;
|
||||
expect(moved.x + moved.width).toBeLessThanOrEqual(area.x + area.width + 0.5);
|
||||
expect(moved.y + moved.height).toBeLessThanOrEqual(area.y + area.height + 0.5);
|
||||
expect(moved.x).toBeLessThan(edge.x);
|
||||
expect(moved.y).toBeLessThan(edge.y);
|
||||
await page.keyboard.press('Escape');
|
||||
// Back in the design nothing stays chosen.
|
||||
await page.mouse.click(edge.x, edge.y);
|
||||
await page.keyboard.press('Escape');
|
||||
await page.getByRole('button', { name: 'Wechselt in den Entwurfsmodus' }).click();
|
||||
await expect(button).toHaveCount(0);
|
||||
await expect(page.locator('.device-menu-button')).toHaveCount(0);
|
||||
await expect(sheet).toHaveCount(0);
|
||||
});
|
||||
|
||||
|
||||
@@ -78,9 +78,8 @@ export async function showFromMenu(page: Page, id: string, item: string | RegExp
|
||||
const open = page.locator('.simulation-sheet__close');
|
||||
if ((await open.count()) > 0) await (touch ? open.tap() : open.click());
|
||||
const at = await onDevice(page, id);
|
||||
// In the simulation a tap opens the device's menu.
|
||||
await (touch ? page.touchscreen.tap(at.x, at.y) : page.mouse.click(at.x, at.y));
|
||||
const button = page.locator('.device-menu-button');
|
||||
await (touch ? button.tap() : button.click());
|
||||
const entry = page.getByRole('menuitem', { name: item, exact: true });
|
||||
await (touch ? entry.tap() : entry.click());
|
||||
}
|
||||
|
||||
@@ -95,10 +95,12 @@ test("opens a computer's desktop from its menu: Java's title, its programs in Ja
|
||||
const id = await deviceNamed(page, 'DNS-Server 2.10');
|
||||
expect((await desktopOf(page, id))!.programs).toHaveLength(3);
|
||||
await start(page);
|
||||
// A tap only chooses the computer (Java's click opens its desktop): its three-dot button is there, no desktop.
|
||||
// A tap opens the computer's menu (Java's click opens its desktop, its menu holds it), no desktop by itself, no three-dot button.
|
||||
await tap(page, id);
|
||||
await expect(page.locator('.device-menu-button')).toBeVisible();
|
||||
await expect(page.getByRole('menuitem')).toHaveText(['Desktop anzeigen', 'Datenaustausch anzeigen (141.99.2.10)']);
|
||||
await expect(page.locator('section[data-desktop]')).toHaveCount(0);
|
||||
await expect(page.locator('.device-menu-button')).toHaveCount(0);
|
||||
await page.keyboard.press('Escape');
|
||||
const window = await showDesktop(page, id);
|
||||
await expect(window.getByRole('heading')).toHaveText('DNS-Server 2.10 - 141.99.2.10');
|
||||
await expect(window.locator('.desktop-icon span')).toHaveText(['Software-Installation', 'Datei-Explorer', 'DNS-Server', 'Webserver']);
|
||||
@@ -117,7 +119,6 @@ test("opens a computer's desktop from its menu: Java's title, its programs in Ja
|
||||
await window.getByRole('button', { name: 'Schließen' }).click();
|
||||
await expect(page.locator('section[data-desktop]')).toHaveCount(0);
|
||||
await tap(page, await deviceNamed(page, 'Switch 1'));
|
||||
await page.locator('.device-menu-button').click();
|
||||
await expect(page.getByRole('menuitem')).toHaveText(['SAT Tabelle anzeigen']);
|
||||
await page.keyboard.press('Escape');
|
||||
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
// anew, set up and pinged across, without a mouse —; two fingers zoom the
|
||||
// surface and never the page (M7); every action without a right click
|
||||
// (long press, the three-dot button, the documentation's menu); every
|
||||
// target at least 44 px, nothing beyond the screen's edge.
|
||||
// target at least 44 px, nothing beyond the screen's edge; in the simulation
|
||||
// a tap opens a device's menu.
|
||||
|
||||
import { expect, test, type Page } from '@playwright/test';
|
||||
import { drawing, onDevice, openEditor, scaleOf, serveEditor } from './editorPage.js';
|
||||
@@ -104,9 +105,8 @@ test('builds FILIUS\' „zwei Rechnernetze“ by finger and pings across (the ac
|
||||
// The simulation; the command line installed on the first computer; a ping across the router.
|
||||
await page.getByRole('button', { name: 'Wechselt in den Aktionsmodus' }).tap();
|
||||
await expect(page.locator('svg.filius-canvas')).toHaveAttribute('data-mode', 'simulation');
|
||||
// A tap chooses the computer; its three-dot button opens its menu, „Desktop anzeigen“ its desktop as the sheet.
|
||||
// A tap opens the computer's menu, „Desktop anzeigen“ its desktop as the sheet.
|
||||
await finger.tap(await onDevice(page, pcA));
|
||||
await page.getByRole('button', { name: 'Aktionen' }).tap();
|
||||
await page.getByRole('menuitem', { name: 'Desktop anzeigen' }).tap();
|
||||
const desktop = page.locator(`section[data-desktop="${pcA}"]`);
|
||||
await desktop.locator('[data-program="installation"]').tap();
|
||||
@@ -332,3 +332,19 @@ test('gives every target 44 px at least and keeps the editor within the screen',
|
||||
expect(await fits()).toBe(true);
|
||||
expect(await small(targets)).toEqual([]);
|
||||
});
|
||||
|
||||
test('draws undo and redo as arrows of their own, in the middle of their buttons, not as a character of whatever font the device has', async ({ page }) => {
|
||||
await openEditor(page, server.base, 'zwei_rechnernetze_komplett.fls');
|
||||
for (const name of ['Rückgängig', 'Wiederholen']) {
|
||||
const button = page.getByRole('button', { name, exact: true });
|
||||
await expect(button).toHaveText('');
|
||||
const [outer, arrow] = [(await button.boundingBox())!, (await button.locator('svg').boundingBox())!];
|
||||
expect([arrow.width, arrow.height]).toEqual([20, 20]);
|
||||
expect(Math.abs(arrow.x + arrow.width / 2 - (outer.x + outer.width / 2))).toBeLessThanOrEqual(0.5);
|
||||
expect(Math.abs(arrow.y + arrow.height / 2 - (outer.y + outer.height / 2))).toBeLessThanOrEqual(0.5);
|
||||
expect(Math.min(outer.width, outer.height)).toBeGreaterThanOrEqual(44);
|
||||
}
|
||||
// Redo is undo's mirror image.
|
||||
const paths = await page.locator('.editor-toolbar svg g').evaluateAll((groups) => groups.map((group) => group.getAttribute('transform')));
|
||||
expect(paths).toEqual([null, 'matrix(-1 0 0 1 24 0)']);
|
||||
});
|
||||
|
||||
@@ -122,8 +122,26 @@ const toolButton: CSSProperties = {
|
||||
color: 'var(--text-primary)',
|
||||
font: 'inherit',
|
||||
cursor: 'pointer',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
};
|
||||
|
||||
/**
|
||||
* Undo's and redo's arrows, drawn: as characters (↶ ↷) each device took
|
||||
* them from whatever font it had, small and askew on an iPad.
|
||||
*/
|
||||
function HistoryIcon({ redo }: { readonly redo: boolean }) {
|
||||
return (
|
||||
<svg width={20} height={20} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<g transform={redo ? 'matrix(-1 0 0 1 24 0)' : undefined}>
|
||||
<path d="M9 14 4 9l5-5" />
|
||||
<path d="M4 9h10.5a5.5 5.5 0 0 1 0 11H11" />
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** A tool that is on: the sheet it opens is open. */
|
||||
const pressedButton: CSSProperties = { borderColor: 'var(--accent)', background: 'var(--accent-soft)' };
|
||||
|
||||
@@ -369,7 +387,7 @@ export function Editor({ doc, createSimHost, projectName, readOnly = false, froz
|
||||
if (target.kind === 'doc') return [];
|
||||
if (simulating) {
|
||||
// Java's menu in the simulation: a computer's desktop, then each card's data exchange with its address. Before them,
|
||||
// what Java's click on the device opens — a switch's or home router's table —: a tap here only chooses.
|
||||
// what Java's click on the device opens — a switch's or home router's table —: a tap here opens this menu.
|
||||
if (target.kind !== 'device') return [];
|
||||
const node = view.nodes.find((each) => each.id === target.id);
|
||||
const items: MenuItem[] =
|
||||
@@ -498,7 +516,6 @@ export function Editor({ doc, createSimHost, projectName, readOnly = false, froz
|
||||
);
|
||||
}}
|
||||
activeLinks={simulation.activeLinks}
|
||||
hasMenu={(target) => menuItems(target).length > 0}
|
||||
onMenu={(target, at) => {
|
||||
if (readOnly && !simulating) return;
|
||||
if (editorMode === 'documentation') {
|
||||
@@ -530,10 +547,10 @@ export function Editor({ doc, createSimHost, projectName, readOnly = false, froz
|
||||
) : readOnly ? null : (
|
||||
<>
|
||||
<button type="button" style={toolButton} disabled={!canUndo} onClick={() => manager?.undo()} aria-label={t('design.undo')} title={t('design.undo')}>
|
||||
↶
|
||||
<HistoryIcon redo={false} />
|
||||
</button>
|
||||
<button type="button" style={toolButton} disabled={!canRedo} onClick={() => manager?.redo()} aria-label={t('design.redo')} title={t('design.redo')}>
|
||||
↷
|
||||
<HistoryIcon redo />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
// tool a drag from one device to another plugs a cable, and so do two
|
||||
// taps, one on each (Java's way).
|
||||
// - On a cable: a tap selects it, a long press opens its menu.
|
||||
// - In the simulation a tap on a device chooses it and opens its menu at
|
||||
// once (Java's click opens a desktop or a table; its menu holds both), and
|
||||
// nothing moves; there is no three-dot button.
|
||||
// - In the documentation mode the documentation items lie over the network
|
||||
// and take the pointer (Java's documentation panel moves to the top): a
|
||||
// tap gives one the focus, a drag on a rectangle (on a text: on the band
|
||||
@@ -84,10 +87,10 @@ export interface CanvasProps {
|
||||
/** A cable drawn active, as Java draws the cable of the card whose tab is open (Kabel.setAktiv). */
|
||||
readonly highlightedLink?: string | null;
|
||||
/**
|
||||
* The simulation: a tap on a device only chooses it, so that its
|
||||
* three-dot button shows as in the design (Java's click opens a desktop or
|
||||
* a table; here the menu does); a long press opens its menu; nothing
|
||||
* moves, as in Java's simulation mode. The documentation mode: see above.
|
||||
* The simulation: a tap on a device chooses it and opens its menu (Java's
|
||||
* click opens a desktop or a table; here the menu holds them), as a long
|
||||
* press does; nothing moves, as in Java's simulation mode. The
|
||||
* documentation mode: see above.
|
||||
*/
|
||||
readonly mode?: 'design' | 'simulation' | 'documentation';
|
||||
/**
|
||||
@@ -104,8 +107,6 @@ export interface CanvasProps {
|
||||
readonly renderDocText?: (item: DocumentationView, frame: ScreenFrame) => React.ReactNode;
|
||||
/** The cables that carry a frame now, drawn as Java draws them then (JCablePanel.farbeBlinken). */
|
||||
readonly activeLinks?: ReadonlySet<string>;
|
||||
/** In the simulation: whether the chosen device has a menu at all (a switch has none) — only then its three-dot button. */
|
||||
readonly hasMenu?: (target: Selection) => boolean;
|
||||
}
|
||||
|
||||
const NO_LINKS: ReadonlySet<string> = new Set();
|
||||
@@ -287,7 +288,6 @@ export const Canvas = forwardRef<CanvasHandle, CanvasProps>(function Canvas(
|
||||
locked = false,
|
||||
chosenBy = NO_CHOICES,
|
||||
activeLinks = NO_LINKS,
|
||||
hasMenu,
|
||||
onDocFrame,
|
||||
renderDocText,
|
||||
},
|
||||
@@ -598,7 +598,10 @@ export const Canvas = forwardRef<CanvasHandle, CanvasProps>(function Canvas(
|
||||
}
|
||||
if (now.longPressed) return;
|
||||
if (simulating) {
|
||||
if (!now.moved) onSelect?.({ kind: 'device', id: now.id });
|
||||
if (now.moved) return;
|
||||
const target = { kind: 'device', id: now.id } as const;
|
||||
onSelect?.(target);
|
||||
onMenu?.(target, now.start);
|
||||
return;
|
||||
}
|
||||
if (locked) {
|
||||
@@ -793,7 +796,7 @@ export const Canvas = forwardRef<CanvasHandle, CanvasProps>(function Canvas(
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{menuButton === null || selection === null || (simulating ? hasMenu?.(selection) !== true : locked) ? null : (
|
||||
{menuButton === null || selection === null || simulating || locked ? null : (
|
||||
<button
|
||||
type="button"
|
||||
className="device-menu-button"
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// The menu of a device or a cable (Java's design mode context menu,
|
||||
// GUIEvents.kontextMenueEntwurfsmodus): on a device "remove cable" (a PC or
|
||||
// notebook, which has one) or "remove all cables", and "delete"; on a cable
|
||||
// "remove cable". Opened by a long press or the three-dot button; every item
|
||||
// a 44 px target.
|
||||
// "remove cable"; in the simulation Java's menu there (a desktop, a table, a
|
||||
// data exchange). Opened by a long press or the three-dot button, in the
|
||||
// simulation by a tap; every item a 44 px target. It opens at the finger and
|
||||
// moves in from the surface's edge as far as it has to.
|
||||
|
||||
import { useEffect, useRef, type CSSProperties } from 'react';
|
||||
import { useEffect, useLayoutEffect, useRef, useState, type CSSProperties } from 'react';
|
||||
import type { Point } from '../canvas/viewport.js';
|
||||
|
||||
export interface MenuItem {
|
||||
@@ -29,6 +31,14 @@ const entry: CSSProperties = {
|
||||
|
||||
export function ContextMenu({ at, items, onClose }: { readonly at: Point; readonly items: readonly MenuItem[]; readonly onClose: () => void }) {
|
||||
const menu = useRef<HTMLDivElement>(null);
|
||||
const [place, setPlace] = useState(at);
|
||||
useLayoutEffect(() => {
|
||||
const element = menu.current;
|
||||
const room = element?.offsetParent;
|
||||
if (element === null || !(room instanceof HTMLElement)) return;
|
||||
const inside = (from: number, size: number, length: number) => Math.max(0, Math.min(from, length - size));
|
||||
setPlace({ x: inside(at.x, element.offsetWidth, room.clientWidth), y: inside(at.y, element.offsetHeight, room.clientHeight) });
|
||||
}, [at]);
|
||||
useEffect(() => {
|
||||
menu.current?.querySelector('button')?.focus();
|
||||
const away = (event: PointerEvent) => {
|
||||
@@ -50,8 +60,8 @@ export function ContextMenu({ at, items, onClose }: { readonly at: Point; readon
|
||||
role="menu"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: at.x,
|
||||
top: at.y,
|
||||
left: place.x,
|
||||
top: place.y,
|
||||
minWidth: 200,
|
||||
padding: '4px 0',
|
||||
borderRadius: 8,
|
||||
|
||||
Reference in New Issue
Block a user