Skip to content

Commit

Permalink
✨ feat: add set method to useThemeMode hooks
Browse files Browse the repository at this point in the history
  • Loading branch information
arvinxx committed May 26, 2023
1 parent e099fc9 commit 0d65a9a
Show file tree
Hide file tree
Showing 9 changed files with 147 additions and 49 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@
"@emotion/react": "^11",
"@emotion/serialize": "^1",
"@emotion/server": "^11",
"@emotion/utils": "^1"
"@emotion/utils": "^1",
"use-merge-value": "^1"
},
"devDependencies": {
"@ant-design/icons": "^5",
Expand Down Expand Up @@ -130,7 +131,6 @@
"three": "^0.148",
"ts-node": "^10",
"typescript": "^5",
"use-merge-value": "^1",
"vitest": "latest",
"zustand": "^4"
},
Expand Down
26 changes: 26 additions & 0 deletions src/context/ThemeModeContext.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { renderHook } from '@testing-library/react';
import { useContext } from 'react';
import { vi } from 'vitest';

import { ThemeModeContext } from './ThemeModeContext';

describe('ThemeModeContext', () => {
it('should have default values', () => {
const { result } = renderHook(() => useContext(ThemeModeContext));
expect(result.current.appearance).toEqual('light');
expect(result.current.isDarkMode).toEqual(false);
expect(result.current.themeMode).toEqual('light');
expect(result.current.browserPrefers).toEqual('light');
expect(result.current.setAppearance).toBeDefined();
expect(result.current.setThemeMode).toBeDefined();
});

it('should return false when window is undefined', () => {
const matchMedia = vi.fn();
Object.defineProperty(window, 'matchMedia', { value: matchMedia });

const { result } = renderHook(() => useContext(ThemeModeContext));
expect(result.current.browserPrefers).toEqual('light');
expect(matchMedia).not.toHaveBeenCalled();
});
});
2 changes: 2 additions & 0 deletions src/context/ThemeModeContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import { matchBrowserPrefers } from '@/utils/matchBrowserPrefers';

export const ThemeModeContext = createContext<ThemeContextState>({
appearance: 'light',
setAppearance: () => {},
isDarkMode: false,
themeMode: 'light',
setThemeMode: () => {},
browserPrefers: matchBrowserPrefers('dark')?.matches ? 'dark' : 'light',
});
93 changes: 93 additions & 0 deletions src/factories/createThemeProvider/ThemeSwitcher.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { fireEvent, render } from '@testing-library/react';
import { vi } from 'vitest';

import { ThemeModeContext } from '@/context';
import { useTheme } from '@/functions';
import { useThemeMode } from '@/hooks';
import { ThemeContextState } from '@/types';
import ThemeSwitcher, { ThemeSwitcherProps } from './ThemeSwitcher';

const mockUseTheme = {
appearance: 'light',
themeMode: 'light',
} as ThemeContextState;

const MockedChild = () => {
const theme = useTheme();
const { setThemeMode, setAppearance } = useThemeMode();
return (
<div style={{ background: theme.colorBgContainer }}>
Mocked Child
<div
onClick={() => {
setAppearance('dark');
}}
>
switch-dark
</div>
<div
onClick={() => {
setThemeMode('dark');
}}
>
theme-mode-dark
</div>
</div>
);
};

const Component = (props: Partial<ThemeSwitcherProps>) => (
<ThemeModeContext.Provider value={mockUseTheme}>
<ThemeSwitcher {...props} useTheme={useTheme}>
<MockedChild />
</ThemeSwitcher>
</ThemeModeContext.Provider>
);

describe('<ThemeSwitcher />', () => {
it('should render the child component', () => {
const { getByText } = render(<Component />);
expect(getByText('Mocked Child')).toBeInTheDocument();
});

it('should render with default appearance', () => {
const { container } = render(<Component />);
expect(container.firstChild).toHaveStyle('background-color: #fff');
});

it.skip('should render with dark appearance', () => {
const { container } = render(<Component appearance={'dark'} />);

expect(container.firstChild).toHaveStyle('background-color: #000');
});

it('should render with light theme mode', () => {
const { container } = render(<Component themeMode={'light'} />);

expect(container.firstChild).toHaveStyle('background-color: #fff');
});

it.skip('should render with dark theme mode', () => {
const { container } = render(<Component themeMode={'dark'} />);

expect(container.firstChild).toHaveStyle('background-color: #000');
});

it('should call onAppearanceChange when appearance is changed', () => {
const onAppearanceChange = vi.fn();
const { getByText } = render(<Component onAppearanceChange={onAppearanceChange} />);

const switchElement = getByText('switch-dark');
fireEvent.click(switchElement);
expect(onAppearanceChange).toHaveBeenCalledWith('dark');
});

it('should call onThemeModeChange when theme mode is changed', () => {
const onThemeModeChange = vi.fn();
const { getByText } = render(<Component onThemeModeChange={onThemeModeChange} />);

const radioElement = getByText('theme-mode-dark');
fireEvent.click(radioElement);
expect(onThemeModeChange).toHaveBeenCalledWith('dark');
});
});
23 changes: 15 additions & 8 deletions src/factories/createThemeProvider/ThemeSwitcher.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useMergeValue } from '@/utils/useMergeValue';
import { FC, memo, ReactNode, useEffect, useLayoutEffect, useMemo, useState } from 'react';
import { FC, memo, ReactNode, useEffect, useLayoutEffect, useState } from 'react';
import useMergeValue from 'use-merge-value';

