= ({ onEditorChanged }) => {
const [docsSplit, setDocsSplit] = useLocalStorage('app-docs-split', 30);
const [terminalSplit, setTerminalSplit] = useLocalStorage('app-terminal-split', 30);
- // darkMode class has to be applied to body element, otherwise it won't
+ // Classes.DARK has to be applied to body element, otherwise it won't
// affect portals
useEffect(() => {
- if (!darkMode) {
+ if (!isDarkMode) {
// no class for light mode, so nothing to do
return;
}
document.body.classList.add(Classes.DARK);
return () => document.body.classList.remove(Classes.DARK);
- }, [darkMode]);
+ }, [isDarkMode]);
return (
diff --git a/src/editor/Editor.tsx b/src/editor/Editor.tsx
index bb277ad5..4cf7c006 100644
--- a/src/editor/Editor.tsx
+++ b/src/editor/Editor.tsx
@@ -9,10 +9,10 @@ import xcodeTheme from 'monaco-themes/themes/Xcode_default.json';
import React, { useState } from 'react';
import MonacoEditor, { monaco } from 'react-monaco-editor';
import { useDispatch } from 'react-redux';
+import { useDarkMode } from 'usehooks-ts';
import { IDisposable } from 'xterm';
import { fileStorageWriteFile } from '../fileStorage/actions';
import { compile } from '../mpy/actions';
-import { useSelector } from '../reducers';
import { toggleBoolean } from '../settings/actions';
import { BooleanSettingId } from '../settings/defaults';
import { isMacOS } from '../utils/os';
@@ -136,13 +136,13 @@ const EditorContextMenu: React.VoidFunctionComponent = (
);
};
-type EditorProps = { onEditorChanged: (editor: EditorType) => void };
+type EditorProps = { onEditorChanged?: (editor: EditorType) => void };
const Editor: React.VoidFunctionComponent = ({ onEditorChanged }) => {
const dispatch = useDispatch();
const [editor, setEditor] = useState(null);
- const darkMode = useSelector((s) => s.settings.darkMode);
+ const { isDarkMode } = useDarkMode();
const [i18n] = useI18n({ id: 'editor', translations: { en }, fallback: en });
@@ -158,7 +158,7 @@ const Editor: React.VoidFunctionComponent = ({ onEditorChanged }) =
>
= ({
onClose,
}) => {
const [isAboutDialogOpen, setIsAboutDialogOpen] = useState(false);
+ const { isDarkMode, toggle: toggleDarkMode } = useDarkMode();
const showDocs = useSelector((s) => s.settings.showDocs);
- const darkMode = useSelector((s) => s.settings.darkMode);
const flashCurrentProgram = useSelector((s) => s.settings.flashCurrentProgram);
const isServiceWorkerRegistered = useSelector(
(s) => s.app.isServiceWorkerRegistered,
@@ -147,15 +148,8 @@ const SettingsDrawer: React.VoidFunctionComponent = ({
label={i18n.translate(
SettingsStringId.AppearanceDarkModeLabel,
)}
- checked={darkMode}
- onChange={(e) =>
- dispatch(
- setBoolean(
- BooleanSettingId.DarkMode,
- (e.target as HTMLInputElement).checked,
- ),
- )
- }
+ checked={isDarkMode}
+ onChange={toggleDarkMode}
/>
diff --git a/src/settings/defaults.ts b/src/settings/defaults.ts
index 5b2a6fb3..a7f7b99a 100644
--- a/src/settings/defaults.ts
+++ b/src/settings/defaults.ts
@@ -1,13 +1,10 @@
// SPDX-License-Identifier: MIT
-// Copyright (c) 2021 The Pybricks Authors
+// Copyright (c) 2021-2022 The Pybricks Authors
// Definitions for user selectable settings.
-import { prefersDarkMode } from '../utils/os';
-
export enum BooleanSettingId {
ShowDocs = 'showDocs',
- DarkMode = 'darkMode',
FlashCurrentProgram = 'flashCurrentProgram',
}
@@ -15,8 +12,6 @@ export function getDefaultBooleanValue(id: BooleanSettingId): boolean {
switch (id) {
case BooleanSettingId.ShowDocs:
return window.innerWidth >= 1024;
- case BooleanSettingId.DarkMode:
- return prefersDarkMode();
case BooleanSettingId.FlashCurrentProgram:
return false;
// istanbul ignore next: it is a programmer error if we hit this
diff --git a/src/settings/reducers.test.ts b/src/settings/reducers.test.ts
index 3b760c71..1c2aaa82 100644
--- a/src/settings/reducers.test.ts
+++ b/src/settings/reducers.test.ts
@@ -1,3 +1,6 @@
+// SPDX-License-Identifier: MIT
+// Copyright (c) 2021 The Pybricks Authors
+
import { AnyAction } from 'redux';
import { didBooleanChange, didStringChange } from './actions';
import { BooleanSettingId, StringSettingId } from './defaults';
@@ -8,7 +11,6 @@ type State = ReturnType;
test('initial state', () => {
expect(reducers(undefined, {} as AnyAction)).toMatchInlineSnapshot(`
Object {
- "darkMode": false,
"flashCurrentProgram": false,
"hubName": "",
"isHubNameValid": true,
@@ -17,17 +19,6 @@ test('initial state', () => {
`);
});
-describe('darkMode', () => {
- test('setting changed', () => {
- expect(
- reducers(
- { darkMode: false } as State,
- didBooleanChange(BooleanSettingId.DarkMode, true),
- ).darkMode,
- ).toBe(true);
- });
-});
-
describe('showDocs', () => {
test('setting changed', () => {
expect(
diff --git a/src/settings/reducers.ts b/src/settings/reducers.ts
index f53f4376..9db878a4 100644
--- a/src/settings/reducers.ts
+++ b/src/settings/reducers.ts
@@ -12,20 +12,6 @@ import {
const encoder = new TextEncoder();
-const darkMode: Reducer = (
- state = getDefaultBooleanValue(BooleanSettingId.DarkMode),
- action,
-) => {
- if (didBooleanChange.matches(action)) {
- if (action.id === BooleanSettingId.DarkMode) {
- return action.newState;
- }
- return state;
- }
-
- return state;
-};
-
const showDocs: Reducer = (
state = getDefaultBooleanValue(BooleanSettingId.ShowDocs),
action,
@@ -87,7 +73,6 @@ const isHubNameValid: Reducer = (state = true, action) => {
};
export default combineReducers({
- darkMode,
showDocs,
flashCurrentProgram,
hubName,
diff --git a/src/settings/sagas.test.ts b/src/settings/sagas.test.ts
index cee297f2..111f7b95 100644
--- a/src/settings/sagas.test.ts
+++ b/src/settings/sagas.test.ts
@@ -151,69 +151,6 @@ describe('startup', () => {
await saga.end();
});
});
-
- describe('darkMode', () => {
- test('with no value set', async () => {
- const saga = new AsyncSaga(settings);
-
- jest.spyOn(
- Object.getPrototypeOf(window.localStorage),
- 'getItem',
- ).mockReturnValue(null);
-
- saga.put(didStart());
-
- // does nothing
-
- await saga.end();
- });
-
- test('with value set to true', async () => {
- const saga = new AsyncSaga(settings);
-
- jest.spyOn(
- Object.getPrototypeOf(window.localStorage),
- 'getItem',
- ).mockImplementation((key) => {
- switch (key) {
- case 'setting.darkMode':
- return 'true';
- default:
- return null;
- }
- });
-
- saga.put(didStart());
-
- // requests to enable dark mode
- const action = await saga.take();
- expect(action).toEqual(didBooleanChange(BooleanSettingId.DarkMode, true));
-
- await saga.end();
- });
-
- test('with value set to false', async () => {
- const saga = new AsyncSaga(settings);
-
- jest.spyOn(
- Object.getPrototypeOf(window.localStorage),
- 'getItem',
- ).mockImplementation((key) => {
- switch (key) {
- case 'setting.darkMode':
- return 'false';
- default:
- return null;
- }
- });
-
- saga.put(didStart());
-
- // does nothing
-
- await saga.end();
- });
- });
});
describe('store settings to local storage', () => {
@@ -276,27 +213,6 @@ describe('store settings to local storage', () => {
await saga.end();
});
- test('darkMode', async () => {
- const saga = new AsyncSaga(settings);
-
- saga.updateState({ settings: { darkMode: true } });
-
- const mockSetItem = jest
- .spyOn(Object.getPrototypeOf(window.localStorage), 'setItem')
- .mockImplementation((key, value) => {
- expect(key).toBe('setting.darkMode');
- expect(value).toBe('false');
- });
-
- saga.put(setBoolean(BooleanSettingId.DarkMode, false));
- expect(mockSetItem).toHaveBeenCalled();
-
- const action = await saga.take();
- expect(action).toEqual(didBooleanChange(BooleanSettingId.DarkMode, false));
-
- await saga.end();
- });
-
test('flashCurrentProgram', async () => {
const saga = new AsyncSaga(settings);
diff --git a/src/terminal/Terminal.tsx b/src/terminal/Terminal.tsx
index e7dfe032..fd7db5b1 100644
--- a/src/terminal/Terminal.tsx
+++ b/src/terminal/Terminal.tsx
@@ -1,14 +1,14 @@
// SPDX-License-Identifier: MIT
-// Copyright (c) 2020-2021 The Pybricks Authors
+// Copyright (c) 2020-2022 The Pybricks Authors
import { Menu, MenuDivider, MenuItem, ResizeSensor } from '@blueprintjs/core';
import { ContextMenu2, ContextMenu2ContentProps } from '@blueprintjs/popover2';
import { useI18n } from '@shopify/react-i18n';
import React, { useContext, useEffect, useMemo, useRef } from 'react';
import { useDispatch } from 'react-redux';
+import { useDarkMode } from 'usehooks-ts';
import { Terminal as XTerm } from 'xterm';
import { FitAddon } from 'xterm-addon-fit';
-import { useSelector } from '../reducers';
import { isMacOS } from '../utils/os';
import { TerminalContext } from './TerminalContext';
import { receiveData } from './actions';
@@ -100,7 +100,7 @@ function createContextMenu(
const Terminal: React.FC = (_props) => {
const { xterm, fitAddon } = useMemo(createXTerm, [createXTerm]);
const terminalRef = useRef(null);
- const darkMode = useSelector((s) => s.settings.darkMode);
+ const { isDarkMode } = useDarkMode();
const dispatch = useDispatch();
const terminalStream = useContext(TerminalContext);
@@ -118,16 +118,16 @@ const Terminal: React.FC = (_props) => {
return () => xterm.dispose();
}, [xterm]);
- // wire up darkMode to terminal
+ // wire up isDarkMode to terminal
useEffect(() => {
xterm.options.theme = {
- background: darkMode ? 'black' : 'white',
- foreground: darkMode ? 'white' : 'black',
- cursor: darkMode ? 'white' : 'black',
+ background: isDarkMode ? 'black' : 'white',
+ foreground: isDarkMode ? 'white' : 'black',
+ cursor: isDarkMode ? 'white' : 'black',
// transparency is needed to work around https://github.com/xtermjs/xterm.js/issues/2808
- selection: darkMode ? 'rgb(81,81,81,0.5)' : 'rgba(181,213,255,0.5)', // this should match AceEditor theme
+ selection: isDarkMode ? 'rgb(81,81,81,0.5)' : 'rgba(181,213,255,0.5)', // this should match AceEditor theme
};
- }, [darkMode]);
+ }, [isDarkMode]);
const handleKeyDownEvent = (e: KeyboardEvent): void => {
// implement CTRL+SHIFT+C keyboard shortcut for copying text from terminal
diff --git a/src/utils/os.test.ts b/src/utils/os.test.ts
index c21b28da..51346573 100644
--- a/src/utils/os.test.ts
+++ b/src/utils/os.test.ts
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2021 The Pybricks Authors
-import { isAndroid, isMacOS, isWindows, prefersDarkMode } from './os';
+import { isAndroid, isMacOS, isWindows } from './os';
afterEach(() => {
jest.resetAllMocks();
@@ -39,18 +39,3 @@ describe('isWindows', () => {
expect(isWindows()).toBeFalsy();
});
});
-
-describe('prefersDarkMode', () => {
- test('is true', () => {
- jest.spyOn(window, 'matchMedia').mockReturnValue({
- matches: true,
- } as MediaQueryList);
- expect(prefersDarkMode()).toBeTruthy();
- });
- test('is false', () => {
- jest.spyOn(window, 'matchMedia').mockReturnValue({
- matches: false,
- } as MediaQueryList);
- expect(prefersDarkMode()).toBeFalsy();
- });
-});
diff --git a/src/utils/os.ts b/src/utils/os.ts
index 5fe83dfd..00913d16 100644
--- a/src/utils/os.ts
+++ b/src/utils/os.ts
@@ -29,11 +29,3 @@ export function isMacOS(): boolean {
export function isWindows(): boolean {
return /win/i.test(navigator.platform);
}
-
-/**
- * Tests if the OS is set to dark mode.
- * @returns: `true` if dark mode should be preferred, otherwise `false`.
- */
-export function prefersDarkMode(): boolean {
- return window.matchMedia('(prefers-color-scheme: dark)').matches;
-}