Skip to content

Commit

Permalink
feat: add useTheme (#2617)
Browse files Browse the repository at this point in the history
* feat: useTheme

* test: useTheme

* feat: useTheme add localStorageKey

* docs: useTheme add localStorageKey

* Update packages/hooks/src/useTheme/index.en-US.md

Co-authored-by: 云泥 <[email protected]>

* Update packages/hooks/src/useTheme/index.ts

add listener type

Co-authored-by: 云泥 <[email protected]>

* fix: missing localStorage.getItem(localStorageKey)

* docs: useTheme add Params

* feat: useTheme add default props

* Update packages/hooks/src/useTheme/index.zh-CN.md

* Update packages/hooks/src/useTheme/index.zh-CN.md

* Update packages/hooks/src/useTheme/index.en-US.md

* style: optimize

* feat: useTheme add onChange callback

* feat: remove onChange

---------

Co-authored-by: 云泥 <[email protected]>
Co-authored-by: lxrsuper <[email protected]>
  • Loading branch information
3 people authored Sep 22, 2024
1 parent 5c7281e commit 5ddd8c2
Show file tree
Hide file tree
Showing 9 changed files with 236 additions and 1 deletion.
1 change: 1 addition & 0 deletions config/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export const menus = [
'useCounter',
'useTextSelection',
'useWebSocket',
'useTheme',
],
},
{
Expand Down
2 changes: 1 addition & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ module.exports = {
testPathIgnorePatterns: ['/.history/'],
modulePathIgnorePatterns: ['<rootDir>/package.json'],
resetMocks: false,
setupFiles: ['./jest.setup.js', 'jest-localstorage-mock'],
setupFiles: ['./jest.setup.js', 'jest-localstorage-mock', './match-media-mock.js'],
setupFilesAfterEnv: ['@testing-library/jest-dom/extend-expect'],
transform: {
'^.+\\.tsx?$': ['ts-jest', { tsconfig: 'tsconfig.json' }],
Expand Down
13 changes: 13 additions & 0 deletions match-media-mock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Object.defineProperty(window, 'matchMedia', {
writable: true,
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(),
})),
});
2 changes: 2 additions & 0 deletions packages/hooks/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ import useVirtualList from './useVirtualList';
import useWebSocket from './useWebSocket';
import useWhyDidYouUpdate from './useWhyDidYouUpdate';
import useMutationObserver from './useMutationObserver';
import useTheme from './useTheme';

