Skip to content

Commit

Permalink
feat: added theme setup and persistence
Browse files Browse the repository at this point in the history
  • Loading branch information
mwargan committed Apr 16, 2024
1 parent aa25f17 commit 8ac17b8
Show file tree
Hide file tree
Showing 6 changed files with 158 additions and 14 deletions.
18 changes: 4 additions & 14 deletions src/components/PageFooter.vue
Original file line number Diff line number Diff line change
@@ -1,23 +1,12 @@
<script setup lang="ts">
import i18n, { SUPPORT_LOCALES, setI18nLanguage } from "@/locales/i18n";
import { eventTypes, useEventsBus } from "@/eventBus/events";
import theme, { setTheme } from "@/themes/useTheme";
const appName = import.meta.env.VITE_APP_NAME;
const $bus = useEventsBus();
const handleLocaleChange = (locale: string) => {
setI18nLanguage(i18n, locale);
};
const setDarkMode = (value: string) => {
if (value === "dark" || value === "light") {
document.documentElement.setAttribute("data-theme", value);
} else {
document.documentElement.removeAttribute("data-theme");
}
$bus.$emit(eventTypes.changed_theme, value);
};
</script>
<template>
<footer>
Expand All @@ -26,10 +15,11 @@ const setDarkMode = (value: string) => {
<li>
<select
name="dark-mode"
@change="setDarkMode(($event.target as HTMLSelectElement).value)"
@change="setTheme(($event.target as HTMLSelectElement).value)"
aria-label="Dark Mode toggle"
:value="theme"
>
<option value="auto">{{ $t("Auto") }}</option>
<option value="system">{{ $t("Auto") }}</option>
<option value="light">{{ $t("Light") }}</option>
<option value="dark">{{ $t("Dark") }}</option>
</select>
Expand Down
11 changes: 11 additions & 0 deletions src/eventBus/listeners/broadcastChannel.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import i18n, { setI18nLanguage } from "@/locales/i18n";
import router from "@/router";
import { eventTypes } from "../events";
import { setTheme } from "@/themes/useTheme";

/**
* Be careful here - its quite easy to accidentally set up an infinite loop
Expand All @@ -24,6 +25,10 @@ bc.onmessage = (event) => {
if (event.data.type === eventTypes.changed_locale) {
setI18nLanguage(i18n, event.data.data, false);
}

if (event.data.type === eventTypes.changed_theme) {
setTheme(event.data.data, false);
}
};

export default {
Expand All @@ -45,4 +50,10 @@ export default {
data: e,
});
},
changed_theme: (e: string) => {
bc.postMessage({
type: eventTypes.changed_theme,
data: e,
});
},
} as Record<eventTypes, any>;
3 changes: 3 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import "./eventBus/listeners/index";
import VueGtagPlugin from "vue-gtag";
import "./assets/main.css";
import i18n, { SUPPORT_LOCALES } from "./locales/i18n";
import { ThemePlugin } from "./themes/useTheme";
import { gatePlugin } from "@m-media/vue3-gate-keeper";

import gates from "./router/gates";
Expand Down Expand Up @@ -71,4 +72,6 @@ app.use(

app.use(EventsPlugin);

app.use(ThemePlugin);

app.mount("#app");
5 changes: 5 additions & 0 deletions src/router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ const router = createRouter({
name: "Examples",
component: () => import("../views/Examples/DealsView.vue"),
},
{
path: "/examples/single-product",
name: "Examples",
component: () => import("../views/Examples/SingleProductView.vue"),
},
// Add a catch-all 404 page
{
path: "/:pathMatch(.*)*",
Expand Down
59 changes: 59 additions & 0 deletions src/themes/__tests__/useTheme.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { beforeEach, describe, expect, it } from "vitest";

import { getCurrentTheme, setBestGuessTheme, setTheme } from "../useTheme"; // Assuming this is the correct path to your module

// We need to get jest to mock the window.matchMedia function

describe("Theme functionality", () => {
beforeEach(() => {
// Reset localStorage and document theme attribute before each test
localStorage.clear();
document.documentElement.removeAttribute("data-theme");
});

it("should set and get dark mode correctly", () => {
setTheme("dark");
expect(document.documentElement.getAttribute("data-theme")).toEqual("dark");
expect(localStorage.getItem("theme")).toEqual("dark");
expect(getCurrentTheme()).toEqual("dark");

setTheme("light");
expect(document.documentElement.getAttribute("data-theme")).toEqual(
"light"
);
expect(localStorage.getItem("theme")).toEqual("light");
expect(getCurrentTheme()).toEqual("light");

setTheme("system");
// data-theme should be removed when set to system
expect(document.documentElement.getAttribute("data-theme")).toBeNull();
expect(localStorage.getItem("theme")).toEqual("system");
expect(getCurrentTheme()).toEqual("system");
});

it("should set best guess theme correctly", () => {
// Ensure localStorage is empty
expect(localStorage.getItem("theme")).toBeNull();

// Simulate no localStorage or document theme attribute set
setBestGuessTheme();
expect(document.documentElement.getAttribute("data-theme")).toBeNull();
expect(localStorage.getItem("theme")).toEqual("system");

// Simulate localStorage set
localStorage.setItem("theme", "dark");
setBestGuessTheme();
expect(document.documentElement.getAttribute("data-theme")).toEqual("dark");
expect(localStorage.getItem("theme")).toEqual("dark");

// Simulate document theme attribute set
// Remove localStorage
localStorage.removeItem("theme");
document.documentElement.setAttribute("data-theme", "light");
setBestGuessTheme();
expect(document.documentElement.getAttribute("data-theme")).toEqual(
"light"
);
expect(localStorage.getItem("theme")).toEqual("light");
});
});
76 changes: 76 additions & 0 deletions src/themes/useTheme.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import $bus, { eventTypes } from "@/eventBus/events";
import { ref } from "vue";

/**
* The supported themes. Dark, light, and system.
*
*/
export const SUPPORT_THEMES = ["system", "dark", "light"];

export const getBestGuessTheme = () => {
return (
localStorage.getItem("theme") ??
// Read the data-theme attribute from the html element
document.documentElement.getAttribute("data-theme") ??
// Use the first supported theme
SUPPORT_THEMES[0]
);
};

const currentTheme = ref(getBestGuessTheme());

export const setTheme = (value: string, emit = true) => {
if (value === "system") {
document.documentElement.removeAttribute("data-theme");
} else {
document.documentElement.setAttribute("data-theme", value);
}
localStorage.setItem("theme", value);
currentTheme.value = value;
if (emit) {
$bus.$emit(eventTypes.changed_theme, value);
}
};

export const getCurrentTheme = () => {
return document.documentElement.getAttribute("data-theme") ?? "system";
};

/**
* Set the current app theme to the best-guessed theme
*/
export function setBestGuessTheme() {
let theme = getBestGuessTheme();
// If the theme is not supported, fallback to system
if (!SUPPORT_THEMES.includes(theme)) {
theme = SUPPORT_THEMES[0];
}

setTheme(theme);
}

/**
* Setup theme
*/
export function setupTheme() {
setBestGuessTheme();

window
.matchMedia("(prefers-color-scheme: dark)")
.addEventListener("change", (e: MediaQueryListEvent) => {
setTheme(e.matches ? "dark" : "light", false);
});
}

/**
* Export as Vue3 plugin
*
*/
export const ThemePlugin = {
install: () => {
// Install by running the setup function
setupTheme();
},
};

export default currentTheme;

0 comments on commit 8ac17b8

Please sign in to comment.