From 5f1dd427df78a8f768f86b99378ffb5ad1875a38 Mon Sep 17 00:00:00 2001 From: MadCcc Date: Mon, 6 Nov 2023 10:31:51 +0800 Subject: [PATCH 01/90] feat: css var (#45589) * feat: css variables theme * chore: temp * chore temp * chore: temp * chore: temp * chore: tmp * chore: temp * feat: full css variables * feat: css var * chore: code clean * chore: code clean * chore: bump cssinjs * test: fix lint * feat: better key logic * feat: useStyle add param rootCls for cssVar scope * chore: fix lint * chore: code clean * chore: fix lint * perf: minimize component token size * chore: make useId compatible * chore: code clean * chore: fix lint * chore: code clean * chore: update test case * feat: genCSSVarRegister * feat: RPN Calculator * chore: add test for css var * chore: code clean * test: add test for calc * feat: better calc type * chore: code clean * chore: update size limit * feat: better useCSSVar * chore: better useCSSVar * test: add cov * feat: better calc logic * test: add test case * chore: code clean --------- Signed-off-by: MadCcc --- .../builtins/Previewer/CodePreviewer.tsx | 2 +- .dumi/theme/slots/Content/index.tsx | 6 +- components/_util/responsiveObserver.ts | 2 +- components/button/button.tsx | 8 +- components/button/style/cssVar.ts | 4 + components/button/style/index.ts | 118 ++++--- .../config-provider/__tests__/theme.test.tsx | 53 ++- components/config-provider/context.ts | 10 + components/config-provider/hooks/useTheme.ts | 25 ++ .../config-provider/hooks/useThemeKey.ts | 7 + components/drawer/demo/classNames.tsx | 2 +- components/modal/demo/classNames.tsx | 2 +- components/theme/__tests__/token.test.tsx | 1 + components/theme/__tests__/util.test.tsx | 127 +++++++ components/theme/context.ts | 4 + components/theme/interface/index.ts | 1 - components/theme/interface/maps/index.ts | 3 +- components/theme/interface/presetColors.ts | 7 - components/theme/internal.ts | 11 +- components/theme/themes/dark/index.ts | 11 +- components/theme/themes/default/index.ts | 13 +- components/theme/useToken.ts | 55 ++- components/theme/util/calc/CSSCalculator.ts | 74 ++++ components/theme/util/calc/NumCalculator.ts | 54 +++ components/theme/util/calc/calculator.ts | 11 + components/theme/util/calc/index.ts | 11 + .../theme/util/genComponentStyleHook.ts | 216 ------------ .../theme/util/genComponentStyleHook.tsx | 327 ++++++++++++++++++ docs/react/compatible-style.en-US.md | 4 +- package.json | 6 +- 30 files changed, 867 insertions(+), 308 deletions(-) create mode 100644 components/button/style/cssVar.ts create mode 100644 components/config-provider/hooks/useThemeKey.ts create mode 100644 components/theme/util/calc/CSSCalculator.ts create mode 100644 components/theme/util/calc/NumCalculator.ts create mode 100644 components/theme/util/calc/calculator.ts create mode 100644 components/theme/util/calc/index.ts delete mode 100644 components/theme/util/genComponentStyleHook.ts create mode 100644 components/theme/util/genComponentStyleHook.tsx diff --git a/.dumi/theme/builtins/Previewer/CodePreviewer.tsx b/.dumi/theme/builtins/Previewer/CodePreviewer.tsx index 4f4bb6c3b1..e5516170cf 100644 --- a/.dumi/theme/builtins/Previewer/CodePreviewer.tsx +++ b/.dumi/theme/builtins/Previewer/CodePreviewer.tsx @@ -5,7 +5,7 @@ import stackblitzSdk from '@stackblitz/sdk'; import { Alert, Badge, Space, Tooltip } from 'antd'; import { createStyles, css } from 'antd-style'; import classNames from 'classnames'; -import { FormattedMessage, useSiteData, LiveContext } from 'dumi'; +import { FormattedMessage, LiveContext, useSiteData } from 'dumi'; import LZString from 'lz-string'; import type { AntdPreviewerProps } from './Previewer'; diff --git a/.dumi/theme/slots/Content/index.tsx b/.dumi/theme/slots/Content/index.tsx index 59365bdd03..a26dd6cf8c 100644 --- a/.dumi/theme/slots/Content/index.tsx +++ b/.dumi/theme/slots/Content/index.tsx @@ -6,7 +6,7 @@ import DayJS from 'dayjs'; import { FormattedMessage, useIntl, useRouteMeta, useTabMeta } from 'dumi'; import type { ReactNode } from 'react'; import React, { useContext, useLayoutEffect, useMemo, useState } from 'react'; -import { Anchor, Avatar, Col, Skeleton, Space, Tooltip, Typography } from 'antd'; +import { Anchor, Avatar, Col, ConfigProvider, Skeleton, Space, Tooltip, Typography } from 'antd'; import useLayoutState from '../../../hooks/useLayoutState'; import useLocation from '../../../hooks/useLocation'; import EditButton from '../../common/EditButton'; @@ -275,7 +275,9 @@ const Content: React.FC<{ children: ReactNode }> = ({ children }) => { ) : null} {!meta.frontmatter.__autoDescription && meta.frontmatter.description} -
{children}
+ +
{children}
+
{(meta.frontmatter?.zhihu_url || meta.frontmatter?.yuque_url || meta.frontmatter?.juejin_url) && ( diff --git a/components/_util/responsiveObserver.ts b/components/_util/responsiveObserver.ts index ca4b3f7844..ea1f863f24 100644 --- a/components/_util/responsiveObserver.ts +++ b/components/_util/responsiveObserver.ts @@ -62,7 +62,7 @@ const validateBreakpoints = (token: GlobalToken) => { }; export default function useResponsiveObserver() { - const [, token] = useToken(); + const [, , , token] = useToken(); const responsiveMap: BreakpointMap = getResponsiveMap(validateBreakpoints(token)); // To avoid repeat create instance, we add `useMemo` here. diff --git a/components/button/button.tsx b/components/button/button.tsx index d224167f09..6df85afb8d 100644 --- a/components/button/button.tsx +++ b/components/button/button.tsx @@ -26,6 +26,7 @@ import IconWrapper from './IconWrapper'; import LoadingIcon from './LoadingIcon'; import useStyle from './style'; import CompactCmp from './style/compactCmp'; +import useCSSVar from './style/cssVar'; export type LegacyButtonType = ButtonType | 'danger'; @@ -118,7 +119,8 @@ const InternalButton: React.ForwardRefRenderFunction< const { getPrefixCls, autoInsertSpaceInButton, direction, button } = useContext(ConfigContext); const prefixCls = getPrefixCls('btn', customizePrefixCls); - const [wrapSSR, hashId] = useStyle(prefixCls); + const [, hashId] = useStyle(prefixCls); + const wrapCSSVar = useCSSVar(prefixCls); const disabled = useContext(DisabledContext); const mergedDisabled = customDisabled ?? disabled; @@ -254,7 +256,7 @@ const InternalButton: React.ForwardRefRenderFunction< children || children === 0 ? spaceChildren(children, needInserted && autoInsertSpace) : null; if (linkButtonRestProps.href !== undefined) { - return wrapSSR( + return wrapCSSVar( ( diff --git a/components/button/style/cssVar.ts b/components/button/style/cssVar.ts new file mode 100644 index 0000000000..e4571f1616 --- /dev/null +++ b/components/button/style/cssVar.ts @@ -0,0 +1,4 @@ +import { genCSSVarRegister } from '../../theme/internal'; +import { prepareComponentToken } from '.'; + +export default genCSSVarRegister('Button', prepareComponentToken); diff --git a/components/button/style/index.ts b/components/button/style/index.ts index 25b17ec87f..c14d9bdcbd 100644 --- a/components/button/style/index.ts +++ b/components/button/style/index.ts @@ -1,9 +1,9 @@ import type { CSSProperties } from 'react'; +import { unit } from '@ant-design/cssinjs'; import type { CSSInterpolation, CSSObject } from '@ant-design/cssinjs'; import { genFocusStyle } from '../../style'; -import type { GlobalToken } from '../../theme'; -import type { FullToken, GenerateStyle } from '../../theme/internal'; +import type { FullToken, GenerateStyle, GetDefaultToken } from '../../theme/internal'; import { genComponentStyleHook, mergeToken } from '../../theme/internal'; import type { GenStyleFn } from '../../theme/util/genComponentStyleHook'; import genGroupStyle from './group'; @@ -90,6 +90,21 @@ export interface ComponentToken { * @descEN Horizontal padding of small button */ paddingInlineSM: CSSProperties['paddingInline']; + /** + * @desc 按钮横向内间距 + * @descEN Horizontal padding of button + */ + paddingBlock: CSSProperties['paddingInline']; + /** + * @desc 大号按钮横向内间距 + * @descEN Horizontal padding of large button + */ + paddingBlockLG: CSSProperties['paddingInline']; + /** + * @desc 小号按钮横向内间距 + * @descEN Horizontal padding of small button + */ + paddingBlockSM: CSSProperties['paddingInline']; /** * @desc 只有图标的按钮图标尺寸 * @descEN Icon size of button which only contains icon @@ -139,6 +154,7 @@ export interface ComponentToken { export interface ButtonToken extends FullToken<'Button'> { buttonPaddingHorizontal: CSSProperties['paddingInline']; + buttonPaddingVertical: CSSProperties['paddingBlock']; buttonIconOnlyFontSize: number; } @@ -156,7 +172,7 @@ const genSharedButtonStyle: GenerateStyle = (token): CSS textAlign: 'center', backgroundImage: 'none', backgroundColor: 'transparent', - border: `${token.lineWidth}px ${token.lineType} transparent`, + border: `${unit(token.lineWidth)} ${token.lineType} transparent`, cursor: 'pointer', transition: `all ${token.motionDurationMid} ${token.motionEaseInOut}`, userSelect: 'none', @@ -220,7 +236,7 @@ const genSharedButtonStyle: GenerateStyle = (token): CSS insetInlineStart: -token.lineWidth, display: 'inline-block', width: token.lineWidth, - height: `calc(100% + ${token.lineWidth * 2}px)`, + height: `calc(100% + ${unit(token.lineWidth)} * 2)`, backgroundColor: token.colorPrimaryHover, content: '""', }, @@ -238,7 +254,7 @@ const genSharedButtonStyle: GenerateStyle = (token): CSS top: -token.lineWidth, insetInlineStart: -token.lineWidth, display: 'inline-block', - width: `calc(100% + ${token.lineWidth * 2}px)`, + width: `calc(100% + ${unit(token.lineWidth)} * 2)`, height: token.lineWidth, backgroundColor: token.colorPrimaryHover, content: '""', @@ -271,8 +287,8 @@ const genCircleButtonStyle: GenerateStyle = (token) => ( const genRoundButtonStyle: GenerateStyle = (token) => ({ borderRadius: token.controlHeight, - paddingInlineStart: token.controlHeight / 2, - paddingInlineEnd: token.controlHeight / 2, + paddingInlineStart: token.calc(token.controlHeight).div(2).equal(), + paddingInlineEnd: token.calc(token.controlHeight).div(2).equal(), }); // =============================== Type =============================== @@ -569,15 +585,12 @@ const genSizeButtonStyle = (token: ButtonToken, sizePrefixCls: string = ''): CSS componentCls, controlHeight, fontSize, - lineHeight, - lineWidth, borderRadius, buttonPaddingHorizontal, iconCls, + buttonPaddingVertical, } = token; - const paddingVertical = Math.max(0, (controlHeight - fontSize * lineHeight) / 2 - lineWidth); - const iconOnlyCls = `${componentCls}-icon-only`; return [ @@ -586,7 +599,7 @@ const genSizeButtonStyle = (token: ButtonToken, sizePrefixCls: string = ''): CSS [`${componentCls}${sizePrefixCls}`]: { fontSize, height: controlHeight, - padding: `${paddingVertical}px ${buttonPaddingHorizontal}px`, + padding: `${unit(buttonPaddingVertical!)} ${unit(buttonPaddingHorizontal!)}`, borderRadius, [`&${iconOnlyCls}`]: { @@ -635,7 +648,8 @@ const genSizeSmallButtonStyle: GenerateStyle = (token) => { controlHeight: token.controlHeightSM, fontSize: token.contentFontSizeSM, padding: token.paddingXS, - buttonPaddingHorizontal: token.paddingInlineSM, // Fixed padding + buttonPaddingHorizontal: token.paddingInlineSM, + buttonPaddingVertical: token.paddingBlockSM, borderRadius: token.borderRadiusSM, buttonIconOnlyFontSize: token.onlyIconSizeSM, }); @@ -648,6 +662,7 @@ const genSizeLargeButtonStyle: GenerateStyle = (token) => { controlHeight: token.controlHeightLG, fontSize: token.contentFontSizeLG, buttonPaddingHorizontal: token.paddingInlineLG, + buttonPaddingVertical: token.paddingBlockLG, borderRadius: token.borderRadiusLG, buttonIconOnlyFontSize: token.onlyIconSizeLG, }); @@ -670,44 +685,63 @@ const genBlockButtonStyle: GenerateStyle = (token) => { export const prepareToken: (token: Parameters>[0]) => ButtonToken = ( token, ) => { - const { paddingInline, onlyIconSize } = token; + const { paddingInline, onlyIconSize, paddingBlock } = token; const buttonToken = mergeToken(token, { buttonPaddingHorizontal: paddingInline, + buttonPaddingVertical: paddingBlock, buttonIconOnlyFontSize: onlyIconSize, }); return buttonToken; }; -export const prepareComponentToken = (token: GlobalToken) => ({ - fontWeight: 400, - defaultShadow: `0 ${token.controlOutlineWidth}px 0 ${token.controlTmpOutline}`, - primaryShadow: `0 ${token.controlOutlineWidth}px 0 ${token.controlOutline}`, - dangerShadow: `0 ${token.controlOutlineWidth}px 0 ${token.colorErrorOutline}`, - primaryColor: token.colorTextLightSolid, - dangerColor: token.colorTextLightSolid, - borderColorDisabled: token.colorBorder, - defaultGhostColor: token.colorBgContainer, - ghostBg: 'transparent', - defaultGhostBorderColor: token.colorBgContainer, - paddingInline: token.paddingContentHorizontal - token.lineWidth, - paddingInlineLG: token.paddingContentHorizontal - token.lineWidth, - paddingInlineSM: 8 - token.lineWidth, - onlyIconSize: token.fontSizeLG, - onlyIconSizeSM: token.fontSizeLG - 2, - onlyIconSizeLG: token.fontSizeLG + 2, - groupBorderColor: token.colorPrimaryHover, - linkHoverBg: 'transparent', - textHoverBg: token.colorBgTextHover, - defaultColor: token.colorText, - defaultBg: token.colorBgContainer, - defaultBorderColor: token.colorBorder, - defaultBorderColorDisabled: token.colorBorder, - contentFontSize: token.fontSize, - contentFontSizeSM: token.fontSize, - contentFontSizeLG: token.fontSizeLG, -}); +export const prepareComponentToken: GetDefaultToken<'Button'> = (token) => { + const contentFontSize = token.fontSize; + const contentFontSizeSM = token.fontSize; + const contentFontSizeLG = token.fontSizeLG; + + return { + fontWeight: 400, + defaultShadow: `0 ${token.controlOutlineWidth}px 0 ${token.controlTmpOutline}`, + primaryShadow: `0 ${token.controlOutlineWidth}px 0 ${token.controlOutline}`, + dangerShadow: `0 ${token.controlOutlineWidth}px 0 ${token.colorErrorOutline}`, + primaryColor: token.colorTextLightSolid, + dangerColor: token.colorTextLightSolid, + borderColorDisabled: token.colorBorder, + defaultGhostColor: token.colorBgContainer, + ghostBg: 'transparent', + defaultGhostBorderColor: token.colorBgContainer, + paddingInline: token.paddingContentHorizontal - token.lineWidth, + paddingInlineLG: token.paddingContentHorizontal - token.lineWidth, + paddingInlineSM: 8 - token.lineWidth, + paddingBlock: Math.max( + (token.controlHeight - contentFontSize * token.lineHeight) / 2 - token.lineWidth, + 0, + ), + paddingBlockSM: Math.max( + (token.controlHeightSM - contentFontSizeSM * token.lineHeight) / 2 - token.lineWidth, + 0, + ), + paddingBlockLG: Math.max( + (token.controlHeightLG - contentFontSizeLG * token.lineHeight) / 2 - token.lineWidth, + 0, + ), + onlyIconSize: token.fontSizeLG, + onlyIconSizeSM: token.fontSizeLG - 2, + onlyIconSizeLG: token.fontSizeLG + 2, + groupBorderColor: token.colorPrimaryHover, + linkHoverBg: 'transparent', + textHoverBg: token.colorBgTextHover, + defaultColor: token.colorText, + defaultBg: token.colorBgContainer, + defaultBorderColor: token.colorBorder, + defaultBorderColorDisabled: token.colorBorder, + contentFontSize, + contentFontSizeSM, + contentFontSizeLG, + }; +}; export default genComponentStyleHook( 'Button', diff --git a/components/config-provider/__tests__/theme.test.tsx b/components/config-provider/__tests__/theme.test.tsx index dc691224be..f20a3fb22f 100644 --- a/components/config-provider/__tests__/theme.test.tsx +++ b/components/config-provider/__tests__/theme.test.tsx @@ -3,9 +3,9 @@ import kebabCase from 'lodash/kebabCase'; import canUseDom from 'rc-util/lib/Dom/canUseDom'; import ConfigProvider from '..'; -import { InputNumber } from '../..'; -import { resetWarned } from '../../_util/warning'; +import { InputNumber, Button } from '../..'; import { render } from '../../../tests/utils'; +import { resetWarned } from '../../_util/warning'; import theme from '../../theme'; import { useToken } from '../../theme/internal'; @@ -197,4 +197,53 @@ describe('ConfigProvider.Theme', () => { ); expect(tokenRef?.colorPrimaryText).toBe('#1677ff'); }); + + describe('cssVar', () => { + it('should work', () => { + const { container } = render( + + + , + ); + + const button = container.querySelector('button')!; + + expect(button).toHaveClass('foo'); + expect(button).toHaveStyle({ + '--antd-color-text': 'rgba(0, 0, 0, 0.88)', + boxShadow: 'var(--antd-button-default-shadow)', + 'line-height': 'var(--antd-line-height)', + }); + }); + + it('prefix', () => { + const { container } = render( + <> + + + + + + + , + ); + + const fooBtn = container.querySelector('.button-foo')!; + const barBtn = container.querySelector('.button-bar')!; + + expect(fooBtn).toHaveClass('foo'); + expect(fooBtn).toHaveStyle({ + '--antd-color-text': 'rgba(0, 0, 0, 0.88)', + boxShadow: 'var(--antd-button-default-shadow)', + 'line-height': 'var(--antd-line-height)', + }); + + expect(barBtn).toHaveClass('bar'); + expect(barBtn).toHaveStyle({ + '--bar-color-text': 'rgba(0, 0, 0, 0.88)', + boxShadow: 'var(--bar-button-default-shadow)', + 'line-height': 'var(--bar-line-height)', + }); + }); + }); }); diff --git a/components/config-provider/context.ts b/components/config-provider/context.ts index c7abb8a7fd..d5eb6ffba9 100644 --- a/components/config-provider/context.ts +++ b/components/config-provider/context.ts @@ -47,6 +47,16 @@ export interface ThemeConfig { algorithm?: MappingAlgorithm | MappingAlgorithm[]; hashed?: boolean; inherit?: boolean; + cssVar?: { + /** + * Prefix for css variable, default to `antd`. + */ + prefix?: string; + /** + * Unique key for theme, should be set manually < react@18. + */ + key?: string; + }; } export interface ComponentStyleConfig { diff --git a/components/config-provider/hooks/useTheme.ts b/components/config-provider/hooks/useTheme.ts index 9ec7372925..0659e5fdea 100644 --- a/components/config-provider/hooks/useTheme.ts +++ b/components/config-provider/hooks/useTheme.ts @@ -3,15 +3,31 @@ import isEqual from 'rc-util/lib/isEqual'; import type { OverrideToken } from '../../theme/interface'; import type { ThemeConfig } from '../context'; import { defaultConfig } from '../../theme/internal'; +import useThemeKey from './useThemeKey'; +import { devUseWarning } from '../../_util/warning'; export default function useTheme( theme?: ThemeConfig, parentTheme?: ThemeConfig, ): ThemeConfig | undefined { + const warning = devUseWarning('ConfigProvider'); + const themeConfig = theme || {}; const parentThemeConfig: ThemeConfig = themeConfig.inherit === false || !parentTheme ? defaultConfig : parentTheme; + const themeKey = useThemeKey(); + + if (process.env.NODE_ENV !== 'production') { + const cssVarEnabled = themeConfig.cssVar || parentThemeConfig.cssVar; + const validKey = !!(themeConfig.cssVar?.key || themeKey); + warning( + !cssVarEnabled || validKey, + 'breaking', + 'Missing key in `cssVar` config. Please upgrade to React 18 or set `cssVar.key` manually in each ConfigProvider inside `cssVar` enabled ConfigProvider.', + ); + } + return useMemo( () => { if (!theme) { @@ -30,6 +46,14 @@ export default function useTheme( } as any; }); + const cssVarKey = `css-var-${themeKey.replace(/:/g, '')}`; + const mergedCssVar = (themeConfig.cssVar || parentThemeConfig.cssVar) && { + prefix: 'antd', // Default to antd + ...parentThemeConfig.cssVar, + ...themeConfig.cssVar, + key: themeConfig.cssVar?.key || cssVarKey, + }; + // Base token return { ...parentThemeConfig, @@ -40,6 +64,7 @@ export default function useTheme( ...themeConfig.token, }, components: mergedComponents, + cssVar: mergedCssVar, }; }, [themeConfig, parentThemeConfig], diff --git a/components/config-provider/hooks/useThemeKey.ts b/components/config-provider/hooks/useThemeKey.ts new file mode 100644 index 0000000000..289bdfd328 --- /dev/null +++ b/components/config-provider/hooks/useThemeKey.ts @@ -0,0 +1,7 @@ +import { useId } from 'react'; + +const useEmptyId = () => ''; + +const useThemeKey = typeof useId === 'undefined' ? useEmptyId : useId; + +export default useThemeKey; diff --git a/components/drawer/demo/classNames.tsx b/components/drawer/demo/classNames.tsx index 1d8b419a55..13698dba68 100644 --- a/components/drawer/demo/classNames.tsx +++ b/components/drawer/demo/classNames.tsx @@ -5,7 +5,7 @@ import type { DrawerClassNames, DrawerStyles } from '../DrawerPanel'; const useStyle = createStyles(({ token }) => ({ 'my-drawer-body': { - background: token['blue-1'], + background: token.blue1, }, 'my-drawer-mask': { boxShadow: `inset 0 0 15px #fff`, diff --git a/components/modal/demo/classNames.tsx b/components/modal/demo/classNames.tsx index 1cada7a08f..67eb1eb5ee 100644 --- a/components/modal/demo/classNames.tsx +++ b/components/modal/demo/classNames.tsx @@ -4,7 +4,7 @@ import { createStyles, useTheme } from 'antd-style'; const useStyle = createStyles(({ token }) => ({ 'my-modal-body': { - background: token['blue-1'], + background: token.blue1, padding: token.paddingSM, }, 'my-modal-mask': { diff --git a/components/theme/__tests__/token.test.tsx b/components/theme/__tests__/token.test.tsx index b7ec4e8afa..afa825618b 100644 --- a/components/theme/__tests__/token.test.tsx +++ b/components/theme/__tests__/token.test.tsx @@ -25,6 +25,7 @@ describe('Theme', () => { ); delete token._hashId; delete token._tokenKey; + delete token._themeKey; return token; }; diff --git a/components/theme/__tests__/util.test.tsx b/components/theme/__tests__/util.test.tsx index 45741d15ba..c6772da4e3 100644 --- a/components/theme/__tests__/util.test.tsx +++ b/components/theme/__tests__/util.test.tsx @@ -1,4 +1,6 @@ import getAlphaColor from '../util/getAlphaColor'; +import genCalc from '../util/calc'; +import type AbstractCalculator from 'antd/es/theme/util/calc/calculator'; describe('util', () => { describe('getAlphaColor', () => { @@ -6,4 +8,129 @@ describe('util', () => { expect(getAlphaColor('rgba(0, 0, 0, 0.5)', 'rgba(255, 255, 255)')).toBe('rgba(0, 0, 0, 0.5)'); }); }); + + describe('calculator', () => { + const cases: [ + (calc: (num: number | AbstractCalculator) => AbstractCalculator) => string | number, + { js: number; css: string }, + ][] = [ + [ + // 1 + 1 + (calc) => calc(1).add(1).equal(), + { + js: 2, + css: 'calc(1px + 1px)', + }, + ], + [ + // (1 + 1) * 4 + (calc) => calc(1).add(1).mul(4).equal(), + { + js: 8, + css: 'calc((1px + 1px) * 4)', + }, + ], + [ + // (2 + 4) / 2 - 2 + (calc) => calc(2).add(4).div(2).sub(2).equal(), + { + js: 1, + css: 'calc((2px + 4px) / 2 - 2px)', + }, + ], + [ + // Bad case + // (2 + 4) / (3 - 2) - 2 + (calc) => calc(2).add(4).div(calc(3).sub(2)).sub(2).equal(), + { + js: 4, + css: 'calc((2px + 4px) / (3px - 2px) - 2px)', + }, + ], + [ + // Bad case + // 2 * (2 + 3) + (calc) => calc(2).mul(calc(2).add(3)).equal(), + { + js: 10, + css: 'calc(2px * (2px + 3px))', + }, + ], + [ + // (1 + 2) * 3 + (calc) => calc(calc(1).add(2)).mul(3).equal(), + { + js: 9, + css: 'calc((1px + 2px) * 3)', + }, + ], + [ + // 1 + (2 - 1) + (calc) => calc(1).add(calc(2).sub(1)).equal(), + { + js: 2, + css: 'calc(1px + (2px - 1px))', + }, + ], + [ + // 1 + 2 * 2 + (calc) => calc(1).add(calc(2).mul(2)).equal(), + { + js: 5, + css: 'calc(1px + 2px * 2)', + }, + ], + [ + // 5 - (2 - 1) + (calc) => calc(5).sub(calc(2).sub(1)).equal(), + { + js: 4, + css: 'calc(5px - (2px - 1px))', + }, + ], + [ + // 2 * 6 / 3 + (calc) => calc(2).mul(6).div(3).equal(), + { + js: 4, + css: 'calc(2px * 6 / 3)', + }, + ], + [ + // 6 / 3 * 2 + (calc) => calc(6).div(3).mul(2).equal(), + { + js: 4, + css: 'calc(6px / 3 * 2)', + }, + ], + [ + // Bad case + // 6 / (3 * 2) + (calc) => calc(6).div(calc(3).mul(2)).equal(), + { + js: 1, + css: 'calc(6px / (3px * 2))', + }, + ], + [ + // 6 + (calc) => calc(6).equal(), + { + js: 6, + css: '6px', + }, + ], + ]; + + cases.forEach(([exp, { js, css }], index) => { + it(`js calc ${index + 1}`, () => { + expect(exp(genCalc('js'))).toBe(js); + }); + + it(`css calc ${index + 1}`, () => { + expect(exp(genCalc('css'))).toBe(css); + }); + }); + }); }); diff --git a/components/theme/context.ts b/components/theme/context.ts index 1803754351..d8225ed3c5 100644 --- a/components/theme/context.ts +++ b/components/theme/context.ts @@ -29,6 +29,10 @@ export interface DesignTokenProviderProps { /** Just merge `token` & `override` at top to save perf */ override: { override: Partial } & ComponentsToken; hashed?: string | boolean; + cssVar?: { + prefix?: string; + key?: string; + }; } export const DesignTokenContext = React.createContext(defaultConfig); diff --git a/components/theme/interface/index.ts b/components/theme/interface/index.ts index 59c7f57079..8091dd1438 100644 --- a/components/theme/interface/index.ts +++ b/components/theme/interface/index.ts @@ -28,7 +28,6 @@ export type { export { PresetColors } from './presetColors'; export type { ColorPalettes, - LegacyColorPalettes, PresetColorKey, PresetColorType, } from './presetColors'; diff --git a/components/theme/interface/maps/index.ts b/components/theme/interface/maps/index.ts index 01a948c514..a67c579615 100644 --- a/components/theme/interface/maps/index.ts +++ b/components/theme/interface/maps/index.ts @@ -1,4 +1,4 @@ -import type { ColorPalettes, LegacyColorPalettes } from '../presetColors'; +import type { ColorPalettes } from '../presetColors'; import type { SeedToken } from '../seeds'; import type { ColorMapToken } from './colors'; import type { FontMapToken } from './font'; @@ -36,7 +36,6 @@ export interface CommonMapToken extends StyleMapToken { export interface MapToken extends SeedToken, - LegacyColorPalettes, ColorPalettes, ColorMapToken, SizeMapToken, diff --git a/components/theme/interface/presetColors.ts b/components/theme/interface/presetColors.ts index 8439f0435d..7adcc1a814 100644 --- a/components/theme/interface/presetColors.ts +++ b/components/theme/interface/presetColors.ts @@ -20,13 +20,6 @@ export type PresetColorType = Record; type ColorPaletteKeyIndex = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10; -export type LegacyColorPalettes = { - /** - * @deprecated - */ - [key in `${keyof PresetColorType}-${ColorPaletteKeyIndex}`]: string; -}; - export type ColorPalettes = { [key in `${keyof PresetColorType}${ColorPaletteKeyIndex}`]: string; }; diff --git a/components/theme/internal.ts b/components/theme/internal.ts index 23bf38fc77..1f63ebfd46 100644 --- a/components/theme/internal.ts +++ b/components/theme/internal.ts @@ -10,20 +10,26 @@ import type { } from './interface'; import { PresetColors } from './interface'; import useToken from './useToken'; -import type { FullToken } from './util/genComponentStyleHook'; -import genComponentStyleHook, { genSubStyleComponent } from './util/genComponentStyleHook'; +import type { FullToken, GetDefaultToken } from './util/genComponentStyleHook'; +import genComponentStyleHook, { + genSubStyleComponent, + genCSSVarRegister, +} from './util/genComponentStyleHook'; import genPresetColor from './util/genPresetColor'; import statisticToken, { merge as mergeToken } from './util/statistic'; import useResetIconStyle from './util/useResetIconStyle'; +import calc from './util/calc'; export { DesignTokenContext, defaultConfig } from './context'; export { PresetColors, genComponentStyleHook, genSubStyleComponent, + genCSSVarRegister, genPresetColor, mergeToken, statisticToken, + calc, // hooks useResetIconStyle, useStyleRegister, @@ -39,4 +45,5 @@ export type { PresetColorType, SeedToken, UseComponentStyleResult, + GetDefaultToken, }; diff --git a/components/theme/themes/dark/index.ts b/components/theme/themes/dark/index.ts index 4b6f5a4eb7..684f0f14e2 100644 --- a/components/theme/themes/dark/index.ts +++ b/components/theme/themes/dark/index.ts @@ -1,12 +1,6 @@ import { generate } from '@ant-design/colors'; import type { DerivativeFunc } from '@ant-design/cssinjs'; -import type { - ColorPalettes, - LegacyColorPalettes, - MapToken, - PresetColorType, - SeedToken, -} from '../../interface'; +import type { ColorPalettes, MapToken, PresetColorType, SeedToken } from '../../interface'; import { defaultPresetColors } from '../seed'; import genColorMapToken from '../shared/genColorMapToken'; import { generateColorPalettes, generateNeutralColorPalettes } from './colors'; @@ -18,7 +12,6 @@ const derivative: DerivativeFunc = (token, mapToken) => { const colors = generate(token[colorKey], { theme: 'dark' }); return new Array(10).fill(1).reduce((prev, _, i) => { - prev[`${colorKey}-${i + 1}`] = colors[i]; prev[`${colorKey}${i + 1}`] = colors[i]; return prev; }, {}) as ColorPalettes; @@ -29,7 +22,7 @@ const derivative: DerivativeFunc = (token, mapToken) => { ...cur, }; return prev; - }, {} as ColorPalettes & LegacyColorPalettes); + }, {} as ColorPalettes); const mergedMapToken = mapToken ?? defaultAlgorithm(token); diff --git a/components/theme/themes/default/index.ts b/components/theme/themes/default/index.ts index d558e2cf87..08d398a0f7 100644 --- a/components/theme/themes/default/index.ts +++ b/components/theme/themes/default/index.ts @@ -1,13 +1,7 @@ import { generate } from '@ant-design/colors'; import genControlHeight from '../shared/genControlHeight'; import genSizeMapToken from '../shared/genSizeMapToken'; -import type { - ColorPalettes, - LegacyColorPalettes, - MapToken, - PresetColorType, - SeedToken, -} from '../../interface'; +import type { ColorPalettes, MapToken, PresetColorType, SeedToken } from '../../interface'; import { defaultPresetColors } from '../seed'; import genColorMapToken from '../shared/genColorMapToken'; import genCommonMapToken from '../shared/genCommonMapToken'; @@ -20,10 +14,9 @@ export default function derivative(token: SeedToken): MapToken { const colors = generate(token[colorKey]); return new Array(10).fill(1).reduce((prev, _, i) => { - prev[`${colorKey}-${i + 1}`] = colors[i]; prev[`${colorKey}${i + 1}`] = colors[i]; return prev; - }, {}) as ColorPalettes & LegacyColorPalettes; + }, {}) as ColorPalettes; }) .reduce((prev, cur) => { prev = { @@ -31,7 +24,7 @@ export default function derivative(token: SeedToken): MapToken { ...cur, }; return prev; - }, {} as ColorPalettes & LegacyColorPalettes); + }, {} as ColorPalettes); return { ...token, diff --git a/components/theme/useToken.ts b/components/theme/useToken.ts index 831c6d4e3e..9a5d5bd9d9 100644 --- a/components/theme/useToken.ts +++ b/components/theme/useToken.ts @@ -9,6 +9,41 @@ import type { AliasToken, GlobalToken, MapToken, SeedToken } from './interface'; import defaultSeedToken from './themes/seed'; import formatToken from './util/alias'; +export const unitless: { + [key in keyof AliasToken]?: boolean; +} = { + lineHeight: true, + lineHeightSM: true, + lineHeightLG: true, + lineHeightHeading1: true, + lineHeightHeading2: true, + lineHeightHeading3: true, + lineHeightHeading4: true, + lineHeightHeading5: true, + opacityLoading: true, + fontWeightStrong: true, + zIndexPopupBase: true, + zIndexBase: true, +}; + +export const ignore: { + [key in keyof AliasToken]?: boolean; +} = { + size: true, + sizeSM: true, + sizeLG: true, + sizeMD: true, + sizeXS: true, + sizeXXS: true, + sizeMS: true, + sizeXL: true, + sizeXXL: true, + sizeUnit: true, + sizeStep: true, + motionBase: true, + motionUnit: true, +}; + export const getComputedToken = ( originToken: SeedToken, overrideToken: DesignTokenProviderProps['components'] & { @@ -57,14 +92,22 @@ export default function useToken(): [ theme: Theme, token: GlobalToken, hashId: string, + realToken: GlobalToken, + cssVar?: DesignTokenProviderProps['cssVar'], ] { - const { token: rootDesignToken, hashed, theme, override } = React.useContext(DesignTokenContext); + const { + token: rootDesignToken, + hashed, + theme, + override, + cssVar, + } = React.useContext(DesignTokenContext); const salt = `${version}-${hashed || ''}`; const mergedTheme = theme || defaultTheme; - const [token, hashId] = useCacheToken( + const [token, hashId, realToken] = useCacheToken( mergedTheme, [defaultSeedToken, rootDesignToken], { @@ -74,8 +117,14 @@ export default function useToken(): [ // formatToken will not be consumed after 1.15.0 with getComputedToken. // But token will break if @ant-design/cssinjs is under 1.15.0 without it formatToken, + cssVar: cssVar && { + prefix: cssVar.prefix, + key: cssVar.key, + unitless, + ignore, + }, }, ); - return [mergedTheme, token, hashed ? hashId : '']; + return [mergedTheme, token, hashed ? hashId : '', realToken, cssVar]; } diff --git a/components/theme/util/calc/CSSCalculator.ts b/components/theme/util/calc/CSSCalculator.ts new file mode 100644 index 0000000000..2ff299ce97 --- /dev/null +++ b/components/theme/util/calc/CSSCalculator.ts @@ -0,0 +1,74 @@ +import AbstractCalculator from './calculator'; +import { unit } from '@ant-design/cssinjs'; + +export default class CSSCalculator extends AbstractCalculator { + result: string = ''; + + lowPriority?: boolean; + + constructor(num: number | AbstractCalculator) { + super(); + if (num instanceof CSSCalculator) { + this.result = `(${num.result})`; + } else if (typeof num === 'number') { + this.result = unit(num); + } + } + + add(num: number | AbstractCalculator): this { + if (num instanceof CSSCalculator) { + this.result = `${this.result} + ${num.getResult()}`; + } else if (typeof num === 'number') { + this.result = `${this.result} + ${unit(num)}`; + } + this.lowPriority = true; + return this; + } + + sub(num: number | AbstractCalculator): this { + if (num instanceof CSSCalculator) { + this.result = `${this.result} - ${num.getResult()}`; + } else if (typeof num === 'number') { + this.result = `${this.result} - ${unit(num)}`; + } + this.lowPriority = true; + return this; + } + + mul(num: number | AbstractCalculator): this { + if (this.lowPriority) { + this.result = `(${this.result})`; + } + if (num instanceof CSSCalculator) { + this.result = `${this.result} * ${num.getResult(true)}`; + } else if (typeof num === 'number') { + this.result = `${this.result} * ${num}`; + } + this.lowPriority = false; + return this; + } + + div(num: number | AbstractCalculator): this { + if (this.lowPriority) { + this.result = `(${this.result})`; + } + if (num instanceof CSSCalculator) { + this.result = `${this.result} / ${num.getResult(true)}`; + } else if (typeof num === 'number') { + this.result = `${this.result} / ${num}`; + } + this.lowPriority = false; + return this; + } + + getResult(force?: boolean): string { + return this.lowPriority || force ? `(${this.result})` : this.result; + } + + equal(): string { + if (typeof this.lowPriority !== 'undefined') { + return `calc(${this.result})`; + } + return this.result; + } +} diff --git a/components/theme/util/calc/NumCalculator.ts b/components/theme/util/calc/NumCalculator.ts new file mode 100644 index 0000000000..16fa7cb7e2 --- /dev/null +++ b/components/theme/util/calc/NumCalculator.ts @@ -0,0 +1,54 @@ +import AbstractCalculator from './calculator'; + +export default class NumCalculator extends AbstractCalculator { + result: number = 0; + + constructor(num: number | string | AbstractCalculator) { + super(); + if (num instanceof NumCalculator) { + this.result = num.result; + } else if (typeof num === 'number') { + this.result = num; + } + } + + add(num: number | AbstractCalculator): this { + if (num instanceof NumCalculator) { + this.result += num.result; + } else if (typeof num === 'number') { + this.result += num; + } + return this; + } + + sub(num: number | AbstractCalculator): this { + if (num instanceof NumCalculator) { + this.result -= num.result; + } else if (typeof num === 'number') { + this.result -= num; + } + return this; + } + + mul(num: number | AbstractCalculator): this { + if (num instanceof NumCalculator) { + this.result *= num.result; + } else if (typeof num === 'number') { + this.result *= num; + } + return this; + } + + div(num: number | AbstractCalculator): this { + if (num instanceof NumCalculator) { + this.result /= num.result; + } else if (typeof num === 'number') { + this.result /= num; + } + return this; + } + + equal(): number { + return this.result; + } +} diff --git a/components/theme/util/calc/calculator.ts b/components/theme/util/calc/calculator.ts new file mode 100644 index 0000000000..51a2359d8d --- /dev/null +++ b/components/theme/util/calc/calculator.ts @@ -0,0 +1,11 @@ +export default abstract class AbstractCalculator { + abstract add(num: number | AbstractCalculator): this; + + abstract sub(num: number | AbstractCalculator): this; + + abstract mul(num: number | AbstractCalculator): this; + + abstract div(num: number | AbstractCalculator): this; + + abstract equal(): string | number; +} diff --git a/components/theme/util/calc/index.ts b/components/theme/util/calc/index.ts new file mode 100644 index 0000000000..0547944975 --- /dev/null +++ b/components/theme/util/calc/index.ts @@ -0,0 +1,11 @@ +import NumCalculator from './NumCalculator'; +import CSSCalculator from './CSSCalculator'; +import type AbstractCalculator from './calculator'; + +const genCalc = (type: 'css' | 'js') => { + const Calculator = type === 'css' ? CSSCalculator : NumCalculator; + + return (num: number | AbstractCalculator) => new Calculator(num); +}; + +export default genCalc; diff --git a/components/theme/util/genComponentStyleHook.ts b/components/theme/util/genComponentStyleHook.ts deleted file mode 100644 index 74fa3d2501..0000000000 --- a/components/theme/util/genComponentStyleHook.ts +++ /dev/null @@ -1,216 +0,0 @@ -/* eslint-disable no-redeclare */ -import { useContext, type ComponentType } from 'react'; -import type { CSSInterpolation } from '@ant-design/cssinjs'; -import { useStyleRegister } from '@ant-design/cssinjs'; -import { warning } from 'rc-util'; - -import { ConfigContext } from '../../config-provider/context'; -import { genCommonStyle, genLinkStyle } from '../../style'; -import type { - ComponentTokenMap, - GlobalToken, - OverrideToken, - UseComponentStyleResult, -} from '../interface'; -import useToken from '../useToken'; -import statisticToken, { merge as mergeToken } from './statistic'; -import useResetIconStyle from './useResetIconStyle'; - -export type OverrideTokenWithoutDerivative = ComponentTokenMap; -export type OverrideComponent = keyof OverrideTokenWithoutDerivative; -export type GlobalTokenWithComponent = GlobalToken & - ComponentTokenMap[ComponentName]; - -type ComponentToken = Exclude< - OverrideToken[ComponentName], - undefined ->; -type ComponentTokenKey = - keyof ComponentToken; - -export interface StyleInfo { - hashId: string; - prefixCls: string; - rootPrefixCls: string; - iconPrefixCls: string; - overrideComponentToken: ComponentTokenMap[ComponentName]; -} - -export type TokenWithCommonCls = T & { - /** Wrap component class with `.` prefix */ - componentCls: string; - /** Origin prefix which do not have `.` prefix */ - prefixCls: string; - /** Wrap icon class with `.` prefix */ - iconCls: string; - /** Wrap ant prefixCls class with `.` prefix */ - antCls: string; -}; -export type FullToken = TokenWithCommonCls< - GlobalTokenWithComponent ->; - -export type GenStyleFn = ( - token: FullToken, - info: StyleInfo, -) => CSSInterpolation; - -export default function genComponentStyleHook( - componentName: ComponentName | [ComponentName, string], - styleFn: GenStyleFn, - getDefaultToken?: - | null - | OverrideTokenWithoutDerivative[ComponentName] - | ((token: GlobalToken) => OverrideTokenWithoutDerivative[ComponentName]), - options: { - resetStyle?: boolean; - // Deprecated token key map [["oldTokenKey", "newTokenKey"], ["oldTokenKey", "newTokenKey"]] - deprecatedTokens?: [ComponentTokenKey, ComponentTokenKey][]; - /** - * Only use component style in client side. Ignore in SSR. - */ - clientOnly?: boolean; - /** - * Set order of component style. Default is -999. - */ - order?: number; - } = {}, -) { - const cells = (Array.isArray(componentName) ? componentName : [componentName, componentName]) as [ - ComponentName, - string, - ]; - - const [component] = cells; - const concatComponent = cells.join('-'); - - return (prefixCls: string): UseComponentStyleResult => { - const [theme, token, hashId] = useToken(); - const { getPrefixCls, iconPrefixCls, csp } = useContext(ConfigContext); - const rootPrefixCls = getPrefixCls(); - - // Shared config - const sharedConfig: Omit[0], 'path'> = { - theme, - token, - hashId, - nonce: () => csp?.nonce!, - clientOnly: options.clientOnly, - - // antd is always at top of styles - order: options.order || -999, - }; - - // Generate style for all a tags in antd component. - useStyleRegister( - { ...sharedConfig, clientOnly: false, path: ['Shared', rootPrefixCls] }, - () => [ - { - // Link - '&': genLinkStyle(token), - }, - ], - ); - - // Generate style for icons - useResetIconStyle(iconPrefixCls, csp); - - return [ - useStyleRegister( - { ...sharedConfig, path: [concatComponent, prefixCls, iconPrefixCls] }, - () => { - const { token: proxyToken, flush } = statisticToken(token); - - const customComponentToken = { ...(token[component] as ComponentToken) }; - if (options.deprecatedTokens) { - const { deprecatedTokens } = options; - deprecatedTokens.forEach(([oldTokenKey, newTokenKey]) => { - if (process.env.NODE_ENV !== 'production') { - warning( - !customComponentToken?.[oldTokenKey], - `The token '${String(oldTokenKey)}' of ${component} had deprecated, use '${String( - newTokenKey, - )}' instead.`, - ); - } - - // Should wrap with `if` clause, or there will be `undefined` in object. - if (customComponentToken?.[oldTokenKey] || customComponentToken?.[newTokenKey]) { - customComponentToken[newTokenKey] ??= customComponentToken?.[oldTokenKey]; - } - }); - } - const defaultComponentToken = - typeof getDefaultToken === 'function' - ? getDefaultToken(mergeToken(proxyToken, customComponentToken ?? {})) - : getDefaultToken; - - const mergedComponentToken = { ...defaultComponentToken, ...customComponentToken }; - - const componentCls = `.${prefixCls}`; - const mergedToken = mergeToken< - TokenWithCommonCls> - >( - proxyToken, - { - componentCls, - prefixCls, - iconCls: `.${iconPrefixCls}`, - antCls: `.${rootPrefixCls}`, - }, - mergedComponentToken, - ); - - const styleInterpolation = styleFn(mergedToken as unknown as FullToken, { - hashId, - prefixCls, - rootPrefixCls, - iconPrefixCls, - overrideComponentToken: customComponentToken as any, - }); - flush(component, mergedComponentToken); - return [ - options.resetStyle === false ? null : genCommonStyle(token, prefixCls), - styleInterpolation, - ]; - }, - ), - hashId, - ]; - }; -} - -export interface SubStyleComponentProps { - prefixCls: string; -} - -// Get from second argument -type RestParameters = T extends [any, ...infer Rest] ? Rest : never; - -export const genSubStyleComponent: ( - componentName: [ComponentName, string], - ...args: RestParameters>> -) => ComponentType = (componentName, styleFn, getDefaultToken, options) => { - const useStyle = genComponentStyleHook(componentName, styleFn, getDefaultToken, { - resetStyle: false, - - // Sub Style should default after root one - order: -998, - ...options, - }); - - const StyledComponent: ComponentType = ({ - prefixCls, - }: SubStyleComponentProps) => { - useStyle(prefixCls); - return null; - }; - - if (process.env.NODE_ENV !== 'production') { - StyledComponent.displayName = `SubStyle_${ - Array.isArray(componentName) ? componentName.join('.') : componentName - }`; - } - - return StyledComponent; -}; diff --git a/components/theme/util/genComponentStyleHook.tsx b/components/theme/util/genComponentStyleHook.tsx new file mode 100644 index 0000000000..f86df1993d --- /dev/null +++ b/components/theme/util/genComponentStyleHook.tsx @@ -0,0 +1,327 @@ +/* eslint-disable no-redeclare */ +import type { ComponentType, FC, ReactElement } from 'react'; +import React, { useContext } from 'react'; +import type { CSSInterpolation } from '@ant-design/cssinjs'; +import { token2CSSVar, useCSSVarRegister, useStyleRegister } from '@ant-design/cssinjs'; +import { warning } from 'rc-util'; + +import { ConfigContext } from '../../config-provider/context'; +import { genCommonStyle, genLinkStyle } from '../../style'; +import type { + ComponentTokenMap, + GlobalToken, + OverrideToken, + UseComponentStyleResult, +} from '../interface'; +import useToken, { ignore, unitless } from '../useToken'; +import statisticToken, { merge as mergeToken } from './statistic'; +import useResetIconStyle from './useResetIconStyle'; +import genCalc from './calc'; +import type AbstractCalculator from './calc/calculator'; +import classNames from 'classnames'; + +export type OverrideTokenWithoutDerivative = ComponentTokenMap; +export type OverrideComponent = keyof OverrideTokenWithoutDerivative; +export type GlobalTokenWithComponent = GlobalToken & + ComponentTokenMap[C]; + +type ComponentToken = Exclude; +type ComponentTokenKey = keyof ComponentToken; + +export interface StyleInfo { + hashId: string; + prefixCls: string; + rootPrefixCls: string; + iconPrefixCls: string; +} + +export type CSSUtil = { + calc: (number: any) => AbstractCalculator; +}; + +export type TokenWithCommonCls = T & { + /** Wrap component class with `.` prefix */ + componentCls: string; + /** Origin prefix which do not have `.` prefix */ + prefixCls: string; + /** Wrap icon class with `.` prefix */ + iconCls: string; + /** Wrap ant prefixCls class with `.` prefix */ + antCls: string; +} & CSSUtil; + +export type FullToken = TokenWithCommonCls< + GlobalTokenWithComponent +>; + +export type GenStyleFn = ( + token: FullToken, + info: StyleInfo, +) => CSSInterpolation; + +export type GetDefaultToken = + | null + | OverrideTokenWithoutDerivative[C] + | ((token: GlobalToken) => OverrideTokenWithoutDerivative[C]); + +const getDefaultComponentToken = ( + component: C, + token: GlobalToken, + getDefaultToken: GetDefaultToken, +) => { + if (typeof getDefaultToken === 'function') { + return getDefaultToken(mergeToken(token, token[component] ?? {})); + } + return getDefaultToken ?? {}; +}; + +const getComponentToken = ( + component: C, + token: GlobalToken, + defaultToken: OverrideTokenWithoutDerivative[C], + options?: { prefix?: boolean; deprecatedTokens?: [ComponentTokenKey, ComponentTokenKey][] }, +) => { + const customToken = { ...(token[component] as ComponentToken) }; + if (options?.deprecatedTokens) { + const { deprecatedTokens } = options; + deprecatedTokens.forEach(([oldTokenKey, newTokenKey]) => { + if (process.env.NODE_ENV !== 'production') { + warning( + !customToken?.[oldTokenKey], + `The token '${String(oldTokenKey)}' of ${component} had deprecated, use '${String( + newTokenKey, + )}' instead.`, + ); + } + + // Should wrap with `if` clause, or there will be `undefined` in object. + if (customToken?.[oldTokenKey] || customToken?.[newTokenKey]) { + customToken[newTokenKey] ??= customToken?.[oldTokenKey]; + } + }); + } + const mergedToken: any = { ...defaultToken, ...customToken }; + + // Remove same value as global token to minimize size + Object.keys(mergedToken).forEach((key) => { + if (mergedToken[key] === token[key as keyof GlobalToken]) { + delete mergedToken[key]; + } + }); + + if (options?.prefix && defaultToken) { + // Prefix component token with component name + Object.keys(defaultToken).forEach((key) => { + const newKey = `${component}${key.slice(0, 1).toUpperCase()}${key.slice(1)}`; + mergedToken[newKey] = mergedToken[key]; + delete mergedToken[key]; + }); + } + + return mergedToken; +}; + +export default function genComponentStyleHook( + componentName: C | [C, string], + styleFn: GenStyleFn, + getDefaultToken?: + | null + | OverrideTokenWithoutDerivative[C] + | ((token: GlobalToken) => OverrideTokenWithoutDerivative[C]), + options: { + resetStyle?: boolean; + // Deprecated token key map [["oldTokenKey", "newTokenKey"], ["oldTokenKey", "newTokenKey"]] + deprecatedTokens?: [ComponentTokenKey, ComponentTokenKey][]; + /** + * Only use component style in client side. Ignore in SSR. + */ + clientOnly?: boolean; + /** + * Set order of component style. Default is -999. + */ + order?: number; + } = {}, +) { + const cells = (Array.isArray(componentName) ? componentName : [componentName, componentName]) as [ + C, + string, + ]; + + const [component] = cells; + const concatComponent = cells.join('-'); + + return (prefixCls: string): UseComponentStyleResult => { + const [theme, token, hashId, realToken, cssVar] = useToken(); + const { getPrefixCls, iconPrefixCls, csp } = useContext(ConfigContext); + const rootPrefixCls = getPrefixCls(); + + const calculator = genCalc(cssVar ? 'css' : 'js'); + + // Shared config + const sharedConfig: Omit[0], 'path'> = { + theme, + token, + hashId, + nonce: () => csp?.nonce!, + clientOnly: options.clientOnly, + + // antd is always at top of styles + order: options.order || -999, + }; + + // Generate style for all a tags in antd component. + useStyleRegister( + { ...sharedConfig, clientOnly: false, path: ['Shared', rootPrefixCls] }, + () => [ + { + // Link + '&': genLinkStyle(token), + }, + ], + ); + + // Generate style for icons + useResetIconStyle(iconPrefixCls, csp); + + const wrapSSR = useStyleRegister( + { ...sharedConfig, path: [concatComponent, prefixCls, iconPrefixCls] }, + () => { + const { token: proxyToken, flush } = statisticToken(token); + + const defaultComponentToken = getDefaultComponentToken( + component, + realToken, + getDefaultToken, + ); + + const componentCls = `.${prefixCls}`; + const componentToken = getComponentToken(component, realToken, defaultComponentToken, { + deprecatedTokens: options.deprecatedTokens, + }); + + if (cssVar) { + Object.keys(defaultComponentToken).forEach((key) => { + defaultComponentToken[key] = `var(${token2CSSVar( + key, + `${cssVar?.prefix}-${component}`, + )})`; + }); + } + const mergedToken = mergeToken< + TokenWithCommonCls> + >( + proxyToken, + { + componentCls, + prefixCls, + iconCls: `.${iconPrefixCls}`, + antCls: `.${rootPrefixCls}`, + calc: calculator, + }, + cssVar ? defaultComponentToken : componentToken, + ); + + const styleInterpolation = styleFn(mergedToken as unknown as FullToken, { + hashId, + prefixCls, + rootPrefixCls, + iconPrefixCls, + }); + flush(component, componentToken); + return [ + options.resetStyle === false ? null : genCommonStyle(token, prefixCls), + styleInterpolation, + ]; + }, + ); + + return [wrapSSR, classNames(hashId, cssVar?.key)]; + }; +} + +export interface SubStyleComponentProps { + prefixCls: string; +} + +// Get from second argument +type RestParameters = T extends [any, ...infer Rest] ? Rest : never; + +export const genSubStyleComponent: ( + componentName: [C, string], + ...args: RestParameters>> +) => ComponentType = (componentName, styleFn, getDefaultToken, options) => { + const useStyle = genComponentStyleHook(componentName, styleFn, getDefaultToken, { + resetStyle: false, + + // Sub Style should default after root one + order: -998, + ...options, + }); + + const StyledComponent: ComponentType = ({ + prefixCls, + }: SubStyleComponentProps) => { + useStyle(prefixCls); + return null; + }; + + if (process.env.NODE_ENV !== 'production') { + StyledComponent.displayName = `SubStyle_${ + Array.isArray(componentName) ? componentName.join('.') : componentName + }`; + } + + return StyledComponent; +}; + +export type CSSVarRegisterProps = { + rootCls: string; + component: string; + cssVar: { + prefix?: string; + key?: string; + }; +}; + +export const genCSSVarRegister = ( + component: C, + getDefaultToken: GetDefaultToken, +) => { + const CSSVarRegister: FC = ({ rootCls, cssVar }) => { + const [, , , realToken] = useToken(); + useCSSVarRegister( + { + path: [component], + prefix: cssVar?.prefix, + key: cssVar?.key!, + unitless, + ignore, + token: realToken, + scope: rootCls, + }, + () => { + const defaultToken = getDefaultComponentToken(component, realToken, getDefaultToken); + return getComponentToken(component, realToken, defaultToken, { + prefix: true, + }); + }, + ); + return null; + }; + + const useCSSVar = (rootCls: string) => { + const [, , , , cssVar] = useToken(); + + return (node: ReactElement): ReactElement => + cssVar ? ( + <> + + {node} + + ) : ( + node + ); + }; + + return useCSSVar; +}; diff --git a/docs/react/compatible-style.en-US.md b/docs/react/compatible-style.en-US.md index 35188cbf56..04cf206c59 100644 --- a/docs/react/compatible-style.en-US.md +++ b/docs/react/compatible-style.en-US.md @@ -56,7 +56,7 @@ Raise priority through plugin: To unify LTR and RTL styles, Ant Design uses CSS logical properties. For example, the original `margin-left` is replaced by `margin-inline-start`, so that it is the starting position spacing under both LTR and RTL. If you need to be compatible with older browsers, you can configure `transformers` through the `StyleProvider` of `@ant-design/cssinjs`: ```tsx -import { StyleProvider, legacyLogicalPropertiesTransformer } from '@ant-design/cssinjs'; +import { legacyLogicalPropertiesTransformer, StyleProvider } from '@ant-design/cssinjs'; // `transformers` provides a way to transform CSS properties export default () => ( @@ -83,7 +83,7 @@ When toggled, styles will downgrade CSS logical properties: In responsive web development, there is a need for a convenient and flexible way to achieve page adaptation and responsive design. The `px2remTransformer` transformer can quickly and accurately convert pixel units in style sheets to rem units relative to the root element (HTML tag), enabling the implementation of adaptive and responsive layouts. ```tsx -import { StyleProvider, px2remTransformer } from '@ant-design/cssinjs'; +import { px2remTransformer, StyleProvider } from '@ant-design/cssinjs'; const px2rem = px2remTransformer({ rootValue: 32, // 32px = 1rem; @default 16 diff --git a/package.json b/package.json index 20ce3a76e2..eac29ef8fa 100644 --- a/package.json +++ b/package.json @@ -113,7 +113,7 @@ ], "dependencies": { "@ant-design/colors": "^7.0.0", - "@ant-design/cssinjs": "^1.17.2", + "@ant-design/cssinjs": "^2.0.0-alpha.1", "@ant-design/icons": "^5.2.6", "@ant-design/react-slick": "~1.0.2", "@babel/runtime": "^7.18.3", @@ -329,11 +329,11 @@ "size-limit": [ { "path": "./dist/antd.min.js", - "limit": "401 KiB" + "limit": "405 KiB" }, { "path": "./dist/antd-with-locales.min.js", - "limit": "460 KiB" + "limit": "465 KiB" } ], "title": "Ant Design", From 96fa23ea7c18e32871b0cb2f1c1aa0c1c5051f17 Mon Sep 17 00:00:00 2001 From: MadCcc Date: Wed, 8 Nov 2023 14:56:15 +0800 Subject: [PATCH 02/90] refactor: Tooltip, Popover, Dropdown, Tour support css var (#45705) * feat: Tooltip, Popover, Dropdown, Tour support css var * chore: update snapshot * chore: code clean * chore: code clean * chore: update snapshot * chore: update snapshot * chore: update snapshot * chore: add test --- components/_util/placements.ts | 18 +- components/_util/responsiveObserver.ts | 2 +- .../__snapshots__/demo-extend.test.ts.snap | 12 +- .../__snapshots__/demo-extend.test.ts.snap | 4 +- components/button/style/compactCmp.ts | 51 ++++++ components/button/style/group.ts | 2 +- components/button/style/index.ts | 38 ---- .../__snapshots__/components.test.tsx.snap | 28 +-- .../__snapshots__/demo-extend.test.ts.snap | 172 +++++++++--------- .../__snapshots__/index.test.tsx.snap | 8 +- components/dropdown/dropdown-button.tsx | 8 +- components/dropdown/dropdown.tsx | 9 +- components/dropdown/style/cssVar.ts | 4 + components/dropdown/style/index.ts | 76 ++++---- .../__snapshots__/demo-extend.test.ts.snap | 8 +- components/menu/OverrideContext.tsx | 2 + components/menu/menu.tsx | 2 +- components/popover/PurePanel.tsx | 6 +- components/popover/index.tsx | 6 +- components/popover/style/cssVar.ts | 4 + components/popover/style/index.tsx | 37 ++-- .../__snapshots__/demo-extend.test.ts.snap | 12 +- components/style/compact-item-vertical.ts | 8 +- components/style/compact-item.ts | 8 +- components/style/placementArrow.ts | 71 +++----- components/style/roundedArrow.ts | 91 +++++++++ .../__snapshots__/Table.filter.test.tsx.snap | 4 +- .../Table.rowSelection.test.tsx.snap | 12 +- .../__snapshots__/demo-extend.test.ts.snap | 92 +++++----- components/theme/__tests__/util.test.tsx | 33 +++- components/theme/useToken.ts | 2 +- components/theme/util/calc/CSSCalculator.ts | 20 +- components/theme/util/calc/calculator.ts | 8 +- .../theme/util/genComponentStyleHook.tsx | 47 +++-- components/theme/util/maxmin.ts | 14 ++ components/tooltip/PurePanel.tsx | 6 +- components/tooltip/index.tsx | 8 +- components/tooltip/style/cssVar.ts | 4 + components/tooltip/style/index.ts | 60 +++--- components/tour/PurePanel.tsx | 6 +- components/tour/index.tsx | 6 +- components/tour/style/cssVar.ts | 4 + components/tour/style/index.ts | 109 +++++++---- .../__snapshots__/demo-extend.test.ts.snap | 112 ++++++------ 44 files changed, 738 insertions(+), 496 deletions(-) create mode 100644 components/dropdown/style/cssVar.ts create mode 100644 components/popover/style/cssVar.ts create mode 100644 components/theme/util/maxmin.ts create mode 100644 components/tooltip/style/cssVar.ts create mode 100644 components/tour/style/cssVar.ts diff --git a/components/_util/placements.ts b/components/_util/placements.ts index f2f39fb679..125e6bf81f 100644 --- a/components/_util/placements.ts +++ b/components/_util/placements.ts @@ -1,7 +1,7 @@ /* eslint-disable default-case */ import type { AlignType, BuildInPlacements } from '@rc-component/trigger'; -import { getArrowOffset } from '../style/placementArrow'; +import { getArrowOffsetToken } from '../style/placementArrow'; export interface AdjustOverflow { adjustX?: 0 | 1; @@ -19,7 +19,7 @@ export interface PlacementsConfig { export function getOverflowOptions( placement: string, - arrowOffset: ReturnType, + arrowOffset: ReturnType, arrowWidth: number, autoAdjustOverflow?: boolean | AdjustOverflow, ) { @@ -38,14 +38,14 @@ export function getOverflowOptions( switch (placement) { case 'top': case 'bottom': - baseOverflow.shiftX = arrowOffset.dropdownArrowOffset * 2 + arrowWidth; + baseOverflow.shiftX = arrowOffset.arrowOffsetHorizontal * 2 + arrowWidth; baseOverflow.shiftY = true; baseOverflow.adjustY = true; break; case 'left': case 'right': - baseOverflow.shiftY = arrowOffset.dropdownArrowOffsetVertical * 2 + arrowWidth; + baseOverflow.shiftY = arrowOffset.arrowOffsetVertical * 2 + arrowWidth; baseOverflow.shiftX = true; baseOverflow.adjustX = true; break; @@ -197,7 +197,7 @@ export default function getPlacements(config: PlacementsConfig) { } // Dynamic offset - const arrowOffset = getArrowOffset({ + const arrowOffset = getArrowOffsetToken({ contentRadius: borderRadius, limitVerticalRadius: true, }); @@ -206,22 +206,22 @@ export default function getPlacements(config: PlacementsConfig) { switch (key) { case 'topLeft': case 'bottomLeft': - placementInfo.offset[0] = -arrowOffset.dropdownArrowOffset - halfArrowWidth; + placementInfo.offset[0] = -arrowOffset.arrowOffsetHorizontal - halfArrowWidth; break; case 'topRight': case 'bottomRight': - placementInfo.offset[0] = arrowOffset.dropdownArrowOffset + halfArrowWidth; + placementInfo.offset[0] = arrowOffset.arrowOffsetHorizontal + halfArrowWidth; break; case 'leftTop': case 'rightTop': - placementInfo.offset[1] = -arrowOffset.dropdownArrowOffset - halfArrowWidth; + placementInfo.offset[1] = -arrowOffset.arrowOffsetHorizontal - halfArrowWidth; break; case 'leftBottom': case 'rightBottom': - placementInfo.offset[1] = arrowOffset.dropdownArrowOffset + halfArrowWidth; + placementInfo.offset[1] = arrowOffset.arrowOffsetHorizontal + halfArrowWidth; break; } } diff --git a/components/_util/responsiveObserver.ts b/components/_util/responsiveObserver.ts index ea1f863f24..ca4b3f7844 100644 --- a/components/_util/responsiveObserver.ts +++ b/components/_util/responsiveObserver.ts @@ -62,7 +62,7 @@ const validateBreakpoints = (token: GlobalToken) => { }; export default function useResponsiveObserver() { - const [, , , token] = useToken(); + const [, token] = useToken(); const responsiveMap: BreakpointMap = getResponsiveMap(validateBreakpoints(token)); // To avoid repeat create instance, we add `useMemo` here. diff --git a/components/breadcrumb/__tests__/__snapshots__/demo-extend.test.ts.snap b/components/breadcrumb/__tests__/__snapshots__/demo-extend.test.ts.snap index 6b0ef22c8a..c6498f5ab8 100644 --- a/components/breadcrumb/__tests__/__snapshots__/demo-extend.test.ts.snap +++ b/components/breadcrumb/__tests__/__snapshots__/demo-extend.test.ts.snap @@ -134,11 +134,11 @@ exports[`renders components/breadcrumb/demo/component-token.tsx extend context c