export {
useRequest,
Expand Down Expand Up @@ -156,4 +157,5 @@ export {
useRafTimeout,
useResetState,
useMutationObserver,
useTheme,
};
29 changes: 29 additions & 0 deletions packages/hooks/src/useTheme/__test__/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { act, renderHook } from '@testing-library/react';
import useTheme from '../index';

describe('useTheme', () => {
test('themeMode init', () => {
const { result } = renderHook(useTheme);
expect(result.current.themeMode).toBe('system');
});

test('setThemeMode light', () => {
const { result } = renderHook(useTheme);
act(() => result.current.setThemeMode('light'));
expect(result.current.theme).toBe('light');
expect(result.current.themeMode).toBe('light');
});

test('setThemeMode dark', () => {
const { result } = renderHook(useTheme);
act(() => result.current.setThemeMode('dark'));
expect(result.current.theme).toBe('dark');
expect(result.current.themeMode).toBe('dark');
});

test('setThemeMode system', () => {
const { result } = renderHook(useTheme);
act(() => result.current.setThemeMode('system'));
expect(result.current.themeMode).toBe('system');
});
});
47 changes: 47 additions & 0 deletions packages/hooks/src/useTheme/demo/demo1.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* title: Basic usage
* desc: The 'theme' is the system display theme ("light" or "dark"), the 'themeMode' can set 'theme' to "light" or "dark" or follow the system setting.
*
* title.zh-CN: 基础用法
* desc.zh-CN: 'theme' 为系统当前显示主题("light" 或 "dark"),'themeMode' 为当前主题设置("light" 或 "dark" 或 "system")。
*/

import { useTheme } from 'ahooks';
import React from 'react';

export default () => {
const { theme, themeMode, setThemeMode } = useTheme({
localStorageKey: 'themeMode',
});

return (
<>
<div>theme: {theme}</div>
<div>themeMode: {themeMode}</div>
<button
type="button"
onClick={() => {
setThemeMode('dark');
}}
>
use dark theme
</button>
<button
type="button"
onClick={() => {
setThemeMode('light');
}}
>
use light theme
</button>
<button
type="button"
onClick={() => {
setThemeMode('system');
}}
>
follow the system
</button>
</>
);
};
36 changes: 36 additions & 0 deletions packages/hooks/src/useTheme/index.en-US.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
---
nav:
path: /hooks
---

# useTheme

This hook is used to get and set the theme, and store the `themeMode` into `localStorage`.

## Examples

### Default usage

<code src="./demo/demo1.tsx" />

## API

```typescript
const { theme, themeMode, setThemeMode } = useTheme({
localStorageKey?: string;
});
```

### Params

| Property | Description | Type | Default |
| --------------- | ----------------------------------------------------- | -------- | --------- |
| localStorageKey | The key in localStorage to store selected theme mode | `string` | `undefined` |

### Result

| Property | Description | Type | Default |
| ------------ | --------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------- |
| theme | current display theme | `"light" \| "dark"` | if themeMode is "system" then equals to system setting,otherwise equals to themeMode |
| themeMode | selected theme mode | `"light" \| "dark" \| "system"` | equals to localStorage "themeMode", otherwise equals to "system" |
| setThemeMode | select theme mode | `(mode: "light" \| "dark" \| "system") => void` | |
71 changes: 71 additions & 0 deletions packages/hooks/src/useTheme/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { useEffect, useState } from 'react';
import useMemoizedFn from '../useMemoizedFn';

export enum ThemeMode {
LIGHT = 'light',
DARK = 'dark',
SYSTEM = 'system',
}

export type ThemeModeType = `${ThemeMode}`;

export type ThemeType = 'light' | 'dark';

const matchMedia = window.matchMedia('(prefers-color-scheme: dark)');

function useCurrentTheme() {
const [theme, setTheme] = useState<ThemeType>(() => {
const init = matchMedia.matches ? ThemeMode.DARK : ThemeMode.LIGHT;
return init;
});

useEffect(() => {
const onThemeChange: MediaQueryList['onchange'] = (event) => {
if (event.matches) {
setTheme(ThemeMode.DARK);
} else {
setTheme(ThemeMode.LIGHT);
}
};

matchMedia.addEventListener('change', onThemeChange);

return () => {
matchMedia.removeEventListener('change', onThemeChange);
};
}, []);

return theme;
}

type Options = {
localStorageKey?: string;
};

export default function useTheme(options: Options = {}) {
const { localStorageKey } = options;

const [themeMode, setThemeMode] = useState<ThemeModeType>(() => {
const preferredThemeMode =
localStorageKey?.length && (localStorage.getItem(localStorageKey) as ThemeModeType | null);

return preferredThemeMode ? preferredThemeMode : ThemeMode.SYSTEM;
});

const setThemeModeWithLocalStorage = (mode: ThemeModeType) => {
setThemeMode(mode);

if (localStorageKey?.length) {
localStorage.setItem(localStorageKey, mode);
}
};

const currentTheme = useCurrentTheme();
const theme = themeMode === ThemeMode.SYSTEM ? currentTheme : themeMode;

return {
theme,
themeMode,
setThemeMode: useMemoizedFn(setThemeModeWithLocalStorage),
};
}
36 changes: 36 additions & 0 deletions packages/hooks/src/useTheme/index.zh-CN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
---
nav:
path: /hooks
---

# useTheme

获取并设置当前主题,并将 `themeMode` 存储在 `localStorage` 中。

## 代码演示

### 基础用法

<code src="./demo/demo1.tsx" />

## API

```typescript
const { theme, themeMode, setThemeMode } = useTheme({
localStorageKey?: string;
});
```

### 参数

| 参数 | 说明 | 类型 | 默认值 |
| --------------- | ------------------------------------ | -------- | --------- |
| localStorageKey | localStorage 中用于存放主题模式的键 | `string` | `undefined` |

### 返回值

|| 说明 | 类型 | 默认值 |
| ------------ | -------------- | ----------------------------------------------- | ---------------------------------------------------------------------- |
| theme | 当前显示的主题 | `"light" \| "dark"` | 若 themeMode 为 "system" 则为系统当前使用主题,否则与 themeMode 值相同 |
| themeMode | 选择的主题模式 | `"light" \| "dark" \| "system"` | 等于 localStorage "themeMode" 字段的值,否则为 "system" |
| setThemeMode | 选择主题模式 | `(mode: "light" \| "dark" \| "system") => void` | |

0 comments on commit 5ddd8c2

Please sign in to comment.