Skip to content

Commit

Permalink
✨ feat: 为 ThemeProvider 增加主题切换的能力
Browse files Browse the repository at this point in the history
  • Loading branch information
arvinxx committed Jan 9, 2023
1 parent 773b773 commit e15668a
Show file tree
Hide file tree
Showing 9 changed files with 190 additions and 159 deletions.
20 changes: 0 additions & 20 deletions src/containers/AppContainer/AntdProvider.tsx

This file was deleted.

17 changes: 8 additions & 9 deletions src/containers/AppContainer/AppContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { memo, ReactElement, ReactNode } from 'react';
import { App } from 'antd';
import { memo, ReactElement } from 'react';

import { ThemeAppearance, ThemeMode } from '@/types';

import ThemeContent, { ThemeContentProps } from './ThemeContent';
import { ThemeProvider, ThemeProviderProps } from '../ThemeProvider';
import ThemeSwitcher from './ThemeSwitcher';

export interface AppContainerProps<T, S = Record<string, string>> extends ThemeContentProps<T, S> {
export interface AppContainerProps<T, S = Record<string, string>> extends ThemeProviderProps<T, S> {
/**
* 应用的展示外观主题,只存在亮色和暗色两种
* @default light
Expand All @@ -20,10 +21,7 @@ export interface AppContainerProps<T, S = Record<string, string>> extends ThemeC
*/
themeMode?: ThemeMode;

children: ReactNode;

className?: string;
prefixCls?: string;
}

export const AppContainer: <T, S>(props: AppContainerProps<T, S>) => ReactElement | null = memo(
Expand All @@ -35,6 +33,7 @@ export const AppContainer: <T, S>(props: AppContainerProps<T, S>) => ReactElemen
themeMode,
customToken,
customStylish,
className,
...props
}) => (
<ThemeSwitcher
Expand All @@ -43,9 +42,9 @@ export const AppContainer: <T, S>(props: AppContainerProps<T, S>) => ReactElemen
appearance={appearance}
onAppearanceChange={onAppearanceChange}
>
<ThemeContent customStylish={customStylish} customToken={customToken} {...props}>
{children}
</ThemeContent>
<ThemeProvider customStylish={customStylish} customToken={customToken} {...props}>
<App className={className}>{children}</App>
</ThemeProvider>
</ThemeSwitcher>
),
);
95 changes: 0 additions & 95 deletions src/containers/AppContainer/ThemeContent.tsx

This file was deleted.

66 changes: 66 additions & 0 deletions src/containers/ThemeProvider/AntdProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { ConfigProvider, message, Modal, notification, theme } from 'antd';
import { memo, useEffect, useMemo, type FC } from 'react';

import { ThemeProviderProps } from '@/containers';
import { useThemeMode } from '@/hooks';
import { ThemeConfig } from 'antd/es/config-provider/context';

type AntdProviderProps = Pick<
ThemeProviderProps<any>,
'theme' | 'prefixCls' | 'getStaticInstance' | 'children'
>;

const AntdProvider: FC<AntdProviderProps> = memo(
({ children, theme: themeProp, prefixCls, getStaticInstance }) => {
const { appearance, isDarkMode } = useThemeMode();
const [messageInstance, messageContextHolder] = message.useMessage();
const [notificationInstance, notificationContextHolder] = notification.useNotification();
const [modalInstance, modalContextHolder] = Modal.useModal();

useEffect(() => {
getStaticInstance?.({
message: messageInstance,
modal: modalInstance,
notification: notificationInstance,
});
}, []);

// 获取 antd 主题
const antdTheme = useMemo<ThemeConfig>(() => {
const baseAlgorithm = isDarkMode ? theme.darkAlgorithm : theme.defaultAlgorithm;

let antdTheme = themeProp as ThemeConfig | undefined;

if (typeof themeProp === 'function') {
antdTheme = themeProp(appearance);
}

if (!antdTheme) {
return { algorithm: baseAlgorithm };
}

// 如果有 themeProp 说明是外部传入的 theme,需要对算法做一个合并处理,因此先把 themeProp 的算法规整为一个数组
const algoProp = !antdTheme.algorithm
? []
: antdTheme.algorithm instanceof Array
? antdTheme.algorithm
: [antdTheme.algorithm];

return {
...antdTheme,
algorithm: !antdTheme.algorithm ? baseAlgorithm : [baseAlgorithm, ...algoProp],
};
}, [themeProp, isDarkMode]);

return (
<ConfigProvider prefixCls={prefixCls} theme={antdTheme}>
{messageContextHolder}
{notificationContextHolder}
{modalContextHolder}
{children}
</ConfigProvider>
);
},
);

export default AntdProvider;
21 changes: 21 additions & 0 deletions src/containers/ThemeProvider/ThemeProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { ReactElement } from 'react';

import AntdProvider from './AntdProvider';
import TokenContainer from './TokenContainer';
import { ThemeProviderProps } from './type';

