mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-09-12 01:23:52 +00:00
settings: replace darkMode with useDarkMode hook
This is a minor breaking change for users that don't have dark mode set to the default value since the localStorage key has changed (hard- coded in 3rd party library).
This commit is contained in:
@@ -6,5 +6,6 @@ import { testRender } from '../../test';
|
||||
import App from './App';
|
||||
|
||||
it.each([false, true])('should render', (darkMode) => {
|
||||
testRender(<App />, { settings: { darkMode } });
|
||||
localStorage.setItem('usehooks-ts-dark-mode', String(darkMode));
|
||||
testRender(<App />);
|
||||
});
|
||||
|
||||
+5
-5
@@ -5,7 +5,7 @@ import { Classes } from '@blueprintjs/core';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import SplitterLayout from 'react-splitter-layout';
|
||||
import { useLocalStorage } from 'usehooks-ts';
|
||||
import { useDarkMode, useLocalStorage } from 'usehooks-ts';
|
||||
import Editor, { EditorType } from '../editor/Editor';
|
||||
import Explorer from '../explorer/Explorer';
|
||||
import { useSelector } from '../reducers';
|
||||
@@ -130,7 +130,7 @@ type AppProps = {
|
||||
};
|
||||
|
||||
const App: React.VoidFunctionComponent<AppProps> = ({ onEditorChanged }) => {
|
||||
const darkMode = useSelector((s): boolean => s.settings.darkMode);
|
||||
const { isDarkMode } = useDarkMode();
|
||||
const showDocs = useSelector((s): boolean => s.settings.showDocs);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const dispatch = useDispatch();
|
||||
@@ -138,17 +138,17 @@ const App: React.VoidFunctionComponent<AppProps> = ({ 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 (
|
||||
<div className="pb-app h-100 w-100 p-absolute">
|
||||
|
||||
@@ -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<EditorContextMenuProps> = (
|
||||
);
|
||||
};
|
||||
|
||||
type EditorProps = { onEditorChanged: (editor: EditorType) => void };
|
||||
type EditorProps = { onEditorChanged?: (editor: EditorType) => void };
|
||||
|
||||
const Editor: React.VoidFunctionComponent<EditorProps> = ({ onEditorChanged }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [editor, setEditor] = useState<EditorType>(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<EditorProps> = ({ onEditorChanged }) =
|
||||
>
|
||||
<MonacoEditor
|
||||
language={pybricksMicroPythonId}
|
||||
theme={darkMode ? tomorrowNightEightiesId : xcodeId}
|
||||
theme={isDarkMode ? tomorrowNightEightiesId : xcodeId}
|
||||
width="100%"
|
||||
height="100%"
|
||||
options={{
|
||||
|
||||
@@ -21,6 +21,7 @@ import { Tooltip2 } from '@blueprintjs/popover2';
|
||||
import { useI18n } from '@shopify/react-i18n';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useDarkMode } from 'usehooks-ts';
|
||||
import AboutDialog from '../about/AboutDialog';
|
||||
import { appCheckForUpdate, appReload, appShowInstallPrompt } from '../app/actions';
|
||||
import {
|
||||
@@ -50,9 +51,9 @@ const SettingsDrawer: React.VoidFunctionComponent<SettingsProps> = ({
|
||||
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<SettingsProps> = ({
|
||||
label={i18n.translate(
|
||||
SettingsStringId.AppearanceDarkModeLabel,
|
||||
)}
|
||||
checked={darkMode}
|
||||
onChange={(e) =>
|
||||
dispatch(
|
||||
setBoolean(
|
||||
BooleanSettingId.DarkMode,
|
||||
(e.target as HTMLInputElement).checked,
|
||||
),
|
||||
)
|
||||
}
|
||||
checked={isDarkMode}
|
||||
onChange={toggleDarkMode}
|
||||
/>
|
||||
</Tooltip2>
|
||||
</FormGroup>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<typeof reducers>;
|
||||
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(
|
||||
|
||||
@@ -12,20 +12,6 @@ import {
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
const darkMode: Reducer<boolean> = (
|
||||
state = getDefaultBooleanValue(BooleanSettingId.DarkMode),
|
||||
action,
|
||||
) => {
|
||||
if (didBooleanChange.matches(action)) {
|
||||
if (action.id === BooleanSettingId.DarkMode) {
|
||||
return action.newState;
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
return state;
|
||||
};
|
||||
|
||||
const showDocs: Reducer<boolean> = (
|
||||
state = getDefaultBooleanValue(BooleanSettingId.ShowDocs),
|
||||
action,
|
||||
@@ -87,7 +73,6 @@ const isHubNameValid: Reducer<boolean> = (state = true, action) => {
|
||||
};
|
||||
|
||||
export default combineReducers({
|
||||
darkMode,
|
||||
showDocs,
|
||||
flashCurrentProgram,
|
||||
hubName,
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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<HTMLDivElement>(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
|
||||
|
||||
+1
-16
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user