yay accessibility!
aria tags + better touchscreen support
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -38,7 +38,6 @@
|
|||||||
|
|
||||||
a,
|
a,
|
||||||
a:visited {
|
a:visited {
|
||||||
outline: none;
|
|
||||||
color: black;
|
color: black;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user