Skip to content

Commit 98a07a8

Browse files
fix(web): prevent dark theme navigation flash (#219)
1 parent 35f76fa commit 98a07a8

5 files changed

Lines changed: 57 additions & 19 deletions

File tree

assets/js/app.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ const AutoDismissFlash = {
4646

4747
// Theme switching logic
4848
function setTheme(theme) {
49+
if (!['system', 'light', 'dark'].includes(theme)) {
50+
theme = 'system'
51+
}
52+
4953
localStorage.setItem('theme', theme)
5054

5155
let appliedTheme = theme
@@ -79,6 +83,13 @@ window.addEventListener('phx:set-theme', (e) => {
7983
}
8084
})
8185

86+
// Keep the theme in sync when another browser window changes it.
87+
window.addEventListener('storage', (e) => {
88+
if (e.key === 'theme') {
89+
setTheme(e.newValue || 'system')
90+
}
91+
})
92+
8293
// Watch for system theme changes
8394
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
8495
const currentTheme = localStorage.getItem('theme')

lib/aludel/web/components/layouts.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -201,9 +201,9 @@ defmodule Aludel.Web.Layouts do
201201
assigns = assign(assigns, :active, active)
202202

203203
~H"""
204-
<a href={@href} class={"aludel-nav-link #{@active}"}>
204+
<.link navigate={@href} class={"aludel-nav-link #{@active}"}>
205205
{render_slot(@inner_block)}
206-
</a>
206+
</.link>
207207
"""
208208
end
209209
end

lib/aludel/web/components/layouts/root.html.heex

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,28 +10,27 @@
1010
{assigns[:page_title] || "Dashboard"}
1111
</.live_title>
1212
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
13-
<link rel="stylesheet" href={asset_path(@conn, :css)} />
14-
<script defer src={asset_path(@conn, :js)}>
15-
</script>
1613
<script>
1714
(() => {
18-
const setTheme = (theme) => {
19-
if (theme === "system") {
20-
localStorage.removeItem("phx:theme");
21-
document.documentElement.removeAttribute("data-theme");
22-
} else {
23-
localStorage.setItem("phx:theme", theme);
24-
document.documentElement.setAttribute("data-theme", theme);
25-
}
26-
};
27-
if (!document.documentElement.hasAttribute("data-theme")) {
28-
setTheme(localStorage.getItem("phx:theme") || "system");
15+
let savedTheme = "system";
16+
17+
try {
18+
savedTheme = localStorage.getItem("theme") || "system";
19+
} catch (_error) {
20+
// Storage can be unavailable in privacy-restricted browsing contexts.
2921
}
30-
window.addEventListener("storage", (e) => e.key === "phx:theme" && setTheme(e.newValue || "system"));
31-
32-
window.addEventListener("phx:set-theme", (e) => setTheme(e.target.dataset.phxTheme));
22+
23+
const theme = ["system", "light", "dark"].includes(savedTheme) ? savedTheme : "system";
24+
const appliedTheme = theme === "system"
25+
? (window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light")
26+
: theme;
27+
28+
document.documentElement.setAttribute("data-theme", appliedTheme);
3329
})();
3430
</script>
31+
<link rel="stylesheet" href={asset_path(@conn, :css)} />
32+
<script defer src={asset_path(@conn, :js)}>
33+
</script>
3534
</head>
3635
<body>
3736
{@inner_content}

priv/static/app.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22227,6 +22227,9 @@ removing illegal node: "${("outerHTML" in childNode && childNode.outerHTML || ch
2222722227
}
2222822228
};
2222922229
function setTheme(theme) {
22230+
if (!["system", "light", "dark"].includes(theme)) {
22231+
theme = "system";
22232+
}
2223022233
localStorage.setItem("theme", theme);
2223122234
let appliedTheme = theme;
2223222235
if (theme === "system") {
@@ -22251,6 +22254,11 @@ removing illegal node: "${("outerHTML" in childNode && childNode.outerHTML || ch
2225122254
setTheme(theme);
2225222255
}
2225322256
});
22257+
window.addEventListener("storage", (e) => {
22258+
if (e.key === "theme") {
22259+
setTheme(e.newValue || "system");
22260+
}
22261+
});
2225422262
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", (e) => {
2225522263
const currentTheme = localStorage.getItem("theme");
2225622264
if (currentTheme === "system" || !currentTheme) {

test/aludel_web/live/dashboard_live_test.exs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,26 @@ defmodule Aludel.Web.DashboardLiveTest do
1313
assert render(view) =~ "Dashboard"
1414
end
1515

16+
test "applies the saved theme before loading styles", %{conn: conn} do
17+
html = conn |> get("/") |> html_response(200)
18+
19+
theme_script_position =
20+
html
21+
|> :binary.match(~s|localStorage.getItem("theme")|)
22+
|> elem(0)
23+
24+
stylesheet_position =
25+
html
26+
|> :binary.match(~s(rel="stylesheet"))
27+
|> elem(0)
28+
29+
assert theme_script_position < stylesheet_position
30+
assert html =~ ~s|document.documentElement.setAttribute("data-theme", appliedTheme)|
31+
refute html =~ "phx:theme"
32+
assert html =~ ~s|href="/prompts"|
33+
assert html =~ ~s|data-phx-link="redirect"|
34+
end
35+
1636
test "shows recent runs", %{conn: conn} do
1737
_run = run_fixture(%{name: "Recent Test Run"})
1838

0 commit comments

Comments
 (0)