diff --git a/packages/ra-ui-materialui/src/theme/useTheme.spec.tsx b/packages/ra-ui-materialui/src/theme/useTheme.spec.tsx index b0923706afa..886a215df31 100644 --- a/packages/ra-ui-materialui/src/theme/useTheme.spec.tsx +++ b/packages/ra-ui-materialui/src/theme/useTheme.spec.tsx @@ -4,6 +4,9 @@ import expect from 'expect'; import { render, screen } from '@testing-library/react'; import { renderHook } from '@testing-library/react-hooks'; import { useTheme } from './useTheme'; +import { AdminContext } from '../AdminContext'; +import { ThemeTestWrapper } from '../layout/ThemeTestWrapper'; +import { defaultDarkTheme } from './defaultTheme'; const authProvider = { login: jest.fn().mockResolvedValueOnce(''), @@ -23,33 +26,102 @@ const Foo = () => { }; describe('useTheme', () => { - it('should return undefined by default', () => { + it('should return the light theme by default', () => { render( ); - expect(screen.queryByLabelText('has-theme')).toBeNull(); + expect(screen.queryByText('light')).not.toBeNull(); + }); + + it('should return the light theme when no dark theme is provided even though user prefers dark mode', () => { + render( + + + + + + ); + expect(screen.queryByText('light')).not.toBeNull(); + }); + + it('should return the light theme when no dark theme is provided even though the stored theme is dark', () => { + const store = memoryStore({ theme: 'dark' }); + render( + + + + ); + expect(screen.queryByText('light')).not.toBeNull(); + }); + + it('should return the user preferred theme by default', async () => { + const ssrMatchMedia = query => ({ + matches: query === '(prefers-color-scheme: dark)' ? true : false, + }); + + render( + + + + + + ); + await screen.findByText('dark'); }); it('should return current theme when set', () => { render( - - + ); expect(screen.getByLabelText('has-theme')).not.toBeNull(); + expect(screen.queryByText('dark')).not.toBeNull(); }); it('should return theme from settings when available', () => { - const { result: storeResult } = renderHook(() => useStore('theme')); + const { result: storeResult } = renderHook(() => useStore('theme'), { + wrapper: ({ children }) => ( + + {children} + + ), + }); const [_, setTheme] = storeResult.current; setTheme('dark'); - const { result: themeResult } = renderHook(() => useTheme()); + const { result: themeResult } = renderHook(() => useTheme(), { + wrapper: ({ children }) => ( + + {children} + + ), + }); const [theme, __] = themeResult.current; expect(theme).toEqual('dark'); diff --git a/packages/ra-ui-materialui/src/theme/useTheme.ts b/packages/ra-ui-materialui/src/theme/useTheme.ts index ec3ead4cdd7..72954324d49 100644 --- a/packages/ra-ui-materialui/src/theme/useTheme.ts +++ b/packages/ra-ui-materialui/src/theme/useTheme.ts @@ -1,5 +1,7 @@ import { useStore } from 'ra-core'; import { RaThemeOptions, ThemeType } from './types'; +import { useMediaQuery } from '@mui/material'; +import { useThemesContext } from './useThemesContext'; export type ThemeSetter = (theme: ThemeType | RaThemeOptions) => void; @@ -23,7 +25,16 @@ export type ThemeSetter = (theme: ThemeType | RaThemeOptions) => void; export const useTheme = ( type?: ThemeType | RaThemeOptions ): [ThemeType | RaThemeOptions, ThemeSetter] => { + const { darkTheme } = useThemesContext(); + const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)', { + noSsr: true, + }); // FIXME: remove legacy mode in v5, and remove the RaThemeOptions type - const [theme, setter] = useStore('theme', type); - return [theme, setter]; + const [theme, setter] = useStore( + 'theme', + type ?? (prefersDarkMode && darkTheme ? 'dark' : 'light') + ); + + // Ensure that even though the store has its value set to 'dark', we still use the light theme when no dark theme is available + return [darkTheme != null ? theme : 'light', setter]; };