Skip to content

Commit 8af60f9

Browse files
committed
Improve nav cues and native lazy images
1 parent f2d6e80 commit 8af60f9

File tree

6 files changed

+112
-97
lines changed

6 files changed

+112
-97
lines changed

src/ui/PodcastView/EpisodeListItem.svelte

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,16 +93,21 @@
9393
}
9494
9595
.podcast-episode-thumbnail-container {
96-
flex-basis: 20%;
96+
flex: 0 0 5rem;
97+
width: 5rem;
98+
height: 5rem;
99+
max-width: 5rem;
100+
max-height: 5rem;
97101
display: flex;
98102
align-items: center;
99103
justify-content: center;
100104
}
101105
102106
:global(.podcast-episode-thumbnail) {
107+
width: 100%;
108+
height: 100%;
109+
object-fit: cover;
103110
border-radius: 15%;
104-
max-width: 5rem;
105-
max-height: 5rem;
106111
cursor: pointer !important;
107112
}
108113
</style>

src/ui/PodcastView/TopBar.svelte

Lines changed: 71 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,19 @@
66
export let canShowEpisodeList: boolean = false;
77
export let canShowPlayer: boolean = false;
88
9+
const gridTooltip = "Browse podcast grid";
10+
const disabledEpisodeTooltip =
11+
"Select a podcast or playlist to view its episodes.";
12+
const disabledPlayerTooltip =
13+
"Start playing an episode to open the player.";
14+
15+
$: episodeTooltip = canShowEpisodeList
16+
? "View episode list"
17+
: disabledEpisodeTooltip;
18+
$: playerTooltip = canShowPlayer
19+
? "Open player"
20+
: disabledPlayerTooltip;
21+
922
function handleClickMenuItem(newState: ViewState) {
1023
if (viewState === newState) return;
1124
@@ -29,6 +42,7 @@
2942
`}
3043
aria-label="Podcast grid"
3144
aria-pressed={viewState === ViewState.PodcastGrid}
45+
title={gridTooltip}
3246
>
3347
<Icon icon="grid" size={20} clickable={false} />
3448
</button>
@@ -38,11 +52,16 @@
3852
class={`
3953
topbar-menu-button
4054
${viewState === ViewState.EpisodeList ? "topbar-selected" : ""}
41-
${canShowEpisodeList ? "topbar-selectable" : ""}
55+
${canShowEpisodeList ? "topbar-selectable" : "topbar-disabled"}
4256
`}
43-
aria-label="Episode list"
57+
aria-label={
58+
canShowEpisodeList
59+
? "Episode list"
60+
: "Episode list (select a podcast or playlist first)"
61+
}
4462
aria-pressed={viewState === ViewState.EpisodeList}
4563
disabled={!canShowEpisodeList}
64+
title={episodeTooltip}
4665
>
4766
<Icon icon="list-minus" size={20} clickable={false} />
4867
</button>
@@ -52,11 +71,16 @@
5271
class={`
5372
topbar-menu-button
5473
${viewState === ViewState.Player ? "topbar-selected" : ""}
55-
${canShowPlayer ? "topbar-selectable" : ""}
74+
${canShowPlayer ? "topbar-selectable" : "topbar-disabled"}
5675
`}
57-
aria-label="Player"
76+
aria-label={
77+
canShowPlayer
78+
? "Player"
79+
: "Player (start playing an episode to open the player)"
80+
}
5881
aria-pressed={viewState === ViewState.Player}
5982
disabled={!canShowPlayer}
83+
title={playerTooltip}
6084
>
6185
<Icon icon="play" size={20} clickable={false} />
6286
</button>
@@ -68,29 +92,64 @@
6892
flex-direction: row;
6993
align-items: center;
7094
justify-content: space-between;
95+
gap: 0.5rem;
96+
padding: 0.25rem 0.5rem;
7197
height: 50px;
7298
min-height: 50px;
7399
border-bottom: 1px solid var(--background-divider);
100+
box-sizing: border-box;
74101
}
75102
76103
.topbar-menu-button {
77104
display: flex;
78105
align-items: center;
79106
justify-content: center;
80107
width: 100%;
81-
height: 100%;
82-
opacity: 0.1;
83-
border: none;
84-
background: none;
85-
padding: 0;
108+
padding: 0.4rem 0.25rem;
109+
flex: 1 1 0;
110+
border: 1px solid var(--background-modifier-border, #3a3a3a);
111+
border-radius: 8px;
112+
background: var(--background-secondary, transparent);
113+
color: var(--text-muted, #8a8a8a);
114+
transition:
115+
background-color 120ms ease,
116+
border-color 120ms ease,
117+
color 120ms ease,
118+
box-shadow 120ms ease,
119+
opacity 120ms ease;
86120
}
87121
88-
.topbar-selected {
89-
opacity: 1 !important;
122+
.topbar-menu-button:focus-visible {
123+
outline: 2px solid var(--interactive-accent, #5c6bf7);
124+
outline-offset: 2px;
90125
}
91126
92127
.topbar-selectable {
93128
cursor: pointer;
94-
opacity: 0.5;
129+
color: var(--text-normal, #e6e6e6);
130+
background: var(--background-secondary-alt, rgba(255, 255, 255, 0.02));
131+
}
132+
133+
.topbar-menu-button:hover.topbar-selectable:not(.topbar-selected) {
134+
background: var(--background-modifier-hover, rgba(255, 255, 255, 0.06));
135+
border-color: var(--interactive-accent, #5c6bf7);
136+
color: var(--text-normal, #e6e6e6);
137+
}
138+
139+
.topbar-selected,
140+
.topbar-selected:hover {
141+
color: var(--text-on-accent, #ffffff);
142+
background: var(--interactive-accent, #5c6bf7);
143+
border-color: var(--interactive-accent, #5c6bf7);
144+
box-shadow: 0 0 0 1px var(--interactive-accent, #5c6bf7);
145+
}
146+
147+
.topbar-disabled,
148+
.topbar-menu-button:disabled {
149+
cursor: not-allowed;
150+
color: var(--text-faint, #a0a0a0);
151+
background: var(--background-modifier-border, #3a3a3a);
152+
border-style: dashed;
153+
opacity: 1;
95154
}
96155
</style>

src/ui/PodcastView/TopBar.test.ts

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,19 @@ describe("TopBar", () => {
1515
});
1616

1717
const grid = getByLabelText("Podcast grid");
18-
const episode = getByLabelText("Episode list");
19-
const player = getByLabelText("Player");
18+
const episode = getByLabelText(/Episode list/);
19+
const player = getByLabelText(/Player/);
2020

2121
expect(grid.className).toContain("topbar-selected");
2222
expect(episode.className).toContain("topbar-selectable");
2323
expect(episode).not.toBeDisabled();
2424
expect(player).toBeDisabled();
2525
expect(player.className).not.toContain("topbar-selectable");
26+
expect(player.className).toContain("topbar-disabled");
27+
expect(player.getAttribute("title")).toBe(
28+
"Start playing an episode to open the player."
29+
);
30+
expect(episode.getAttribute("title")).toBe("View episode list");
2631
});
2732

2833
test("activates episode list when clicked", async () => {
@@ -34,8 +39,8 @@ describe("TopBar", () => {
3439
},
3540
});
3641

37-
const episodeButton = getByLabelText("Episode list");
38-
const playerButton = getByLabelText("Player");
42+
const episodeButton = getByLabelText(/Episode list/);
43+
const playerButton = getByLabelText(/Player/);
3944

4045
await fireEvent.click(episodeButton);
4146

@@ -52,12 +57,20 @@ describe("TopBar", () => {
5257
},
5358
});
5459

55-
const episodeButton = getByLabelText("Episode list");
56-
const playerButton = getByLabelText("Player");
60+
const episodeButton = getByLabelText(/Episode list/);
61+
const playerButton = getByLabelText(/Player/);
5762

5863
expect(episodeButton).toBeDisabled();
5964
expect(playerButton).toBeDisabled();
6065
expect(episodeButton.className).not.toContain("topbar-selectable");
66+
expect(episodeButton.className).toContain("topbar-disabled");
67+
expect(playerButton.className).toContain("topbar-disabled");
68+
expect(episodeButton.getAttribute("title")).toBe(
69+
"Select a podcast or playlist to view its episodes."
70+
);
71+
expect(playerButton.getAttribute("title")).toBe(
72+
"Start playing an episode to open the player."
73+
);
6174
});
6275

6376
test("activates player control when clicked", async () => {
@@ -69,7 +82,7 @@ describe("TopBar", () => {
6982
},
7083
});
7184

72-
const playerButton = getByLabelText("Player");
85+
const playerButton = getByLabelText(/Player/);
7386

7487
await fireEvent.click(playerButton);
7588

src/ui/common/Image.svelte

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
export let fadeIn: boolean = false;
77
export let opacity: number = 0; // Falsey value so condition isn't triggered if not set.
88
export let interactive: boolean = false;
9+
export let loadingMode: "lazy" | "eager" | undefined = "lazy";
910
export {_class as class};
1011
let _class = "";
1112
@@ -31,6 +32,7 @@
3132
draggable="false"
3233
{src}
3334
{alt}
35+
loading={loadingMode}
3436
class={_class}
3537
style:opacity={opacity ? opacity : !fadeIn ? 1 : loaded ? 1 : 0}
3638
style:transition={fadeIn ? "opacity 0.5s ease-out" : ""}
@@ -44,6 +46,7 @@
4446
draggable="false"
4547
{src}
4648
{alt}
49+
loading={loadingMode}
4750
class={_class}
4851
style:opacity={opacity ? opacity : !fadeIn ? 1 : loaded ? 1 : 0}
4952
style:transition={fadeIn ? "opacity 0.5s ease-out" : ""}

src/ui/common/ImageLoader.svelte

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,25 @@
11
<script lang="ts">
2-
// https://css-tricks.com/lazy-loading-images-in-svelte/
32
export let src: string;
43
export let alt: string;
54
export let fadeIn: boolean = false;
65
export let interactive: boolean = false;
6+
export let loadingMode: "lazy" | "eager" | undefined = "lazy";
77
export { _class as class };
88
99
let _class: string = "";
1010
11-
import IntersectionObserver from "./IntersectionObserver.svelte";
1211
import Image from "./Image.svelte";
1312
import { createEventDispatcher } from "svelte";
1413
1514
const dispatcher = createEventDispatcher();
1615
</script>
1716

18-
<IntersectionObserver once={true} let:intersecting>
19-
{#if intersecting}
20-
<Image
21-
{alt}
22-
{src}
23-
{fadeIn}
24-
{interactive}
25-
on:click={event => dispatcher('click', { event })}
26-
class={_class}
27-
/>
28-
{/if}
29-
</IntersectionObserver>
17+
<Image
18+
{alt}
19+
{src}
20+
{fadeIn}
21+
{interactive}
22+
loadingMode={loadingMode}
23+
on:click={event => dispatcher("click", { event })}
24+
class={_class}
25+
/>

src/ui/common/IntersectionObserver.svelte

Lines changed: 0 additions & 61 deletions
This file was deleted.

0 commit comments

Comments
 (0)