Skip to content

Commit

Permalink
Merge branch 'master' into biome
Browse files Browse the repository at this point in the history
  • Loading branch information
crazylxr authored Sep 25, 2024
2 parents bbd555c + 5ddd8c2 commit 9c4f949
Show file tree
Hide file tree
Showing 10 changed files with 239 additions and 2 deletions.
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,
};
4 changes: 3 additions & 1 deletion packages/hooks/src/useControllableValue/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ function useControllableValue<T = any>(
props?: Props,
options?: Options<T>,
): [T, (v: SetStateAction<T>, ...args: any[]) => void];
function useControllableValue<T = any>(props: Props = {}, options: Options<T> = {}) {
function useControllableValue<T = any>(defaultProps: Props, options: Options<T> = {}) {
const props = defaultProps ?? {};

const {
defaultValue,
defaultValuePropName = 'defaultValue',
Expand Down
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 9c4f949

Please sign in to comment.