The keymap was scattered over ShortcutsService if-chains, the file panel's key switch, macOS-only menu accelerators and hand-typed hints in 16 tooltips x 7 locales, and large parts of the app had no keyboard path at all. src/shared/keymap.ts is now the one table: scopes, per-platform defaults, exact matching, formatting with localized key names, conflicts, reserved combos and native-menu accelerators, proven by keymap.spec.ts. - ShortcutsService resolves against the effective keymap: claim() takes the key (preventDefault, held keys repeat only movement, never mid-IME), and from a text field only chords and F-keys fire. The file panel, App, editor, manager and Settings windows all dispatch through it. - FileActionsService.run is the one file-command dispatcher for the list keys, the row menu, the macOS menu bar and the toolbar. - New keys: the row menu (Shift+F10 / Menu / Ctrl+Return), the source picker (Mod+K), the path bar (Mod+L), transfers (Mod+J), sorting (Alt+1-7), Shift+movement selection, close other tabs, move and disconnect tabs, refresh and edit on Windows/Linux, permissions, copy path, reveal, trash, open in default app, editor find next/previous. - Right-click menus: soft shortcut hints, icons on the everyday verbs (the toolbar's glyphs), section headers, scrolling, item keys while open, a highlighted first item when opened from the keyboard. The panel source picker now renders through the same menu. - Dialogs: role=dialog, a Tab trap via cycleFocus (WKWebView skips buttons on Tab without Keyboard navigation), initial focus, Esc that stops propagating; the chmod dialog takes focus and applies on Enter. - Fixed: Ctrl+Tab also flipped the active panel; Esc in a manager dialog closed the manager; Ctrl-F/T fired Cmd shortcuts on macOS; shortcuts fired under dialogs; keys died after any button click on macOS; the editor's save prompt and find box never got focus; WebView2 reload keys; a window could save default settings before its first load. - Settings closes on its own close-window key (new settingsClose request). Co-Authored-By: Claude <noreply@anthropic.com>
82 lines
2.5 KiB
TypeScript
82 lines
2.5 KiB
TypeScript
import { Component, ElementRef, afterNextRender, inject, input, viewChild } from '@angular/core';
|
|
import { I18nService } from '@core/i18n.service';
|
|
import { DialogsService, type ConfirmRequest } from '@core/dialogs.service';
|
|
import { cycleFocus } from '@core/focus-cycle';
|
|
import { FocusService } from '@core/focus.service';
|
|
|
|
@Component({
|
|
selector: 'app-confirm-dialog',
|
|
template: `
|
|
<div class="fixed inset-0 z-40 grid place-items-center bg-black/40" (mousedown)="cancel()">
|
|
<div
|
|
role="dialog"
|
|
aria-modal="true"
|
|
[attr.aria-label]="request().title"
|
|
class="w-96 border border-border bg-card p-4 text-card-foreground shadow-xl"
|
|
(mousedown)="$event.stopPropagation()"
|
|
(keydown)="onKeydown($event)"
|
|
>
|
|
<h2 class="text-sm font-semibold">{{ request().title }}</h2>
|
|
<p class="mt-2 text-xs text-muted-foreground">{{ request().body }}</p>
|
|
<div class="mt-4 flex justify-end gap-2">
|
|
<button
|
|
(click)="cancel()"
|
|
class="px-3 py-1.5 text-xs font-medium hover:bg-accent"
|
|
>
|
|
{{ i18n.t('common.cancel') }}
|
|
</button>
|
|
<button
|
|
#confirmBtn
|
|
(click)="submit()"
|
|
[class]="
|
|
request().destructive
|
|
? 'bg-destructive px-3 py-1.5 text-xs font-medium text-destructive-foreground hover:bg-destructive/90'
|
|
: 'bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground hover:bg-primary/90'
|
|
"
|
|
>
|
|
{{ request().confirmLabel }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`,
|
|
})
|
|
export class ConfirmDialog {
|
|
readonly i18n = inject(I18nService);
|
|
readonly request = input.required<ConfirmRequest>();
|
|
|
|
private readonly confirmBtn = viewChild.required<ElementRef<HTMLButtonElement>>('confirmBtn');
|
|
private readonly dialogs = inject(DialogsService);
|
|
private readonly focus = inject(FocusService);
|
|
|
|
constructor() {
|
|
afterNextRender(() => this.confirmBtn().nativeElement.focus());
|
|
}
|
|
|
|
/** Esc cancels (and stops here — see PromptDialog); Tab stays inside. */
|
|
onKeydown(event: KeyboardEvent): void {
|
|
if (event.key === 'Escape') {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
this.cancel();
|
|
} else {
|
|
cycleFocus(event.currentTarget as Element, event);
|
|
}
|
|
}
|
|
|
|
submit(): void {
|
|
const req = this.request();
|
|
this.close();
|
|
req.onConfirm();
|
|
}
|
|
|
|
cancel(): void {
|
|
this.close();
|
|
}
|
|
|
|
private close(): void {
|
|
this.dialogs.confirm.set(null);
|
|
this.focus.bump();
|
|
}
|
|
}
|