({
state.isMulti
? {
...base,
- borderRadius: 0,
+ borderTopLeftRadius: "0.25rem",
+ borderBottomLeftRadius: "0.25rem",
backgroundColor: state.isDisabled
? theme.disabledValueContainerBackground
: theme.valueContainerBackground,
@@ -65,7 +66,8 @@ const ReactSelectStyles = theme => ({
}
: {
...base,
- borderRadius: 0,
+ borderTopLeftRadius: "0.25rem",
+ borderBottomLeftRadius: "0.25rem",
backgroundColor: state.isDisabled
? theme.disabledValueContainerBackground
: theme.valueContainerBackground
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 => {