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:
@@ -53,39 +53,57 @@ export class AppSidebar extends LitElement {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
/* The nav item is a real <button>: it was a bare <li @click>,
|
||||
which is why tabbing through the whole app reached fourteen
|
||||
controls and not one of them was navigation (H-5). */
|
||||
li {
|
||||
display: block;
|
||||
}
|
||||
|
||||
li button {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
padding: 8px;
|
||||
cursor: pointer;
|
||||
background: none;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
|
||||
li wa-icon {
|
||||
li button wa-icon {
|
||||
font-size: var(--yj-icon-md);
|
||||
flex-shrink: 0;
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
li:hover {
|
||||
li button:hover {
|
||||
background-color: var(--yj-bg-elevated, #343a40);
|
||||
}
|
||||
|
||||
li.active {
|
||||
li button:focus-visible {
|
||||
outline: 2px solid var(--yj-accent, #ffd43b);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
li button.active {
|
||||
background-color: var(--yj-bg-overlay, #495057);
|
||||
}
|
||||
|
||||
li p {
|
||||
li button p {
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
li.drag-hover {
|
||||
li button.drag-hover {
|
||||
background-color: var(
|
||||
--yj-accent-bg-strong,
|
||||
rgba(255, 212, 59, 0.15)
|
||||
@@ -94,7 +112,7 @@ export class AppSidebar extends LitElement {
|
||||
outline-offset: -1px;
|
||||
}
|
||||
|
||||
li p {
|
||||
li button p {
|
||||
font-size: var(--yj-text-md);
|
||||
}
|
||||
|
||||
@@ -103,16 +121,16 @@ export class AppSidebar extends LitElement {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
:host(.collapsed) li {
|
||||
:host(.collapsed) li button {
|
||||
justify-content: center;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
:host(.collapsed) li p {
|
||||
:host(.collapsed) li button p {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:host(.collapsed) li wa-icon {
|
||||
:host(.collapsed) li button wa-icon {
|
||||
font-size: var(--yj-icon-md);
|
||||
}
|
||||
`];
|
||||
@@ -199,6 +217,7 @@ export class AppSidebar extends LitElement {
|
||||
class="resize-handle ${this.isDragging ? 'dragging' : ''}"
|
||||
@mousedown=${this.handleMouseDown}
|
||||
></div>
|
||||
<nav aria-label="Main">
|
||||
<ul>
|
||||
${this.navItems.map((item) => {
|
||||
const classes = [
|
||||
@@ -213,34 +232,38 @@ export class AppSidebar extends LitElement {
|
||||
.join(' ');
|
||||
|
||||
return html`
|
||||
<li
|
||||
class=${classes}
|
||||
data-testid="nav-${item.id}"
|
||||
aria-current=${this.activeView === item.id
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
class=${classes}
|
||||
data-testid="nav-${item.id}"
|
||||
aria-current=${this.activeView === item.id
|
||||
? 'page'
|
||||
: 'false'}
|
||||
@click=${() =>
|
||||
@click=${() =>
|
||||
this.navigate(item.id)}
|
||||
@dragover=${(e: DragEvent) =>
|
||||
@dragover=${(e: DragEvent) =>
|
||||
this.onNavDragOver(
|
||||
e,
|
||||
item.id,
|
||||
)}
|
||||
@dragleave=${() =>
|
||||
@dragleave=${() =>
|
||||
this.onNavDragLeave(
|
||||
item.id,
|
||||
)}
|
||||
@drop=${(e: DragEvent) =>
|
||||
@drop=${(e: DragEvent) =>
|
||||
this.onNavDrop(e)}
|
||||
>
|
||||
<wa-icon
|
||||
name=${item.icon}
|
||||
></wa-icon>
|
||||
<p>${item.label}</p>
|
||||
>
|
||||
<wa-icon
|
||||
name=${item.icon}
|
||||
></wa-icon>
|
||||
<p>${item.label}</p>
|
||||
</button>
|
||||
</li>
|
||||
`;
|
||||
})}
|
||||
</ul>
|
||||
</nav>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user