`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.
46 lines
1.6 KiB
TypeScript
46 lines
1.6 KiB
TypeScript
/**
|
|
* The ambient shortcut scope: which panel's bindings apply when nothing
|
|
* inside a panel has focus.
|
|
*
|
|
* `resolveScope` in the shortcut service walks up from the focused
|
|
* element looking for `data-shortcut-scope`, which is the right answer
|
|
* when something is focused — but this app is mostly driven with the
|
|
* mouse, so the usual state is that focus sits on `<body>` and the walk
|
|
* finds nothing. Without a fallback the panel bindings would only work
|
|
* after a click landed inside the panel, which is not the behaviour the
|
|
* Autotag page has today and not one worth regressing to.
|
|
*
|
|
* A claim is held by the *active view* (see `utils/view-lifecycle.ts`),
|
|
* so it is released the moment the view leaves the screen — which is
|
|
* what stops an off-screen view's keys from firing.
|
|
*/
|
|
|
|
/** Claims, innermost last. A stack rather than a single value so that
|
|
* releasing an outer claim out of order cannot resurrect it. */
|
|
const claims: Array<{ scope: string }> = [];
|
|
|
|
/**
|
|
* Claim `scope` as the ambient panel scope. Returns the release.
|
|
*/
|
|
export function claimShortcutScope(scope: string): () => void {
|
|
const claim = { scope };
|
|
|
|
claims.push(claim);
|
|
|
|
return () => {
|
|
const at = claims.indexOf(claim);
|
|
|
|
if (at >= 0) claims.splice(at, 1);
|
|
};
|
|
}
|
|
|
|
/** The innermost claimed scope, or null. */
|
|
export function ambientShortcutScope(): string | null {
|
|
return claims.length > 0 ? claims[claims.length - 1]!.scope : null;
|
|
}
|
|
|
|
/** Drop every claim. Test-only escape hatch. */
|
|
export function resetShortcutScopes(): void {
|
|
claims.length = 0;
|
|
}
|