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:
@@ -64,4 +64,25 @@ test.describe('terminal dock', () => {
|
|||||||
await input.press('Tab');
|
await input.press('Tab');
|
||||||
await expect(input).toHaveValue('navigate');
|
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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,11 +11,9 @@ export interface SignatureCopy {
|
|||||||
readonly inputLabel: string;
|
readonly inputLabel: string;
|
||||||
readonly inputPlaceholder: string;
|
readonly inputPlaceholder: string;
|
||||||
readonly collapseLabel: string;
|
readonly collapseLabel: string;
|
||||||
readonly expandLabel: string;
|
|
||||||
readonly restoreLabel: string;
|
readonly restoreLabel: string;
|
||||||
readonly maximizeLabel: string;
|
readonly maximizeLabel: string;
|
||||||
readonly outputLabel: string;
|
readonly outputLabel: string;
|
||||||
readonly emptySuggestions: string;
|
|
||||||
readonly unknownCommand: string;
|
readonly unknownCommand: string;
|
||||||
readonly unknownTarget: string;
|
readonly unknownTarget: string;
|
||||||
readonly validTargetsLabel: string;
|
readonly validTargetsLabel: string;
|
||||||
@@ -61,11 +59,9 @@ export const SIGNATURE_COPY: Record<AppLocale, SignatureCopy> = {
|
|||||||
inputLabel: 'Befehl',
|
inputLabel: 'Befehl',
|
||||||
inputPlaceholder: 'Befehl eingeben',
|
inputPlaceholder: 'Befehl eingeben',
|
||||||
collapseLabel: 'Terminal einklappen',
|
collapseLabel: 'Terminal einklappen',
|
||||||
expandLabel: 'Terminal öffnen',
|
|
||||||
restoreLabel: 'Terminal verkleinern',
|
restoreLabel: 'Terminal verkleinern',
|
||||||
maximizeLabel: 'Terminal vergrößern',
|
maximizeLabel: 'Terminal vergrößern',
|
||||||
outputLabel: 'Ausgabe',
|
outputLabel: 'Ausgabe',
|
||||||
emptySuggestions: 'Keine passenden Befehle.',
|
|
||||||
unknownCommand: 'Unbekannter Befehl: {command}',
|
unknownCommand: 'Unbekannter Befehl: {command}',
|
||||||
unknownTarget: 'Unbekanntes Ziel: {target}',
|
unknownTarget: 'Unbekanntes Ziel: {target}',
|
||||||
validTargetsLabel: 'Gültige Ziele:',
|
validTargetsLabel: 'Gültige Ziele:',
|
||||||
@@ -117,11 +113,9 @@ export const SIGNATURE_COPY: Record<AppLocale, SignatureCopy> = {
|
|||||||
inputLabel: 'Command',
|
inputLabel: 'Command',
|
||||||
inputPlaceholder: 'Type a command',
|
inputPlaceholder: 'Type a command',
|
||||||
collapseLabel: 'Collapse the terminal',
|
collapseLabel: 'Collapse the terminal',
|
||||||
expandLabel: 'Open the terminal',
|
|
||||||
restoreLabel: 'Restore the terminal',
|
restoreLabel: 'Restore the terminal',
|
||||||
maximizeLabel: 'Maximise the terminal',
|
maximizeLabel: 'Maximise the terminal',
|
||||||
outputLabel: 'Output',
|
outputLabel: 'Output',
|
||||||
emptySuggestions: 'No matching commands.',
|
|
||||||
unknownCommand: 'Unknown command: {command}',
|
unknownCommand: 'Unknown command: {command}',
|
||||||
unknownTarget: 'Unknown target: {target}',
|
unknownTarget: 'Unknown target: {target}',
|
||||||
validTargetsLabel: 'Valid targets:',
|
validTargetsLabel: 'Valid targets:',
|
||||||
|
|||||||
@@ -13,7 +13,13 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
@if (open()) {
|
@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">
|
<div class="terminal-dock-toolbar cluster">
|
||||||
<p class="terminal-dock-title">{{ copy().panelLabel }}</p>
|
<p class="terminal-dock-title">{{ copy().panelLabel }}</p>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -48,6 +48,10 @@
|
|||||||
background: var(--color-surface-raised);
|
background: var(--color-surface-raised);
|
||||||
box-shadow: var(--shadow-raised);
|
box-shadow: var(--shadow-raised);
|
||||||
color: var(--color-text);
|
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 {
|
.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) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
.terminal-dock,
|
|
||||||
.terminal-dock-panel {
|
.terminal-dock-panel {
|
||||||
|
animation: none;
|
||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -148,6 +148,24 @@ describe('TerminalDock', () => {
|
|||||||
expect(document.activeElement).toBe(trigger(fixture));
|
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) => {
|
it.each(APP_LOCALES)('does not lock scroll or mark the page inert (%s)', async (locale) => {
|
||||||
document.body.style.overflow = 'auto';
|
document.body.style.overflow = 'auto';
|
||||||
const fixture = await createFixture(locale);
|
const fixture = await createFixture(locale);
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import {
|
|||||||
COMMAND_IDS,
|
COMMAND_IDS,
|
||||||
navigateTargetKeys,
|
navigateTargetKeys,
|
||||||
parseCommand,
|
parseCommand,
|
||||||
suggestCompletions,
|
|
||||||
} from './terminal-commands';
|
} from './terminal-commands';
|
||||||
import { TerminalDockService } from './terminal-dock.service';
|
import { TerminalDockService } from './terminal-dock.service';
|
||||||
|
|
||||||
@@ -69,7 +68,6 @@ export class TerminalDock {
|
|||||||
);
|
);
|
||||||
protected readonly open = computed(() => this.state() !== 'collapsed');
|
protected readonly open = computed(() => this.state() !== 'collapsed');
|
||||||
protected readonly maximized = computed(() => this.state() === 'maximized');
|
protected readonly maximized = computed(() => this.state() === 'maximized');
|
||||||
protected readonly suggestions = computed(() => suggestCompletions(this.query()));
|
|
||||||
protected readonly maximizeLabel = computed(() =>
|
protected readonly maximizeLabel = computed(() =>
|
||||||
this.maximized() ? this.copy().restoreLabel : this.copy().maximizeLabel,
|
this.maximized() ? this.copy().restoreLabel : this.copy().maximizeLabel,
|
||||||
);
|
);
|
||||||
@@ -121,13 +119,16 @@ export class TerminalDock {
|
|||||||
this.runRaw(this.query());
|
this.runRaw(this.query());
|
||||||
}
|
}
|
||||||
|
|
||||||
protected onInputKeydown(event: KeyboardEvent): void {
|
protected onPanelKeydown(event: KeyboardEvent): void {
|
||||||
if (event.key === 'Escape') {
|
if (event.key !== 'Escape') {
|
||||||
event.preventDefault();
|
|
||||||
this.dock.collapse();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
this.dock.collapse();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected onInputKeydown(event: KeyboardEvent): void {
|
||||||
if (event.key === 'ArrowUp') {
|
if (event.key === 'ArrowUp') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.stepHistory(-1);
|
this.stepHistory(-1);
|
||||||
|
|||||||
Reference in New Issue
Block a user