Skip to content

Commit ed8f785

Browse files
authored
fix(theming): nested ColorSchemeProvider system color scheme handling (#2000)
1 parent 300f5f9 commit ed8f785

File tree

2 files changed

+56
-36
lines changed

2 files changed

+56
-36
lines changed

packages/theming/src/elements/ColorSchemeProvider.tsx

+40-36
Original file line numberDiff line numberDiff line change
@@ -13,58 +13,39 @@ import React, {
1313
useMemo,
1414
useState
1515
} from 'react';
16+
import PropTypes from 'prop-types';
1617
import {
1718
ColorScheme,
1819
IColorSchemeContext,
1920
IColorSchemeProviderProps,
2021
IGardenTheme
2122
} from '../types';
2223

23-
const useColorScheme = (initialState?: ColorScheme, colorSchemeKey = 'color-scheme') => {
24+
const mediaQuery =
25+
typeof window === 'undefined' ? undefined : window.matchMedia('(prefers-color-scheme: dark)');
26+
27+
const useColorScheme = (initialState: ColorScheme, colorSchemeKey: string) => {
2428
/* eslint-disable-next-line n/no-unsupported-features/node-builtins */
2529
const localStorage = typeof window === 'undefined' ? undefined : window.localStorage;
26-
const mediaQuery =
27-
typeof window === 'undefined' ? undefined : window.matchMedia('(prefers-color-scheme: dark)');
2830

29-
const getState = useCallback(
30-
(_state?: ColorScheme | null) => {
31-
const isSystem = _state === 'system' || _state === undefined || _state === null;
32-
let colorScheme: IGardenTheme['colors']['base'];
31+
const getState = useCallback((_state?: ColorScheme | null) => {
32+
const isSystem = _state === 'system' || _state === undefined || _state === null;
33+
let colorScheme: IGardenTheme['colors']['base'];
3334

34-
if (isSystem) {
35-
colorScheme = mediaQuery?.matches ? 'dark' : 'light';
36-
} else {
37-
colorScheme = _state;
38-
}
35+
if (isSystem) {
36+
colorScheme = mediaQuery?.matches ? 'dark' : 'light';
37+
} else {
38+
colorScheme = _state;
39+
}
3940

40-
return { isSystem, colorScheme };
41-
},
42-
[mediaQuery?.matches]
43-
);
41+
return { isSystem, colorScheme };
42+
}, []);
4443

4544
const [state, setState] = useState<{
4645
isSystem: boolean;
4746
colorScheme: IGardenTheme['colors']['base'];
4847
}>(getState((localStorage?.getItem(colorSchemeKey) as ColorScheme) || initialState));
4948

50-
useEffect(() => {
51-
// Listen for changes to the system color scheme
52-
/* istanbul ignore next */
53-
const eventListener = () => {
54-
setState(getState('system'));
55-
};
56-
57-
if (state.isSystem) {
58-
mediaQuery?.addEventListener('change', eventListener);
59-
} else {
60-
mediaQuery?.removeEventListener('change', eventListener);
61-
}
62-
63-
return () => {
64-
mediaQuery?.removeEventListener('change', eventListener);
65-
};
66-
}, [getState, state.isSystem, mediaQuery]);
67-
6849
return {
6950
isSystem: state.isSystem,
7051
colorScheme: state.colorScheme,
@@ -79,8 +60,8 @@ export const ColorSchemeContext = createContext<IColorSchemeContext | undefined>
7960

8061
export const ColorSchemeProvider = ({
8162
children,
82-
colorSchemeKey,
83-
initialColorScheme
63+
colorSchemeKey = 'color-scheme',
64+
initialColorScheme = 'system'
8465
}: PropsWithChildren<IColorSchemeProviderProps>) => {
8566
const { isSystem, colorScheme, setColorScheme } = useColorScheme(
8667
initialColorScheme,
@@ -91,5 +72,28 @@ export const ColorSchemeProvider = ({
9172
[isSystem, colorScheme, setColorScheme]
9273
);
9374

75+
useEffect(() => {
76+
// Listen for changes to the system color scheme
77+
/* istanbul ignore next */
78+
const eventListener = () => {
79+
setColorScheme('system');
80+
};
81+
82+
if (isSystem) {
83+
mediaQuery?.addEventListener('change', eventListener);
84+
} else {
85+
mediaQuery?.removeEventListener('change', eventListener);
86+
}
87+
88+
return () => {
89+
mediaQuery?.removeEventListener('change', eventListener);
90+
};
91+
}, [isSystem, setColorScheme]);
92+
9493
return <ColorSchemeContext.Provider value={contextValue}>{children}</ColorSchemeContext.Provider>;
9594
};
95+
96+
ColorSchemeProvider.propTypes = {
97+
colorSchemeKey: PropTypes.string,
98+
initialColorScheme: PropTypes.oneOf(['light', 'dark', 'system'])
99+
};

utils/test/jest.setup.js

+16
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,19 @@ import '@testing-library/jest-dom';
1414
import { TextEncoder } from 'node:util';
1515

1616
global.TextEncoder = TextEncoder;
17+
18+
// https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom
19+
Object.defineProperty(window, 'matchMedia', {
20+
writable: true,
21+
/* eslint-disable no-undef */
22+
value: jest.fn().mockImplementation(query => ({
23+
matches: false,
24+
media: query,
25+
onchange: null,
26+
addListener: jest.fn(), // deprecated
27+
removeListener: jest.fn(), // deprecated
28+
addEventListener: jest.fn(),
29+
removeEventListener: jest.fn(),
30+
dispatchEvent: jest.fn()
31+
}))
32+
});

0 commit comments

Comments
 (0)