diff --git a/components/__tests__/__snapshots__/index.test.js.snap b/components/__tests__/__snapshots__/index.test.js.snap index 3e17f31437..97137270d1 100644 --- a/components/__tests__/__snapshots__/index.test.js.snap +++ b/components/__tests__/__snapshots__/index.test.js.snap @@ -67,6 +67,7 @@ Array [ "Upload", "message", "notification", + "useDesignToken", "version", ] `; diff --git a/components/config-provider/__tests__/token.test.tsx b/components/config-provider/__tests__/token.test.tsx deleted file mode 100644 index 1add605cbc..0000000000 --- a/components/config-provider/__tests__/token.test.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import classNames from 'classnames'; -import React from 'react'; -import ConfigProvider from '..'; -import { render } from '../../../tests/utils'; -import type { AliasToken } from '../../theme'; -import type { UseCustomStyle, UseToken } from '../../theme/util/useStyle'; - -const { useCustomToken, useStyle, useCustomStyle } = ConfigProvider; - -describe('ConfigProvider.Token', () => { - type MySeedToken = { - colorAlert: string; - }; - - type MyAliasToken = { - colorAlertActive: string; - }; - - type MyToken = AliasToken & MySeedToken & MyAliasToken; - - const useMyToken: UseToken = () => - useCustomToken({ - seedToken: { colorAlert: 'red' }, - formatToken: derivativeToken => ({ - ...derivativeToken, - colorAlertActive: 'purple', - }), - }); - - const useMyStyle: UseCustomStyle = (componentName, styleFn) => { - const { token, hashId } = useMyToken(); - return useCustomStyle(componentName, styleFn, token, hashId); - }; - - it('should support customSeedToken & customOverride', () => { - const Demo = () => { - const { token } = useMyToken(); - return
{JSON.stringify(token)}
; - }; - - const { container } = render(); - const finalToken = JSON.parse(container.firstElementChild?.innerHTML ?? ''); - expect(finalToken).toHaveProperty('colorAlert', 'red'); - expect(finalToken).toHaveProperty('colorAlertActive', 'purple'); - }); - - it('useCustomStyle', () => { - const Demo = () => { - const { wrapSSR, hashId } = useMyStyle('box', token => ({ - '.box': { - backgroundColor: token.colorAlert, - color: token.colorAlertActive, - }, - })); - return wrapSSR(
test registerToken
); - }; - - render(); - const dynamicStyles = Array.from(document.querySelectorAll('style[data-css-hash]')).map( - item => item?.innerHTML ?? '', - ); - expect( - dynamicStyles.some(style => style.includes('.box{background-color:red;color:purple;}')), - ).toBeTruthy(); - }); - - it('useStyle', () => { - const Demo = () => { - const { wrapSSR, hashId } = useStyle('default-useStyle', token => ({ - '.default-genStyleHook': { - backgroundColor: token.colorPrimary, - color: token.colorSuccess, - }, - })); - return wrapSSR( -
test registerToken
, - ); - }; - - render(); - const dynamicStyles = Array.from(document.querySelectorAll('style[data-css-hash]')).map( - item => item?.innerHTML ?? '', - ); - expect( - dynamicStyles.some(style => - style.includes('.default-genStyleHook{background-color:#1890ff;color:#52c41a;}'), - ), - ).toBeTruthy(); - }); -}); diff --git a/components/config-provider/index.tsx b/components/config-provider/index.tsx index 2c5f4c5fa5..87689586a8 100644 --- a/components/config-provider/index.tsx +++ b/components/config-provider/index.tsx @@ -8,9 +8,8 @@ import type { Locale } from '../locale-provider'; import LocaleProvider, { ANT_MARK } from '../locale-provider'; import LocaleReceiver from '../locale-provider/LocaleReceiver'; import defaultLocale from '../locale/default'; -import { DesignTokenContext, useCustomToken, useToken } from '../theme'; +import { DesignTokenContext } from '../theme'; import defaultSeedToken from '../theme/themes/seed'; -import { useCustomStyle, useStyle } from '../theme/util/useStyle'; import type { ConfigConsumerProps, CSPConfig, DirectionType, Theme, ThemeConfig } from './context'; import { ConfigConsumer, ConfigContext, defaultIconPrefixCls } from './context'; import { registerTheme } from './cssVariables'; @@ -282,10 +281,6 @@ const ConfigProvider: React.FC & { ConfigContext: typeof ConfigContext; SizeContext: typeof SizeContext; config: typeof setGlobalConfig; - useToken: typeof useToken; - useStyle: typeof useStyle; - useCustomToken: typeof useCustomToken; - useCustomStyle: typeof useCustomStyle; } = props => ( {(_, __, legacyLocale) => ( @@ -305,9 +300,5 @@ const ConfigProvider: React.FC & { ConfigProvider.ConfigContext = ConfigContext; ConfigProvider.SizeContext = SizeContext; ConfigProvider.config = setGlobalConfig; -ConfigProvider.useToken = useToken; -ConfigProvider.useStyle = useStyle; -ConfigProvider.useCustomToken = useCustomToken; -ConfigProvider.useCustomStyle = useCustomStyle; export default ConfigProvider; diff --git a/components/index.tsx b/components/index.tsx index 8ea50236b9..1f336b3b1b 100644 --- a/components/index.tsx +++ b/components/index.tsx @@ -148,6 +148,7 @@ export { default as Tabs } from './tabs'; export type { TabPaneProps, TabsProps } from './tabs'; export { default as Tag } from './tag'; export type { TagProps, TagType } from './tag'; +export { useDesignToken } from './theme/export'; export { default as TimePicker } from './time-picker'; export type { TimePickerProps, TimeRangePickerProps } from './time-picker'; export { default as Timeline } from './timeline'; diff --git a/components/theme/__tests__/token.test.tsx b/components/theme/__tests__/token.test.tsx new file mode 100644 index 0000000000..69b382209a --- /dev/null +++ b/components/theme/__tests__/token.test.tsx @@ -0,0 +1,47 @@ +import { Theme } from '@ant-design/cssinjs'; +import * as React from 'react'; +import { render, renderHook } from '../../../tests/utils'; +import ConfigProvider from '../../config-provider'; +import { useDesignToken } from '../export'; + +describe('Theme', () => { + it('useTheme', () => { + const result = renderHook(() => useDesignToken()); + + expect(result.current.theme instanceof Theme).toBeTruthy(); + expect(result.current.hashId).toBeFalsy(); + expect(result.current.token).toEqual( + expect.objectContaining({ + colorPrimary: '#1890ff', + }), + ); + }); + + it('ConfigProvider with seed', () => { + const Demo = React.forwardRef((_, ref: any) => { + const themeObj = useDesignToken(); + ref.current = themeObj; + return null; + }); + + const themeRef = React.createRef>(); + render( + + + , + ); + + expect(themeRef.current!.token).toEqual( + expect.objectContaining({ + colorPrimary: 'red', + colorPrimaryHover: '#ff3029', // It's safe to modify if theme logic changed + }), + ); + }); +}); diff --git a/components/theme/export.ts b/components/theme/export.ts new file mode 100644 index 0000000000..7066c7cf77 --- /dev/null +++ b/components/theme/export.ts @@ -0,0 +1,14 @@ +/* eslint-disable import/prefer-default-export */ +import { useToken } from '.'; + +// ZombieJ: We export as object to user but array in internal. +// This is used to minimize the bundle size for antd package but safe to refactor as object also. +// Please do not export internal `useToken` directly to avoid something export unexpected. +/** + * Get current context Design Token. Will be different if you using nest theme config. + */ +export function useDesignToken() { + const [theme, token, hashId] = useToken(); + + return { theme, token, hashId }; +} diff --git a/components/theme/index.tsx b/components/theme/index.tsx index e57337ae42..efaef61218 100644 --- a/components/theme/index.tsx +++ b/components/theme/index.tsx @@ -71,7 +71,7 @@ export function useToken(): [Theme, GlobalToken, string] { hashed, } = React.useContext(DesignTokenContext); - const theme = new Theme(derivative); + const theme = React.useMemo(() => new Theme(derivative), [derivative]); const salt = `${saltPrefix}-${hashed || ''}`; @@ -94,45 +94,3 @@ export type GenerateStyle< ComponentToken extends object = AliasToken, ReturnType = CSSInterpolation, > = (token: ComponentToken) => ReturnType; - -export const emptyTheme = new Theme(token => token); - -export type CustomTokenOptions< - CustomSeedToken extends Record, - CustomAliasToken extends Record = {}, -> = { - /** The original tokens, which may affect other tokens. */ - seedToken?: CustomSeedToken; - /** Generate token based on seedToken. */ - formatToken?: ( - mergedToken: AliasToken & CustomSeedToken, - ) => AliasToken & CustomSeedToken & CustomAliasToken; -}; - -/** - * Generate custom tokens with tokens of ant-design. - */ -export function useCustomToken< - CustomSeedToken extends Record, - CustomAliasToken extends Record = {}, ->({ - seedToken, - formatToken: customFormatToken, -}: CustomTokenOptions): { - token: AliasToken & CustomSeedToken & CustomAliasToken; - hashId: string; -} { - const [, antdToken, hashed] = useToken(); - - const salt = `${saltPrefix}-${hashed || ''}`; - - const [token, hashId] = useCacheToken(emptyTheme, [antdToken, seedToken ?? {}], { - salt, - formatToken: customFormatToken, - }); - - return { - token, - hashId: hashed ? hashId : '', - }; -} diff --git a/components/theme/util/useStyle.tsx b/components/theme/util/useStyle.tsx deleted file mode 100644 index cc29e41d77..0000000000 --- a/components/theme/util/useStyle.tsx +++ /dev/null @@ -1,48 +0,0 @@ -/* eslint-disable no-redeclare */ -import type { CSSInterpolation } from '@ant-design/cssinjs'; -import { useStyleRegister } from '@ant-design/cssinjs'; -import type React from 'react'; -import type { AliasToken } from '..'; -import { emptyTheme, useToken } from '..'; - -export type UseStyleResult = { - wrapSSR: (node: React.ReactNode) => React.ReactElement; - hashId: string; -}; - -export type UseToken = () => { - token: T; - hashId: string; -}; - -export type UseCustomStyle = ( - componentName: string, - styleFn: (token: T) => CSSInterpolation, -) => UseStyleResult; - -export function useStyle( - componentName: string, - styleFn: (token: AliasToken) => CSSInterpolation, -): UseStyleResult { - const [, token, hashId] = useToken(); - return { - wrapSSR: useStyleRegister({ theme: emptyTheme, token, hashId, path: [componentName] }, () => - styleFn(token), - ), - hashId, - }; -} - -export function useCustomStyle( - componentName: string, - styleFn: (token: T) => CSSInterpolation, - token: T, - hashId: string, -): UseStyleResult { - return { - wrapSSR: useStyleRegister({ theme: emptyTheme, token, hashId, path: [componentName] }, () => - styleFn(token), - ), - hashId, - }; -} diff --git a/site/theme/template/Layout/DynamicTheme/Token.tsx b/site/theme/template/Layout/DynamicTheme/Token.tsx index bf0bed1a32..752e75c926 100644 --- a/site/theme/template/Layout/DynamicTheme/Token.tsx +++ b/site/theme/template/Layout/DynamicTheme/Token.tsx @@ -1,6 +1,6 @@ /* eslint-disable react/no-array-index-key */ import type { TableProps } from 'antd'; -import { Alert, Col, ConfigProvider, Row, Select, Space, Table } from 'antd'; +import { Alert, Col, Row, Select, Space, Table, useDesignToken } from 'antd'; import * as React from 'react'; import { statistic } from '../../../../../components/theme'; @@ -85,7 +85,7 @@ export default () => { ); // Full token - const [, token] = ConfigProvider.useToken(); + const { token } = useDesignToken(); const tokenList = React.useMemo( () => Object.keys(token) diff --git a/tests/__snapshots__/index.test.js.snap b/tests/__snapshots__/index.test.js.snap index a1e9ba738e..d805303d64 100644 --- a/tests/__snapshots__/index.test.js.snap +++ b/tests/__snapshots__/index.test.js.snap @@ -67,6 +67,7 @@ Array [ "Upload", "message", "notification", + "useDesignToken", "version", ] `; diff --git a/tests/utils.ts b/tests/utils.tsx similarity index 81% rename from tests/utils.ts rename to tests/utils.tsx index 672b33fdfa..e674964e5e 100644 --- a/tests/utils.ts +++ b/tests/utils.tsx @@ -1,11 +1,11 @@ -import MockDate from 'mockdate'; -import type { ReactElement } from 'react'; -import { StrictMode } from 'react'; -import { act } from 'react-dom/test-utils'; import type { RenderOptions } from '@testing-library/react'; import { render } from '@testing-library/react'; -import { _rs as onLibResize } from 'rc-resize-observer/lib/utils/observerUtil'; +import MockDate from 'mockdate'; import { _rs as onEsResize } from 'rc-resize-observer/es/utils/observerUtil'; +import { _rs as onLibResize } from 'rc-resize-observer/lib/utils/observerUtil'; +import type { ReactElement } from 'react'; +import React, { StrictMode } from 'react'; +import { act } from 'react-dom/test-utils'; export function setMockDate(dateString = '2017-09-18T03:30:07.795') { MockDate.set(dateString); @@ -28,6 +28,7 @@ export const sleep = async (timeout = 0) => { const customRender = (ui: ReactElement, options?: Omit) => render(ui, { wrapper: StrictMode, ...options }); +export * from '@testing-library/react'; export { customRender as render }; export const triggerResize = (target: Element) => { @@ -40,4 +41,14 @@ export const triggerResize = (target: Element) => { target.getBoundingClientRect = originGetBoundingClientRect; }; -export * from '@testing-library/react'; +export function renderHook(func: () => T): { current: T } { + const outerRef = React.createRef(); + const Demo = React.forwardRef((_, ref: any) => { + ref.current = func(); + return null; + }); + + render(); + + return outerRef as any; +}