Feat/use dark mode preference to set initial theme.light (#861)

* feat(redux-devtools-app): use `prefers-color-scheme` to set `theme.light` if user has not set a preferred theme

* chore(@redux-devtools/app): fix lint error

Error message:

0:0  error  Parsing error: "parserOptions.project" has been set for @typescript-eslint/parser.
The file does not match your project config: test/__mocks__/styleMock.ts.
The file must be included in at least one of the projects provided

* chore: run prettier

* feat(app): add theme color dropdown

* refactor(rename): system preference option from default to auto
This commit is contained in:
Fabrizio Vitale 2021-11-06 14:52:43 +01:00 committed by GitHub
parent a9ef668d9b
commit 90cde4cfdf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 147 additions and 18 deletions

View File

@ -62,10 +62,12 @@ class App extends Component<Props> {
function mapStateToProps(state: StoreState) { function mapStateToProps(state: StoreState) {
const instances = state.instances; const instances = state.instances;
const id = getActiveInstance(instances); const id = getActiveInstance(instances);
const { themeColorPreference, ...themeData } = state.theme;
return { return {
options: instances.options[id], options: instances.options[id],
section: state.section, section: state.section,
theme: state.theme, theme: themeData,
notification: state.notification, notification: state.notification,
}; };
} }

View File

@ -8,7 +8,10 @@ import configureStore from '../../../app/stores/panelStore';
import '../../views/devpanel.pug'; import '../../views/devpanel.pug';
import { Action, Store } from 'redux'; import { Action, Store } from 'redux';
import { StoreAction } from '@redux-devtools/app/lib/actions'; import {
applyMediaFeaturesPreferences,
StoreAction,
} from '@redux-devtools/app/lib/actions';
import { PanelMessage } from '../../../app/middlewares/api'; import { PanelMessage } from '../../../app/middlewares/api';
import { StoreStateWithoutSocket } from '../../../app/reducers/panel'; import { StoreStateWithoutSocket } from '../../../app/reducers/panel';
import { PersistGate } from 'redux-persist/integration/react'; import { PersistGate } from 'redux-persist/integration/react';
@ -33,9 +36,20 @@ function renderDevTools() {
unmountComponentAtNode(node!); unmountComponentAtNode(node!);
clearTimeout(naTimeout); clearTimeout(naTimeout);
({ store, persistor } = configureStore(position, bgConnection)); ({ store, persistor } = configureStore(position, bgConnection));
const onBeforeLift = () => {
if (store) {
store.dispatch(applyMediaFeaturesPreferences());
}
};
render( render(
<Provider store={store}> <Provider store={store}>
<PersistGate loading={null} persistor={persistor}> <PersistGate
loading={null}
persistor={persistor}
onBeforeLift={onBeforeLift}
>
<App position={position} /> <App position={position} />
</PersistGate> </PersistGate>
</Provider>, </Provider>,

View File

@ -8,6 +8,7 @@ import configureStore from '../../../app/stores/windowStore';
import { MonitorMessage } from '../../../app/middlewares/api'; import { MonitorMessage } from '../../../app/middlewares/api';
import '../../views/window.pug'; import '../../views/window.pug';
import { applyMediaFeaturesPreferences } from '@redux-devtools/app/lib/actions';
const position = location.hash; const position = location.hash;
@ -25,9 +26,17 @@ chrome.runtime.getBackgroundPage((window) => {
bg.onMessage.addListener(update); bg.onMessage.addListener(update);
update(); update();
const onBeforeLift = () => {
localStore.dispatch(applyMediaFeaturesPreferences());
};
render( render(
<Provider store={localStore}> <Provider store={localStore}>
<PersistGate loading={null} persistor={persistor}> <PersistGate
loading={null}
persistor={persistor}
onBeforeLift={onBeforeLift}
>
<App position={position} /> <App position={position} />
</PersistGate> </PersistGate>
</Provider>, </Provider>,

View File

@ -3,6 +3,6 @@ module.exports = {
setupFilesAfterEnv: ['<rootDir>/test/setup.ts'], setupFilesAfterEnv: ['<rootDir>/test/setup.ts'],
testEnvironment: 'jsdom', testEnvironment: 'jsdom',
moduleNameMapper: { moduleNameMapper: {
'\\.css$': '<rootDir>/test/__mocks__/styleMock.ts', '\\.css$': '<rootDir>/test/__mocks__/styleMock.js',
}, },
}; };

View File

@ -3,6 +3,7 @@ import { AuthStates, States } from 'socketcluster-client/lib/scclientsocket';
import { import {
CHANGE_SECTION, CHANGE_SECTION,
CHANGE_THEME, CHANGE_THEME,
APPLY_MEDIA_FEATURES_PREFERENCES,
SELECT_INSTANCE, SELECT_INSTANCE,
SELECT_MONITOR, SELECT_MONITOR,
UPDATE_MONITOR_STATE, UPDATE_MONITOR_STATE,
@ -44,9 +45,10 @@ import {
import { Action } from 'redux'; import { Action } from 'redux';
import { Features, State } from '../reducers/instances'; import { Features, State } from '../reducers/instances';
import { MonitorStateMonitorState } from '../reducers/monitor'; import { MonitorStateMonitorState } from '../reducers/monitor';
import { LiftedAction } from '@redux-devtools/core'; import { LiftedAction, LiftedState } from '@redux-devtools/core';
import { Data } from '../reducers/reports'; import { Data } from '../reducers/reports';
import { LiftedState } from '@redux-devtools/core'; import { prefersDarkColorScheme } from '../utils/media-queries';
import { ThemeColorPreference } from '../reducers/theme';
let monitorReducer: ( let monitorReducer: (
monitorProps: unknown, monitorProps: unknown,
@ -66,9 +68,9 @@ export function changeSection(section: string): ChangeSectionAction {
interface ChangeThemeFormData { interface ChangeThemeFormData {
readonly theme: Theme; readonly theme: Theme;
readonly scheme: Scheme; readonly scheme: Scheme;
readonly dark: boolean; readonly themeColorPreference: ThemeColorPreference;
} }
interface ChangeThemeData { export interface ChangeThemeData {
readonly formData: ChangeThemeFormData; readonly formData: ChangeThemeFormData;
} }
export interface ChangeThemeAction { export interface ChangeThemeAction {
@ -76,9 +78,43 @@ export interface ChangeThemeAction {
readonly theme: Theme; readonly theme: Theme;
readonly scheme: Scheme; readonly scheme: Scheme;
readonly dark: boolean; readonly dark: boolean;
readonly themeColorPreference: ThemeColorPreference;
} }
export interface ApplyMediaFeaturesPreferencesAction {
readonly type: typeof APPLY_MEDIA_FEATURES_PREFERENCES;
readonly prefersDarkColorScheme: boolean;
}
export function changeTheme(data: ChangeThemeData): ChangeThemeAction { export function changeTheme(data: ChangeThemeData): ChangeThemeAction {
return { type: CHANGE_THEME, ...data.formData }; const { themeColorPreference } = data.formData;
let dark: boolean;
switch (themeColorPreference) {
case 'light':
dark = false;
break;
case 'dark':
dark = true;
break;
default:
dark = prefersDarkColorScheme();
}
return { type: CHANGE_THEME, ...data.formData, dark };
}
/**
* @see https://developer.mozilla.org/en-US/docs/Web/CSS/Media_Queries/Using_media_queries#media_features
*/
export function applyMediaFeaturesPreferences(
payload?: Partial<Omit<ApplyMediaFeaturesPreferencesAction, 'type'>>
): ApplyMediaFeaturesPreferencesAction {
return {
prefersDarkColorScheme: prefersDarkColorScheme(),
...payload,
type: APPLY_MEDIA_FEATURES_PREFERENCES,
};
} }
export interface InitMonitorAction { export interface InitMonitorAction {
@ -564,6 +600,7 @@ export interface ErrorAction {
export type StoreActionWithoutUpdateStateOrLiftedAction = export type StoreActionWithoutUpdateStateOrLiftedAction =
| ChangeSectionAction | ChangeSectionAction
| ChangeThemeAction | ChangeThemeAction
| ApplyMediaFeaturesPreferencesAction
| MonitorActionAction | MonitorActionAction
| SelectInstanceAction | SelectInstanceAction
| SelectMonitorAction | SelectMonitorAction

View File

@ -4,6 +4,10 @@ import { Container, Form } from '@redux-devtools/ui';
import { listSchemes, listThemes } from '@redux-devtools/ui/lib/utils/theme'; import { listSchemes, listThemes } from '@redux-devtools/ui/lib/utils/theme';
import { changeTheme } from '../../actions'; import { changeTheme } from '../../actions';
import { StoreState } from '../../reducers'; import { StoreState } from '../../reducers';
import {
defaultThemeColorPreference,
themeColorPreferences,
} from '../../reducers/theme';
type StateProps = ReturnType<typeof mapStateToProps>; type StateProps = ReturnType<typeof mapStateToProps>;
type DispatchProps = ResolveThunks<typeof actionCreators>; type DispatchProps = ResolveThunks<typeof actionCreators>;
@ -15,7 +19,8 @@ export class Themes extends Component<Props> {
const formData = { const formData = {
theme: theme.theme, theme: theme.theme,
scheme: theme.scheme, scheme: theme.scheme,
dark: !theme.light, themeColorPreference:
theme.themeColorPreference ?? defaultThemeColorPreference,
}; };
return ( return (
@ -33,8 +38,10 @@ export class Themes extends Component<Props> {
type: 'string', type: 'string',
enum: listSchemes(), enum: listSchemes(),
}, },
dark: { themeColorPreference: {
type: 'boolean', title: 'theme color',
type: 'string',
enum: themeColorPreferences as unknown as string[],
}, },
}, },
}} }}

View File

@ -1,5 +1,7 @@
export const CHANGE_SECTION = 'main/CHANGE_SECTION'; export const CHANGE_SECTION = 'main/CHANGE_SECTION';
export const CHANGE_THEME = 'main/CHANGE_THEME'; export const CHANGE_THEME = 'main/CHANGE_THEME';
export const APPLY_MEDIA_FEATURES_PREFERENCES =
'main/APPLY_MEDIA_FEATURES_PREFERENCES';
export const UPDATE_STATE = 'devTools/UPDATE_STATE'; export const UPDATE_STATE = 'devTools/UPDATE_STATE';
export const SET_STATE = 'devTools/SET_STATE'; export const SET_STATE = 'devTools/SET_STATE';

View File

@ -7,7 +7,7 @@ import configureStore from './store/configureStore';
import { CONNECT_REQUEST } from './constants/socketActionTypes'; import { CONNECT_REQUEST } from './constants/socketActionTypes';
import App from './containers/App'; import App from './containers/App';
import { StoreState } from './reducers'; import { StoreState } from './reducers';
import { StoreAction } from './actions'; import { StoreAction, applyMediaFeaturesPreferences } from './actions';
class Root extends Component { class Root extends Component {
store?: Store<StoreState, StoreAction>; store?: Store<StoreState, StoreAction>;
@ -27,11 +27,26 @@ class Root extends Component {
this.persistor = persistor; this.persistor = persistor;
} }
/**
* @hidden
* @private
*/
private _checkMediaFeaturesPreferences = () => {
if (this.store) {
this.store.dispatch(applyMediaFeaturesPreferences());
}
};
render() { render() {
if (!this.store) return null; if (!this.store) return null;
return ( return (
<Provider store={this.store}> <Provider store={this.store}>
<PersistGate loading={null} persistor={this.persistor!}> <PersistGate
loading={null}
persistor={this.persistor!}
onBeforeLift={this._checkMediaFeaturesPreferences}
>
<App /> <App />
</PersistGate> </PersistGate>
</Provider> </Provider>

View File

@ -1,11 +1,25 @@
import { Scheme, Theme } from '@redux-devtools/ui'; import { Theme, Scheme } from '@redux-devtools/ui';
import { CHANGE_THEME } from '../constants/actionTypes'; import {
CHANGE_THEME,
APPLY_MEDIA_FEATURES_PREFERENCES,
} from '../constants/actionTypes';
import { StoreAction } from '../actions'; import { StoreAction } from '../actions';
export const defaultThemeColorPreference = 'auto';
export const themeColorPreferences = [
defaultThemeColorPreference,
'light',
'dark',
] as const;
export type ThemeColorPreference = typeof themeColorPreferences[number];
export interface ThemeState { export interface ThemeState {
readonly theme: Theme; readonly theme: Theme;
readonly scheme: Scheme; readonly scheme: Scheme;
readonly light: boolean; readonly light: boolean;
readonly themeColorPreference?: ThemeColorPreference;
} }
export default function theme( export default function theme(
@ -13,6 +27,7 @@ export default function theme(
theme: 'default' as const, theme: 'default' as const,
scheme: 'default' as const, scheme: 'default' as const,
light: true, light: true,
themeColorPreference: defaultThemeColorPreference,
}, },
action: StoreAction action: StoreAction
) { ) {
@ -21,7 +36,22 @@ export default function theme(
theme: action.theme, theme: action.theme,
scheme: action.scheme, scheme: action.scheme,
light: !action.dark, light: !action.dark,
themeColorPreference: action.themeColorPreference,
}; };
} }
if (
action.type === APPLY_MEDIA_FEATURES_PREFERENCES &&
(!state.themeColorPreference ||
state.themeColorPreference === defaultThemeColorPreference)
) {
return {
...state,
themeColorPreference:
state.themeColorPreference ?? defaultThemeColorPreference,
light: !action.prefersDarkColorScheme,
};
}
return state; return state;
} }

View File

@ -0,0 +1,13 @@
/**
* @see https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme
*/
export function prefersDarkColorScheme(): boolean {
if (
typeof window !== 'undefined' &&
typeof window.matchMedia === 'function'
) {
return window.matchMedia('(prefers-color-scheme: dark)').matches;
}
return false;
}

View File

@ -0,0 +1 @@
module.exports = {};

View File

@ -1 +0,0 @@
export default {};