Skip to content

Commit

Permalink
fix(theming): nested ColorSchemeProvider system color scheme handli…
Browse files Browse the repository at this point in the history
…ng (#2000)
  • Loading branch information
jzempel authored Jan 16, 2025
1 parent 300f5f9 commit ed8f785
Show file tree
Hide file tree
Showing 2 changed files with 56 additions and 36 deletions.
76 changes: 40 additions & 36 deletions packages/theming/src/elements/ColorSchemeProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,58 +13,39 @@ import React, {
useMemo,
useState
} from 'react';
import PropTypes from 'prop-types';
import {
ColorScheme,
IColorSchemeContext,
IColorSchemeProviderProps,
IGardenTheme
} from '../types';

const useColorScheme = (initialState?: ColorScheme, colorSchemeKey = 'color-scheme') => {
const mediaQuery =
typeof window === 'undefined' ? undefined : window.matchMedia('(prefers-color-scheme: dark)');

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

const getState = useCallback(
(_state?: ColorScheme | null) => {
const isSystem = _state === 'system' || _state === undefined || _state === null;
let colorScheme: IGardenTheme['colors']['base'];
const getState = useCallback((_state?: ColorScheme | null) => {
const isSystem = _state === 'system' || _state === undefined || _state === null;
let colorScheme: IGardenTheme['colors']['base'];

if (isSystem) {
colorScheme = mediaQuery?.matches ? 'dark' : 'light';
} else {
colorScheme = _state;
}
if (isSystem) {
colorScheme = mediaQuery?.matches ? 'dark' : 'light';
} else {
colorScheme = _state;
}

return { isSystem, colorScheme };
},
[mediaQuery?.matches]
);
return { isSystem, colorScheme };
}, []);

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

useEffect(() => {
// Listen for changes to the system color scheme
/* istanbul ignore next */
const eventListener = () => {
setState(getState('system'));
};

if (state.isSystem) {
mediaQuery?.addEventListener('change', eventListener);
} else {
mediaQuery?.removeEventListener('change', eventListener);
}

return () => {
mediaQuery?.removeEventListener('change', eventListener);
};
}, [getState, state.isSystem, mediaQuery]);

return {
isSystem: state.isSystem,
colorScheme: state.colorScheme,
Expand All @@ -79,8 +60,8 @@ export const ColorSchemeContext = createContext<IColorSchemeContext | undefined>

export const ColorSchemeProvider = ({
children,
colorSchemeKey,
initialColorScheme
colorSchemeKey = 'color-scheme',
initialColorScheme = 'system'
}: PropsWithChildren<IColorSchemeProviderProps>) => {
const { isSystem, colorScheme, setColorScheme } = useColorScheme(
initialColorScheme,
Expand All @@ -91,5 +72,28 @@ export const ColorSchemeProvider = ({
[isSystem, colorScheme, setColorScheme]
);

useEffect(() => {
// Listen for changes to the system color scheme
/* istanbul ignore next */
const eventListener = () => {
setColorScheme('system');
};

if (isSystem) {
mediaQuery?.addEventListener('change', eventListener);
} else {
mediaQuery?.removeEventListener('change', eventListener);
}

return () => {
mediaQuery?.removeEventListener('change', eventListener);
};
}, [isSystem, setColorScheme]);

return <ColorSchemeContext.Provider value={contextValue}>{children}</ColorSchemeContext.Provider>;
};

ColorSchemeProvider.propTypes = {
colorSchemeKey: PropTypes.string,
initialColorScheme: PropTypes.oneOf(['light', 'dark', 'system'])
};
16 changes: 16 additions & 0 deletions utils/test/jest.setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,19 @@ import '@testing-library/jest-dom';
import { TextEncoder } from 'node:util';

global.TextEncoder = TextEncoder;

// https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom
Object.defineProperty(window, 'matchMedia', {
writable: true,
/* eslint-disable no-undef */
value: jest.fn().mockImplementation(query => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(), // deprecated
removeListener: jest.fn(), // deprecated
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn()
}))
});

0 comments on commit ed8f785

Please sign in to comment.