chore: improve ThemeSwitcher implementation (#39398)

* docs: theme should be in searchParams

* chore: use locale text

* chore: fix searchParams usage

* fix: compact can use with dark/light theme

* chore: fix lint
This commit is contained in:
afc163 2022-12-08 17:44:49 +08:00 committed by GitHub
parent 33deda5e38
commit cf476d9477
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 71 additions and 126 deletions

View File

@ -3,7 +3,7 @@ import JsonML from 'jsonml.js/lib/utils';
import toReactComponent from 'jsonml-to-react-element'; import toReactComponent from 'jsonml-to-react-element';
import Prism from 'prismjs'; import Prism from 'prismjs';
import 'prismjs/components/prism-typescript'; import 'prismjs/components/prism-typescript';
import { useLocation, useIntl, type IPreviewerProps } from 'dumi'; import { useLocation, useSearchParams, useIntl, type IPreviewerProps } from 'dumi';
import { ping } from '../../utils'; import { ping } from '../../utils';
let pingDeferrer: PromiseLike<boolean>; let pingDeferrer: PromiseLike<boolean>;
@ -36,6 +36,7 @@ export default function fromDumiProps<P extends object>(
const hoc = function DumiPropsAntdPreviewer(props: IPreviewerProps) { const hoc = function DumiPropsAntdPreviewer(props: IPreviewerProps) {
const showRiddleButton = useShowRiddleButton(); const showRiddleButton = useShowRiddleButton();
const location = useLocation(); const location = useLocation();
const [searchParams] = useSearchParams();
const { asset, children, demoUrl, expand, description = '', ...meta } = props; const { asset, children, demoUrl, expand, description = '', ...meta } = props;
const intl = useIntl(); const intl = useIntl();
const entryCode = asset.dependencies['index.tsx'].value; const entryCode = asset.dependencies['index.tsx'].value;
@ -88,8 +89,7 @@ export default function fromDumiProps<P extends object>(
expand, expand,
// FIXME: confirm is there has any case? // FIXME: confirm is there has any case?
highlightedStyle: '', highlightedStyle: '',
// FIXME: dumi support usePrefersColor theme: searchParams.get('theme'),
theme: 'light',
} as P; } as P;
return <WrappedComponent {...transformedProps} />; return <WrappedComponent {...transformedProps} />;

View File

@ -4,7 +4,7 @@ import Icon from '@ant-design/icons';
const ThemeIcon: React.FC<{ className?: string }> = (props) => { const ThemeIcon: React.FC<{ className?: string }> = (props) => {
const SVGIcon = React.useCallback( const SVGIcon = React.useCallback(
() => ( () => (
<svg width={21} height={21} viewBox="0 0 21 21" fill="currentColor" {...props}> <svg width={20} height={20} viewBox="0 0 24 24" fill="currentColor" {...props}>
<g fillRule="evenodd"> <g fillRule="evenodd">
<g fillRule="nonzero"> <g fillRule="nonzero">
<path d="M7.02 3.635l12.518 12.518a1.863 1.863 0 010 2.635l-1.317 1.318a1.863 1.863 0 01-2.635 0L3.068 7.588A2.795 2.795 0 117.02 3.635zm2.09 14.428a.932.932 0 110 1.864.932.932 0 010-1.864zm-.043-9.747L7.75 9.635l9.154 9.153 1.318-1.317-9.154-9.155zM3.52 12.473c.514 0 .931.417.931.931v.932h.932a.932.932 0 110 1.864h-.932v.931a.932.932 0 01-1.863 0l-.001-.931h-.93a.932.932 0 010-1.864h.93v-.932c0-.514.418-.931.933-.931zm15.374-3.727a1.398 1.398 0 110 2.795 1.398 1.398 0 010-2.795zM4.385 4.953a.932.932 0 000 1.317l2.046 2.047L7.75 7 5.703 4.953a.932.932 0 00-1.318 0zM14.701.36a.932.932 0 01.931.932v.931h.932a.932.932 0 010 1.864h-.933l.001.932a.932.932 0 11-1.863 0l-.001-.932h-.93a.932.932 0 110-1.864h.93v-.931a.932.932 0 01.933-.932z" /> <path d="M7.02 3.635l12.518 12.518a1.863 1.863 0 010 2.635l-1.317 1.318a1.863 1.863 0 01-2.635 0L3.068 7.588A2.795 2.795 0 117.02 3.635zm2.09 14.428a.932.932 0 110 1.864.932.932 0 010-1.864zm-.043-9.747L7.75 9.635l9.154 9.153 1.318-1.317-9.154-9.155zM3.52 12.473c.514 0 .931.417.931.931v.932h.932a.932.932 0 110 1.864h-.932v.931a.932.932 0 01-1.863 0l-.001-.931h-.93a.932.932 0 010-1.864h.93v-.932c0-.514.418-.931.933-.931zm15.374-3.727a1.398 1.398 0 110 2.795 1.398 1.398 0 010-2.795zM4.385 4.953a.932.932 0 000 1.317l2.046 2.047L7.75 7 5.703 4.953a.932.932 0 00-1.318 0zM14.701.36a.932.932 0 01.931.932v.931h.932a.932.932 0 010 1.864h-.933l.001.932a.932.932 0 11-1.863 0l-.001-.932h-.93a.932.932 0 110-1.864h.93v-.931a.932.932 0 01.933-.932z" />

View File

@ -1,68 +1,51 @@
import React from 'react'; import React from 'react';
import { FloatButton, theme } from 'antd'; import { FloatButton } from 'antd';
import { FormattedMessage } from 'dumi';
import { DarkTheme, Light, CompactTheme } from 'antd-token-previewer/es/icons'; import { DarkTheme, Light, CompactTheme } from 'antd-token-previewer/es/icons';
import ThemeIcon from './ThemeIcon'; import ThemeIcon from './ThemeIcon';
const { defaultAlgorithm, darkAlgorithm, compactAlgorithm } = theme; export type ThemeName = 'light' | 'dark' | 'compact';
export type ThemeSwitchProps = { export type ThemeSwitchProps = {
value: typeof defaultAlgorithm[]; value?: ThemeName[];
onChange: (value: typeof defaultAlgorithm[]) => void; onChange: (value: ThemeName[]) => void;
}; };
const ThemeSwitch: React.FC<ThemeSwitchProps> = ({ value, onChange }) => { const ThemeSwitch: React.FC<ThemeSwitchProps> = ({ value, onChange }) => (
const handleLightSwitch = () => { <FloatButton.Group trigger="click" icon={<ThemeIcon />}>
let newValue = [...value]; <FloatButton
if (value.includes(darkAlgorithm)) { icon={<Light />}
newValue = newValue.filter((item) => item !== darkAlgorithm); type={!value.includes('dark') ? 'primary' : 'default'}
} onClick={() => {
if (!value.includes(defaultAlgorithm)) { if (value.includes('dark')) {
newValue.unshift(defaultAlgorithm); onChange(value.filter((theme) => theme !== 'dark'));
} }
onChange(newValue); }}
}; tooltip={<FormattedMessage id="app.theme.switch.default" />}
/>
const handleDarkSwitch = () => { <FloatButton
let newValue = [...value]; icon={<DarkTheme />}
if (value.includes(defaultAlgorithm)) { type={value.includes('dark') ? 'primary' : 'default'}
newValue = newValue.filter((item) => item !== defaultAlgorithm); onClick={() => {
} if (!value.includes('dark')) {
if (!value.includes(darkAlgorithm)) { onChange([...value, 'dark']);
newValue.push(darkAlgorithm); }
} }}
onChange(newValue); tooltip={<FormattedMessage id="app.theme.switch.dark" />}
}; />
<FloatButton
const handleCompactSwitch = () => { icon={<CompactTheme />}
if (value.includes(compactAlgorithm)) { type={value.includes('compact') ? 'primary' : 'default'}
onChange(value.filter((item) => item !== compactAlgorithm)); onClick={() => {
} else { if (value.includes('compact')) {
onChange([...value, compactAlgorithm]); onChange(value.filter((theme) => theme !== 'compact'));
} } else {
}; onChange([...value, 'compact']);
}
return ( }}
<FloatButton.Group trigger="click" icon={<ThemeIcon />}> tooltip={<FormattedMessage id="app.theme.switch.compact" />}
<FloatButton />
icon={<Light />} </FloatButton.Group>
type={value.includes(defaultAlgorithm) ? 'primary' : 'default'} );
onClick={handleLightSwitch}
tooltip="Light"
/>
<FloatButton
icon={<DarkTheme />}
type={value.includes(darkAlgorithm) ? 'primary' : 'default'}
onClick={handleDarkSwitch}
tooltip="Dark"
/>
<FloatButton
icon={<CompactTheme />}
type={value.includes(compactAlgorithm) ? 'primary' : 'default'}
onClick={handleCompactSwitch}
tooltip="Compact"
/>
</FloatButton.Group>
);
};
export default ThemeSwitch; export default ThemeSwitch;

View File

@ -1,9 +1,9 @@
import React, { startTransition, useLayoutEffect } from 'react'; import React from 'react';
import { useOutlet } from 'dumi'; import { useOutlet, useSearchParams } from 'dumi';
import { ConfigProvider, theme as antdTheme } from 'antd'; import { ConfigProvider, theme as antdTheme } from 'antd';
import type { ThemeConfig } from 'antd/es/config-provider/context';
import { createCache, StyleProvider } from '@ant-design/cssinjs'; import { createCache, StyleProvider } from '@ant-design/cssinjs';
import ThemeSwitch from '../common/ThemeSwitch'; import ThemeSwitch from '../common/ThemeSwitch';
import type { ThemeName } from '../common/ThemeSwitch';
import useLocation from '../../hooks/useLocation'; import useLocation from '../../hooks/useLocation';
const styleCache = createCache(); const styleCache = createCache();
@ -11,77 +11,39 @@ if (typeof global !== 'undefined') {
(global as any).styleCache = styleCache; (global as any).styleCache = styleCache;
} }
const ANT_DESIGN_SITE_THEME = 'antd-site-theme'; const getAlgorithm = (themes: ThemeName[]) =>
(themes || []).map((theme) => {
const getAlgorithm = (theme: string) => { if (theme === 'dark') {
if (theme === 'dark') { return antdTheme.darkAlgorithm;
return antdTheme.darkAlgorithm; }
} if (theme === 'compact') {
if (theme === 'compact') { return antdTheme.compactAlgorithm;
return antdTheme.compactAlgorithm; }
} return antdTheme.defaultAlgorithm;
return antdTheme.defaultAlgorithm; });
};
const getThemeString = (algorithm: typeof antdTheme.defaultAlgorithm) => {
if (algorithm === antdTheme.darkAlgorithm) {
return 'dark';
}
if (algorithm === antdTheme.compactAlgorithm) {
return 'compact';
}
return 'light';
};
const GlobalLayout: React.FC = () => { const GlobalLayout: React.FC = () => {
const outlet = useOutlet(); const outlet = useOutlet();
const { pathname } = useLocation(); const { pathname } = useLocation();
const [searchParams, setSearchParams] = useSearchParams();
const [theme, setTheme] = React.useState<ThemeConfig>({ const theme = searchParams.getAll('theme') as unknown as ThemeName[];
algorithm: [antdTheme.defaultAlgorithm], const handleThemeChange = (value: ThemeName[]) => {
}); setSearchParams({
...searchParams,
const handleThemeChange = (newTheme: ThemeConfig, ignoreAlgorithm: boolean = true) => { theme: value,
const nextTheme = { ...newTheme }; });
if (ignoreAlgorithm) {
nextTheme.algorithm = theme.algorithm;
}
setTheme(nextTheme);
localStorage.setItem(
ANT_DESIGN_SITE_THEME,
JSON.stringify(nextTheme, (key, value) => {
if (key === 'algorithm') {
return Array.isArray(value) ? value.map((item) => getThemeString(item)) : ['light'];
}
return value;
}),
);
}; };
useLayoutEffect(() => {
const localTheme = localStorage.getItem(ANT_DESIGN_SITE_THEME);
if (localTheme) {
const themeConfig = JSON.parse(localTheme);
if (themeConfig.algorithm) {
themeConfig.algorithm = themeConfig.algorithm.map((item: string) => getAlgorithm(item));
} else {
themeConfig.algorithm = [antdTheme.defaultAlgorithm];
}
startTransition(() => {
setTheme(themeConfig);
});
}
}, []);
return ( return (
<StyleProvider cache={styleCache}> <StyleProvider cache={styleCache}>
<ConfigProvider theme={theme}> <ConfigProvider
theme={{
algorithm: getAlgorithm(theme),
}}
>
{outlet} {outlet}
{!pathname.startsWith('/~demos') && ( {!pathname.startsWith('/~demos') && (
<ThemeSwitch <ThemeSwitch value={theme} onChange={handleThemeChange} />
value={theme.algorithm as []}
onChange={(value) => handleThemeChange({ ...theme, algorithm: value }, false)}
/>
)} )}
</ConfigProvider> </ConfigProvider>
</StyleProvider> </StyleProvider>