Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
266e7032dd |
@@ -1480,16 +1480,12 @@ export class CoverGrid
|
||||
source: 'cover-grid',
|
||||
});
|
||||
|
||||
// Single album: show its cover, badged with how many tracks are
|
||||
// on the way -- an album is 1 track or 30 and the thumbnail is
|
||||
// the same picture either way, so the number the drop is about
|
||||
// was the one thing this drag did not say.
|
||||
// Multiple albums: show the track-count badge alone.
|
||||
// Single album: show cover art thumbnail.
|
||||
// Multiple albums: show track-count badge.
|
||||
if (isSingleAlbum && hit.album.CoverArtPath) {
|
||||
this.dragImageEl =
|
||||
createAlbumArtDragImage(
|
||||
this.getCoverUrl(hit.album),
|
||||
filePaths.length,
|
||||
);
|
||||
} else {
|
||||
this.dragImageEl = createDragImage(
|
||||
|
||||
@@ -2,6 +2,7 @@ import { LitElement, html, css, nothing } from 'lit';
|
||||
import { customElement, property, state, query } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { designTokens } from '../../styles/tokens.css';
|
||||
import { srOnly } from '../../styles/sr-only.css';
|
||||
import {
|
||||
LookupReleaseGroup,
|
||||
BrowseReleases,
|
||||
@@ -289,6 +290,7 @@ export class ExploreAlbumDetails extends LitElement implements ContextMenuHost {
|
||||
designTokens,
|
||||
exploreLinkStyles,
|
||||
contextMenuStyles,
|
||||
srOnly,
|
||||
css`
|
||||
:host {
|
||||
display: flex;
|
||||
@@ -3033,11 +3035,21 @@ export class ExploreAlbumDetails extends LitElement implements ContextMenuHost {
|
||||
|
||||
/* ── Tracklist ── */
|
||||
|
||||
/**
|
||||
* The heading is there and is not drawn.
|
||||
*
|
||||
* A list of numbered titles with durations under an album's cover
|
||||
* does not need a word above it saying what it is — it was the
|
||||
* only thing on this page labelling something already obvious. But
|
||||
* the section is a landmark and the page's heading structure runs
|
||||
* through it, so what goes is the *ink*, not the element: a reader
|
||||
* jumping by heading still finds the tracklist.
|
||||
*/
|
||||
private renderTracklist() {
|
||||
if (this.loadingReleases) {
|
||||
return html`
|
||||
<section>
|
||||
<h3 class="section-header">Tracklist</h3>
|
||||
<h3 class="sr-only">Tracklist</h3>
|
||||
<div class="section-loading">Loading tracks\u2026</div>
|
||||
</section>
|
||||
`;
|
||||
@@ -3050,7 +3062,7 @@ export class ExploreAlbumDetails extends LitElement implements ContextMenuHost {
|
||||
if (!current) {
|
||||
return html`
|
||||
<section>
|
||||
<h3 class="section-header">Tracklist</h3>
|
||||
<h3 class="sr-only">Tracklist</h3>
|
||||
<div class="section-error">
|
||||
<wa-icon name="triangle-exclamation"></wa-icon>
|
||||
No release data available.
|
||||
@@ -3063,7 +3075,7 @@ export class ExploreAlbumDetails extends LitElement implements ContextMenuHost {
|
||||
if (tracks.length === 0) {
|
||||
return html`
|
||||
<section>
|
||||
<h3 class="section-header">Tracklist</h3>
|
||||
<h3 class="sr-only">Tracklist</h3>
|
||||
<div
|
||||
style="color: var(--yj-text-tertiary, #888); font-size: var(--yj-text-md)"
|
||||
>
|
||||
@@ -3079,7 +3091,7 @@ export class ExploreAlbumDetails extends LitElement implements ContextMenuHost {
|
||||
|
||||
return html`
|
||||
<section>
|
||||
<h3 class="section-header">Tracklist</h3>
|
||||
<h3 class="sr-only">Tracklist</h3>
|
||||
<div class="tracklist">
|
||||
${discNumbers.map((discNum) => {
|
||||
const discTracks = discMap.get(discNum) ?? [];
|
||||
|
||||
@@ -29,23 +29,11 @@ export function createDragImage(count: number): HTMLElement {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a drag image showing an album cover art thumbnail, with a
|
||||
* corner badge saying how many tracks are on the way.
|
||||
*
|
||||
* The count is not decoration. The cover says *what* is being dragged
|
||||
* and nothing said *how much* — an album is 1 track or 30 and the
|
||||
* thumbnail is identical either way, so the one number the drop is
|
||||
* about was the one thing the drag did not show. Every other drag in
|
||||
* the app says it (`createDragImage` is a count and nothing else);
|
||||
* this one was the exception because it had a picture to show instead.
|
||||
*
|
||||
* A count of 1 draws no badge: "1" over a single album cover is noise,
|
||||
* and the absence is unambiguous next to a badge that only ever
|
||||
* appears when there is more than one.
|
||||
* Creates a drag image showing an album cover art thumbnail.
|
||||
* Falls back to the track-count badge if the image fails to load.
|
||||
*/
|
||||
export function createAlbumArtDragImage(
|
||||
coverUrl: string,
|
||||
count = 1,
|
||||
): HTMLElement {
|
||||
const size = 64;
|
||||
const wrapper = document.createElement('div');
|
||||
@@ -56,12 +44,6 @@ export function createAlbumArtDragImage(
|
||||
'left: -1000px',
|
||||
'pointer-events: none',
|
||||
'z-index: 9999',
|
||||
// The badge is positioned against this box, and the box stays
|
||||
// exactly the cover's size: anything outside it risks being
|
||||
// clipped out of the snapshot the browser takes, and padding
|
||||
// it instead would move the cover away from the cursor.
|
||||
`width: ${size}px`,
|
||||
`height: ${size}px`,
|
||||
].join(';');
|
||||
|
||||
const img = document.createElement('img');
|
||||
@@ -79,44 +61,11 @@ export function createAlbumArtDragImage(
|
||||
].join(';');
|
||||
|
||||
wrapper.appendChild(img);
|
||||
|
||||
if (count > 1) {
|
||||
wrapper.appendChild(countBadge(count));
|
||||
}
|
||||
|
||||
document.body.appendChild(wrapper);
|
||||
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
/** The corner badge on a multi-track drag image. */
|
||||
function countBadge(count: number): HTMLElement {
|
||||
const badge = document.createElement('span');
|
||||
|
||||
badge.className = 'drag-count-badge';
|
||||
badge.textContent = String(count);
|
||||
badge.style.cssText = [
|
||||
'position: absolute',
|
||||
'top: 3px',
|
||||
'right: 3px',
|
||||
'min-width: 20px',
|
||||
'height: 20px',
|
||||
'padding: 0 5px',
|
||||
'box-sizing: border-box',
|
||||
'border-radius: 10px',
|
||||
'background: #ffd43b',
|
||||
'color: #000',
|
||||
'font-size: 12px',
|
||||
'font-weight: 600',
|
||||
'font-family: inherit',
|
||||
'line-height: 20px',
|
||||
'text-align: center',
|
||||
'box-shadow: 0 1px 4px rgba(0,0,0,0.5)',
|
||||
].join(';');
|
||||
|
||||
return badge;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a drag image styled like a queue track card showing the
|
||||
* track title and artist. Used when dragging a single track.
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* The tracklist's own heading.
|
||||
*
|
||||
* A list of numbered titles with durations, under the album's cover, is
|
||||
* the one thing on this page that did not need a word above it saying
|
||||
* what it was — "TRACKLIST" labelled the only thing already obvious.
|
||||
*
|
||||
* What goes is the *ink*, not the element. The section is a landmark
|
||||
* and the page's heading structure runs through it, so a reader moving
|
||||
* by heading still has to be able to find it, and it is hidden the way
|
||||
* `sr-only` hides things: `clip-path`, never `display: none`, which
|
||||
* would take it out of the accessibility tree along with the layout.
|
||||
*/
|
||||
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) {
|
||||
return {
|
||||
position: n,
|
||||
discNumber: 1,
|
||||
title: `Track ${n}`,
|
||||
length: 200000,
|
||||
mbid: `mbid-${n}`,
|
||||
inLibrary: true,
|
||||
};
|
||||
}
|
||||
|
||||
async function albumPage(): Promise<LitElement> {
|
||||
const el = await fixture<LitElement>('explore-album-details', {
|
||||
albumName: 'Glass Harbour',
|
||||
releaseGroupMBID: 'rg-1',
|
||||
});
|
||||
|
||||
Object.assign(el, {
|
||||
versionEntries: [
|
||||
{
|
||||
key: 'v1',
|
||||
label: '2019',
|
||||
sublabel: '2 tracks',
|
||||
tracks: [track(1), track(2)],
|
||||
},
|
||||
],
|
||||
selectedVersionKey: 'v1',
|
||||
loadingReleases: false,
|
||||
loadingInfo: false,
|
||||
});
|
||||
el.requestUpdate();
|
||||
await flush();
|
||||
await el.updateComplete;
|
||||
|
||||
return el;
|
||||
}
|
||||
|
||||
const tracklistHeading = (el: LitElement) =>
|
||||
shadowAll(el, 'h3').find((h) => h.textContent?.trim() === 'Tracklist');
|
||||
|
||||
describe('the album tracklist heading', () => {
|
||||
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 still in the tree', async () => {
|
||||
expect(tracklistHeading(await albumPage())).toBeTruthy();
|
||||
});
|
||||
|
||||
it('takes up no room on the page', async () => {
|
||||
const heading = tracklistHeading(await albumPage())!;
|
||||
const box = heading.getBoundingClientRect();
|
||||
|
||||
expect(box.width).toBeLessThanOrEqual(1);
|
||||
expect(box.height).toBeLessThanOrEqual(1);
|
||||
// Hidden by clipping, not by removal: display:none and
|
||||
// visibility:hidden both take it out of the accessibility tree.
|
||||
expect(getComputedStyle(heading).display).not.toBe('none');
|
||||
expect(getComputedStyle(heading).visibility).not.toBe('hidden');
|
||||
});
|
||||
});
|
||||
@@ -1,71 +0,0 @@
|
||||
/**
|
||||
* A drag says how much it is carrying.
|
||||
*
|
||||
* Every drag in the app already did — `createDragImage` is a count and
|
||||
* nothing else — except the one with a picture to show instead. An
|
||||
* album dragged to the queue put its cover under the cursor and said
|
||||
* nothing about how many tracks that was, and an album is 1 track or 30
|
||||
* with the same thumbnail either way. The number is the thing the drop
|
||||
* is about.
|
||||
*
|
||||
* A count of 1 draws no badge: "1" over a single cover is noise, and
|
||||
* the absence reads unambiguously beside a badge that only ever appears
|
||||
* when there is more than one.
|
||||
*/
|
||||
import { describe, expect, it, afterEach } from 'vitest';
|
||||
|
||||
import {
|
||||
createAlbumArtDragImage,
|
||||
removeDragImage,
|
||||
} from '@utils/drag-image';
|
||||
|
||||
const made: HTMLElement[] = [];
|
||||
|
||||
function dragImage(count?: number): HTMLElement {
|
||||
const el =
|
||||
count === undefined
|
||||
? createAlbumArtDragImage('data:image/gif;base64,R0lGODlhAQABAAAAACw=')
|
||||
: createAlbumArtDragImage(
|
||||
'data:image/gif;base64,R0lGODlhAQABAAAAACw=',
|
||||
count,
|
||||
);
|
||||
|
||||
made.push(el);
|
||||
|
||||
return el;
|
||||
}
|
||||
|
||||
const badge = (el: HTMLElement) =>
|
||||
el.querySelector<HTMLElement>('.drag-count-badge');
|
||||
|
||||
describe('the album drag image', () => {
|
||||
afterEach(() => {
|
||||
while (made.length > 0) removeDragImage(made.pop()!);
|
||||
});
|
||||
|
||||
it('says how many tracks are being dragged', () => {
|
||||
expect(badge(dragImage(12))?.textContent).toBe('12');
|
||||
});
|
||||
|
||||
it('says nothing when there is only one track', () => {
|
||||
expect(badge(dragImage(1))).toBeNull();
|
||||
});
|
||||
|
||||
it('still draws a bare cover for a caller that gives no count', () => {
|
||||
// The count is optional so the helper stays usable from a call site
|
||||
// that has a cover and no list; it must not badge such a drag "1".
|
||||
expect(badge(dragImage())).toBeNull();
|
||||
});
|
||||
|
||||
it('keeps the badge inside the cover', () => {
|
||||
// setDragImage snapshots the element, and anything outside its box
|
||||
// risks being clipped out of that snapshot — while padding the box
|
||||
// instead would move the cover away from the cursor.
|
||||
const el = dragImage(30);
|
||||
const outer = el.getBoundingClientRect();
|
||||
const mark = badge(el)!.getBoundingClientRect();
|
||||
|
||||
expect(mark.right).toBeLessThanOrEqual(outer.right);
|
||||
expect(mark.top).toBeGreaterThanOrEqual(outer.top);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user