Skip to content

Commit

Permalink
Prevent FOUT when using System Theme (mozilla#3004)
Browse files Browse the repository at this point in the history
When system theme is selected, we apply the right theme in the client, because system settings can only be accessed from the client. That results in FOUT (flash of unstyled text) when navigating
between pages if the system theme is Light, because Pontoon defaults to the dark theme and then immediatelly changes it to light when the JS executed.

This patch prevents that by storing the system theme setting in a cookie. Cookie is read by the server, so we set the theme accordingly already in a template.
  • Loading branch information
mathjazz authored Oct 26, 2023
1 parent a37da45 commit 74815a1
Show file tree
Hide file tree
Showing 6 changed files with 54 additions and 14 deletions.
21 changes: 16 additions & 5 deletions pontoon/base/static/js/theme-switcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,26 +13,37 @@ $(function () {
function applyTheme(newTheme) {
if (newTheme === 'system') {
newTheme = getSystemTheme();
storeSystemTheme(newTheme);
}
$('body')
.removeClass('dark-theme light-theme system-theme')
.addClass(`${newTheme}-theme`);
}

/*
* Storing system theme setting in a cookie makes the setting available to the server.
* That allows us to set the theme class already in the Django template, which (unlike
* setting it on the client) prevents FOUC.
*/
function storeSystemTheme(systemTheme) {
document.cookie = `system_theme=${systemTheme}; path=/; max-age=${
60 * 60 * 24 * 365
}; Secure`;
}

window
.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', function (e) {
.addEventListener('change', function () {
// Check the 'data-theme' attribute on the body element
let userThemeSetting = $('body').data('theme');

if (userThemeSetting === 'system') {
applyTheme(e.matches ? 'dark' : 'light');
applyTheme(userThemeSetting);
}
});

if ($('body').hasClass('system-theme')) {
let systemTheme = getSystemTheme();
$('body').removeClass('system-theme').addClass(`${systemTheme}-theme`);
if ($('body').data('theme') === 'system') {
applyTheme('system');
}

$('.appearance .toggle-button button').click(function (e) {
Expand Down
4 changes: 2 additions & 2 deletions pontoon/base/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@
</head>

<body
class="{% block class %}{% endblock %} {{ theme(request.user) }}-theme"
class="{% block class %}{% endblock %} {{ theme_class(request) }}"
{% if csrf_token %}data-csrf="{{ csrf_token }}"{% endif %}
data-theme="{{ theme(request.user) }}"
data-theme="{{ user_theme(request.user) }}"
>
{% block content %}

Expand Down
17 changes: 16 additions & 1 deletion pontoon/base/templatetags/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,28 @@ def return_url(request):


@library.global_function
def theme(user):
def user_theme(user):
"""Get user's theme or return 'dark' if user is not authenticated."""
if user.is_authenticated:
return user.profile.theme
return "dark"


@library.global_function
def theme_class(request):
"""Get theme class name based on user preferences and system settings."""
theme = "dark"
user = request.user

if user.is_authenticated:
theme = user.profile.theme

if theme == "system":
theme = request.COOKIES.get("system_theme", "system")

return f"{theme}-theme"


@library.global_function
def static(path):
return staticfiles_storage.url(path)
Expand Down
2 changes: 1 addition & 1 deletion translate/public/translate.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

{% include "tracker.html" %}
</head>
<body class="{% block class %}{% endblock %} {{ theme(request.user) }}-theme" data-theme="{{ theme(request.user) }}">
<body class="{% block class %}{% endblock %} {{ theme_class(request) }}" data-theme="{{ user_theme(request.user) }}">
<noscript>
You need to enable JavaScript to run this app.
</noscript>
Expand Down
10 changes: 6 additions & 4 deletions translate/src/context/Theme.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,19 @@ export function ThemeProvider({ children }: { children: React.ReactElement }) {
useEffect(() => {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');

function handleThemeChange(e: MediaQueryListEvent) {
let userThemeSetting = document.body.getAttribute('data-theme') || 'dark';
function handleThemeChange() {
let userThemeSetting = document.body.getAttribute('data-theme');

if (userThemeSetting === 'system') {
applyTheme(e.matches ? 'dark' : 'light');
applyTheme(userThemeSetting);
}
}

mediaQuery.addEventListener('change', handleThemeChange);

applyTheme(theme);
if (theme === 'system') {
applyTheme(theme);
}

return () => {
mediaQuery.removeEventListener('change', handleThemeChange);
Expand Down
14 changes: 13 additions & 1 deletion translate/src/hooks/useTheme.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
export function useTheme() {
/*
* Storing system theme setting in a cookie makes the setting available to the server.
* That allows us to set the theme class already in the Django template, which (unlike
* setting it on the client) prevents FOUC.
*/
function storeSystemTheme(systemTheme: string) {
document.cookie = `system_theme=${systemTheme}; path=/; max-age=${
60 * 60 * 24 * 365
}; Secure`;
}

function getSystemTheme(): string {
if (
window.matchMedia &&
Expand All @@ -10,9 +21,10 @@ export function useTheme() {
}
}

return function (newTheme: string) {
return function useTheme(newTheme: string) {
if (newTheme === 'system') {
newTheme = getSystemTheme();
storeSystemTheme(newTheme);
}
document.body.classList.remove('dark-theme', 'light-theme', 'system-theme');
document.body.classList.add(`${newTheme}-theme`);
Expand Down

0 comments on commit 74815a1

Please sign in to comment.