yay accessibility!

aria tags + better touchscreen support
This commit is contained in:
2026-09-25 21:50:00 +02:00
parent 83cc73acff
commit 3aae8a0130
8 changed files with 80 additions and 15 deletions
+12 -1
View File
@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { TAG_WEIGHT_NAMES } from '$lib/stores.svelte.ts';
import type { TagWeights } from '$lib/types'; import type { TagWeights } from '$lib/types';
let { tags, highlighted = {} }: { tags: TagWeights; highlighted?: Record<string, number> } = let { tags, highlighted = {} }: { tags: TagWeights; highlighted?: Record<string, number> } =
@@ -9,7 +10,8 @@
{#each sorted as [tag, weight], i}{i ? ', ' : ''}<span {#each sorted as [tag, weight], i}{i ? ', ' : ''}<span
class="weight-{weight}" class="weight-{weight}"
class:highlighted={tag in highlighted}>{tag}</span class:highlighted={tag in highlighted}
>{tag}<span class="screen-reader-only"> ({TAG_WEIGHT_NAMES[weight]})</span></span
>{/each} >{/each}
<style> <style>
@@ -30,4 +32,13 @@
.highlighted { .highlighted {
background: rgba(255, 220, 0, 0.18); background: rgba(255, 220, 0, 0.18);
} }
.screen-reader-only {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
</style> </style>
+1
View File
@@ -9,6 +9,7 @@ $effect.root(() => {
}); });
export const MAX_TAG_WEIGHT = 3; export const MAX_TAG_WEIGHT = 3;
export const TAG_WEIGHT_NAMES = ['off', 'light', 'medium', 'heavy'];
export const tagList = [ export const tagList = [
'acoustic', 'acoustic',
-1
View File
@@ -38,7 +38,6 @@
a, a,
a:visited { a:visited {
outline: none;
color: black; color: black;
} }
</style> </style>
+31 -3
View File
@@ -102,12 +102,16 @@
/> />
<button <button
onclick={() => (cachedOnly = !cachedOnly)} onclick={() => (cachedOnly = !cachedOnly)}
aria-label="Downloaded only"
aria-pressed={cachedOnly}
class="material-icons filter-download {cachedOnly class="material-icons filter-download {cachedOnly
? 'select-faves-star' ? 'select-faves-star'
: 'unselect-faves-star'}">file_download</button : 'unselect-faves-star'}">file_download</button
> >
<button <button
onclick={() => (favoritesOnly = !favoritesOnly)} onclick={() => (favoritesOnly = !favoritesOnly)}
aria-label="Favorites only"
aria-pressed={favoritesOnly}
class="material-icons {favoritesOnly ? '' : 'un'}select-faves-star">star</button class="material-icons {favoritesOnly ? '' : 'un'}select-faves-star">star</button
> >
</div> </div>
@@ -125,7 +129,11 @@
class="stream-item {current ? 'current-stream' : ''}" class="stream-item {current ? 'current-stream' : ''}"
id="stream-{stream.id}" id="stream-{stream.id}"
> >
<button class="stream-item-button" onclick={() => goto('/streams/' + stream.id)}> <button
class="stream-item-button"
aria-current={current ? 'page' : undefined}
onclick={() => goto('/streams/' + stream.id)}
>
<span class="stream-item-id"> <span class="stream-item-id">
ID: ID:
{shorthandCode(stream.id)}</span {shorthandCode(stream.id)}</span
@@ -135,12 +143,12 @@
<span class="stream-item-length">{formatSecondsToHms(stream.length_seconds)}</span> <span class="stream-item-length">{formatSecondsToHms(stream.length_seconds)}</span>
<p class="stream-item-tags" hidden={!Object.keys(stream.tags).length}> <span class="stream-item-tags" hidden={!Object.keys(stream.tags).length}>
<span class="stream-item-tags-label">Tags:</span> <span class="stream-item-tags-label">Tags:</span>
<span class="stream-item-tags-list" <span class="stream-item-tags-list"
><WeightedTags tags={stream.tags} highlighted={filteredTags} /></span ><WeightedTags tags={stream.tags} highlighted={filteredTags} /></span
> >
</p> </span>
</button> </button>
<button <button
@@ -148,6 +156,8 @@
e.stopPropagation(); e.stopPropagation();
updateFavorites(stream); updateFavorites(stream);
}} }}
aria-label="Favorite"
aria-pressed={favorited}
class="material-icons stream-item-star {favorited ? 'stream-item-star-faved' : ''}" class="material-icons stream-item-star {favorited ? 'stream-item-star-faved' : ''}"
>{favorited ? 'star' : 'star_border'}</button >{favorited ? 'star' : 'star_border'}</button
> >
@@ -159,6 +169,7 @@
e.stopPropagation(); e.stopPropagation();
streamCache.remove(stream.id); streamCache.remove(stream.id);
}} }}
aria-label="Delete download"
class="material-icons stream-item-cached stream-item-cached-btn">delete</button class="material-icons stream-item-cached stream-item-cached-btn">delete</button
> >
{:else} {:else}
@@ -168,6 +179,7 @@
e.stopPropagation(); e.stopPropagation();
streamCache.download(stream); streamCache.download(stream);
}} }}
aria-label={partial ? 'Resume download' : 'Download'}
title={partial title={partial
? `Partially downloaded (${Math.round((partial.received / partial.total) * 100)}%). Click to resume.` ? `Partially downloaded (${Math.round((partial.received / partial.total) * 100)}%). Click to resume.`
: undefined} : undefined}
@@ -350,6 +362,22 @@
border: 0; border: 0;
} }
.stream-item .stream-item-star:focus-visible,
.stream-item .stream-item-cached-btn:focus-visible {
opacity: 1;
}
/* touch screens can't hover, so always display */
@media (hover: none) {
.stream-item-star {
opacity: 0.5;
}
.stream-item-cached {
opacity: 0.4;
}
}
@media (max-width: 575px) { @media (max-width: 575px) {
.stream-item-star { .stream-item-star {
opacity: 0.5; opacity: 0.5;
+2 -1
View File
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import Select from 'svelte-select'; import Select from 'svelte-select';
import { tagList, MAX_TAG_WEIGHT } from '$lib/stores.svelte.ts'; import { tagList, MAX_TAG_WEIGHT, TAG_WEIGHT_NAMES } from '$lib/stores.svelte.ts';
// type not exported in the lib itself // type not exported in the lib itself
type SelectItem = Record<string, unknown>; type SelectItem = Record<string, unknown>;
@@ -115,6 +115,7 @@
<button <button
type="button" type="button"
class="weight" class="weight"
aria-label="{selection.label}, at least {TAG_WEIGHT_NAMES[min]}, press to change"
onpointerup={(e) => e.stopPropagation()} onpointerup={(e) => e.stopPropagation()}
onclick={(e) => cycle(tag, e.shiftKey)} onclick={(e) => cycle(tag, e.shiftKey)}
> >
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { page } from '$app/state'; import { page } from '$app/state';
import { tagList, MAX_TAG_WEIGHT } from '$lib/stores.svelte.ts'; import { tagList, MAX_TAG_WEIGHT, TAG_WEIGHT_NAMES } from '$lib/stores.svelte.ts';
import type { TagWeights } from '$lib/types'; import type { TagWeights } from '$lib/types';
let { original = $bindable() } = $props(); let { original = $bindable() } = $props();
@@ -42,6 +42,7 @@
type="button" type="button"
class="tag-pill" class="tag-pill"
class:off={!weight} class:off={!weight}
aria-label="{tag}, {TAG_WEIGHT_NAMES[weight]}"
onclick={(e) => cycle(tag, e.shiftKey)} onclick={(e) => cycle(tag, e.shiftKey)}
> >
{tag}<span class="dots">{'·'.repeat(weight)}</span> {tag}<span class="dots">{'·'.repeat(weight)}</span>
+16 -7
View File
@@ -246,7 +246,7 @@
seekText = formatSeconds(seekValue); seekText = formatSeconds(seekValue);
} }
function trackMouse(event: MouseEvent) { function trackPointer(event: MouseEvent) {
if (seeking) updateSeekVisual(event); if (seeking) updateSeekVisual(event);
if (showTooltip && !disableTooltip) seekTooltip(event); if (showTooltip && !disableTooltip) seekTooltip(event);
if (volumeSeeking) seekVolume(event); if (volumeSeeking) seekVolume(event);
@@ -265,7 +265,7 @@
<svelte:window <svelte:window
bind:innerWidth bind:innerWidth
bind:innerHeight bind:innerHeight
onmouseup={() => { onpointerup={() => {
if (seeking && pendingSeekTime != null) { if (seeking && pendingSeekTime != null) {
audio.currentTime = pendingSeekTime; audio.currentTime = pendingSeekTime;
currentTime = pendingSeekTime; currentTime = pendingSeekTime;
@@ -274,7 +274,11 @@
} }
seeking = volumeSeeking = false; seeking = volumeSeeking = false;
}} }}
onmousemove={trackMouse} onpointercancel={() => {
pendingSeekTime = null;
seeking = volumeSeeking = false;
}}
onpointermove={trackPointer}
/> />
{#if display} {#if display}
@@ -282,6 +286,7 @@
<button <button
class="material-icons" class="material-icons"
style="--icon-color:{iconColor}" style="--icon-color:{iconColor}"
aria-label={paused ? 'Play' : 'Pause'}
onclick={() => (audio.paused ? audio.play() : audio.pause())} onclick={() => (audio.paused ? audio.play() : audio.pause())}
> >
{#if paused} {#if paused}
@@ -294,12 +299,12 @@
bind:this={songBar} bind:this={songBar}
value={seeking && pendingSeekTime != null ? pendingSeekTime : currentTime || 0} value={seeking && pendingSeekTime != null ? pendingSeekTime : currentTime || 0}
max={duration} max={duration}
onmousedown={(e) => { onpointerdown={(e) => {
seeking = true; seeking = true;
updateSeekVisual(e); updateSeekVisual(e);
}} }}
onmouseenter={() => (showTooltip = true)} onpointerenter={() => (showTooltip = true)}
onmouseleave={() => (showTooltip = false)} onpointerleave={() => (showTooltip = false)}
style="--primary-color:{barPrimaryColor}; --secondary-color:{barSecondaryColor}" style="--primary-color:{barPrimaryColor}; --secondary-color:{barSecondaryColor}"
class="song-progress" class="song-progress"
></progress> ></progress>
@@ -309,6 +314,8 @@
<button <button
style="--icon-color:{iconColor}" style="--icon-color:{iconColor}"
class="material-icons" class="material-icons"
aria-label="Mute"
aria-pressed={muted}
onclick={() => (muted = !muted)} onclick={() => (muted = !muted)}
> >
{#if muted} {#if muted}
@@ -324,7 +331,7 @@
<progress <progress
bind:this={volumeBar} bind:this={volumeBar}
value={volume} value={volume}
onmousedown={() => (volumeSeeking = true)} onpointerdown={() => (volumeSeeking = true)}
onclick={seekVolume} onclick={seekVolume}
style="--primary-color:{barPrimaryColor}; --secondary-color:{barSecondaryColor}" style="--primary-color:{barPrimaryColor}; --secondary-color:{barSecondaryColor}"
class="volume-progress" class="volume-progress"
@@ -448,6 +455,8 @@
margin: auto; margin: auto;
margin-left: 5px; margin-left: 5px;
margin-right: 5px; margin-right: 5px;
/* make finger drags seek */
touch-action: none;
} }
progress::-webkit-progress-bar { progress::-webkit-progress-bar {
@@ -31,12 +31,16 @@
<tbody> <tbody>
<tr><th>Timestamp</th><th>Artist</th><th>Title</th></tr> <tr><th>Timestamp</th><th>Artist</th><th>Title</th></tr>
{#each ctx.current.tracks as track, i} {#each ctx.current.tracks as track, i}
<tr class:current={i == ctx.songIndex}> <tr
class:current={i == ctx.songIndex}
aria-current={i == ctx.songIndex ? 'true' : undefined}
>
<td class="timestamp-field" <td class="timestamp-field"
><div class="timestamp-field-flex"> ><div class="timestamp-field-flex">
{formatTrackTime(track[0])} {formatTrackTime(track[0])}
<button <button
onclick={() => jumpToTrack(track[0])} onclick={() => jumpToTrack(track[0])}
aria-label="Play from {formatTrackTime(track[0])}"
class="material-icons" class="material-icons"
style="padding: 4px 6px; margin-top: -4px; margin-bottom: -4px; margin-right: -6px" style="padding: 4px 6px; margin-top: -4px; margin-bottom: -4px; margin-right: -6px"
>fast_forward</button >fast_forward</button
@@ -180,6 +184,17 @@
border: 0; border: 0;
} }
tr .material-icons:focus-visible {
opacity: 1;
}
/* touch screens can't hover, so always display */
@media (hover: none) {
.material-icons {
opacity: 0.5;
}
}
@media (max-width: 575px) { @media (max-width: 575px) {
.material-icons { .material-icons {
opacity: 0.5; opacity: 0.5;