feat(frontend): give a cached view a lifecycle and a keyboard owner
`index.ts` caches primary views and hides them with a class so scrollTop survives navigation. Nothing else was told: `disconnectedCallback` never fires for one, so everything written to clean up there never cleans up. The worst case was not a leak — pressing `s` on Settings skipped two albums out of the Autotag queue, and `a` on that same live handler rewrites tags on disk. - `utils/view-lifecycle.ts` is the missing half: `viewActivated` / `viewDeactivated`, with `listenWhileActive`, `intervalWhileActive` and `whileActive` torn down on the way out, and an off-screen view that does not render. `registerViewAware` gives a shared reactive controller the same treatment, because a controller cannot know whether its host is a cached view — `ContextMenuController` bound three document listeners in `hostConnected`, which for a cached host is "forever". - `services/shortcut-scope.ts` publishes the ambient scope. Resolving scope from focus alone was not enough: this app is driven with the mouse, focus sits on `<body>`, and a focus-only rule would have made the panel keys work only after a click landed inside the panel. - Global bindings yield to a focused control that owns the key — button, select, slider, checkbox, menu, grid row, or anything inside an open dialog — so the unmodified single-key bindings stop stealing Space and the arrows. - `utils/roving-grid.ts` gives a card grid one tab stop moved with the arrows, since a card per tab stop makes a library-length tab sequence.
This commit is contained in:
@@ -5,6 +5,8 @@ import type {
|
||||
} from 'lit';
|
||||
import type WaPopup from '@awesome.me/webawesome/dist/components/popup/popup.js';
|
||||
|
||||
import { registerViewAware } from './view-lifecycle';
|
||||
|
||||
/**
|
||||
* Host interface for components using the ContextMenuController.
|
||||
* The host must provide access to the popup elements (typically
|
||||
@@ -96,7 +98,31 @@ export class ContextMenuController
|
||||
// LIFECYCLE
|
||||
// =================================================================
|
||||
|
||||
/** Whether the document listeners are currently installed. */
|
||||
private listening = false;
|
||||
|
||||
hostConnected(): void {
|
||||
// On a cached view, connection is not the right signal: it never
|
||||
// un-happens, so these listeners would stay on the document for
|
||||
// the life of the session and close a menu belonging to a page
|
||||
// the user left. A lifecycle host drives attach/detach instead.
|
||||
const managed = registerViewAware(this.host, {
|
||||
onHostActivate: () => this.attach(),
|
||||
onHostDeactivate: () => this.detach(),
|
||||
});
|
||||
|
||||
if (!managed) this.attach();
|
||||
}
|
||||
|
||||
hostDisconnected(): void {
|
||||
this.detach();
|
||||
this.clearSubmenuCloseTimer();
|
||||
}
|
||||
|
||||
private attach(): void {
|
||||
if (this.listening) return;
|
||||
|
||||
this.listening = true;
|
||||
document.addEventListener(
|
||||
'click',
|
||||
this.closeHandler,
|
||||
@@ -111,7 +137,10 @@ export class ContextMenuController
|
||||
);
|
||||
}
|
||||
|
||||
hostDisconnected(): void {
|
||||
private detach(): void {
|
||||
if (!this.listening) return;
|
||||
|
||||
this.listening = false;
|
||||
document.removeEventListener(
|
||||
'click',
|
||||
this.closeHandler,
|
||||
@@ -124,7 +153,6 @@ export class ContextMenuController
|
||||
'mousedown',
|
||||
this.mousedownCloseHandler,
|
||||
);
|
||||
this.clearSubmenuCloseTimer();
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
|
||||
Reference in New Issue
Block a user