Collapse the terminal dock from Escape anywhere in the panel.

Drop unused suggestion state and copy, and give the panel a reduced-motion-aware open transition.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-26 13:13:43 +02:00
parent 35a575dba9
commit 19d00da9bb
6 changed files with 70 additions and 14 deletions

View File

@@ -64,4 +64,25 @@ test.describe('terminal dock', () => {
await input.press('Tab');
await expect(input).toHaveValue('navigate');
});
test('Escape from a panel control collapses the dock and returns focus to the trigger', async ({
page,
}) => {
const copy = SIGNATURE_COPY.de.terminal;
await page.goto('/');
const trigger = page.locator('.terminal-dock-trigger');
const panel = page.locator('.terminal-dock-panel');
await trigger.click();
await expect(panel).toBeVisible();
const collapse = page.getByRole('button', { name: copy.collapseLabel });
await collapse.focus();
await expect(collapse).toBeFocused();
await page.keyboard.press('Escape');
await expect(panel).toHaveCount(0);
await expect(trigger).toBeFocused();
});
});

View File

@@ -11,11 +11,9 @@ export interface SignatureCopy {
readonly inputLabel: string;
readonly inputPlaceholder: string;
readonly collapseLabel: string;
readonly expandLabel: string;
readonly restoreLabel: string;
readonly maximizeLabel: string;
readonly outputLabel: string;
readonly emptySuggestions: string;
readonly unknownCommand: string;
readonly unknownTarget: string;
readonly validTargetsLabel: string;
@@ -61,11 +59,9 @@ export const SIGNATURE_COPY: Record<AppLocale, SignatureCopy> = {
inputLabel: 'Befehl',
inputPlaceholder: 'Befehl eingeben',
collapseLabel: 'Terminal einklappen',
expandLabel: 'Terminal öffnen',
restoreLabel: 'Terminal verkleinern',
maximizeLabel: 'Terminal vergrößern',
outputLabel: 'Ausgabe',
emptySuggestions: 'Keine passenden Befehle.',
unknownCommand: 'Unbekannter Befehl: {command}',
unknownTarget: 'Unbekanntes Ziel: {target}',
validTargetsLabel: 'Gültige Ziele:',
@@ -117,11 +113,9 @@ export const SIGNATURE_COPY: Record<AppLocale, SignatureCopy> = {
inputLabel: 'Command',
inputPlaceholder: 'Type a command',
collapseLabel: 'Collapse the terminal',
expandLabel: 'Open the terminal',
restoreLabel: 'Restore the terminal',
maximizeLabel: 'Maximise the terminal',
outputLabel: 'Output',
emptySuggestions: 'No matching commands.',
unknownCommand: 'Unknown command: {command}',
unknownTarget: 'Unknown target: {target}',
validTargetsLabel: 'Valid targets:',

View File

@@ -13,7 +13,13 @@
</button>
@if (open()) {
<section class="terminal-dock-panel" [id]="panelId" [attr.aria-label]="copy().panelLabel">
<section
class="terminal-dock-panel"
[id]="panelId"
tabindex="-1"
[attr.aria-label]="copy().panelLabel"
(keydown)="onPanelKeydown($event)"
>
<div class="terminal-dock-toolbar cluster">
<p class="terminal-dock-title">{{ copy().panelLabel }}</p>
<button

View File

@@ -48,6 +48,10 @@
background: var(--color-surface-raised);
box-shadow: var(--shadow-raised);
color: var(--color-text);
animation: terminal-dock-in var(--duration-base) var(--ease-standard);
transition:
width var(--duration-base) var(--ease-standard),
max-height var(--duration-base) var(--ease-standard);
}
.terminal-dock.is-open {
@@ -130,9 +134,21 @@
}
}
@keyframes terminal-dock-in {
from {
opacity: 0;
transform: translateY(0.4rem);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (prefers-reduced-motion: reduce) {
.terminal-dock,
.terminal-dock-panel {
animation: none;
transition: none;
}
}

View File

@@ -148,6 +148,24 @@ describe('TerminalDock', () => {
expect(document.activeElement).toBe(trigger(fixture));
});
it.each(APP_LOCALES)(
'closes on Escape from a panel control and returns focus to the trigger (%s)',
async (locale) => {
const fixture = await createFixture(locale);
await openViaTrigger(fixture);
const maximize = fixture.nativeElement.querySelector('[aria-pressed]') as HTMLButtonElement;
maximize.focus();
expect(document.activeElement).toBe(maximize);
maximize.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
await flush(fixture);
expect(panel(fixture)).toBeNull();
expect(document.activeElement).toBe(trigger(fixture));
},
);
it.each(APP_LOCALES)('does not lock scroll or mark the page inert (%s)', async (locale) => {
document.body.style.overflow = 'auto';
const fixture = await createFixture(locale);

View File

@@ -22,7 +22,6 @@ import {
COMMAND_IDS,
navigateTargetKeys,
parseCommand,
suggestCompletions,
} from './terminal-commands';
import { TerminalDockService } from './terminal-dock.service';
@@ -69,7 +68,6 @@ export class TerminalDock {
);
protected readonly open = computed(() => this.state() !== 'collapsed');
protected readonly maximized = computed(() => this.state() === 'maximized');
protected readonly suggestions = computed(() => suggestCompletions(this.query()));
protected readonly maximizeLabel = computed(() =>
this.maximized() ? this.copy().restoreLabel : this.copy().maximizeLabel,
);
@@ -121,13 +119,16 @@ export class TerminalDock {
this.runRaw(this.query());
}
protected onInputKeydown(event: KeyboardEvent): void {
if (event.key === 'Escape') {
event.preventDefault();
this.dock.collapse();
protected onPanelKeydown(event: KeyboardEvent): void {
if (event.key !== 'Escape') {
return;
}
event.preventDefault();
this.dock.collapse();
}
protected onInputKeydown(event: KeyboardEvent): void {
if (event.key === 'ArrowUp') {
event.preventDefault();
this.stepHistory(-1);