diff --git a/packages/theming/src/elements/ColorSchemeProvider.tsx b/packages/theming/src/elements/ColorSchemeProvider.tsx index ea7a5a6792..90bc042df3 100644 --- a/packages/theming/src/elements/ColorSchemeProvider.tsx +++ b/packages/theming/src/elements/ColorSchemeProvider.tsx @@ -13,6 +13,7 @@ import React, { useMemo, useState } from 'react'; +import PropTypes from 'prop-types'; import { ColorScheme, IColorSchemeContext, @@ -20,51 +21,31 @@ import { 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, @@ -79,8 +60,8 @@ export const ColorSchemeContext = createContext export const ColorSchemeProvider = ({ children, - colorSchemeKey, - initialColorScheme + colorSchemeKey = 'color-scheme', + initialColorScheme = 'system' }: PropsWithChildren) => { const { isSystem, colorScheme, setColorScheme } = useColorScheme( initialColorScheme, @@ -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 {children}; }; + +ColorSchemeProvider.propTypes = { + colorSchemeKey: PropTypes.string, + initialColorScheme: PropTypes.oneOf(['light', 'dark', 'system']) +}; diff --git a/utils/test/jest.setup.js b/utils/test/jest.setup.js index a150fe4c8c..f960446ccf 100644 --- a/utils/test/jest.setup.js +++ b/utils/test/jest.setup.js @@ -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() + })) +});