-
-
-
-
-
- Experimental
-
-
-
+
+
);
}
diff --git a/ui/src/Components/MainModal/Configuration/ThemeConfiguration.test.js b/ui/src/Components/MainModal/Configuration/ThemeConfiguration.test.js
index 6e73f4117..ec74f918e 100644
--- a/ui/src/Components/MainModal/Configuration/ThemeConfiguration.test.js
+++ b/ui/src/Components/MainModal/Configuration/ThemeConfiguration.test.js
@@ -5,6 +5,11 @@ import { mount } from "enzyme";
import toDiffableHtml from "diffable-html";
import { Settings } from "Stores/Settings";
+import { ThemeContext } from "Components/Theme";
+import {
+ ReactSelectColors,
+ ReactSelectStyles
+} from "Components/Theme/ReactSelect";
import { ThemeConfiguration } from "./ThemeConfiguration";
let settingsStore;
@@ -13,7 +18,15 @@ beforeEach(() => {
});
const FakeConfiguration = () => {
- return mount(
);
+ return mount(
+
+
+
+ );
};
describe("
", () => {
@@ -22,32 +35,41 @@ describe("
", () => {
expect(toDiffableHtml(tree.html())).toMatchSnapshot();
});
- it("darkTheme is 'false' by default", () => {
- expect(settingsStore.themeConfig.config.darkTheme).toBe(false);
- });
-
- it("unchecking the checkbox sets stored darkTheme value to 'false'", done => {
+ it("resets stored config to defaults if it is invalid", done => {
+ settingsStore.themeConfig.config.theme = "foo";
const tree = FakeConfiguration();
- const checkbox = tree.find("#configuration-theme");
-
- settingsStore.themeConfig.config.darkTheme = true;
- expect(settingsStore.themeConfig.config.darkTheme).toBe(true);
- checkbox.simulate("change", { target: { checked: false } });
+ const select = tree.find("div.react-select__value-container");
+ expect(select.text()).toBe(settingsStore.themeConfig.options.auto.label);
setTimeout(() => {
- expect(settingsStore.themeConfig.config.darkTheme).toBe(false);
+ expect(settingsStore.themeConfig.config.theme).toBe(
+ settingsStore.themeConfig.options.auto.value
+ );
done();
}, 200);
});
- it("checking the checkbox sets stored darkTheme value to 'true'", done => {
+ it("rendered correct default value", done => {
+ settingsStore.themeConfig.config.theme =
+ settingsStore.themeConfig.options.auto.value;
const tree = FakeConfiguration();
- const checkbox = tree.find("#configuration-theme");
-
- settingsStore.themeConfig.config.darkTheme = false;
- expect(settingsStore.themeConfig.config.darkTheme).toBe(false);
- checkbox.simulate("change", { target: { checked: true } });
+ const select = tree.find("div.react-select__value-container");
setTimeout(() => {
- expect(settingsStore.themeConfig.config.darkTheme).toBe(true);
+ expect(select.text()).toBe(settingsStore.themeConfig.options.auto.label);
+ done();
+ }, 200);
+ });
+
+ it("clicking on a label option updates settingsStore", done => {
+ const tree = FakeConfiguration();
+ tree
+ .find("input#react-select-configuration-theme-input")
+ .simulate("change", { target: { value: " " } });
+ const options = tree.find("div.react-select__option");
+ options.at(1).simulate("click");
+ setTimeout(() => {
+ expect(settingsStore.themeConfig.config.theme).toBe(
+ settingsStore.themeConfig.options.dark.value
+ );
done();
}, 200);
});
diff --git a/ui/src/Components/MainModal/Configuration/__snapshots__/AlertGroupTitleBarColor.test.js.snap b/ui/src/Components/MainModal/Configuration/__snapshots__/AlertGroupTitleBarColor.test.js.snap
index db92ecc3f..1dc05774d 100644
--- a/ui/src/Components/MainModal/Configuration/__snapshots__/AlertGroupTitleBarColor.test.js.snap
+++ b/ui/src/Components/MainModal/Configuration/__snapshots__/AlertGroupTitleBarColor.test.js.snap
@@ -2,7 +2,7 @@
exports[`
matches snapshot with default values 1`] = `
"
-
+
diff --git a/ui/src/Components/Theme/index.js b/ui/src/Components/Theme/index.js
index 769f543e1..2bde87d9d 100644
--- a/ui/src/Components/Theme/index.js
+++ b/ui/src/Components/Theme/index.js
@@ -1,8 +1,6 @@
-import React from "react";
+import React, { Component } from "react";
import ReactDOM from "react-dom";
-import { observer } from "mobx-react";
-
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faSun } from "@fortawesome/free-solid-svg-icons/faSun";
@@ -41,16 +39,30 @@ const Placeholder = () => {
);
};
-const Theme = observer(({ settingsStore }) => (
-
}>
- {settingsStore.themeConfig.config.darkTheme ? (
-
- ) : (
-
- )}
-
-));
-
const ThemeContext = React.createContext();
-export { Theme, ThemeContext };
+class BodyTheme extends Component {
+ onToggleBodyClass = isDark => {
+ document.body.classList.toggle("theme-dark", isDark);
+ document.body.classList.toggle("theme-light", !isDark);
+ };
+
+ componentDidMount() {
+ this.onToggleBodyClass(this.context.isDark);
+ }
+
+ componentDidUpdate() {
+ this.onToggleBodyClass(this.context.isDark);
+ }
+
+ render() {
+ return (
+
}>
+ {this.context.isDark ?
:
}
+
+ );
+ }
+}
+BodyTheme.contextType = ThemeContext;
+
+export { BodyTheme, ThemeContext };
diff --git a/ui/src/Components/Theme/index.test.js b/ui/src/Components/Theme/index.test.js
index 00157e7bf..7a83ed401 100644
--- a/ui/src/Components/Theme/index.test.js
+++ b/ui/src/Components/Theme/index.test.js
@@ -1,26 +1,44 @@
-import React from "react";
+import * as React from "react";
import { mount } from "enzyme";
-import { Settings } from "Stores/Settings";
-import { Theme } from ".";
-
-let settingsStore;
+import { BodyTheme, ThemeContext } from ".";
beforeEach(() => {
- settingsStore = new Settings();
+ document.body.classList.remove("theme-light");
+ document.body.classList.remove("theme-dark");
});
-describe("
", () => {
- it("renders DarkTheme when settingsStore.themeConfig.config.darkTheme is true", () => {
- settingsStore.themeConfig.config.darkTheme = true;
- const tree = mount(
);
- expect(tree.text()).toBe("");
+describe("
", () => {
+ it("uses light theme when ThemeContext->isDark is false", () => {
+ mount(
, {
+ wrappingComponent: ThemeContext.Provider,
+ wrappingComponentProps: { value: { isDark: false } }
+ });
+ expect(document.body.classList.contains("theme-light")).toEqual(true);
});
- it("renders LightTheme when settingsStore.themeConfig.config.darkTheme is false", () => {
- settingsStore.themeConfig.config.darkTheme = false;
- const tree = mount(
);
- expect(tree.text()).toBe("");
+ it("uses dark theme when ThemeContext->isDark is true", () => {
+ mount(
, {
+ wrappingComponent: ThemeContext.Provider,
+ wrappingComponentProps: { value: { isDark: true } }
+ });
+ expect(document.body.classList.contains("theme-dark")).toEqual(true);
+ });
+
+ it("updates theme when ThemeContext->isDark is updated", () => {
+ const tree = mount(
, {
+ wrappingComponent: ThemeContext.Provider,
+ wrappingComponentProps: { value: { isDark: true } }
+ });
+ expect(document.body.classList.contains("theme-dark")).toEqual(true);
+
+ document.body.classList.remove("theme-light");
+ document.body.classList.remove("theme-dark");
+
+ const provider = tree.getWrappingComponent();
+ provider.setProps({ value: { isDark: false } });
+
+ expect(document.body.classList.contains("theme-light")).toEqual(true);
});
});
diff --git a/ui/src/Stores/Settings.js b/ui/src/Stores/Settings.js
index 4a714617c..520bd841e 100644
--- a/ui/src/Stores/Settings.js
+++ b/ui/src/Stores/Settings.js
@@ -111,11 +111,19 @@ class FilterBarConfig {
}
class ThemeConfig {
- constructor(darkTheme) {
+ options = Object.freeze({
+ auto: {
+ label: "Automatic theme, follow browser preferences",
+ value: "auto"
+ },
+ light: { label: "Light theme", value: "light" },
+ dark: { label: "Dark theme", value: "dark" }
+ });
+ constructor(defaultTheme) {
this.config = localStored(
"themeConfig",
{
- darkTheme: darkTheme
+ theme: defaultTheme
},
{
delay: 100
@@ -132,7 +140,7 @@ class Settings {
Refresh: 30 * 1000 * 1000 * 1000,
HideFiltersWhenIdle: true,
ColorTitlebar: false,
- DarkTheme: false,
+ Theme: "auto",
MinimalGroupWidth: 420,
AlertsPerGroup: 5,
CollapseGroups: "collapsedOnMobile"
@@ -155,7 +163,7 @@ class Settings {
this.filterBarConfig = new FilterBarConfig(
defaultSettings.HideFiltersWhenIdle
);
- this.themeConfig = new ThemeConfig(defaultSettings.DarkTheme);
+ this.themeConfig = new ThemeConfig(defaultSettings.Theme);
}
}
diff --git a/ui/src/__mocks__/Defaults.js b/ui/src/__mocks__/Defaults.js
index 381458357..333074b16 100644
--- a/ui/src/__mocks__/Defaults.js
+++ b/ui/src/__mocks__/Defaults.js
@@ -1,10 +1,10 @@
const DefaultsBase64 =
- "eyJSZWZyZXNoIjo0NTAwMDAwMDAwMCwiSGlkZUZpbHRlcnNXaGVuSWRsZSI6ZmFsc2UsIkNvbG9yVGl0bGViYXIiOmZhbHNlLCJEYXJrTW9kZSI6ZmFsc2UsIk1pbmltYWxHcm91cFdpZHRoIjo1NTUsIkFsZXJ0c1Blckdyb3VwIjoxNSwiQ29sbGFwc2VHcm91cHMiOiJleHBhbmRlZCJ9Cg==";
+ "eyJSZWZyZXNoIjo0NTAwMDAwMDAwMCwiSGlkZUZpbHRlcnNXaGVuSWRsZSI6ZmFsc2UsIkNvbG9yVGl0bGViYXIiOmZhbHNlLCJUaGVtZSI6ImxpZ2h0IiwiTWluaW1hbEdyb3VwV2lkdGgiOjU1NSwiQWxlcnRzUGVyR3JvdXAiOjE1LCJDb2xsYXBzZUdyb3VwcyI6ImV4cGFuZGVkIn0K==";
const DefaultsObject = {
Refresh: 45000000000,
HideFiltersWhenIdle: false,
ColorTitlebar: false,
- DarkMode: false,
+ Theme: "light",
MinimalGroupWidth: 555,
AlertsPerGroup: 15,
CollapseGroups: "expanded"
diff --git a/ui/src/__mocks__/matchMedia.js b/ui/src/__mocks__/matchMedia.js
new file mode 100644
index 000000000..f2a0ddab4
--- /dev/null
+++ b/ui/src/__mocks__/matchMedia.js
@@ -0,0 +1,16 @@
+const mockMatchMedia = mapOfMedia => {
+ return jest.fn().mockImplementation(query => {
+ return {
+ matches: mapOfMedia[query] ? mapOfMedia[query].matches : false,
+ media: mapOfMedia[query] ? mapOfMedia[query].media : "not all",
+ onchange: null,
+ addListener: jest.fn(),
+ removeListener: jest.fn(),
+ addEventListener: jest.fn(),
+ removeEventListener: jest.fn(),
+ dispatchEvent: jest.fn()
+ };
+ });
+};
+
+export { mockMatchMedia };
diff --git a/ui/src/index.test.js b/ui/src/index.test.js
index a836de339..fd56ccb24 100644
--- a/ui/src/index.test.js
+++ b/ui/src/index.test.js
@@ -1,5 +1,6 @@
import { EmptyAPIResponse } from "__mocks__/Fetch";
import { DefaultsBase64 } from "__mocks__/Defaults";
+import { mockMatchMedia } from "__mocks__/matchMedia";
const settingsElement = {
dataset: {
@@ -9,6 +10,14 @@ const settingsElement = {
}
};
+beforeEach(() => {
+ window.matchMedia = mockMatchMedia({});
+});
+
+afterEach(() => {
+ jest.restoreAllMocks();
+});
+
it("renders without crashing with missing defaults div", () => {
const root = document.createElement("div");
jest.spyOn(global.document, "getElementById").mockImplementation(name => {