Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions scripts/browser-canvas-help.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import assert from 'node:assert/strict';
import fs from 'node:fs';

/** The panel must supply working help even when its host knows only ViewerPanel. */
export async function exerciseStandaloneCanvasHelp(page, origin) {
Expand All @@ -8,6 +9,12 @@ export async function exerciseStandaloneCanvasHelp(page, origin) {
import ViewerPanel from './viewer-panel.js';
export default viewerComponent('ViewerHost', '<ViewerPanel />', { ViewerPanel });
` }));
// Reproduce the review screenshot: the general dialog theme is cached without
// the newer help-specific rules. Component styles must still center the help.
const cssURL = origin + '/src/ui/styles/dialogs.css';
const oldCSS = fs.readFileSync(new URL('../src/ui/styles/dialogs.css', import.meta.url), 'utf8')
.split('dialog.canvas-help-dialog')[0];
await page.route(cssURL, route => route.fulfill({ contentType: 'text/css', body: oldCSS }));
try {
await page.goto(origin + '/index.html?showRenderingOnly=1&sequence=AAAA%26UUUU&structure=((..%26..))&forceLayout=0',
{ waitUntil: 'networkidle' });
Expand All @@ -20,12 +27,43 @@ export async function exerciseStandaloneCanvasHelp(page, origin) {
assert.equal(await dialog.count(), 1, 'The help button supplies exactly one accessible dialog');
assert.equal(await dialog.isVisible(), true, 'Keyboard activation opens visible help');
assert.equal(await dialog.evaluate(element => element.matches(':modal')), true, 'Help enters the modal top layer');
await checkCanvasHelpStyles(page);
await page.keyboard.press('Escape');
assert.equal(await help.evaluate(element => element === document.activeElement), true);
await help.click();
await dialog.getByRole('button', { name: 'Close', exact: true }).click();
assert.equal(await dialog.count(), 0, 'Close dismisses help from the accessibility tree');
} finally {
await page.unroute(appURL);
await page.unroute(cssURL);
}
}

/** Check geometry and recognizable, noninteractive button names in the help. */
export async function checkCanvasHelpStyles(page) {
const dialog = page.locator('#canvasHelpDialog');
const box = await dialog.boundingBox();
const viewport = page.viewportSize();
assert.ok(box.width <= 542 && box.width <= viewport.width - 30, 'Help has a bounded width');
assert.ok(Math.abs(box.x + box.width / 2 - viewport.width / 2) < 2, 'Help is horizontally centered');
assert.ok(Math.abs(box.y + box.height / 2 - viewport.height / 2) < 2, 'Help is vertically centered');
assert.equal(await dialog.locator('dt').first().evaluate(el => getComputedStyle(el).fontWeight), '700');
const keys = dialog.locator('dt .canvas-position-key');
assert.deepEqual(await keys.allTextContents(), ['Moved', 'Release', 'Reset', 'Undo']);
const appearances = await keys.evaluateAll(elements => elements.map(el => {
const style = getComputedStyle(el);
return { border: style.border, radius: style.borderRadius, padding: style.padding,
height: style.height, fontSize: style.fontSize, weight: style.fontWeight, tag: el.tagName };
}));
// Enable a toolbar button only to compare its normal appearance with the key.
const expected = await page.locator('#selectMovedBtn').evaluate(el => {
const disabled = el.disabled;
el.disabled = false;
const style = getComputedStyle(el);
const result = { border: style.border, radius: style.borderRadius, padding: style.padding,
height: style.height, fontSize: style.fontSize, weight: '700', tag: 'SPAN' };
el.disabled = disabled;
return result;
});
appearances.forEach(appearance => assert.deepEqual(appearance, expected));
}
4 changes: 3 additions & 1 deletion scripts/browser-position-layout.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import assert from 'node:assert/strict';
import path from 'node:path';
import { checkCanvasHelpStyles } from './browser-canvas-help.js';

/** Controls must be visible and hit-testable without Playwright scrolling them into view. */
export async function exercisePositionLayout(page, origin, output) {
const original = page.viewportSize();
try {
for (const [width, height, embedded] of [[938, 480, false], [1280, 569, false], [938, 569, false],
[901, 731, false], [1280, 720, false], [640, 480, true]]) {
[1759, 570, false], [901, 731, false], [1280, 720, false], [640, 480, true]]) {
await page.setViewportSize({ width, height });
await page.goto(origin + '/index.html' + (embedded
? '?showRenderingOnly=1&sequence=AAAA%26UUUU&structure=((..%26..))&forceLayout=0' : ''), { waitUntil: 'networkidle' });
Expand Down Expand Up @@ -37,6 +38,7 @@ export async function exercisePositionLayout(page, origin, output) {
for (const text of ['Command', 'Select', 'Move', 'Rotate', 'Release', 'Reset', 'Undo']) {
assert.ok((await dialog.textContent()).includes(text), `Help explains ${text}`);
}
await checkCanvasHelpStyles(page);
const box = await dialog.boundingBox();
assert.ok(box.x >= 0 && box.y >= 0 && box.x + box.width <= width && box.y + box.height <= height);
await page.screenshot({ path: path.join(output, `canvas-help-${width}x${height}.png`) });
Expand Down
14 changes: 9 additions & 5 deletions src/ui/components/canvas-help-dialog.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { viewerComponent } from './context.js';

export default viewerComponent('canvas-help-dialog', `<dialog id="canvasHelpDialog" class="app-dialog canvas-help-dialog"
// Load help styles with the component, including when the host caches older CSS.
const stylesheet = new URL('../styles/canvas-help.css', import.meta.url).href;

export default viewerComponent('canvas-help-dialog', `<link rel="stylesheet" href="${stylesheet}">
<dialog id="canvasHelpDialog" class="app-dialog canvas-help-dialog"
aria-labelledby="canvasHelpTitle" v-on:close="actions.closeDialog('canvasHelpDialog')">
<form method="dialog" v-on:submit="actions.submitDialog('canvasHelpDialog', $event)">
<div class="dialog-header" id="canvasHelpTitle" tabindex="-1" autofocus>Canvas interactions</div>
Expand All @@ -14,12 +18,12 @@ export default viewerComponent('canvas-help-dialog', `<dialog id="canvasHelpDial
Dragging an unselected element clears the selection. Moved nucleotides and numbering labels stay fixed, even with Force layout enabled.</dd>
<dt>Rotate</dt><dd>Select at least two elements, then Ctrl/Command-scroll to rotate them around the mouse pointer.
Text stays upright. Ordinary scrolling zooms, and dragging the background pans.</dd>
<dt>Moved</dt><dd>The count includes manually positioned nucleotides and numbering labels. Click it to select them all.</dd>
<dt>Release</dt><dd>Unfix selected nodes at their current positions. They resume moving when Force layout is enabled.
<dt><span class="canvas-position-key">Moved</span></dt><dd>The count includes manually positioned nucleotides and numbering labels. Click it to select them all.</dd>
<dt><span class="canvas-position-key">Release</span></dt><dd>Unfix selected nodes at their current positions. They resume moving when Force layout is enabled.
Released nodes leave the Moved count. Text labels are unaffected.</dd>
<dt>Reset</dt><dd>Restore selected manually positioned nodes to their positions and fixation before their first edit.
<dt><span class="canvas-position-key">Reset</span></dt><dd>Restore selected manually positioned nodes to their positions and fixation before their first edit.
Text labels are unaffected. To reset a released node, undo its release first.</dd>
<dt>Undo</dt><dd>Reverse the last drag, rotation, release, or reset, including text movements.
<dt><span class="canvas-position-key">Undo</span></dt><dd>Reverse the last drag, rotation, release, or reset, including text movements.
Ctrl/Command+Z also works while the canvas is focused. Up to 100 edits are kept for the current rendering.</dd>
</dl>
<p>Rerendering clears temporary node positions and undo history. SVG and PNG exports preserve the layout without selection outlines.
Expand Down
24 changes: 24 additions & 0 deletions src/ui/styles/canvas-help.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
dialog.canvas-help-dialog {
inset: 0;
margin: auto;
width: 540px;
height: fit-content;
min-width: 0;
max-width: calc(100vw - 32px);
max-height: calc(100vh - 32px);
overflow: auto;
font-size: 13px;
line-height: 1.45;
}
.canvas-help-dialog .dialog-header { cursor: default; }
.canvas-help-dialog dl { margin: 12px 0; }
.canvas-help-dialog dt { font-weight: 700; margin-top: 8px; }

.manual-position-controls button,
.canvas-position-key {
font: inherit; font-size: .75rem; line-height: 18px; height: 20px;
padding: 0 .4rem; white-space: nowrap;
color: var(--brand-blue-dark); background: #f5f8fb;
border: 1px solid #cbd7e2; border-radius: 4px;
}
.canvas-position-key { display: inline-block; font-weight: 700; }
7 changes: 1 addition & 6 deletions src/ui/styles/canvas.css
Original file line number Diff line number Diff line change
Expand Up @@ -251,11 +251,6 @@
.viz-control > .cropping-control { flex: 0.6 1 50px; width: auto; min-width: 50px; }
.cropping-control > input { width: 100%; display: block; }
.manual-position-controls { display: flex; flex: none; gap: .25rem; align-items: center; margin-left: auto; }
.manual-position-controls button {
font: inherit; font-size: .75rem; line-height: 18px; height: 20px;
padding: 0 .4rem; white-space: nowrap;
color: var(--brand-blue-dark); background: #f5f8fb;
border: 1px solid #cbd7e2; border-radius: 4px; cursor: pointer;
}
.manual-position-controls button { cursor: pointer; }
.manual-position-controls button:hover:enabled { background: #e8f0f8; }
.manual-position-controls button:disabled { color: #8b929a; background: #f3f4f5; cursor: default; }
15 changes: 0 additions & 15 deletions src/ui/styles/dialogs.css
Original file line number Diff line number Diff line change
Expand Up @@ -134,18 +134,3 @@ dialog.png-export-dialog {
.png-hint:last-child { margin-bottom: 0; }
.png-export-error { margin: 8px 0 0; color: #a12622; font-size: 13px; }
.png-export-dialog button:disabled { opacity: 0.6; cursor: wait; }

dialog.canvas-help-dialog {
inset: 0;
margin: auto;
width: 540px;
min-width: 0;
max-width: calc(100vw - 32px);
max-height: calc(100vh - 32px);
overflow: auto;
font-size: 13px;
line-height: 1.45;
}
.canvas-help-dialog .dialog-header { cursor: default; }
.canvas-help-dialog dl { margin: 12px 0; }
.canvas-help-dialog dt { font-weight: 700; margin-top: 8px; }
Loading