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 conflictspull/2090/head^2
parent
78a9faacfe
commit
150116a172
|
@ -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).
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
##
|
##
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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()}>
|
||||||
|
|
Loading…
Reference in New Issue