feat(web): implement automatic theme switch for light/dark (#2046)

* Implement an automatic theme

The "auto" theme will automatically switch between "dark" and "light"
depending on user preference. This allows for automatic dark mode.

* fix(configuration): allow the "auto" theme when validating

The new theme "auto" was not allowed to be used in a configuration file.

* docs: clarify what critera controls the automatic theme

How the "auto" theme functioned was unclear.

* docs: typeset themes as code

* fix(web): apply useEffector to media query watch

* docs: add technical details

* fix(configuration): resolve merge conflicts
pull/2090/head^2
Alex Gustafsson 2021-06-17 08:42:03 +02:00 committed by GitHub
parent 78a9faacfe
commit 150116a172
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 23 additions and 7 deletions

View File

@ -31,3 +31,5 @@ There are currently 3 available themes for Authelia:
* light (default) * light (default)
* dark * dark
* grey * grey
To enable automatic switching between themes, you can set `theme` to `auto`. The theme will be set to either `dark` or `light` depending on the user's system preference which is determined using media queries. To read more technical details about the media queries used, read the [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme).

View File

@ -17,7 +17,7 @@ port: 9091
## They should be in base64 format, and have one of the following extensions: *.cer, *.crt, *.pem. ## They should be in base64 format, and have one of the following extensions: *.cer, *.crt, *.pem.
# certificates_directory: /config/certificates # certificates_directory: /config/certificates
## The theme to display: light, dark, grey. ## The theme to display: light, dark, grey, auto.
theme: light theme: light
## ##

View File

@ -13,8 +13,9 @@ func ValidateTheme(configuration *schema.Configuration, validator *schema.Struct
configuration.Theme = "light" configuration.Theme = "light"
} }
validThemes := regexp.MustCompile("light|dark|grey") validThemes := regexp.MustCompile("light|dark|grey|auto")
if !validThemes.MatchString(configuration.Theme) { if !validThemes.MatchString(configuration.Theme) {
validator.Push(fmt.Errorf("Theme: %s is not valid, valid themes are: \"light\", \"dark\" or \"grey\"", configuration.Theme)) validator.Push(fmt.Errorf("Theme: %s is not valid, valid themes are: \"light\", \"dark\", \"grey\" or \"auto\"", configuration.Theme))
} }
} }

View File

@ -36,7 +36,7 @@ func (suite *Theme) TestShouldRaiseErrorWhenInvalidThemeProvided() {
suite.Assert().False(suite.validator.HasWarnings()) suite.Assert().False(suite.validator.HasWarnings())
suite.Require().Len(suite.validator.Errors(), 1) suite.Require().Len(suite.validator.Errors(), 1)
suite.Assert().EqualError(suite.validator.Errors()[0], "Theme: invalid is not valid, valid themes are: \"light\", \"dark\" or \"grey\"") suite.Assert().EqualError(suite.validator.Errors()[0], "Theme: invalid is not valid, valid themes are: \"light\", \"dark\", \"grey\" or \"auto\"")
} }
func TestThemes(t *testing.T) { func TestThemes(t *testing.T) {

View File

@ -1,4 +1,4 @@
import React, { useState } from "react"; import React, { useState, useEffect } from "react";
import { config as faConfig } from "@fortawesome/fontawesome-svg-core"; import { config as faConfig } from "@fortawesome/fontawesome-svg-core";
import { CssBaseline, ThemeProvider } from "@material-ui/core"; import { CssBaseline, ThemeProvider } from "@material-ui/core";
@ -37,6 +37,8 @@ function Theme() {
return themes.Dark; return themes.Dark;
case "grey": case "grey":
return themes.Grey; return themes.Grey;
case "auto":
return window.matchMedia("(prefers-color-scheme: dark)").matches ? themes.Dark : themes.Light;
default: default:
return themes.Light; return themes.Light;
} }
@ -44,9 +46,20 @@ function Theme() {
const App: React.FC = () => { const App: React.FC = () => {
const [notification, setNotification] = useState(null as Notification | null); const [notification, setNotification] = useState(null as Notification | null);
const [theme, setTheme] = useState(Theme());
useEffect(() => {
if (getTheme() === "auto") {
const query = window.matchMedia("(prefers-color-scheme: dark)");
// MediaQueryLists does not inherit from EventTarget in Internet Explorer
if (query.addEventListener) {
query.addEventListener("change", (e) => {
setTheme(e.matches ? themes.Dark : themes.Light);
});
}
}
}, []);
return ( return (
<ThemeProvider theme={Theme()}> <ThemeProvider theme={theme}>
<CssBaseline /> <CssBaseline />
<NotificationsContext.Provider value={{ notification, setNotification }}> <NotificationsContext.Provider value={{ notification, setNotification }}>
<Router basename={getBasePath()}> <Router basename={getBasePath()}>