fix(a11y): make the library badge a badge, not an inert button
Build & publish Arch package / arch-package (push) Successful in 2m1s
CI / check (push) Successful in 2m13s
Search index maintenance / maintain-index (push) Successful in 6s
CI / e2e (push) Successful in 5m12s

`library-status-indicator` was a <button> whose click handler was a
stopPropagation() and a comment saying to wire up the download client
later. On an Explore results page that is 20 of 66 tab stops (measured
in the running app, before and after: 66/20 → 46/0) that announce
themselves as buttons and do nothing.

It is role="img" with its existing label now, and the label for an
unowned entity says "… is not in your library" rather than "Add … to
library" — the old copy was the button's promise written out. The day
there is a download client to call, the right change is a <button>
*with* a handler, not a handler bolted onto something already shaped
like one.

box-sizing: border-box is explicit because a <button> gets it from the
UA stylesheet and a <span> does not, so the badge grew 36px → 38px.
Caught by the stored screenshot.
This commit is contained in:
2026-08-12 17:30:33 -04:00
parent f5621bf7c5
commit dddf54ba0c
3 changed files with 88 additions and 86 deletions
+29
View File
@@ -64,6 +64,35 @@ test.describe('playing an album from its page', () => {
app.locator('explore-album-details').locator('.tracklist-legend'),
).toContainText('in your library');
});
test('the ticks are badges, not keyboard stops', async ({ app }) => {
// Every one of them was a <button> whose click handler was a
// stopPropagation() and a comment saying to wire up the download
// client later: on an Explore results page, 20 of 66 tab stops
// promised an action and performed none. Counted here rather than
// reasoned about, because the count is the finding.
const stops = await app.evaluate(() => {
const badges = [
...(document
.querySelector('explore-album-details')
?.shadowRoot?.querySelectorAll('library-status-indicator') ?? []),
];
return {
badges: badges.length,
focusable: badges.filter((b) =>
b.shadowRoot?.querySelector('button, [tabindex]:not([tabindex="-1"])'),
).length,
labelled: badges.filter((b) =>
b.shadowRoot?.querySelector('[role="img"][aria-label]'),
).length,
};
});
expect(stops.badges).toBeGreaterThan(0);
expect(stops.focusable).toBe(0);
expect(stops.labelled).toBe(stops.badges);
});
});
/** Albums → click the second card, which navigates to the album page. */