From 73dc80bdc9c758fa6be3ccbca497898d151b28c8 Mon Sep 17 00:00:00 2001 From: Logan Date: Tue, 18 Aug 2026 11:31:49 -0400 Subject: [PATCH] fix(explore): stop hiding the request badge until the row is hovered The badge on a row you do not own was transparent until the row was hovered or focused. That rule was inherited from the green ticks it replaced, and it does not survive the reason those went: a tick marked the *common* case, while this marks the rows that are not here. A mark on the exception is the information on this page, and one that appears only under the pointer cannot be seen, counted, or reached by anyone driving the app with a finger. The repaint half of #33 is fixed in #82; this is only the visibility, rebased to leave that alone. Refs #33 --- .../explore-album-details.ts | 37 +++---- .../album-request-badge-visibility.test.ts | 97 +++++++++++++++++++ 2 files changed, 109 insertions(+), 25 deletions(-) create mode 100644 frontend/test/components/album-request-badge-visibility.test.ts diff --git a/frontend/src/components/explore-album-details/explore-album-details.ts b/frontend/src/components/explore-album-details/explore-album-details.ts index 45a02d5..924f80a 100644 --- a/frontend/src/components/explore-album-details/explore-album-details.ts +++ b/frontend/src/components/explore-album-details/explore-album-details.ts @@ -662,34 +662,21 @@ export class ExploreAlbumDetails extends LitElement implements ContextMenuHost { font-weight: 400; } - /* The request control is only offered where there is - * something to request, and only when the row is being - * attended to — a column of plus signs down a mostly-owned - * album is the clutter the green ticks were. + /* The request control is offered on every row that has + * something to request, and is not revealed on hover. * - * Hidden with opacity, never display:none or visibility, - * so it keeps its place in the layout (rows do not reflow - * as the pointer moves) and stays in the tab order and the - * accessibility tree. focus-within is what makes it - * reachable without a mouse: tabbing to the button reveals - * it, and the row's own focus reveals it before you get - * there. */ + * It used to be transparent until the row was hovered or + * focused, on the reasoning that a column of plus signs + * down a mostly-owned album is clutter. That reasoning was + * inherited from the green ticks it replaced and does not + * survive the rule those were removed for: a tick marked + * the *common* case, while this marks the rows that are + * **not** here. A mark on the exception is the information + * on this page — and one that appears only under the + * pointer cannot be seen, counted, or reached by anyone + * driving this with a finger. */ .track-row .track-request { flex-shrink: 0; - opacity: 0; - transition: opacity 0.12s ease; - } - - .track-row:hover .track-request, - .track-row:focus-within .track-request, - .track-row .track-request:focus-visible { - opacity: 1; - } - - @media (prefers-reduced-motion: reduce) { - .track-row .track-request { - transition: none; - } } `, ]; diff --git a/frontend/test/components/album-request-badge-visibility.test.ts b/frontend/test/components/album-request-badge-visibility.test.ts new file mode 100644 index 0000000..71b032e --- /dev/null +++ b/frontend/test/components/album-request-badge-visibility.test.ts @@ -0,0 +1,97 @@ +/** + * The request badge on an unowned row is there without being hovered. + * + * It used to be transparent until the row was hovered or focused, on + * the reasoning that a column of plus signs down a mostly-owned album + * is clutter. That reasoning came from the green ticks it replaced and + * does not survive the rule those were removed for: a tick marked the + * **common** case, while this marks the rows that are *not* here. A + * mark on the exception is the information on this page, and one that + * exists only under the pointer cannot be seen, counted, or reached by + * anyone driving the app with a finger. + * + * That the badge *repaints* when clicked is the other half of #33 and + * is covered by `album-track-request.test.ts`. + */ +import { describe, expect, it, beforeEach } from 'vitest'; +import type { LitElement } from 'lit'; + +import '@components/explore-album-details/explore-album-details'; +import { stub, flush, resetHarness } from '@test/support/harness'; +import { fixture, shadowAll } from '@test/support/render'; + +function track(n: number, owned: boolean) { + return { + position: n, + discNumber: 1, + title: `Track ${n}`, + length: 200000, + mbid: `mbid-${n}`, + inLibrary: owned, + }; +} + +/** An album with one owned track and one that is not here. */ +async function albumWithAnUnownedTrack(): Promise { + const el = await fixture('explore-album-details', { + albumName: 'Glass Harbour', + releaseGroupMBID: 'rg-1', + }); + + stub('library.Library.GetFilePathsByRecordingMBIDs', { + 'mbid-1': ['/music/mbid-1.mp3'], + }); + + Object.assign(el, { + versionEntries: [ + { + key: 'v1', + label: '2019', + sublabel: '2 tracks', + tracks: [track(1, true), track(2, false)], + }, + ], + selectedVersionKey: 'v1', + loadingReleases: false, + loadingInfo: false, + }); + el.requestUpdate(); + await flush(); + await el.updateComplete; + + return el; +} + +const badges = (el: LitElement) => + shadowAll(el, 'library-status-indicator.track-request'); + +describe('the tracklist’s request badge', () => { + beforeEach(() => { + resetHarness(); + stub('library.Library.GetFilePathsByRecordingMBIDs', {}); + stub('library.Library.GetFilePathsByAlbums', {}); + stub('library.Library.GetAlbumTracks', []); + stub('library.Library.GetAllLibrariesWithTrackCounts', []); + stub('download.Service.ProviderKinds', []); + stub('download.Service.ListProviders', []); + stub('download.Service.ListDownloads', []); + stub('download.Service.ListRequests', []); + }); + + it('is visible without a pointer anywhere near it', async () => { + const el = await albumWithAnUnownedTrack(); + const [badge] = badges(el); + + expect(badge).toBeTruthy(); + // Computed opacity rather than the absence of a rule, because the + // rule could come back under a different selector. + expect(getComputedStyle(badge!).opacity).toBe('1'); + }); + + it('is still only on the rows with something to request', async () => { + // Always-visible is not the same as everywhere: an owned track has + // nothing left to ask for, and a badge on it would be the column of + // green ticks this page deliberately stopped drawing. + expect(badges(await albumWithAnUnownedTrack())).toHaveLength(1); + }); +});