export const ThemeProvider: <T, S>(props: ThemeProviderProps<T, S>) => ReactElement | null = ({
children,
customToken,
customStylish,
theme: themeProp,
prefixCls,
}) => {
return (
<AntdProvider prefixCls={prefixCls} theme={themeProp}>
<TokenContainer customToken={customToken} customStylish={customStylish}>
{children}
</TokenContainer>
</AntdProvider>
);
};
58 changes: 58 additions & 0 deletions src/containers/ThemeProvider/TokenContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { ThemeProvider as Provider } from '@emotion/react';
import { ReactElement, useMemo } from 'react';

import { useThemeMode } from '@/hooks';
import { useAntdTheme } from '@/hooks/useAntdTheme';
import type { ThemeProviderProps } from './type';

import { Theme } from '@/types';

type TokenContainerProps<T, S = Record<string, string>> = Pick<
ThemeProviderProps<T, S>,
'children' | 'customToken' | 'customStylish'
>;

const TokenContainer: <T, S>(props: TokenContainerProps<T, S>) => ReactElement | null = ({
children,
customToken: customTokenOrFn,
customStylish: stylishOrGetStylish,
}) => {
const themeState = useThemeMode();
const { appearance, isDarkMode } = themeState;
const { stylish: antdStylish, ...token } = useAntdTheme();

// 获取 自定义 token
const customToken = useMemo(() => {
if (customTokenOrFn instanceof Function) {
return customTokenOrFn({ token, appearance, isDarkMode });
}

return customTokenOrFn;
}, [customTokenOrFn, token, appearance]);

// 获取 stylish
const customStylish = useMemo(() => {
if (stylishOrGetStylish instanceof Function) {
return stylishOrGetStylish({
token: { ...token, ...customToken },
stylish: antdStylish,
appearance,
isDarkMode,
});
}
return stylishOrGetStylish;
}, [stylishOrGetStylish, token, customToken, antdStylish, appearance]);

const stylish = { ...customStylish, ...antdStylish };

const theme: Theme = {
...token,
...customToken,
stylish,
...themeState,
};

return <Provider theme={theme}>{children}</Provider>;
};

export default TokenContainer;
36 changes: 2 additions & 34 deletions src/containers/ThemeProvider/index.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,2 @@
import { useAntdTheme, useThemeMode } from '@/hooks';
import { Theme } from '@/types';
import { ThemeProvider as Provider } from '@emotion/react';
import { memo, PropsWithChildren, ReactElement } from 'react';

export interface ThemeProviderProps<CT, CS = Record<string, string>> {
/**
* 自定义 token, 可在 antd v5 token 规范基础上扩展和新增自己需要的 token
*/
customToken?: CT;
/**
* 自定义 stylish 可以自行扩展和新增自己需要的复合样式
* @internal
*/
customStylish?: CS;
}

export const ThemeProvider: <T = Record<string, string>, S = Record<string, string>>(
props: PropsWithChildren<ThemeProviderProps<T, S>>,
) => ReactElement | null = memo(({ children, customToken = {}, customStylish = {} }) => {
const { stylish: antdStylish, ...antdToken } = useAntdTheme();
const themeState = useThemeMode();

const stylish = { ...customStylish, ...antdStylish };

const theme: Theme = {
...antdToken,
...customToken,
stylish,
...themeState,
};

return <Provider theme={theme}>{children}</Provider>;
});
export * from './ThemeProvider';
export * from './type';
34 changes: 34 additions & 0 deletions src/containers/ThemeProvider/type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { GetAntdThemeConfig, GetCustomStylish, GetCustomToken } from '@/types';
import { ThemeConfig } from 'antd/es/config-provider/context';
import { MessageInstance } from 'antd/es/message/interface';
import { ModalStaticFunctions } from 'antd/es/modal/confirm';
import { NotificationInstance } from 'antd/es/notification/interface';
import { ReactNode } from 'react';

export interface ThemeProviderProps<T, S = Record<string, string>> {
children: ReactNode;
/**
* 自定义 Token
*/
customToken?: T | GetCustomToken<T>;
/**
* 自定义 Stylish
*/
customStylish?: S | GetCustomStylish<S>;
/**
* 直接传入 antd 主题,或者传入一个函数,根据当前的主题模式返回对应的主题
*/
theme?: ThemeConfig | GetAntdThemeConfig;
prefixCls?: string;
/**
* 从 ThemeProvider 中获取静态方法的实例对象
* @param instances
*/
getStaticInstance?: (instances: StaticInstance) => void;
}

export interface StaticInstance {
message: MessageInstance;
notification: NotificationInstance;
modal: Omit<ModalStaticFunctions, 'warn'>;
}
2 changes: 1 addition & 1 deletion tests/containers/AppContainer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ describe('AppContainer', () => {

const { result: dark } = renderHook(useTheme, { wrapper: Darker });
expect(dark.current.customColor).toEqual('#000');
expect(dark.current.customBrandColor).toEqual('#1677ff');
expect(dark.current.customBrandColor).toEqual('#1668dc');
});

it('注入自定义 stylish', () => {
Expand Down

0 comments on commit e15668a

Please sign in to comment.