From 43f816ad25393ae437bf858a6e69d9731dfd77bd Mon Sep 17 00:00:00 2001 From: Xenuel Date: Mon, 16 Mar 2026 23:46:25 +0100 Subject: [PATCH] refactor(ui): improve accessibility and security in "In Your Library" section Replace h2 toggle with semantic button and aria-expanded attribute, switch from style.display to hidden attribute for visibility control, use DOM methods instead of innerHTML for source labels and modal options to prevent XSS, improve artist matching with ID-based lookup, and clean up event listeners before re-attaching to prevent leaks. --- index.html | 14 ++++++------ js/db.js | 2 ++ js/ui.js | 63 +++++++++++++++++++++++++++++++++++++----------------- styles.css | 8 +++++-- 4 files changed, 59 insertions(+), 28 deletions(-) diff --git a/index.html b/index.html index ce0ac89..633d243 100644 --- a/index.html +++ b/index.html @@ -3297,13 +3297,15 @@

Albums

diff --git a/js/db.js b/js/db.js index 122155b..1924e49 100644 --- a/js/db.js +++ b/js/db.js @@ -585,6 +585,7 @@ export class MusicDatabase { // TRIGGER SYNC this._dispatchPlaylistSync('create', playlist); + window.dispatchEvent(new CustomEvent('playlist-tracks-changed')); return playlist; } @@ -657,6 +658,7 @@ export class MusicDatabase { // TRIGGER SYNC (but for deleting) this._dispatchPlaylistSync('delete', { id: playlistId }); + window.dispatchEvent(new CustomEvent('playlist-tracks-changed')); } async getPlaylist(playlistId) { diff --git a/js/ui.js b/js/ui.js index e363cc8..15a6017 100644 --- a/js/ui.js +++ b/js/ui.js @@ -3953,11 +3953,13 @@ export class UIRenderer { if (inLibrarySection) inLibrarySection.style.display = 'none'; if (inLibraryContainer) { inLibraryContainer.innerHTML = ''; - inLibraryContainer.style.display = 'none'; + inLibraryContainer.hidden = true; } - // Reset chevron state + // Reset chevron and toggle state const chevronEl = document.getElementById('in-library-chevron'); if (chevronEl) chevronEl.style.transform = 'rotate(0deg)'; + const toggleBtn = document.getElementById('in-library-toggle'); + if (toggleBtn) toggleBtn.setAttribute('aria-expanded', 'false'); try { const artist = await this.api.getArtist(artistId, provider); @@ -4199,12 +4201,16 @@ export class UIRenderer { const isTrackByArtist = (track) => { if (track.artists && Array.isArray(track.artists)) { return track.artists.some( - (a) => a && a.name && a.name.toLowerCase() === artistNameLower + (a) => a && ((artist.id && a.id === artist.id) || (a.name && a.name.toLowerCase() === artistNameLower)) ); } if (track.artist) { - const artistStr = typeof track.artist === 'string' ? track.artist : track.artist.name; - if (artistStr && artistStr.toLowerCase() === artistNameLower) return true; + if (typeof track.artist === 'object') { + if (artist.id && track.artist.id === artist.id) return true; + if (track.artist.name && track.artist.name.toLowerCase() === artistNameLower) return true; + } else if (typeof track.artist === 'string') { + if (track.artist.toLowerCase() === artistNameLower) return true; + } } return false; }; @@ -4285,18 +4291,27 @@ export class UIRenderer { const sourceSpan = document.createElement('span'); sourceSpan.className = 'library-source'; + const labelSpan = document.createElement('span'); + labelSpan.className = 'library-source-label'; + labelSpan.textContent = '· Source:\u00a0'; + + const linkSpan = document.createElement('span'); + linkSpan.className = 'library-source-link'; + + sourceSpan.style.cursor = 'pointer'; + sourceSpan.appendChild(labelSpan); + sourceSpan.appendChild(linkSpan); + if (sources.length === 1) { const srcLabel = sources[0].label.length > 15 ? sources[0].label.slice(0, 15) + '…' : sources[0].label; - sourceSpan.innerHTML = `· Source: ${srcLabel}`; - sourceSpan.style.cursor = 'pointer'; + linkSpan.textContent = srcLabel; sourceSpan.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); navigate(sources[0].href); }); } else { - sourceSpan.innerHTML = `· Source: Multiple Playlists`; - sourceSpan.style.cursor = 'pointer'; + linkSpan.textContent = 'Multiple Playlists'; sourceSpan.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); @@ -4306,11 +4321,16 @@ export class UIRenderer { const cancelBtn = document.getElementById('goto-playlist-cancel'); const overlay = modal.querySelector('.modal-overlay'); - list.innerHTML = sources.map((s) => - `` - ).join(''); + list.innerHTML = ''; + sources.forEach((s) => { + const option = document.createElement('div'); + option.className = 'modal-option'; + option.dataset.href = s.href; + const span = document.createElement('span'); + span.textContent = s.label; + option.appendChild(span); + list.appendChild(option); + }); const closeModal = () => { modal.classList.remove('active'); @@ -4357,7 +4377,7 @@ export class UIRenderer { // Initial load refreshInLibrary().then(() => { - inLibraryContainer.style.display = 'none'; + inLibraryContainer.hidden = true; }); // Setup chevron toggle (once) @@ -4365,8 +4385,9 @@ export class UIRenderer { const chevron = document.getElementById('in-library-chevron'); if (toggle) { toggle.onclick = () => { - const isOpen = inLibraryContainer.style.display !== 'none'; - inLibraryContainer.style.display = isOpen ? 'none' : ''; + const isOpen = !inLibraryContainer.hidden; + inLibraryContainer.hidden = isOpen; + toggle.setAttribute('aria-expanded', String(!isOpen)); if (chevron) { chevron.style.transform = isOpen ? 'rotate(0deg)' : 'rotate(90deg)'; } @@ -4379,15 +4400,17 @@ export class UIRenderer { clearTimeout(refreshTimeout); refreshTimeout = setTimeout(() => refreshInLibrary(), 300); }; - window.addEventListener('favorites-changed', debouncedRefresh); - window.addEventListener('playlist-tracks-changed', debouncedRefresh); - // Cleanup listeners when navigating away + // Cleanup previous listeners before attaching new ones const cleanupOnNav = () => { window.removeEventListener('favorites-changed', debouncedRefresh); window.removeEventListener('playlist-tracks-changed', debouncedRefresh); window.removeEventListener('popstate', cleanupOnNav); }; + cleanupOnNav(); + + window.addEventListener('favorites-changed', debouncedRefresh); + window.addEventListener('playlist-tracks-changed', debouncedRefresh); window.addEventListener('popstate', cleanupOnNav, { once: true }); } diff --git a/styles.css b/styles.css index 71a5579..1740d4f 100644 --- a/styles.css +++ b/styles.css @@ -1994,6 +1994,10 @@ input[type='search']::-webkit-search-cancel-button { gap: 2px var(--spacing-xl); } +#artist-detail-in-library[hidden] { + display: none; +} + .library-source { color: var(--muted-foreground); font-size: inherit; @@ -2365,8 +2369,8 @@ input[type='search']::-webkit-search-cancel-button { align-items: center; gap: 1rem; flex-wrap: wrap; - overflow-wrap: break-word; - word-break: break-word; + overflow-wrap: anywhere; + word-break: normal; min-width: 0; }