fix(web): a11y + reactivity warnings (paying down accumulated warnings)
Cleans up the 8 svelte-check warnings I deferred in prior CI runs.
Two distinct categories — handling them differently:
REAL REACTIVITY BUGS — fixed:
- AlbumMenu.svelte / ArtistMenu.svelte: cachedTracks initialized
from the `tracks` prop only at component creation. When the menu
is reused across rows (operator opens menu on one album, then
another), the cache from the first open serves stale tracks.
Fix: initialize cachedTracks to null; ensureTracks() reads the
current `tracks` prop on each call before falling through to
the network fetch.
INTENTIONAL PATTERNS LINT FLAGGED — suppressed with explanatory
comments:
- AlbumMenu / ArtistMenu / PlaylistCard menu containers: the
onclick={(e) => e.stopPropagation()} is a guard against the
window-level "any click closes the menu" listener — not
user-facing interaction. A paired keyboard handler is
intentionally absent (Escape-to-close lives on svelte:window
onkeydown). Suppressed a11y_click_events_have_key_events with
explanatory comments.
- CompactTrackCard overlay div: catches clicks on its inner
LikeButton + queue button so they don't bubble to the wrapping
play-card button. Inner buttons carry their own keyboard
handling. Suppressed both a11y_click_events_have_key_events and
a11y_no_static_element_interactions.
- theme.svelte.ts module-scope `_resolved = $state(resolve(_theme))`:
module-init reads _theme to seed _resolved; subsequent updates
go through setTheme() and the matchMedia listener. $derived
doesn't apply at module scope. Suppressed state_referenced_locally
with an explanatory comment.
Promised in the prior fix-forward: "I'll fix bugs when I see them
or open a follow-up task with a specific name, not park them in an
umbrella." Following through.
This commit is contained in:
@@ -28,7 +28,12 @@
|
|||||||
|
|
||||||
let menuOpen = $state(false);
|
let menuOpen = $state(false);
|
||||||
let addOpen = $state(false);
|
let addOpen = $state(false);
|
||||||
let cachedTracks: TrackRef[] | null = $state(tracks ?? null);
|
// Lazy track cache. Starts null so the `tracks` prop is read inside
|
||||||
|
// ensureTracks() each call rather than captured at component-creation
|
||||||
|
// time — the menu is reused across different album rows, and pinning
|
||||||
|
// the prop's initial value here would serve a stale list when the
|
||||||
|
// user opens the menu on a second album.
|
||||||
|
let cachedTracks: TrackRef[] | null = $state(null);
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const likedQ = $derived(createLikedIdsQuery());
|
const likedQ = $derived(createLikedIdsQuery());
|
||||||
@@ -50,6 +55,10 @@
|
|||||||
|
|
||||||
async function ensureTracks(): Promise<TrackRef[]> {
|
async function ensureTracks(): Promise<TrackRef[]> {
|
||||||
if (cachedTracks) return cachedTracks;
|
if (cachedTracks) return cachedTracks;
|
||||||
|
if (tracks) {
|
||||||
|
cachedTracks = tracks;
|
||||||
|
return tracks;
|
||||||
|
}
|
||||||
const fetched = await listAlbumTracks(albumId);
|
const fetched = await listAlbumTracks(albumId);
|
||||||
cachedTracks = fetched;
|
cachedTracks = fetched;
|
||||||
return fetched;
|
return fetched;
|
||||||
@@ -105,6 +114,11 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
{#if menuOpen}
|
{#if menuOpen}
|
||||||
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||||
|
<!-- onclick is a click-outside guard (stops bubble to the
|
||||||
|
<svelte:window onclick> menu-closer above); not user-facing
|
||||||
|
interaction, so a paired keyboard handler is intentionally
|
||||||
|
absent. Escape-to-close lives on svelte:window onkeydown. -->
|
||||||
<div
|
<div
|
||||||
role="menu"
|
role="menu"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
|
|||||||
@@ -25,7 +25,10 @@
|
|||||||
|
|
||||||
let menuOpen = $state(false);
|
let menuOpen = $state(false);
|
||||||
let addOpen = $state(false);
|
let addOpen = $state(false);
|
||||||
let cachedTracks: TrackRef[] | null = $state(tracks ?? null);
|
// Lazy track cache — see AlbumMenu.svelte for the same reasoning.
|
||||||
|
// Starts null so the `tracks` prop is read inside ensureTracks() each
|
||||||
|
// call rather than captured at component-creation time.
|
||||||
|
let cachedTracks: TrackRef[] | null = $state(null);
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const likedQ = $derived(createLikedIdsQuery());
|
const likedQ = $derived(createLikedIdsQuery());
|
||||||
@@ -47,6 +50,10 @@
|
|||||||
|
|
||||||
async function ensureTracks(): Promise<TrackRef[]> {
|
async function ensureTracks(): Promise<TrackRef[]> {
|
||||||
if (cachedTracks) return cachedTracks;
|
if (cachedTracks) return cachedTracks;
|
||||||
|
if (tracks) {
|
||||||
|
cachedTracks = tracks;
|
||||||
|
return tracks;
|
||||||
|
}
|
||||||
const fetched = await listArtistTracks(artistId);
|
const fetched = await listArtistTracks(artistId);
|
||||||
cachedTracks = fetched;
|
cachedTracks = fetched;
|
||||||
return fetched;
|
return fetched;
|
||||||
@@ -97,6 +104,10 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
{#if menuOpen}
|
{#if menuOpen}
|
||||||
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||||
|
<!-- See AlbumMenu.svelte for the same intentional pattern: the
|
||||||
|
onclick is a stopPropagation guard against the window-level
|
||||||
|
menu-closer; Escape-to-close is on svelte:window onkeydown. -->
|
||||||
<div
|
<div
|
||||||
role="menu"
|
role="menu"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
|
|||||||
@@ -52,6 +52,12 @@
|
|||||||
<div class="mt-2 truncate text-xs font-medium text-text-primary">{track.title}</div>
|
<div class="mt-2 truncate text-xs font-medium text-text-primary">{track.title}</div>
|
||||||
<div class="truncate text-xs text-text-secondary">{track.artist_name}</div>
|
<div class="truncate text-xs text-text-secondary">{track.artist_name}</div>
|
||||||
</button>
|
</button>
|
||||||
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||||
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
|
<!-- Overlay div catches clicks on its inner buttons so they don't
|
||||||
|
bubble to the wrapping play-card button. Not interactive on
|
||||||
|
its own; the inner LikeButton + queue button carry their own
|
||||||
|
keyboard handling. -->
|
||||||
<div class="absolute right-2 top-2 z-10 flex gap-1" onclick={(e) => e.stopPropagation()}>
|
<div class="absolute right-2 top-2 z-10 flex gap-1" onclick={(e) => e.stopPropagation()}>
|
||||||
<LikeButton entityType="track" entityId={track.id} />
|
<LikeButton entityType="track" entityId={track.id} />
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -126,6 +126,9 @@
|
|||||||
<MoreVertical size={16} strokeWidth={1} />
|
<MoreVertical size={16} strokeWidth={1} />
|
||||||
</button>
|
</button>
|
||||||
{#if menuOpen}
|
{#if menuOpen}
|
||||||
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||||
|
<!-- onclick is a stopPropagation guard against the window-level
|
||||||
|
click-closes-menu pattern; not user-facing interaction. -->
|
||||||
<div
|
<div
|
||||||
role="menu"
|
role="menu"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
|
|||||||
@@ -28,6 +28,12 @@ function applyToDOM(resolved: ResolvedTheme): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let _theme = $state<ThemePreference>(readPref());
|
let _theme = $state<ThemePreference>(readPref());
|
||||||
|
// svelte-ignore state_referenced_locally
|
||||||
|
// Module-scope state with manual sync — _theme is read once at module
|
||||||
|
// init to seed _resolved; subsequent updates flow through setTheme()
|
||||||
|
// (which writes both) and the matchMedia listener (which writes
|
||||||
|
// _resolved when prefers-color-scheme flips). Not a missed-reactivity
|
||||||
|
// case; $derived doesn't apply at module scope here.
|
||||||
let _resolved = $state<ResolvedTheme>(resolve(_theme));
|
let _resolved = $state<ResolvedTheme>(resolve(_theme));
|
||||||
|
|
||||||
export const theme = {
|
export const theme = {
|
||||||
|
|||||||
Reference in New Issue
Block a user