From ebf52122a29d02d6e9efa56a273dfb5544fde7ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Thu, 24 Mar 2022 18:44:42 +0800 Subject: [PATCH] refactor: Button use full token & remove all `withPrefixCls` & support overrides (#34690) * chore: init * chore: button token * chore: rm all withPrefixCls * feat: overrides work * feat: theme can be nest * test: Update snapshot * fix: memo logic --- components/_util/theme/index.tsx | 27 +- components/_util/theme/interface.ts | 25 +- components/_util/theme/themes/default.ts | 12 +- .../__snapshots__/demo-extend.test.ts.snap | 32 ++ .../__tests__/__snapshots__/demo.test.ts.snap | 32 ++ components/button/demo/debug-token.md | 42 +++ components/button/style/index.tsx | 276 ++++++++++-------- components/config-provider/context.tsx | 9 + components/config-provider/hooks/useTheme.ts | 55 ++++ components/config-provider/index.tsx | 34 +-- components/switch/style/index.tsx | 2 +- package.json | 4 +- .../template/Layout/DynamicTheme/index.tsx | 5 + 13 files changed, 378 insertions(+), 177 deletions(-) create mode 100644 components/button/demo/debug-token.md create mode 100644 components/config-provider/hooks/useTheme.ts diff --git a/components/_util/theme/index.tsx b/components/_util/theme/index.tsx index 8d3ca9aaed..77869031da 100644 --- a/components/_util/theme/index.tsx +++ b/components/_util/theme/index.tsx @@ -1,11 +1,5 @@ import React from 'react'; -import { - CSSInterpolation, - CSSObject, - Theme, - useCacheToken, - useStyleRegister, -} from '@ant-design/cssinjs'; +import { CSSInterpolation, Theme, useCacheToken, useStyleRegister } from '@ant-design/cssinjs'; import defaultSeedToken, { derivative as defaultDerivative } from './themes/default'; import version from '../../version'; import { resetComponent, resetIcon, clearFix } from './util'; @@ -21,7 +15,7 @@ import { slideRightIn, slideRightOut, } from './util/slide'; -import { PresetColors } from './interface'; +import { GlobalToken, PresetColors } from './interface'; import type { SeedToken, DerivativeToken, @@ -69,7 +63,7 @@ export const DesignTokenContext = React.createContext<{ }); // ================================== Hook ================================== -export function useToken(): [Theme, AliasToken, string] { +export function useToken(): [Theme, GlobalToken, string] { const { token: rootDesignToken, theme = defaultTheme, @@ -79,7 +73,7 @@ export function useToken(): [Theme, AliasToken, stri const salt = `${version}-${hashed || ''}`; - const [token, hashId] = useCacheToken( + const [token, hashId] = useCacheToken( theme, [defaultSeedToken, rootDesignToken], { @@ -98,16 +92,3 @@ export type GenerateStyle ReturnType; - -// ================================== Util ================================== -export function withPrefix( - style: CSSObject, - prefixCls: string, - additionalClsList: string[] = [], -): CSSObject { - const fullClsList = [prefixCls, ...additionalClsList].filter(cls => cls).map(cls => `.${cls}`); - - return { - [fullClsList.join('')]: style, - }; -} diff --git a/components/_util/theme/interface.ts b/components/_util/theme/interface.ts index 16131b6abd..c0ed72ba8a 100644 --- a/components/_util/theme/interface.ts +++ b/components/_util/theme/interface.ts @@ -1,4 +1,5 @@ import * as React from 'react'; +import type { ComponentToken as ButtonComponentToken } from '../../button/style'; export const PresetColors = [ 'blue', @@ -27,10 +28,15 @@ export type ColorPalettes = { }; export interface OverrideToken { - derivative: Partial; - [componentName: string]: object; // FIXME: tmp of component token + derivative?: Partial; + + // Customize component + button?: ButtonComponentToken; } +/** Final token which contains the components level override */ +export type GlobalToken = AliasToken & Omit; + // ====================================================================== // == Seed Token == // ====================================================================== @@ -91,6 +97,9 @@ export interface SeedToken extends PresetColorType { // πŸ”₯πŸ”₯πŸ”₯πŸ”₯πŸ”₯πŸ”₯πŸ”₯ DO NOT MODIFY THIS. PLEASE CONTACT DESIGNER. πŸ”₯πŸ”₯πŸ”₯πŸ”₯πŸ”₯πŸ”₯πŸ”₯ export interface DerivativeToken extends SeedToken, ColorPalettes { // Color + /** Used for DefaultButton, Switch which has default outline */ + colorDefaultOutline: string; + colorPrimaryHover: string; colorPrimaryActive: string; colorPrimaryOutline: string; @@ -162,8 +171,18 @@ export interface DerivativeToken extends SeedToken, ColorPalettes { // ====================================================================== // == Alias Token == // ====================================================================== +// FIXME: DerivativeToken should part pick +type OmitDerivativeKey = + | 'colorText2' + | 'colorTextBelow' + | 'colorTextBelow2' + | 'colorTextBelow3' + | 'colorBg2' + | 'colorBgBelow' + | 'colorBgBelow2'; + // πŸ”₯πŸ”₯πŸ”₯πŸ”₯πŸ”₯πŸ”₯πŸ”₯ DO NOT MODIFY THIS. PLEASE CONTACT DESIGNER. πŸ”₯πŸ”₯πŸ”₯πŸ”₯πŸ”₯πŸ”₯πŸ”₯ -export interface AliasToken extends DerivativeToken { +export interface AliasToken extends Omit { // Font fontSizeSM: number; fontSize: number; diff --git a/components/_util/theme/themes/default.ts b/components/_util/theme/themes/default.ts index 6cab5792a5..9d0249b6da 100644 --- a/components/_util/theme/themes/default.ts +++ b/components/_util/theme/themes/default.ts @@ -64,6 +64,10 @@ export function derivative(token: SeedToken): DerivativeToken { const fontSizes = getFontSizes(fontSizeBase); + const colorBg2 = new TinyColor({ h: 0, s: 0, v: 98 }).toHexString(); + const colorBgBelow = new TinyColor({ h: 0, s: 0, v: 98 }).toHexString(); + const colorBgBelow2 = new TinyColor({ h: 0, s: 0, v: 96 }).toHexString(); + return { ...token, ...colorPalettes, @@ -96,9 +100,11 @@ export function derivative(token: SeedToken): DerivativeToken { radiusXL: radiusBase * 4, // color - colorBg2: new TinyColor({ h: 0, s: 0, v: 98 }).toHexString(), - colorBgBelow: new TinyColor({ h: 0, s: 0, v: 98 }).toHexString(), - colorBgBelow2: new TinyColor({ h: 0, s: 0, v: 96 }).toHexString(), + colorBg2, + colorBgBelow, + colorBgBelow2, + + colorDefaultOutline: colorBgBelow2, colorPrimaryActive: primaryColors[6], colorPrimaryHover: primaryColors[4], diff --git a/components/button/__tests__/__snapshots__/demo-extend.test.ts.snap b/components/button/__tests__/__snapshots__/demo-extend.test.ts.snap index ce9310f0bf..e63e2879c0 100644 --- a/components/button/__tests__/__snapshots__/demo-extend.test.ts.snap +++ b/components/button/__tests__/__snapshots__/demo-extend.test.ts.snap @@ -128,6 +128,38 @@ Array [ ] `; +exports[`renders ./components/button/demo/debug-token.md extend context correctly 1`] = ` +
+
+ +
+
+ +
+
+`; + exports[`renders ./components/button/demo/disabled.md extend context correctly 1`] = ` Array [ + +
+ +
+ +`; + exports[`renders ./components/button/demo/disabled.md correctly 1`] = ` Array [ + + + + + + , + mountNode, +); +``` diff --git a/components/button/style/index.tsx b/components/button/style/index.tsx index b6937258b0..c458cace2a 100644 --- a/components/button/style/index.tsx +++ b/components/button/style/index.tsx @@ -2,48 +2,61 @@ import { CSSInterpolation, CSSObject } from '@ant-design/cssinjs'; import { TinyColor } from '@ctrl/tinycolor'; import { - DerivativeToken, + AliasToken, UseComponentStyleResult, useStyleRegister, useToken, - withPrefix, + GenerateStyle, } from '../../_util/theme'; +/** Component only token. Which will handle additional calculation of alias token */ +export interface ComponentToken { + colorBgTextHover: string; + colorBgTextActive: string; +} + +interface ButtonToken extends AliasToken, ComponentToken { + btnCls: string; + iconPrefixCls: string; +} + // ============================== Shared ============================== -const genSharedButtonStyle = ( - prefixCls: string, - iconPrefixCls: string, - token: DerivativeToken, -): CSSObject => ({ - outline: 'none', - position: 'relative', - display: 'inline-block', - fontWeight: 400, - whiteSpace: 'nowrap', - textAlign: 'center', - backgroundImage: 'none', - backgroundColor: 'transparent', - border: `${token.controlLineWidth}px ${token.controlLineType} transparent`, - cursor: 'pointer', - transition: `all ${token.motionDurationSlow} ${token.motionEaseInOut}`, - userSelect: 'none', - touchAction: 'manipulation', - lineHeight: token.lineHeight, - color: token.colorText, +const genSharedButtonStyle: GenerateStyle = (token): CSSObject => { + const { btnCls, iconPrefixCls } = token; - '> span': { - display: 'inline-block', - }, + return { + [btnCls]: { + outline: 'none', + position: 'relative', + display: 'inline-block', + fontWeight: 400, + whiteSpace: 'nowrap', + textAlign: 'center', + backgroundImage: 'none', + backgroundColor: 'transparent', + border: `${token.controlLineWidth}px ${token.controlLineType} transparent`, + cursor: 'pointer', + transition: `all ${token.motionDurationSlow} ${token.motionEaseInOut}`, + userSelect: 'none', + touchAction: 'manipulation', + lineHeight: token.lineHeight, + color: token.colorText, - // Leave a space between icon and text. - [`> .${iconPrefixCls} + span, > span + .${iconPrefixCls}`]: { - marginInlineStart: token.marginXS, - }, + '> span': { + display: 'inline-block', + }, - [`&.${prefixCls}-block`]: { - width: '100%', - }, -}); + // Leave a space between icon and text. + [`> .${iconPrefixCls} + span, > span + .${iconPrefixCls}`]: { + marginInlineStart: token.marginXS, + }, + + [`&${btnCls}-block`]: { + width: '100%', + }, + }, + }; +}; const genHoverActiveButtonStyle = (hoverStyle: CSSObject, activeStyle: CSSObject): CSSObject => ({ '&:not(:disabled)': { @@ -53,14 +66,14 @@ const genHoverActiveButtonStyle = (hoverStyle: CSSObject, activeStyle: CSSObject }); // ============================== Shape =============================== -const genCircleButtonStyle = (token: DerivativeToken): CSSObject => ({ +const genCircleButtonStyle: GenerateStyle = token => ({ minWidth: token.controlHeight, paddingInlineStart: 0, paddingInlineEnd: 0, borderRadius: '50%', }); -const genRoundButtonStyle = (token: DerivativeToken): CSSObject => ({ +const genRoundButtonStyle: GenerateStyle = token => ({ borderRadius: token.controlHeight, paddingInlineStart: token.controlHeight / 2, paddingInlineEnd: token.controlHeight / 2, @@ -69,16 +82,17 @@ const genRoundButtonStyle = (token: DerivativeToken): CSSObject => ({ // =============================== Type =============================== const genGhostButtonStyle = ( - prefixCls: string, + btnCls: string, textColor: string | false, borderColor: string | false, textColorDisabled: string | false, borderColorDisabled: string | false, ): CSSObject => ({ - [`&.${prefixCls}-background-ghost`]: { + [`&${btnCls}-background-ghost`]: { color: textColor || undefined, backgroundColor: 'transparent', borderColor: borderColor || undefined, + boxShadow: 'none', '&:disabled': { cursor: 'not-allowed', @@ -88,7 +102,7 @@ const genGhostButtonStyle = ( }, }); -const genSolidDisabledButtonStyle = (token: DerivativeToken): CSSObject => ({ +const genSolidDisabledButtonStyle: GenerateStyle = token => ({ '&:disabled': { cursor: 'not-allowed', borderColor: token.colorBorder, @@ -98,13 +112,13 @@ const genSolidDisabledButtonStyle = (token: DerivativeToken): CSSObject => ({ }, }); -const genSolidButtonStyle = (token: DerivativeToken): CSSObject => ({ +const genSolidButtonStyle: GenerateStyle = token => ({ borderRadius: token.controlRadius, ...genSolidDisabledButtonStyle(token), }); -const genPureDisabledButtonStyle = (token: DerivativeToken): CSSObject => ({ +const genPureDisabledButtonStyle: GenerateStyle = token => ({ '&:disabled': { cursor: 'not-allowed', color: token.colorTextDisabled, @@ -112,13 +126,13 @@ const genPureDisabledButtonStyle = (token: DerivativeToken): CSSObject => ({ }); // Type: Default -const genDefaultButtonStyle = (prefixCls: string, token: DerivativeToken): CSSObject => ({ +const genDefaultButtonStyle: GenerateStyle = token => ({ ...genSolidButtonStyle(token), backgroundColor: token.colorBgComponent, borderColor: token.colorBorder, - boxShadow: '0 2px 0 rgba(0, 0, 0, 0.015)', + boxShadow: `0 ${token.controlOutlineWidth}px 0 ${token.colorDefaultOutline}`, ...genHoverActiveButtonStyle( { @@ -132,14 +146,14 @@ const genDefaultButtonStyle = (prefixCls: string, token: DerivativeToken): CSSOb ), ...genGhostButtonStyle( - prefixCls, + token.btnCls, token.colorBgComponent, token.colorBgComponent, token.colorTextDisabled, token.colorBorder, ), - [`&.${prefixCls}-dangerous`]: { + [`&${token.btnCls}-dangerous`]: { color: token.colorError, borderColor: token.colorError, @@ -155,7 +169,7 @@ const genDefaultButtonStyle = (prefixCls: string, token: DerivativeToken): CSSOb ), ...genGhostButtonStyle( - prefixCls, + token.btnCls, token.colorError, token.colorError, token.colorTextDisabled, @@ -166,13 +180,13 @@ const genDefaultButtonStyle = (prefixCls: string, token: DerivativeToken): CSSOb }); // Type: Primary -const genPrimaryButtonStyle = (prefixCls: string, token: DerivativeToken): CSSObject => ({ +const genPrimaryButtonStyle: GenerateStyle = token => ({ ...genSolidButtonStyle(token), color: '#FFF', backgroundColor: token.colorPrimary, - boxShadow: '0 2px 0 rgba(0, 0, 0, 0.045)', + boxShadow: `0 ${token.controlOutlineWidth}px 0 ${token.colorPrimaryOutline}`, ...genHoverActiveButtonStyle( { @@ -184,15 +198,16 @@ const genPrimaryButtonStyle = (prefixCls: string, token: DerivativeToken): CSSOb ), ...genGhostButtonStyle( - prefixCls, + token.btnCls, token.colorPrimary, token.colorPrimary, token.colorTextDisabled, token.colorBorder, ), - [`&.${prefixCls}-dangerous`]: { + [`&${token.btnCls}-dangerous`]: { backgroundColor: token.colorError, + boxShadow: `0 ${token.controlOutlineWidth}px 0 ${token.colorErrorOutline}`, ...genHoverActiveButtonStyle( { @@ -204,7 +219,7 @@ const genPrimaryButtonStyle = (prefixCls: string, token: DerivativeToken): CSSOb ), ...genGhostButtonStyle( - prefixCls, + token.btnCls, token.colorError, token.colorError, token.colorTextDisabled, @@ -215,14 +230,14 @@ const genPrimaryButtonStyle = (prefixCls: string, token: DerivativeToken): CSSOb }); // Type: Dashed -const genDashedButtonStyle = (prefixCls: string, token: DerivativeToken): CSSObject => ({ - ...genDefaultButtonStyle(prefixCls, token), +const genDashedButtonStyle: GenerateStyle = token => ({ + ...genDefaultButtonStyle(token), borderStyle: 'dashed', }); // Type: Link -const genLinkButtonStyle = (prefixCls: string, token: DerivativeToken): CSSObject => ({ +const genLinkButtonStyle: GenerateStyle = token => ({ color: token.colorLink, ...genHoverActiveButtonStyle( @@ -236,7 +251,7 @@ const genLinkButtonStyle = (prefixCls: string, token: DerivativeToken): CSSObjec ...genPureDisabledButtonStyle(token), - [`&.${prefixCls}-dangerous`]: { + [`&${token.btnCls}-dangerous`]: { color: token.colorError, ...genHoverActiveButtonStyle( @@ -253,59 +268,55 @@ const genLinkButtonStyle = (prefixCls: string, token: DerivativeToken): CSSObjec }); // Type: Text -const genTextButtonStyle = (prefixCls: string, token: DerivativeToken): CSSObject => { - const backgroundColor = new TinyColor({ r: 0, g: 0, b: 0, a: 0.018 }); +const genTextButtonStyle: GenerateStyle = token => ({ + borderRadius: token.controlRadius, - return { - ...genHoverActiveButtonStyle( - { - backgroundColor: backgroundColor.toRgbString(), - }, - { - backgroundColor: backgroundColor - .clone() - .setAlpha(backgroundColor.getAlpha() * 1.5) - .toRgbString(), - }, - ), + ...genHoverActiveButtonStyle( + { + backgroundColor: token.colorBgTextHover, + }, + { + backgroundColor: token.colorBgTextActive, + }, + ), + + ...genPureDisabledButtonStyle(token), + + [`&${token.btnCls}-dangerous`]: { + color: token.colorError, ...genPureDisabledButtonStyle(token), + }, +}); - [`&.${prefixCls}-dangerous`]: { - color: token.colorError, +const genTypeButtonStyle: GenerateStyle = token => { + const { btnCls } = token; - ...genPureDisabledButtonStyle(token), - }, + return { + [`${btnCls}-default`]: genDefaultButtonStyle(token), + [`${btnCls}-primary`]: genPrimaryButtonStyle(token), + [`${btnCls}-dashed`]: genDashedButtonStyle(token), + [`${btnCls}-link`]: genLinkButtonStyle(token), + [`${btnCls}-text`]: genTextButtonStyle(token), }; }; -const genTypeButtonStyle = (prefixCls: string, token: DerivativeToken): CSSInterpolation => [ - withPrefix(genDefaultButtonStyle(prefixCls, token), `${prefixCls}-default`, []), - withPrefix(genPrimaryButtonStyle(prefixCls, token), `${prefixCls}-primary`, []), - withPrefix(genDashedButtonStyle(prefixCls, token), `${prefixCls}-dashed`, []), - withPrefix(genLinkButtonStyle(prefixCls, token), `${prefixCls}-link`, []), - withPrefix(genTextButtonStyle(prefixCls, token), `${prefixCls}-text`, []), -]; - // =============================== Size =============================== -const genSizeButtonStyle = ( - prefixCls: string, - iconPrefixCls: string, - sizePrefixCls: string, - token: DerivativeToken, -): CSSInterpolation => { +const genSizeButtonStyle = (token: ButtonToken, sizePrefixCls: string = ''): CSSInterpolation => { + const { btnCls, iconPrefixCls } = token; + const paddingVertical = Math.max( 0, (token.controlHeight - token.fontSize * token.lineHeight) / 2 - token.controlLineWidth, ); const paddingHorizontal = token.padding - token.controlLineWidth; - const iconOnlyCls = `.${prefixCls}-icon-only`; + const iconOnlyCls = `${btnCls}-icon-only`; return [ // Size - withPrefix( - { + { + [`${btnCls}${sizePrefixCls}`]: { fontSize: token.fontSize, height: token.controlHeight, padding: `${paddingVertical}px ${paddingHorizontal}px`, @@ -321,61 +332,51 @@ const genSizeButtonStyle = ( }, // Loading - [`&.${prefixCls}-loading`]: { + [`&${btnCls}-loading`]: { opacity: 0.65, cursor: 'default', }, - [`.${prefixCls}-loading-icon`]: { + [`${btnCls}-loading-icon`]: { transition: `width ${token.motionDurationSlow} ${token.motionEaseInOut}, opacity ${token.motionDurationSlow} ${token.motionEaseInOut}`, }, - [`&:not(${iconOnlyCls}) .${prefixCls}-loading-icon > .${iconPrefixCls}`]: { + [`&:not(${iconOnlyCls}) ${btnCls}-loading-icon > .${iconPrefixCls}`]: { marginInlineEnd: token.marginXS, }, }, - prefixCls, - [sizePrefixCls], - ), + }, // Shape - patch prefixCls again to override solid border radius style - withPrefix(genCircleButtonStyle(token), `${prefixCls}-circle`, [prefixCls, sizePrefixCls]), - withPrefix(genRoundButtonStyle(token), `${prefixCls}-round`, [prefixCls, sizePrefixCls]), + { + [`${btnCls}${btnCls}-circle${sizePrefixCls}`]: genCircleButtonStyle(token), + }, + { + [`${btnCls}${btnCls}-round${sizePrefixCls}`]: genRoundButtonStyle(token), + }, ]; }; -const genSizeBaseButtonStyle = ( - prefixCls: string, - iconPrefixCls: string, - token: DerivativeToken, -): CSSInterpolation => genSizeButtonStyle(prefixCls, iconPrefixCls, '', token); +const genSizeBaseButtonStyle: GenerateStyle = token => genSizeButtonStyle(token); -const genSizeSmallButtonStyle = ( - prefixCls: string, - iconPrefixCls: string, - token: DerivativeToken, -): CSSInterpolation => { - const largeToken: DerivativeToken = { +const genSizeSmallButtonStyle: GenerateStyle = token => { + const largeToken: ButtonToken = { ...token, controlHeight: token.controlHeightSM, padding: token.paddingXS, }; - return genSizeButtonStyle(prefixCls, iconPrefixCls, `${prefixCls}-sm`, largeToken); + return genSizeButtonStyle(largeToken, `${token.btnCls}-sm`); }; -const genSizeLargeButtonStyle = ( - prefixCls: string, - iconPrefixCls: string, - token: DerivativeToken, -): CSSInterpolation => { - const largeToken: DerivativeToken = { +const genSizeLargeButtonStyle: GenerateStyle = token => { + const largeToken: ButtonToken = { ...token, controlHeight: token.controlHeightLG, fontSize: token.fontSizeLG, }; - return genSizeButtonStyle(prefixCls, iconPrefixCls, `${prefixCls}-lg`, largeToken); + return genSizeButtonStyle(largeToken, `${token.btnCls}-lg`); }; // ============================== Export ============================== @@ -386,18 +387,41 @@ export default function useStyle( const [theme, token, hashId] = useToken(); return [ - useStyleRegister({ theme, token, hashId, path: [prefixCls] }, () => [ - // Shared - withPrefix(genSharedButtonStyle(prefixCls, iconPrefixCls, token), prefixCls), + useStyleRegister({ theme, token, hashId, path: [prefixCls] }, () => { + const { colorText, button = {} } = token; + const textColor = new TinyColor(colorText); - // Size - genSizeSmallButtonStyle(prefixCls, iconPrefixCls, token), - genSizeBaseButtonStyle(prefixCls, iconPrefixCls, token), - genSizeLargeButtonStyle(prefixCls, iconPrefixCls, token), + const buttonToken: ButtonToken = { + ...token, + colorBgTextHover: textColor + .clone() + .setAlpha(textColor.getAlpha() * 0.02) + .toRgbString(), + colorBgTextActive: textColor + .clone() + .setAlpha(textColor.getAlpha() * 0.03) + .toRgbString(), - // Group (type, ghost, danger, disabled, loading) - genTypeButtonStyle(prefixCls, token), - ]), + iconPrefixCls, + btnCls: `.${prefixCls}`, + + // Override by developer + ...button, + }; + + return [ + // Shared + genSharedButtonStyle(buttonToken), + + // Size + genSizeSmallButtonStyle(buttonToken), + genSizeBaseButtonStyle(buttonToken), + genSizeLargeButtonStyle(buttonToken), + + // Group (type, ghost, danger, disabled, loading) + genTypeButtonStyle(buttonToken), + ]; + }), hashId, ]; } diff --git a/components/config-provider/context.tsx b/components/config-provider/context.tsx index 0022683778..30800e8769 100644 --- a/components/config-provider/context.tsx +++ b/components/config-provider/context.tsx @@ -3,6 +3,8 @@ import defaultRenderEmpty, { RenderEmptyHandler } from './renderEmpty'; import { Locale } from '../locale-provider'; import { SizeType } from './SizeContext'; import { RequiredMark } from '../form/Form'; +import type { SeedToken } from '../_util/theme'; +import type { OverrideToken } from '../_util/theme/interface'; export const defaultIconPrefixCls = 'anticon'; @@ -21,6 +23,12 @@ export interface CSPConfig { export type DirectionType = 'ltr' | 'rtl' | undefined; +export interface ThemeConfig { + token?: Partial; + override?: OverrideToken; + hashed?: boolean; +} + export interface ConfigConsumerProps { getTargetContainer?: () => HTMLElement; getPopupContainer?: (triggerNode?: HTMLElement) => HTMLElement; @@ -47,6 +55,7 @@ export interface ConfigConsumerProps { requiredMark?: RequiredMark; colon?: boolean; }; + theme?: ThemeConfig; } const defaultGetPrefixCls = (suffixCls?: string, customizePrefixCls?: string) => { diff --git a/components/config-provider/hooks/useTheme.ts b/components/config-provider/hooks/useTheme.ts new file mode 100644 index 0000000000..9f9734a990 --- /dev/null +++ b/components/config-provider/hooks/useTheme.ts @@ -0,0 +1,55 @@ +import useMemo from 'rc-util/lib/hooks/useMemo'; +import shallowEqual from 'shallowequal'; +import type { OverrideToken } from '../../_util/theme/interface'; +import type { ThemeConfig } from '../context'; + +export default function useTheme( + theme?: ThemeConfig, + parentTheme?: ThemeConfig, +): ThemeConfig | undefined { + const themeConfig = theme || {}; + const parentThemeConfig = parentTheme || {}; + + const mergedTheme = useMemo( + () => { + if (!theme) { + return parentTheme; + } + + // Override + const mergedOverride = { + ...parentThemeConfig.override, + }; + + Object.keys(theme.override || {}).forEach((componentName: keyof OverrideToken) => { + mergedOverride[componentName] = { + ...mergedOverride[componentName], + ...theme.override![componentName], + } as any; + }); + + // Base token + const merged = { + ...parentThemeConfig, + ...themeConfig, + + token: { + ...parentThemeConfig.token, + ...themeConfig.token, + }, + override: mergedOverride, + }; + + return merged; + }, + [themeConfig, parentThemeConfig], + (prev, next) => + prev.some((prevTheme, index) => { + const nextTheme = next[index]; + + return !shallowEqual(prevTheme, nextTheme); + }), + ); + + return mergedTheme; +} diff --git a/components/config-provider/index.tsx b/components/config-provider/index.tsx index 1519ec9a2a..1c6e78759c 100644 --- a/components/config-provider/index.tsx +++ b/components/config-provider/index.tsx @@ -6,22 +6,16 @@ import useMemo from 'rc-util/lib/hooks/useMemo'; import { RenderEmptyHandler } from './renderEmpty'; import LocaleProvider, { ANT_MARK, Locale } from '../locale-provider'; import LocaleReceiver from '../locale-provider/LocaleReceiver'; -import { - ConfigConsumer, - ConfigContext, - CSPConfig, - DirectionType, - ConfigConsumerProps, - Theme, - defaultIconPrefixCls, -} from './context'; +import { ConfigConsumer, ConfigContext, defaultIconPrefixCls } from './context'; +import type { CSPConfig, DirectionType, ConfigConsumerProps, Theme, ThemeConfig } from './context'; import SizeContext, { SizeContextProvider, SizeType } from './SizeContext'; import message from '../message'; import notification from '../notification'; import { RequiredMark } from '../form/Form'; import { registerTheme } from './cssVariables'; import defaultLocale from '../locale/default'; -import { SeedToken, DesignTokenContext, useToken } from '../_util/theme'; +import { DesignTokenContext, useToken } from '../_util/theme'; +import useTheme from './hooks/useTheme'; import defaultSeedToken from '../_util/theme/themes/default'; export { @@ -83,10 +77,7 @@ export interface ConfigProviderProps { }; virtual?: boolean; dropdownMatchSelectWidth?: boolean; - theme?: { - token?: Partial; - hashed?: boolean; - }; + theme?: ThemeConfig; } interface ProviderChildrenProps extends ConfigProviderProps { @@ -166,7 +157,7 @@ const ProviderChildren: React.FC = props => { legacyLocale, parentContext, iconPrefixCls, - theme = {}, + theme, } = props; const getPrefixCls = React.useCallback( @@ -182,6 +173,8 @@ const ProviderChildren: React.FC = props => { [parentContext.getPrefixCls, props.prefixCls], ); + const mergedTheme = useTheme(theme, parentContext.theme); + const config = { ...parentContext, csp, @@ -192,6 +185,7 @@ const ProviderChildren: React.FC = props => { virtual, dropdownMatchSelectWidth, getPrefixCls, + theme: mergedTheme, }; // Pass the props used by `useContext` directly with child component. @@ -257,19 +251,19 @@ const ProviderChildren: React.FC = props => { } // ================================ Dynamic theme ================================ - // FIXME: Multiple theme support for pass Theme & override const memoTheme = React.useMemo( () => ({ + ...mergedTheme, + token: { ...defaultSeedToken, - ...theme?.token, + ...mergedTheme?.token, }, - hashed: theme?.hashed, }), - [theme?.token, theme?.hashed], + [mergedTheme], ); - if (theme?.token || theme?.hashed) { + if (theme) { childNode = ( {childNode} ); diff --git a/components/switch/style/index.tsx b/components/switch/style/index.tsx index 013a95b21c..8f7756e44c 100644 --- a/components/switch/style/index.tsx +++ b/components/switch/style/index.tsx @@ -170,7 +170,7 @@ const genSwitchStyle = (token: SwitchToken): CSSObject => { '&:focus-visible': { outline: 0, - boxShadow: `0 0 0 ${token.controlOutlineWidth}px ${token.colorBgComponentDisabled}`, + boxShadow: `0 0 0 ${token.controlOutlineWidth}px ${token.colorDefaultOutline}`, }, [`&${token.switchCls}-checked:focus-visible`]: { diff --git a/package.json b/package.json index acd1372cdb..7ffe7c2f16 100644 --- a/package.json +++ b/package.json @@ -155,7 +155,8 @@ "rc-trigger": "^5.2.10", "rc-upload": "~4.3.0", "rc-util": "^5.19.3", - "scroll-into-view-if-needed": "^2.2.25" + "scroll-into-view-if-needed": "^2.2.25", + "shallowequal": "^1.1.0" }, "devDependencies": { "@ant-design/bisheng-plugin": "^3.0.1", @@ -180,6 +181,7 @@ "@types/react-copy-to-clipboard": "^5.0.0", "@types/react-dom": "^17.0.0", "@types/react-window": "^1.8.2", + "@types/shallowequal": "^1.1.1", "@types/warning": "^3.0.0", "@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/parser": "^5.0.0", diff --git a/site/theme/template/Layout/DynamicTheme/index.tsx b/site/theme/template/Layout/DynamicTheme/index.tsx index 157cf44a11..4d829dade0 100644 --- a/site/theme/template/Layout/DynamicTheme/index.tsx +++ b/site/theme/template/Layout/DynamicTheme/index.tsx @@ -5,6 +5,7 @@ import { useIntl } from 'react-intl'; import { BugOutlined, EyeOutlined } from '@ant-design/icons'; import { SeedToken } from '../../../../../components/_util/theme'; import defaultSeedToken from '../../../../../components/_util/theme/themes/default'; +import { PresetColors } from '../../../../../components/_util/theme/interface'; import Preview from './Preview'; export interface ThemeConfigProps { @@ -70,6 +71,10 @@ export default ({ onChangeTheme, defaultToken, componentName }: ThemeConfigProps autoComplete="off" > {keys.map((key: keyof typeof defaultToken) => { + if (PresetColors.includes(key as any)) { + return null; + } + const originValue = defaultToken[key]; const originValueType = typeof originValue;