import { ThemeModeContext } from '@/context';
import { BrowserPrefers, ThemeAppearance, ThemeMode, UseTheme } from '@/types';
Expand Down Expand Up @@ -70,13 +70,15 @@ export interface ThemeSwitcherProps {
*/
appearance?: ThemeAppearance;
defaultAppearance?: ThemeAppearance;
onAppearanceChange?: (mode: ThemeAppearance) => void;
onAppearanceChange?: (appearance: ThemeAppearance) => void;
/**
* 主题的展示模式,有三种配置:跟随系统、亮色、暗色
* 默认不开启自动模式,需要手动进行配置
* @default light
*/
themeMode?: ThemeMode;
defaultThemeMode?: ThemeMode;
onThemeModeChange?: (themeMode: ThemeMode) => void;

children: ReactNode;
useTheme: UseTheme;
Expand All @@ -89,19 +91,22 @@ const ThemeSwitcher: FC<ThemeSwitcherProps> = memo(
defaultAppearance,
onAppearanceChange,
themeMode: themeModeProps,
defaultThemeMode,
onThemeModeChange,
useTheme,
}) => {
const { appearance: upperAppearance, themeMode: upperThemeMode } = useTheme();

const themeMode = useMemo(
() => themeModeProps ?? upperThemeMode,
[themeModeProps, upperThemeMode],
);
const [themeMode, setThemeMode] = useMergeValue<ThemeMode>('light', {
value: themeModeProps,
defaultValue: defaultThemeMode ?? upperThemeMode,
onChange: (v) => onThemeModeChange?.(v),
});

const [appearance, setAppearance] = useMergeValue<ThemeAppearance>('light', {
value: appearanceProp,
defaultValue: defaultAppearance ?? upperAppearance,
onChange: onAppearanceChange,
onChange: (v) => onAppearanceChange?.(v),
});

const [browserPrefers, setBrowserPrefers] = useState<BrowserPrefers>(
Expand All @@ -119,7 +124,9 @@ const ThemeSwitcher: FC<ThemeSwitcherProps> = memo(
<ThemeModeContext.Provider
value={{
themeMode,
setThemeMode,
appearance,
setAppearance,
isDarkMode: appearance === 'dark',
browserPrefers,
}}
Expand Down
4 changes: 4 additions & 0 deletions src/factories/createThemeProvider/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ export const createThemeProvider = (
defaultAppearance,
onAppearanceChange,
themeMode,
defaultThemeMode,
onThemeModeChange,
styled,
}) => {
const {
Expand All @@ -82,6 +84,8 @@ export const createThemeProvider = (
>
<ThemeSwitcher
themeMode={themeMode}
defaultThemeMode={defaultThemeMode}
onThemeModeChange={onThemeModeChange}
defaultAppearance={defaultAppearance}
appearance={appearance}
onAppearanceChange={onAppearanceChange}
Expand Down
4 changes: 3 additions & 1 deletion src/factories/createThemeProvider/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,15 @@ export interface ThemeProviderProps<T, S = Record<string, string>> {
*/
appearance?: ThemeAppearance;
defaultAppearance?: ThemeAppearance;
onAppearanceChange?: (mode: ThemeAppearance) => void;
onAppearanceChange?: (appearance: ThemeAppearance) => void;
/**
* 主题的展示模式,有三种配置:跟随系统、亮色、暗色
* 默认不开启自动模式,需要手动进行配置
* @default light
*/
themeMode?: ThemeMode;
defaultThemeMode?: ThemeMode;
onThemeModeChange?: (mode: ThemeMode) => void;
}

/**
Expand Down
2 changes: 2 additions & 0 deletions src/types/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ export interface ThemeContextState {
* @title 外观
*/
appearance: ThemeAppearance;
setAppearance: (appearance: ThemeAppearance) => void;
/**
* @title 主题模式
* @enum ["light", "dark"]
* @enumNames ["亮色模式", "暗色模式"]
* @default "light"
*/
themeMode: ThemeMode;
setThemeMode: (themeMode: ThemeMode) => void;
/**
* @title 是否为暗色模式
*/
Expand Down
38 changes: 0 additions & 38 deletions src/utils/useMergeValue.ts

This file was deleted.

0 comments on commit 0d65a9a

Please sign in to comment.