From 74815a176534ffe749da653d22100d39a6f5bab2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matja=C5=BE=20Horvat?= Date: Thu, 26 Oct 2023 21:13:50 +0200 Subject: [PATCH] Prevent FOUT when using System Theme (#3004) 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. --- pontoon/base/static/js/theme-switcher.js | 21 ++++++++++++++++----- pontoon/base/templates/base.html | 4 ++-- pontoon/base/templatetags/helpers.py | 17 ++++++++++++++++- translate/public/translate.html | 2 +- translate/src/context/Theme.tsx | 10 ++++++---- translate/src/hooks/useTheme.ts | 14 +++++++++++++- 6 files changed, 54 insertions(+), 14 deletions(-) diff --git a/pontoon/base/static/js/theme-switcher.js b/pontoon/base/static/js/theme-switcher.js index 5cd793e6c4..69c5556cad 100644 --- a/pontoon/base/static/js/theme-switcher.js +++ b/pontoon/base/static/js/theme-switcher.js @@ -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) { diff --git a/pontoon/base/templates/base.html b/pontoon/base/templates/base.html index 117fa90058..e89144c445 100644 --- a/pontoon/base/templates/base.html +++ b/pontoon/base/templates/base.html @@ -33,9 +33,9 @@ {% block content %} diff --git a/pontoon/base/templatetags/helpers.py b/pontoon/base/templatetags/helpers.py index 4507d91e5c..a1e1cca062 100644 --- a/pontoon/base/templatetags/helpers.py +++ b/pontoon/base/templatetags/helpers.py @@ -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) diff --git a/translate/public/translate.html b/translate/public/translate.html index 689962dba9..c4891e6a4b 100644 --- a/translate/public/translate.html +++ b/translate/public/translate.html @@ -24,7 +24,7 @@ {% include "tracker.html" %} - + diff --git a/translate/src/context/Theme.tsx b/translate/src/context/Theme.tsx index 6166cd3a60..a61d4169df 100644 --- a/translate/src/context/Theme.tsx +++ b/translate/src/context/Theme.tsx @@ -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); diff --git a/translate/src/hooks/useTheme.ts b/translate/src/hooks/useTheme.ts index bb9d252684..a4464c7934 100644 --- a/translate/src/hooks/useTheme.ts +++ b/translate/src/hooks/useTheme.ts @@ -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 && @@ -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`);