diff --git a/ui/src/app/dialogs/chmod-dialog.ts b/ui/src/app/dialogs/chmod-dialog.ts index 70f337b..2c1d0d1 100644 --- a/ui/src/app/dialogs/chmod-dialog.ts +++ b/ui/src/app/dialogs/chmod-dialog.ts @@ -1,6 +1,6 @@ import { Component, computed, inject, input, signal } from '@angular/core'; import { DialogsService, type ChmodRequest } from '../dialogs.service'; -import { PanelsService } from '../panels.service'; +import { FocusService } from '../focus.service'; const ROLES = ['Owner', 'Group', 'Others'] as const; const PERMS = ['r', 'w', 'x'] as const; @@ -81,7 +81,7 @@ export class ChmodDialog { readonly perms = PERMS; private readonly dialogs = inject(DialogsService); - private readonly panels = inject(PanelsService); + private readonly focus = inject(FocusService); // mode is initialized lazily from the request on first read private readonly modeOverride = signal(null); @@ -120,6 +120,6 @@ export class ChmodDialog { private close(): void { this.dialogs.chmod.set(null); - this.panels.refocus(); + this.focus.bump(); } } diff --git a/ui/src/app/dialogs/confirm-dialog.ts b/ui/src/app/dialogs/confirm-dialog.ts index f95b19d..3fb260c 100644 --- a/ui/src/app/dialogs/confirm-dialog.ts +++ b/ui/src/app/dialogs/confirm-dialog.ts @@ -1,6 +1,6 @@ import { Component, ElementRef, afterNextRender, inject, input, viewChild } from '@angular/core'; import { DialogsService, type ConfirmRequest } from '../dialogs.service'; -import { PanelsService } from '../panels.service'; +import { FocusService } from '../focus.service'; @Component({ selector: 'app-confirm-dialog', @@ -41,7 +41,7 @@ export class ConfirmDialog { private readonly confirmBtn = viewChild.required>('confirmBtn'); private readonly dialogs = inject(DialogsService); - private readonly panels = inject(PanelsService); + private readonly focus = inject(FocusService); constructor() { afterNextRender(() => this.confirmBtn().nativeElement.focus()); @@ -59,6 +59,6 @@ export class ConfirmDialog { private close(): void { this.dialogs.confirm.set(null); - this.panels.refocus(); + this.focus.bump(); } } diff --git a/ui/src/app/dialogs/overwrite-dialog.ts b/ui/src/app/dialogs/overwrite-dialog.ts index c731d46..9da9e18 100644 --- a/ui/src/app/dialogs/overwrite-dialog.ts +++ b/ui/src/app/dialogs/overwrite-dialog.ts @@ -10,7 +10,7 @@ import { import { NgIcon } from '@ng-icons/core'; import { DialogsService, type OverwriteChoice, type OverwriteRequest } from '../dialogs.service'; import { formatDate, formatSize } from '../format'; -import { PanelsService } from '../panels.service'; +import { FocusService } from '../focus.service'; @Component({ selector: 'app-overwrite-dialog', @@ -125,7 +125,7 @@ export class OverwriteDialog { readonly formatDate = formatDate; private readonly dialogs = inject(DialogsService); - private readonly panels = inject(PanelsService); + private readonly focus = inject(FocusService); private readonly primaryBtn = viewChild.required>('primaryBtn'); constructor() { @@ -145,7 +145,7 @@ export class OverwriteDialog { pick(choice: OverwriteChoice): void { const req = this.request(); this.dialogs.overwrite.set(null); - this.panels.refocus(); + this.focus.bump(); req.resolve(choice); } } diff --git a/ui/src/app/dialogs/prompt-dialog.ts b/ui/src/app/dialogs/prompt-dialog.ts index 583e64f..89dd709 100644 --- a/ui/src/app/dialogs/prompt-dialog.ts +++ b/ui/src/app/dialogs/prompt-dialog.ts @@ -8,7 +8,7 @@ import { viewChild, } from '@angular/core'; import { DialogsService, type PromptRequest } from '../dialogs.service'; -import { PanelsService } from '../panels.service'; +import { FocusService } from '../focus.service'; @Component({ selector: 'app-prompt-dialog', @@ -54,7 +54,7 @@ export class PromptDialog { private readonly box = viewChild.required>('box'); private readonly dialogs = inject(DialogsService); - private readonly panels = inject(PanelsService); + private readonly focus = inject(FocusService); constructor() { afterNextRender(() => { @@ -79,6 +79,6 @@ export class PromptDialog { private close(): void { this.dialogs.prompt.set(null); - this.panels.refocus(); + this.focus.bump(); } } diff --git a/ui/src/app/focus.service.ts b/ui/src/app/focus.service.ts new file mode 100644 index 0000000..77a9576 --- /dev/null +++ b/ui/src/app/focus.service.ts @@ -0,0 +1,16 @@ +import { Injectable, signal } from '@angular/core'; + +/** + * "Give keyboard focus back to the main surface" signal. Dependency-free so + * dialogs can bump it from ANY window kind — in the main window the file + * panels listen (via PanelsService.focusTick); elsewhere nobody does, which + * is exactly right. + */ +@Injectable({ providedIn: 'root' }) +export class FocusService { + readonly tick = signal(0); + + bump(): void { + this.tick.update((n) => n + 1); + } +} diff --git a/ui/src/app/panels.service.ts b/ui/src/app/panels.service.ts index db93c2c..698c2d6 100644 --- a/ui/src/app/panels.service.ts +++ b/ui/src/app/panels.service.ts @@ -1,5 +1,6 @@ -import { Injectable, computed, signal, type WritableSignal } from '@angular/core'; +import { Injectable, computed, inject, signal, type WritableSignal } from '@angular/core'; import type { ConnectionConfig, FileEntry, SavedConnection } from '@shared/types'; +import { FocusService } from './focus.service'; import { RpcService } from './rpc.service'; import { ToastService } from './toast.service'; @@ -201,8 +202,9 @@ export class PanelsService { const list = this.workspaces(); return list.find((w) => w.id === this.activeWorkspaceId()) ?? list[0]; }); + private readonly focus = inject(FocusService); /** Bumped to tell the active panel to grab keyboard focus again (e.g. after a dialog closes). */ - readonly focusTick = signal(0); + readonly focusTick = this.focus.tick; private nextWorkspaceId = 1; @@ -265,7 +267,7 @@ export class PanelsService { } refocus(): void { - this.focusTick.update((n) => n + 1); + this.focus.bump(); } // ---- workspace (tab) management ----