diff --git a/components/_util/cssinjs/Cache.ts b/components/_util/cssinjs/Cache.ts index 010a0e3f6c..cff14b4752 100644 --- a/components/_util/cssinjs/Cache.ts +++ b/components/_util/cssinjs/Cache.ts @@ -1,11 +1,14 @@ export type KeyType = string | number; type ValueType = [number, any]; // [times, realValue] + const SPLIT = '%'; + class Entity { instanceId: string; constructor(instanceId: string) { this.instanceId = instanceId; } + /** @private Internal cache map. Do not access this directly */ cache = new Map(); diff --git a/components/_util/cssinjs/StyleContext.tsx b/components/_util/cssinjs/StyleContext.tsx index faf49f4b5d..6d062eece2 100644 --- a/components/_util/cssinjs/StyleContext.tsx +++ b/components/_util/cssinjs/StyleContext.tsx @@ -31,7 +31,6 @@ export function createCache() { Array.from(styles).forEach(style => { (style as any)[CSS_IN_JS_INSTANCE] = (style as any)[CSS_IN_JS_INSTANCE] || cssinjsInstanceId; - // Not force move if no head // Not force move if no head if ((style as any)[CSS_IN_JS_INSTANCE] === cssinjsInstanceId) { document.head.insertBefore(style, firstChild); diff --git a/components/_util/cssinjs/extractStyle.ts b/components/_util/cssinjs/extractStyle.ts new file mode 100644 index 0000000000..3fa3411cd9 --- /dev/null +++ b/components/_util/cssinjs/extractStyle.ts @@ -0,0 +1,82 @@ +import type Cache from './Cache'; +import { extract as tokenExtractStyle, TOKEN_PREFIX } from './hooks/useCacheToken'; +import { CSS_VAR_PREFIX, extract as cssVarExtractStyle } from './hooks/useCSSVarRegister'; +import { extract as styleExtractStyle, STYLE_PREFIX } from './hooks/useStyleRegister'; +import { toStyleStr } from './util'; +import { ATTR_CACHE_MAP, serialize as serializeCacheMap } from './util/cacheMapUtil'; + +const ExtractStyleFns = { + [STYLE_PREFIX]: styleExtractStyle, + [TOKEN_PREFIX]: tokenExtractStyle, + [CSS_VAR_PREFIX]: cssVarExtractStyle, +}; + +type ExtractStyleType = keyof typeof ExtractStyleFns; + +function isNotNull(value: T | null): value is T { + return value !== null; +} + +export default function extractStyle( + cache: Cache, + options?: + | boolean + | { + plain?: boolean; + types?: ExtractStyleType | ExtractStyleType[]; + }, +) { + const { plain = false, types = ['style', 'token', 'cssVar'] } = + typeof options === 'boolean' ? { plain: options } : options || {}; + + const matchPrefixRegexp = new RegExp( + `^(${(typeof types === 'string' ? [types] : types).join('|')})%`, + ); + + // prefix with `style` is used for `useStyleRegister` to cache style context + const styleKeys = Array.from(cache.cache.keys()).filter(key => matchPrefixRegexp.test(key)); + + // Common effect styles like animation + const effectStyles: Record = {}; + + // Mapping of cachePath to style hash + const cachePathMap: Record = {}; + + let styleText = ''; + + styleKeys + .map<[number, string] | null>(key => { + const cachePath = key.replace(matchPrefixRegexp, '').replace(/%/g, '|'); + const [prefix] = key.split('%'); + const extractFn = ExtractStyleFns[prefix as keyof typeof ExtractStyleFns]; + const extractedStyle = extractFn(cache.cache.get(key)![1], effectStyles, { + plain, + }); + if (!extractedStyle) { + return null; + } + const [order, styleId, styleStr] = extractedStyle; + if (key.startsWith('style')) { + cachePathMap[cachePath] = styleId; + } + return [order, styleStr]; + }) + .filter(isNotNull) + .sort(([o1], [o2]) => o1 - o2) + .forEach(([, style]) => { + styleText += style; + }); + + // ==================== Fill Cache Path ==================== + styleText += toStyleStr( + `.${ATTR_CACHE_MAP}{content:"${serializeCacheMap(cachePathMap)}";}`, + undefined, + undefined, + { + [ATTR_CACHE_MAP]: ATTR_CACHE_MAP, + }, + plain, + ); + + return styleText; +} diff --git a/components/_util/cssinjs/hooks/useCSSVarRegister.ts b/components/_util/cssinjs/hooks/useCSSVarRegister.ts new file mode 100644 index 0000000000..e2b5d4d764 --- /dev/null +++ b/components/_util/cssinjs/hooks/useCSSVarRegister.ts @@ -0,0 +1,108 @@ +import { removeCSS, updateCSS } from '../../../vc-util/Dom/dynamicCSS'; +import { ATTR_MARK, ATTR_TOKEN, CSS_IN_JS_INSTANCE, useStyleInject } from '../StyleContext'; +import { isClientSide, toStyleStr } from '../util'; +import type { TokenWithCSSVar } from '../util/css-variables'; +import { transformToken } from '../util/css-variables'; +import type { ExtractStyle } from './useGlobalCache'; +import useGlobalCache from './useGlobalCache'; +import { uniqueHash } from './useStyleRegister'; +import type { ComputedRef } from 'vue'; +import { computed } from 'vue'; + +export const CSS_VAR_PREFIX = 'cssVar'; + +type CSSVarCacheValue = Record> = [ + cssVarToken: TokenWithCSSVar, + cssVarStr: string, + styleId: string, + cssVarKey: string, +]; + +const useCSSVarRegister = >( + config: ComputedRef<{ + path: string[]; + key: string; + prefix?: string; + unitless?: Record; + ignore?: Record; + scope?: string; + token: any; + }>, + fn: () => T, +) => { + const styleContext = useStyleInject(); + + const stylePath = computed(() => { + return [ + ...config.value.path, + config.value.key, + config.value.scope || '', + config.value.token?._tokenKey, + ]; + }); + + const cache = useGlobalCache>( + CSS_VAR_PREFIX, + stylePath, + () => { + const originToken = fn(); + const [mergedToken, cssVarsStr] = transformToken(originToken, config.value.key, { + prefix: config.value.prefix, + unitless: config.value.unitless, + ignore: config.value.ignore, + scope: config.value.scope || '', + }); + + const styleId = uniqueHash(stylePath.value, cssVarsStr); + return [mergedToken, cssVarsStr, styleId, config.value.key]; + }, + ([, , styleId]) => { + if (isClientSide) { + removeCSS(styleId, { mark: ATTR_MARK }); + } + }, + ([, cssVarsStr, styleId]) => { + if (!cssVarsStr) { + return; + } + + const style = updateCSS(cssVarsStr, styleId, { + mark: ATTR_MARK, + prepend: 'queue', + attachTo: styleContext.value.container, + priority: -999, + }); + + (style as any)[CSS_IN_JS_INSTANCE] = styleContext.value.cache?.instanceId; + + // Used for `useCacheToken` to remove on batch when token removed + style.setAttribute(ATTR_TOKEN, config.value.key); + }, + ); + + return cache; +}; + +export const extract: ExtractStyle> = (cache, _effectStyles, options) => { + const [, styleStr, styleId, cssVarKey] = cache; + const { plain } = options || {}; + + if (!styleStr) { + return null; + } + + const order = -999; + + // ====================== Style ====================== + // Used for rc-util + const sharedAttrs = { + 'data-vc-order': 'prependQueue', + 'data-vc-priority': `${order}`, + }; + + const styleText = toStyleStr(styleStr, cssVarKey, styleId, sharedAttrs, plain); + + return [order, styleId, styleText]; +}; + +export default useCSSVarRegister; diff --git a/components/_util/cssinjs/hooks/useCacheToken.tsx b/components/_util/cssinjs/hooks/useCacheToken.tsx index f97d146b55..6067cdd62a 100644 --- a/components/_util/cssinjs/hooks/useCacheToken.tsx +++ b/components/_util/cssinjs/hooks/useCacheToken.tsx @@ -1,20 +1,19 @@ import hash from '@emotion/hash'; -import { ATTR_TOKEN, CSS_IN_JS_INSTANCE, useStyleInject } from '../StyleContext'; +import { updateCSS } from '../../../vc-util/Dom/dynamicCSS'; +import { ATTR_MARK, ATTR_TOKEN, CSS_IN_JS_INSTANCE, useStyleInject } from '../StyleContext'; import type Theme from '../theme/Theme'; +import { flattenToken, memoResult, token2key, toStyleStr } from '../util'; +import { transformToken } from '../util/css-variables'; +import type { ExtractStyle } from './useGlobalCache'; import useGlobalCache from './useGlobalCache'; -import { flattenToken, token2key } from '../util'; import type { Ref } from 'vue'; import { ref, computed } from 'vue'; const EMPTY_OVERRIDE = {}; -const isProduction = process.env.NODE_ENV === 'production'; -// nuxt generate when NODE_ENV is prerender -const isPrerender = process.env.NODE_ENV === 'prerender'; - // Generate different prefix to make user selector break in production env. // This helps developer not to do style override directly on the hash id. -const hashPrefix = !isProduction && !isPrerender ? 'css-dev-only-do-not-override' : 'css'; +const hashPrefix = process.env.NODE_ENV !== 'production' ? 'css-dev-only-do-not-override' : 'css'; export interface Option { /** @@ -46,6 +45,22 @@ export interface Option { override: object, theme: Theme, ) => DerivativeToken; + + /** + * Transform token to css variables. + */ + cssVar?: { + /** Prefix for css variables */ + prefix?: string; + /** Tokens that should not be appended with unit */ + unitless?: Record; + /** Tokens that should not be transformed to css variables */ + ignore?: Record; + /** Tokens that preserves origin value */ + preserve?: Record; + /** Key for current theme. Useful for customizing and should be unique */ + key?: string; + }; } const tokenKeys = new Map(); @@ -94,6 +109,7 @@ export const getComputedToken = DerivativeToken, ) => { const derivativeToken = theme.getDerivativeToken(originToken); + // Merge with override let mergedDerivativeToken = { ...derivativeToken, @@ -108,6 +124,16 @@ export const getComputedToken = = [ + token: DerivativeToken & { _tokenKey: string; _themeKey: string }, + hashId: string, + realToken: DerivativeToken & { _tokenKey: string }, + cssVarStr: string, + cssVarKey: string, +]; + /** * Cache theme derivative token as global shared one * @param theme Theme entity @@ -119,21 +145,27 @@ export default function useCacheToken>, tokens: Ref[]>, option: Ref> = ref({}), -) { - const style = useStyleInject(); +): Ref> { + const styleContext = useStyleInject(); // Basic - We do basic cache here - const mergedToken = computed(() => Object.assign({}, ...tokens.value)); + const mergedToken = computed(() => + memoResult(() => Object.assign({}, ...tokens.value), tokens.value), + ); + const tokenStr = computed(() => flattenToken(mergedToken.value)); - const overrideTokenStr = computed(() => flattenToken(option.value.override || EMPTY_OVERRIDE)); + const overrideTokenStr = computed(() => flattenToken(option.value.override ?? EMPTY_OVERRIDE)); - const cachedToken = useGlobalCache<[DerivativeToken & { _tokenKey: string }, string]>( - 'token', + const cssVarStr = computed(() => (option.value.cssVar ? flattenToken(option.value.cssVar) : '')); + + const cachedToken = useGlobalCache>( + TOKEN_PREFIX, computed(() => [ - option.value.salt || '', - theme.value.id, + option.value.salt ?? '', + theme.value?.id, tokenStr.value, overrideTokenStr.value, + cssVarStr.value, ]), () => { const { @@ -141,25 +173,82 @@ export default function useCacheToken { // Remove token will remove all related style - cleanTokenStyle(cache[0]._tokenKey, style.value?.cache.instanceId); + cleanTokenStyle(cache[0]._themeKey, styleContext.value?.cache?.instanceId); + }, + ([token, , , cssVarsStr]) => { + const { cssVar } = option.value; + if (cssVar && cssVarsStr) { + const style = updateCSS(cssVarsStr, hash(`css-variables-${token._themeKey}`), { + mark: ATTR_MARK, + prepend: 'queue', + attachTo: styleContext.value?.container, + priority: -999, + }); + + (style as any)[CSS_IN_JS_INSTANCE] = styleContext.value?.cache?.instanceId; + + // Used for `useCacheToken` to remove on batch when token removed + style.setAttribute(ATTR_TOKEN, token._themeKey); + } }, ); return cachedToken; } + +export const extract: ExtractStyle> = (cache, _effectStyles, options) => { + const [, , realToken, styleStr, cssVarKey] = cache; + const { plain } = options || {}; + + if (!styleStr) { + return null; + } + + const styleId = realToken._tokenKey; + const order = -999; + + // ====================== Style ====================== + // Used for rc-util + const sharedAttrs = { + 'data-vc-order': 'prependQueue', + 'data-vc-priority': `${order}`, + }; + + const styleText = toStyleStr(styleStr, cssVarKey, styleId, sharedAttrs, plain); + + return [order, styleId, styleText]; +}; diff --git a/components/_util/cssinjs/hooks/useCompatibleInsertionEffect.tsx b/components/_util/cssinjs/hooks/useCompatibleInsertionEffect.tsx new file mode 100644 index 0000000000..e3ac2e794f --- /dev/null +++ b/components/_util/cssinjs/hooks/useCompatibleInsertionEffect.tsx @@ -0,0 +1,30 @@ +// import canUseDom from 'rc-util/lib/Dom/canUseDom'; +import useLayoutEffect from '../../../_util/hooks/useLayoutEffect'; +import type { ShallowRef, WatchCallback } from 'vue'; +import { watch } from 'vue'; + +type UseCompatibleInsertionEffect = ( + renderEffect: WatchCallback, + effect: (polyfill?: boolean) => ReturnType, + deps: ShallowRef, +) => void; + +/** + * Polyfill `useInsertionEffect` for React < 18 + * @param renderEffect will be executed in `useMemo`, and do not have callback + * @param effect will be executed in `useLayoutEffect` + * @param deps + */ +const useInsertionEffectPolyfill: UseCompatibleInsertionEffect = (renderEffect, effect, deps) => { + watch(deps, renderEffect, { immediate: true }); + useLayoutEffect(() => effect(true), deps); +}; + +/** + * Compatible `useInsertionEffect` + * will use `useInsertionEffect` if React version >= 18, + * otherwise use `useInsertionEffectPolyfill`. + */ +const useCompatibleInsertionEffect: UseCompatibleInsertionEffect = useInsertionEffectPolyfill; + +export default useCompatibleInsertionEffect; diff --git a/components/_util/cssinjs/hooks/useEffectCleanupRegister.ts b/components/_util/cssinjs/hooks/useEffectCleanupRegister.ts new file mode 100644 index 0000000000..c557e58967 --- /dev/null +++ b/components/_util/cssinjs/hooks/useEffectCleanupRegister.ts @@ -0,0 +1,8 @@ +const useRun = () => { + return function (fn: () => void) { + fn(); + }; +}; +const useEffectCleanupRegister = useRun; + +export default useEffectCleanupRegister; diff --git a/components/_util/cssinjs/hooks/useGlobalCache.tsx b/components/_util/cssinjs/hooks/useGlobalCache.tsx index 4a4940ced1..de2e707558 100644 --- a/components/_util/cssinjs/hooks/useGlobalCache.tsx +++ b/components/_util/cssinjs/hooks/useGlobalCache.tsx @@ -1,58 +1,115 @@ import { useStyleInject } from '../StyleContext'; import type { KeyType } from '../Cache'; +import useCompatibleInsertionEffect from './useCompatibleInsertionEffect'; import useHMR from './useHMR'; import type { ShallowRef, Ref } from 'vue'; -import { onBeforeUnmount, watch, watchEffect, shallowRef } from 'vue'; -export default function useClientCache( +import { onBeforeUnmount, watch, computed } from 'vue'; + +export type ExtractStyle = ( + cache: CacheValue, + effectStyles: Record, + options?: { + plain?: boolean; + }, +) => [order: number, styleId: string, style: string] | null; + +export default function useGlobalCache( prefix: string, keyPath: Ref, cacheFn: () => CacheType, onCacheRemove?: (cache: CacheType, fromHMR: boolean) => void, + // Add additional effect trigger by `useInsertionEffect` + onCacheEffect?: (cachedValue: CacheType) => void, ): ShallowRef { const styleContext = useStyleInject(); - const fullPathStr = shallowRef(''); - const res = shallowRef(); - watchEffect(() => { - fullPathStr.value = [prefix, ...keyPath.value].join('%'); - }); + const globalCache = computed(() => styleContext.value?.cache); + const deps = computed(() => [prefix, ...keyPath.value].join('%')); + const HMRUpdate = useHMR(); - const clearCache = (pathStr: string) => { - styleContext.value.cache.update(pathStr, prevCache => { - const [times = 0, cache] = prevCache || []; - const nextCount = times - 1; - if (nextCount === 0) { - onCacheRemove?.(cache, false); - return null; + + type UpdaterArgs = [times: number, cache: CacheType]; + + const buildCache = (updater?: (data: UpdaterArgs) => UpdaterArgs) => { + globalCache.value.update(deps.value, prevCache => { + const [times = 0, cache] = prevCache || [undefined, undefined]; + + // HMR should always ignore cache since developer may change it + let tmpCache = cache; + if (process.env.NODE_ENV !== 'production' && cache && HMRUpdate) { + onCacheRemove?.(tmpCache, HMRUpdate); + tmpCache = null; } - return [times - 1, cache]; + const mergedCache = tmpCache || cacheFn(); + + const data: UpdaterArgs = [times, mergedCache]; + + // Call updater if need additional logic + return updater ? updater(data) : data; }); }; watch( - fullPathStr, - (newStr, oldStr) => { - if (oldStr) clearCache(oldStr); - // Create cache - styleContext.value.cache.update(newStr, prevCache => { - const [times = 0, cache] = prevCache || []; - - // HMR should always ignore cache since developer may change it - let tmpCache = cache; - if (process.env.NODE_ENV !== 'production' && cache && HMRUpdate) { - onCacheRemove?.(tmpCache, HMRUpdate); - tmpCache = null; - } - const mergedCache = tmpCache || cacheFn(); + deps, + () => { + buildCache(); + }, + { immediate: true }, + ); + + let cacheEntity = globalCache.value.get(deps.value); + + // HMR clean the cache but not trigger `useMemo` again + // Let's fallback of this + // ref https://github.com/ant-design/cssinjs/issues/127 + if (process.env.NODE_ENV !== 'production' && !cacheEntity) { + buildCache(); + cacheEntity = globalCache.value.get(deps.value); + } - return [times + 1, mergedCache]; + const cacheContent = computed( + () => + (globalCache.value.get(deps.value) && globalCache.value.get(deps.value)![1]) || + cacheEntity![1], + ); + + // Remove if no need anymore + useCompatibleInsertionEffect( + () => { + onCacheEffect?.(cacheContent.value); + }, + polyfill => { + // It's bad to call build again in effect. + // But we have to do this since StrictMode will call effect twice + // which will clear cache on the first time. + buildCache(([times, cache]) => { + if (polyfill && times === 0) { + onCacheEffect?.(cacheContent.value); + } + return [times + 1, cache]; }); - res.value = styleContext.value.cache.get(fullPathStr.value)![1]; + + return () => { + globalCache.value.update(deps.value, prevCache => { + const [times = 0, cache] = prevCache || []; + const nextCount = times - 1; + if (nextCount <= 0) { + if (polyfill || !globalCache.value.get(deps.value)) { + onCacheRemove?.(cache, false); + } + return null; + } + + return [times - 1, cache]; + }); + }; }, - { immediate: true }, + deps, ); + onBeforeUnmount(() => { - clearCache(fullPathStr.value); + buildCache(); }); - return res; + + return cacheContent; } diff --git a/components/_util/cssinjs/hooks/useStyleRegister/index.tsx b/components/_util/cssinjs/hooks/useStyleRegister.tsx similarity index 69% rename from components/_util/cssinjs/hooks/useStyleRegister/index.tsx rename to components/_util/cssinjs/hooks/useStyleRegister.tsx index d264d0744e..0a39abbad6 100644 --- a/components/_util/cssinjs/hooks/useStyleRegister/index.tsx +++ b/components/_util/cssinjs/hooks/useStyleRegister.tsx @@ -3,38 +3,30 @@ import type * as CSS from 'csstype'; // @ts-ignore import unitless from '@emotion/unitless'; import { compile, serialize, stringify } from 'stylis'; -import type { Theme, Transformer } from '../..'; -import type Cache from '../../Cache'; -import type Keyframes from '../../Keyframes'; -import type { Linter } from '../../linters'; -import { contentQuotesLinter, hashedAnimationLinter } from '../../linters'; -import type { HashPriority } from '../../StyleContext'; +import type { Theme, Transformer } from '..'; +import type Keyframes from '../Keyframes'; +import type { Linter } from '../linters'; +import { contentQuotesLinter, hashedAnimationLinter } from '../linters'; +import type { HashPriority } from '../StyleContext'; import { useStyleInject, ATTR_CACHE_PATH, ATTR_MARK, ATTR_TOKEN, CSS_IN_JS_INSTANCE, -} from '../../StyleContext'; -import { supportLayer } from '../../util'; -import useGlobalCache from '../useGlobalCache'; -import { removeCSS, updateCSS } from '../../../../vc-util/Dom/dynamicCSS'; +} from '../StyleContext'; +import { isClientSide, supportLayer, toStyleStr } from '../util'; +import { CSS_FILE_STYLE, existPath, getStyleAndHash } from '../util/cacheMapUtil'; +import type { ExtractStyle } from './useGlobalCache'; +import useGlobalCache from './useGlobalCache'; +import { removeCSS, updateCSS } from '../../../vc-util/Dom/dynamicCSS'; import type { Ref } from 'vue'; import { computed } from 'vue'; -import type { VueNode } from '../../../type'; -import canUseDom from '../../../../_util/canUseDom'; - -import { - ATTR_CACHE_MAP, - existPath, - getStyleAndHash, - serialize as serializeCacheMap, -} from './cacheMapUtil'; - -const isClientSide = canUseDom(); +import type { VueNode } from '../../type'; const SKIP_CHECK = '_skip_check_'; const MULTI_VALUE = '_multi_value_'; + export type CSSProperties = Omit, 'animationName'> & { animationName?: CSS.PropertiesFallback['animationName'] | Keyframes; }; @@ -60,6 +52,7 @@ export type CSSInterpolation = InterpolationPrimitive | ArrayCSSInterpolation | export type CSSOthersObject = Record; +// @ts-ignore export interface CSSObject extends CSSPropertiesWithMultiValues, CSSPseudos, CSSOthersObject {} // ============================================================================ @@ -114,16 +107,6 @@ export interface ParseInfo { parentSelectors: string[]; } -// Global effect style will mount once and not removed -// The effect will not save in SSR cache (e.g. keyframes) -const globalEffectStyleKeys = new Set(); - -/** - * @private Test only. Clear the global effect style keys. - */ -export const _cf = - process.env.NODE_ENV !== 'production' ? () => globalEffectStyleKeys.clear() : undefined; - // Parse CSSObject to style content export const parseStyle = ( interpolation: CSSInterpolation, @@ -258,6 +241,7 @@ export const parseStyle = ( styleStr += `${styleName}:${formatValue};`; } + const actualValue = (value as any)?.value ?? value; if ( typeof value === 'object' && @@ -295,7 +279,7 @@ export const parseStyle = ( // ============================================================================ // == Register == // ============================================================================ -function uniqueHash(path: (string | number)[], styleStr: string) { +export function uniqueHash(path: (string | number)[], styleStr: string) { return hash(`${path.join('%')}${styleStr}`); } @@ -303,6 +287,17 @@ function uniqueHash(path: (string | number)[], styleStr: string) { // return null; // } +export const STYLE_PREFIX = 'style'; + +type StyleCacheValue = [ + styleStr: string, + tokenKey: string, + styleId: string, + effectStyle: Record, + clientOnly: boolean | undefined, + order: number, +]; + /** * Register a style to the global style sheet. */ @@ -337,22 +332,14 @@ export default function useStyleRegister( } // const [cacheStyle[0], cacheStyle[1], cacheStyle[2]] - useGlobalCache< - [ - styleStr: string, - tokenKey: string, - styleId: string, - effectStyle: Record, - clientOnly: boolean | undefined, - order: number, - ] - >( - 'style', + useGlobalCache( + STYLE_PREFIX, fullPath, // Create cache if needed () => { - const { path, hashId, layer, nonce, clientOnly, order = 0 } = info.value; + const { path, hashId, layer, clientOnly, order = 0 } = info.value; const cachePath = fullPath.value.join('|'); + // Get style from SSR inline style directly if (existPath(cachePath)) { const [inlineCacheStyleStr, styleHash] = getStyleAndHash(cachePath); @@ -360,8 +347,10 @@ export default function useStyleRegister( return [inlineCacheStyleStr, tokenKey.value, styleHash, {}, clientOnly, order]; } } + + // Generate style const styleObj = styleFn(); - const { hashPriority, container, transformers, linters, cache } = styleContext.value; + const { hashPriority, transformers, linters } = styleContext.value; const [parsedStyle, effectStyle] = parseStyle(styleObj, { hashId, @@ -371,18 +360,32 @@ export default function useStyleRegister( transformers, linters, }); + const styleStr = normalizeStyle(parsedStyle); const styleId = uniqueHash(fullPath.value, styleStr); - if (isMergedClientSide) { + return [styleStr, tokenKey.value, styleId, effectStyle, clientOnly, order]; + }, + + // Remove cache if no need + ([, , styleId], fromHMR) => { + if ((fromHMR || styleContext.value.autoClear) && isClientSide) { + removeCSS(styleId, { mark: ATTR_MARK }); + } + }, + + // Effect: Inject style here + ([styleStr, , styleId, effectStyle]) => { + if (isMergedClientSide && styleStr !== CSS_FILE_STYLE) { const mergedCSSConfig: Parameters[2] = { mark: ATTR_MARK, prepend: 'queue', - attachTo: container, - priority: order, + attachTo: styleContext.value.container, + priority: info.value.order, }; - const nonceStr = typeof nonce === 'function' ? nonce() : nonce; + const nonceStr = + typeof info.value.nonce === 'function' ? info.value.nonce() : info.value.nonce; if (nonceStr) { mergedCSSConfig.csp = { nonce: nonceStr }; @@ -390,45 +393,33 @@ export default function useStyleRegister( const style = updateCSS(styleStr, styleId, mergedCSSConfig); - (style as any)[CSS_IN_JS_INSTANCE] = cache.instanceId; + (style as any)[CSS_IN_JS_INSTANCE] = styleContext.value.cache.instanceId; // Used for `useCacheToken` to remove on batch when token removed style.setAttribute(ATTR_TOKEN, tokenKey.value); - // Dev usage to find which cache path made this easily + // Debug usage. Dev only if (process.env.NODE_ENV !== 'production') { style.setAttribute(ATTR_CACHE_PATH, fullPath.value.join('|')); } // Inject client side effect style Object.keys(effectStyle).forEach(effectKey => { - if (!globalEffectStyleKeys.has(effectKey)) { - globalEffectStyleKeys.add(effectKey); - - // Inject - updateCSS(normalizeStyle(effectStyle[effectKey]), `_effect-${effectKey}`, { - mark: ATTR_MARK, - prepend: 'queue', - attachTo: container, - }); - } + updateCSS( + normalizeStyle(effectStyle[effectKey]), + `_effect-${effectKey}`, + mergedCSSConfig, + ); }); } - - return [styleStr, tokenKey.value, styleId, effectStyle, clientOnly, order]; - }, - // Remove cache if no need - ([, , styleId], fromHMR) => { - if ((fromHMR || styleContext.value.autoClear) && isClientSide) { - removeCSS(styleId, { mark: ATTR_MARK }); - } }, ); return (node: VueNode) => { return node; // let styleNode: VueNode; - // if (!styleContext.ssrInline || isMergedClientSide || !styleContext.defaultCache) { + + // if (!styleContext.value.ssrInline || isMergedClientSide || !styleContext.value.defaultCache) { // styleNode = ; // } else { // styleNode = ( @@ -451,116 +442,43 @@ export default function useStyleRegister( }; } -// ============================================================================ -// == SSR == -// ============================================================================ -export function extractStyle(cache: Cache, plain = false) { - const matchPrefix = `style%`; - - // prefix with `style` is used for `useStyleRegister` to cache style context - const styleKeys = Array.from(cache.cache.keys()).filter(key => key.startsWith(matchPrefix)); - - // Common effect styles like animation - const effectStyles: Record = {}; - - // Mapping of cachePath to style hash - const cachePathMap: Record = {}; - - let styleText = ''; - - function toStyleStr( - style: string, - tokenKey?: string, - styleId?: string, - customizeAttrs: Record = {}, - ) { - const attrs: Record = { - ...customizeAttrs, - [ATTR_TOKEN]: tokenKey, - [ATTR_MARK]: styleId, - }; - - const attrStr = Object.keys(attrs) - .map(attr => { - const val = attrs[attr]; - return val ? `${attr}="${val}"` : null; - }) - .filter(v => v) - .join(' '); - - return plain ? style : ``; - } +export const extract: ExtractStyle = (cache, effectStyles, options) => { + const [styleStr, tokenKey, styleId, effectStyle, clientOnly, order]: StyleCacheValue = cache; + const { plain } = options || {}; - // ====================== Fill Style ====================== - type OrderStyle = [order: number, style: string]; - - const orderStyles: OrderStyle[] = styleKeys - .map(key => { - const cachePath = key.slice(matchPrefix.length).replace(/%/g, '|'); - - const [styleStr, tokenKey, styleId, effectStyle, clientOnly, order]: [ - string, - string, - string, - Record, - boolean, - number, - ] = cache.cache.get(key)![1]; - - // Skip client only style - if (clientOnly) { - return null! as OrderStyle; - } - - // ====================== Style ====================== - // Used for vc-util - const sharedAttrs = { - 'data-vc-order': 'prependQueue', - 'data-vc-priority': `${order}`, - }; + // Skip client only style + if (clientOnly) { + return null; + } - let keyStyleText = toStyleStr(styleStr, tokenKey, styleId, sharedAttrs); + let keyStyleText = styleStr; - // Save cache path with hash mapping - cachePathMap[cachePath] = styleId; + // ====================== Style ====================== + // Used for rc-util + const sharedAttrs = { + 'data-vc-order': 'prependQueue', + 'data-vc-priority': `${order}`, + }; - // =============== Create effect style =============== - if (effectStyle) { - Object.keys(effectStyle).forEach(effectKey => { - // Effect style can be reused - if (!effectStyles[effectKey]) { - effectStyles[effectKey] = true; - keyStyleText += toStyleStr( - normalizeStyle(effectStyle[effectKey]), - tokenKey, - `_effect-${effectKey}`, - sharedAttrs, - ); - } - }); + keyStyleText = toStyleStr(styleStr, tokenKey, styleId, sharedAttrs, plain); + + // =============== Create effect style =============== + if (effectStyle) { + Object.keys(effectStyle).forEach(effectKey => { + // Effect style can be reused + if (!effectStyles[effectKey]) { + effectStyles[effectKey] = true; + const effectStyleStr = normalizeStyle(effectStyle[effectKey]); + keyStyleText += toStyleStr( + effectStyleStr, + tokenKey, + `_effect-${effectKey}`, + sharedAttrs, + plain, + ); } - - const ret: OrderStyle = [order, keyStyleText]; - - return ret; - }) - .filter(o => o); - - orderStyles - .sort((o1, o2) => o1[0] - o2[0]) - .forEach(([, style]) => { - styleText += style; }); + } - // ==================== Fill Cache Path ==================== - styleText += toStyleStr( - `.${ATTR_CACHE_MAP}{content:"${serializeCacheMap(cachePathMap)}";}`, - undefined, - undefined, - { - [ATTR_CACHE_MAP]: ATTR_CACHE_MAP, - }, - ); - - return styleText; -} + return [order, styleId, keyStyleText]; +}; diff --git a/components/_util/cssinjs/index.ts b/components/_util/cssinjs/index.ts index 511045180e..5dc10a654c 100644 --- a/components/_util/cssinjs/index.ts +++ b/components/_util/cssinjs/index.ts @@ -1,51 +1,37 @@ -import useCacheToken from './hooks/useCacheToken'; +import extractStyle from './extractStyle'; +import useCacheToken, { getComputedToken } from './hooks/useCacheToken'; +import useCSSVarRegister from './hooks/useCSSVarRegister'; import type { CSSInterpolation, CSSObject } from './hooks/useStyleRegister'; -import useStyleRegister, { extractStyle } from './hooks/useStyleRegister'; +import useStyleRegister from './hooks/useStyleRegister'; import Keyframes from './Keyframes'; import type { Linter } from './linters'; -import { legacyNotSelectorLinter, logicalPropertiesLinter, parentSelectorLinter } from './linters'; -import type { StyleContextProps, StyleProviderProps } from './StyleContext'; -import { createCache, useStyleInject, useStyleProvider, StyleProvider } from './StyleContext'; +import { + legacyNotSelectorLinter, + logicalPropertiesLinter, + NaNLinter, + parentSelectorLinter, +} from './linters'; +import type { StyleProviderProps } from './StyleContext'; +import { createCache, StyleProvider } from './StyleContext'; import type { DerivativeFunc, TokenType } from './theme'; import { createTheme, Theme } from './theme'; import type { Transformer } from './transformers/interface'; import legacyLogicalPropertiesTransformer from './transformers/legacyLogicalProperties'; import px2remTransformer from './transformers/px2rem'; -import { supportLogicProps, supportWhere } from './util'; +import { supportLogicProps, supportWhere, unit } from './util'; +import { token2CSSVar } from './util/css-variables'; -const cssinjs = { - Theme, - createTheme, - useStyleRegister, - useCacheToken, - createCache, - useStyleInject, - useStyleProvider, - Keyframes, - extractStyle, - - // Transformer - legacyLogicalPropertiesTransformer, - px2remTransformer, - - // Linters - logicalPropertiesLinter, - legacyNotSelectorLinter, - parentSelectorLinter, - - // cssinjs - StyleProvider, -}; export { Theme, createTheme, useStyleRegister, + useCSSVarRegister, useCacheToken, createCache, - useStyleInject, - useStyleProvider, + StyleProvider, Keyframes, extractStyle, + getComputedToken, // Transformer legacyLogicalPropertiesTransformer, @@ -55,9 +41,11 @@ export { logicalPropertiesLinter, legacyNotSelectorLinter, parentSelectorLinter, + NaNLinter, - // cssinjs - StyleProvider, + // util + token2CSSVar, + unit, }; export type { TokenType, @@ -66,12 +54,9 @@ export type { DerivativeFunc, Transformer, Linter, - StyleContextProps, StyleProviderProps, }; export const _experimental = { supportModernCSS: () => supportWhere() && supportLogicProps(), }; - -export default cssinjs; diff --git a/components/_util/cssinjs/linters/NaNLinter.ts b/components/_util/cssinjs/linters/NaNLinter.ts new file mode 100644 index 0000000000..72bded76c1 --- /dev/null +++ b/components/_util/cssinjs/linters/NaNLinter.ts @@ -0,0 +1,10 @@ +import type { Linter } from './interface'; +import { lintWarning } from './utils'; + +const linter: Linter = (key, value, info) => { + if ((typeof value === 'string' && /NaN/g.test(value)) || Number.isNaN(value)) { + lintWarning(`Unexpected 'NaN' in property '${key}: ${value}'.`, info); + } +}; + +export default linter; diff --git a/components/_util/cssinjs/linters/index.ts b/components/_util/cssinjs/linters/index.ts index ae7d8cc9a7..2e31efe53d 100644 --- a/components/_util/cssinjs/linters/index.ts +++ b/components/_util/cssinjs/linters/index.ts @@ -3,4 +3,5 @@ export { default as hashedAnimationLinter } from './hashedAnimationLinter'; export type { Linter } from './interface'; export { default as legacyNotSelectorLinter } from './legacyNotSelectorLinter'; export { default as logicalPropertiesLinter } from './logicalPropertiesLinter'; +export { default as NaNLinter } from './NaNLinter'; export { default as parentSelectorLinter } from './parentSelectorLinter'; diff --git a/components/_util/cssinjs/linters/utils.ts b/components/_util/cssinjs/linters/utils.ts index 5b0853ff2f..83c80cb3ce 100644 --- a/components/_util/cssinjs/linters/utils.ts +++ b/components/_util/cssinjs/linters/utils.ts @@ -6,8 +6,8 @@ export function lintWarning(message: string, info: LinterInfo) { devWarning( false, - `[Ant Design Vue CSS-in-JS] ${path ? `Error in '${path}': ` : ''}${message}${ - parentSelectors.length ? ` Selector info: ${parentSelectors.join(' -> ')}` : '' + `[Ant Design Vue CSS-in-JS] ${path ? `Error in ${path}: ` : ''}${message}${ + parentSelectors.length ? ` Selector: ${parentSelectors.join(' | ')}` : '' }`, ); } diff --git a/components/_util/cssinjs/transformers/legacyLogicalProperties.ts b/components/_util/cssinjs/transformers/legacyLogicalProperties.ts index 58e00c89f4..7a33c1a0c5 100644 --- a/components/_util/cssinjs/transformers/legacyLogicalProperties.ts +++ b/components/_util/cssinjs/transformers/legacyLogicalProperties.ts @@ -1,34 +1,36 @@ import type { CSSObject } from '..'; import type { Transformer } from './interface'; -function splitValues(value: string | number) { +function splitValues(value: string | number): [values: (string | number)[], important: boolean] { if (typeof value === 'number') { - return [value]; + return [[value], false]; } - const splitStyle = String(value).split(/\s+/); + const rawStyle = String(value).trim(); + const importantCells = rawStyle.match(/(.*)(!important)/); + + const splitStyle = (importantCells ? importantCells[1] : rawStyle).trim().split(/\s+/); // Combine styles split in brackets, like `calc(1px + 2px)` let temp = ''; let brackets = 0; - return splitStyle.reduce((list, item) => { - if (item.includes('(')) { - temp += item; - brackets += item.split('(').length - 1; - } else if (item.includes(')')) { - temp += ` ${item}`; - brackets -= item.split(')').length - 1; + return [ + splitStyle.reduce((list, item) => { + if (item.includes('(') || item.includes(')')) { + const left = item.split('(').length - 1; + const right = item.split(')').length - 1; + brackets += left - right; + } if (brackets === 0) { - list.push(temp); + list.push(temp + item); temp = ''; + } else if (brackets > 0) { + temp += item; } - } else if (brackets > 0) { - temp += ` ${item}`; - } else { - list.push(item); - } - return list; - }, []); + return list; + }, []), + !!importantCells, + ]; } type MatchValue = string[] & { @@ -105,8 +107,14 @@ const keyMap: Record = { borderEndEndRadius: ['borderBottomRightRadius'], }; -function skipCheck(value: string | number) { - return { _skip_check_: true, value }; +function wrapImportantAndSkipCheck(value: string | number, important: boolean) { + let parsedValue = value; + + if (important) { + parsedValue = `${parsedValue} !important`; + } + + return { _skip_check_: true, value: parsedValue }; } /** @@ -127,25 +135,28 @@ const transform: Transformer = { const matchValue = keyMap[key]; if (matchValue && (typeof value === 'number' || typeof value === 'string')) { - const values = splitValues(value); + const [values, important] = splitValues(value); if (matchValue.length && matchValue.notSplit) { // not split means always give same value like border matchValue.forEach(matchKey => { - clone[matchKey] = skipCheck(value); + clone[matchKey] = wrapImportantAndSkipCheck(value, important); }); } else if (matchValue.length === 1) { // Handle like `marginBlockStart` => `marginTop` - clone[matchValue[0]] = skipCheck(value); + clone[matchValue[0]] = wrapImportantAndSkipCheck(value, important); } else if (matchValue.length === 2) { // Handle like `marginBlock` => `marginTop` & `marginBottom` matchValue.forEach((matchKey, index) => { - clone[matchKey] = skipCheck(values[index] ?? values[0]); + clone[matchKey] = wrapImportantAndSkipCheck(values[index] ?? values[0], important); }); } else if (matchValue.length === 4) { // Handle like `inset` => `top` & `right` & `bottom` & `left` matchValue.forEach((matchKey, index) => { - clone[matchKey] = skipCheck(values[index] ?? values[index - 2] ?? values[0]); + clone[matchKey] = wrapImportantAndSkipCheck( + values[index] ?? values[index - 2] ?? values[0], + important, + ); }); } else { clone[key] = value; diff --git a/components/_util/cssinjs/transformers/px2rem.ts b/components/_util/cssinjs/transformers/px2rem.ts index 4ada83a7d2..593a107b24 100644 --- a/components/_util/cssinjs/transformers/px2rem.ts +++ b/components/_util/cssinjs/transformers/px2rem.ts @@ -1,6 +1,7 @@ /** * respect https://github.com/cuth/postcss-pxtorem */ +// @ts-ignore import unitless from '@emotion/unitless'; import type { CSSObject } from '..'; import type { Transformer } from './interface'; diff --git a/components/_util/cssinjs/hooks/useStyleRegister/cacheMapUtil.ts b/components/_util/cssinjs/util/cacheMapUtil.ts similarity index 95% rename from components/_util/cssinjs/hooks/useStyleRegister/cacheMapUtil.ts rename to components/_util/cssinjs/util/cacheMapUtil.ts index 69a57c9335..813af183d1 100644 --- a/components/_util/cssinjs/hooks/useStyleRegister/cacheMapUtil.ts +++ b/components/_util/cssinjs/util/cacheMapUtil.ts @@ -1,5 +1,5 @@ -import canUseDom from '../../../../_util/canUseDom'; -import { ATTR_MARK } from '../../StyleContext'; +import canUseDom from '../../canUseDom'; +import { ATTR_MARK } from '../StyleContext'; export const ATTR_CACHE_MAP = 'data-ant-cssinjs-cache-path'; diff --git a/components/_util/cssinjs/util/css-variables.ts b/components/_util/cssinjs/util/css-variables.ts new file mode 100644 index 0000000000..6a3dc6c36e --- /dev/null +++ b/components/_util/cssinjs/util/css-variables.ts @@ -0,0 +1,58 @@ +export const token2CSSVar = (token: string, prefix = '') => { + return `--${prefix ? `${prefix}-` : ''}${token}` + .replace(/([a-z0-9])([A-Z])/g, '$1-$2') + .replace(/([A-Z]+)([A-Z][a-z0-9]+)/g, '$1-$2') + .replace(/([a-z])([A-Z0-9])/g, '$1-$2') + .toLowerCase(); +}; + +export const serializeCSSVar = >( + cssVars: T, + hashId: string, + options?: { + scope?: string; + }, +) => { + if (!Object.keys(cssVars).length) { + return ''; + } + return `.${hashId}${options?.scope ? `.${options.scope}` : ''}{${Object.entries(cssVars) + .map(([key, value]) => `${key}:${value};`) + .join('')}}`; +}; + +export type TokenWithCSSVar = Record> = { + [key in keyof T]?: string | V; +}; + +export const transformToken = = Record>( + token: T, + themeKey: string, + config?: { + prefix?: string; + ignore?: { + [key in keyof T]?: boolean; + }; + unitless?: { + [key in keyof T]?: boolean; + }; + preserve?: { + [key in keyof T]?: boolean; + }; + scope?: string; + }, +): [TokenWithCSSVar, string] => { + const cssVars: Record = {}; + const result: TokenWithCSSVar = {}; + Object.entries(token).forEach(([key, value]) => { + if (config?.preserve?.[key]) { + result[key as keyof T] = value; + } else if ((typeof value === 'string' || typeof value === 'number') && !config?.ignore?.[key]) { + const cssVar = token2CSSVar(key, config?.prefix); + cssVars[cssVar] = + typeof value === 'number' && !config?.unitless?.[key] ? `${value}px` : String(value); + result[key as keyof T] = `var(${cssVar})`; + } + }); + return [result, serializeCSSVar(cssVars, themeKey, { scope: config?.scope })]; +}; diff --git a/components/_util/cssinjs/util.ts b/components/_util/cssinjs/util/index.ts similarity index 63% rename from components/_util/cssinjs/util.ts rename to components/_util/cssinjs/util/index.ts index f22b226d88..ae59f90e7a 100644 --- a/components/_util/cssinjs/util.ts +++ b/components/_util/cssinjs/util/index.ts @@ -1,12 +1,37 @@ import hash from '@emotion/hash'; -import { removeCSS, updateCSS } from '../../vc-util/Dom/dynamicCSS'; -import canUseDom from '../canUseDom'; +import canUseDom from '../../canUseDom'; +import { removeCSS, updateCSS } from '../../../vc-util/Dom/dynamicCSS'; +import { ATTR_MARK, ATTR_TOKEN } from '../StyleContext'; +import { Theme } from '../theme'; + +// Create a cache for memo concat +type NestWeakMap = WeakMap | T>; +const resultCache: NestWeakMap = new WeakMap(); +const RESULT_VALUE = {}; + +export function memoResult(callback: () => R, deps: T[]): R { + let current: WeakMap = resultCache; + for (let i = 0; i < deps.length; i += 1) { + const dep = deps[i]; + if (!current.has(dep)) { + current.set(dep, new WeakMap()); + } + current = current.get(dep)!; + } -import { Theme } from './theme'; + if (!current.has(RESULT_VALUE)) { + current.set(RESULT_VALUE, callback()); + } + + return current.get(RESULT_VALUE); +} // Create a cache here to avoid always loop generate const flattenTokenCache = new WeakMap(); +/** + * Flatten token to string, this will auto cache the result when token not change + */ export function flattenToken(token: any) { let str = flattenTokenCache.get(token) || ''; @@ -116,3 +141,39 @@ export function supportLogicProps(): boolean { return canLogic!; } + +export const isClientSide = canUseDom(); + +export function unit(num: string | number) { + if (typeof num === 'number') { + return `${num}px`; + } + return num; +} + +export function toStyleStr( + style: string, + tokenKey?: string, + styleId?: string, + customizeAttrs: Record = {}, + plain = false, +) { + if (plain) { + return style; + } + const attrs: Record = { + ...customizeAttrs, + [ATTR_TOKEN]: tokenKey, + [ATTR_MARK]: styleId, + }; + + const attrStr = Object.keys(attrs) + .map(attr => { + const val = attrs[attr]; + return val ? `${attr}="${val}"` : null; + }) + .filter(v => v) + .join(' '); + + return ``; +} diff --git a/components/_util/getScroll.ts b/components/_util/getScroll.ts index ca0b10005d..f3e42f1bc9 100644 --- a/components/_util/getScroll.ts +++ b/components/_util/getScroll.ts @@ -2,32 +2,31 @@ export function isWindow(obj: any): obj is Window { return obj !== null && obj !== undefined && obj === obj.window; } -export default function getScroll( - target: HTMLElement | Window | Document | null, - top: boolean, -): number { +const getScroll = (target: HTMLElement | Window | Document | null): number => { if (typeof window === 'undefined') { return 0; } - const method = top ? 'scrollTop' : 'scrollLeft'; let result = 0; if (isWindow(target)) { - result = target[top ? 'scrollY' : 'scrollX']; + result = target.pageYOffset; } else if (target instanceof Document) { - result = target.documentElement[method]; + result = target.documentElement.scrollTop; } else if (target instanceof HTMLElement) { - result = target[method]; + result = target.scrollTop; } else if (target) { // According to the type inference, the `target` is `never` type. // Since we configured the loose mode type checking, and supports mocking the target with such shape below:: // `{ documentElement: { scrollLeft: 200, scrollTop: 400 } }`, // the program may falls into this branch. // Check the corresponding tests for details. Don't sure what is the real scenario this happens. - result = target[method]; + /* biome-ignore lint/complexity/useLiteralKeys: target is a never type */ /* eslint-disable-next-line dot-notation */ + result = target['scrollTop']; } if (target && !isWindow(target) && typeof result !== 'number') { - result = ((target.ownerDocument ?? target) as any).documentElement?.[method]; + result = (target.ownerDocument ?? (target as Document)).documentElement?.scrollTop; } return result; -} +}; + +export default getScroll; diff --git a/components/_util/hooks/useLayoutEffect.ts b/components/_util/hooks/useLayoutEffect.ts new file mode 100644 index 0000000000..6f17aedaa3 --- /dev/null +++ b/components/_util/hooks/useLayoutEffect.ts @@ -0,0 +1,48 @@ +import type { Ref, ShallowRef } from 'vue'; + +import { shallowRef, ref, watch, nextTick, onMounted, onUnmounted } from 'vue'; + +function useLayoutEffect( + fn: (mount: boolean) => void | VoidFunction, + deps?: Ref | Ref[] | ShallowRef | ShallowRef[], +) { + const firstMount = shallowRef(true); + const cleanupFn = ref(null); + let stopWatch = null; + + stopWatch = watch( + deps, + () => { + nextTick(() => { + if (cleanupFn.value) { + cleanupFn.value(); + } + cleanupFn.value = fn(firstMount.value); + }); + }, + { immediate: true, flush: 'post' }, + ); + + onMounted(() => { + firstMount.value = false; + }); + + onUnmounted(() => { + if (cleanupFn.value) { + cleanupFn.value(); + } + if (stopWatch) { + stopWatch(); + } + }); +} + +export const useLayoutUpdateEffect = (callback, deps) => { + useLayoutEffect(firstMount => { + if (!firstMount) { + return callback(); + } + }, deps); +}; + +export default useLayoutEffect; diff --git a/components/_util/scrollTo.ts b/components/_util/scrollTo.ts index 992d6a930e..59bb1436a5 100644 --- a/components/_util/scrollTo.ts +++ b/components/_util/scrollTo.ts @@ -14,7 +14,7 @@ interface ScrollToOptions { export default function scrollTo(y: number, options: ScrollToOptions = {}) { const { getContainer = () => window, callback, duration = 450 } = options; const container = getContainer(); - const scrollTop = getScroll(container, true); + const scrollTop = getScroll(container); const startTime = Date.now(); const frameFunc = () => { diff --git a/components/affix/index.tsx b/components/affix/index.tsx index 8f4b37710f..95155dd397 100644 --- a/components/affix/index.tsx +++ b/components/affix/index.tsx @@ -27,10 +27,11 @@ import useStyle from './style'; function getDefaultTarget() { return typeof window !== 'undefined' ? window : null; } -enum AffixStatus { - None, - Prepare, -} +const AFFIX_STATUS_NONE = 0; +const AFFIX_STATUS_PREPARE = 1; + +type AffixStatus = typeof AFFIX_STATUS_NONE | typeof AFFIX_STATUS_PREPARE; + export interface AffixState { affixStyle?: CSSProperties; placeholderStyle?: CSSProperties; @@ -82,7 +83,7 @@ const Affix = defineComponent({ const state = reactive({ affixStyle: undefined, placeholderStyle: undefined, - status: AffixStatus.None, + status: AFFIX_STATUS_NONE, lastAffix: false, prevTarget: null, timeout: null, @@ -98,7 +99,12 @@ const Affix = defineComponent({ const measure = () => { const { status, lastAffix } = state; const { target } = props; - if (status !== AffixStatus.Prepare || !fixedNode.value || !placeholderNode.value || !target) { + if ( + status !== AFFIX_STATUS_PREPARE || + !fixedNode.value || + !placeholderNode.value || + !target + ) { return; } @@ -108,7 +114,7 @@ const Affix = defineComponent({ } const newState = { - status: AffixStatus.None, + status: AFFIX_STATUS_NONE, } as AffixState; const placeholderRect = getTargetRect(placeholderNode.value as HTMLElement); @@ -172,7 +178,7 @@ const Affix = defineComponent({ }; const prepareMeasure = () => { Object.assign(state, { - status: AffixStatus.Prepare, + status: AFFIX_STATUS_PREPARE, affixStyle: undefined, placeholderStyle: undefined, }); @@ -253,12 +259,13 @@ const Affix = defineComponent({ }); const { prefixCls } = useConfigInject('affix', props); - const [wrapSSR, hashId] = useStyle(prefixCls); + const [wrapSSR, hashId, cssVarCls] = useStyle(prefixCls); return () => { const { affixStyle, placeholderStyle, status } = state; const className = classNames({ [prefixCls.value]: affixStyle, [hashId.value]: true, + [cssVarCls.value]: true, }); const restProps = omit(props, [ 'prefixCls', diff --git a/components/affix/style/index.ts b/components/affix/style/index.ts index c33c3176ee..be895766d1 100644 --- a/components/affix/style/index.ts +++ b/components/affix/style/index.ts @@ -1,15 +1,21 @@ import type { CSSObject } from '../../_util/cssinjs'; -import type { FullToken, GenerateStyle } from '../../theme/internal'; -import { genComponentStyleHook, mergeToken } from '../../theme/internal'; +import { FullToken, GenerateStyle, genStyleHooks, GetDefaultToken } from '../../theme/internal'; -interface AffixToken extends FullToken<'Affix'> { +export interface ComponentToken { + /** + * @desc 弹出层的 z-index + * @descEN z-index of popup + */ zIndexPopup: number; } +interface AffixToken extends FullToken<'Affix'> { + // +} + // ============================== Shared ============================== const genSharedAffixStyle: GenerateStyle = (token): CSSObject => { const { componentCls } = token; - return { [componentCls]: { position: 'fixed', @@ -18,10 +24,9 @@ const genSharedAffixStyle: GenerateStyle = (token): CSSObject => { }; }; -// ============================== Export ============================== -export default genComponentStyleHook('Affix', token => { - const affixToken = mergeToken(token, { - zIndexPopup: token.zIndexBase + 10, - }); - return [genSharedAffixStyle(affixToken)]; +export const prepareComponentToken: GetDefaultToken<'Affix'> = token => ({ + zIndexPopup: token.zIndexBase + 10, }); + +// ============================== Export ============================== +export default genStyleHooks('Affix', genSharedAffixStyle, prepareComponentToken); diff --git a/components/affix/utils.ts b/components/affix/utils.ts index 62ce50f275..08b46ea1e2 100644 --- a/components/affix/utils.ts +++ b/components/affix/utils.ts @@ -9,8 +9,11 @@ export function getTargetRect(target: BindElement): DOMRect { : ({ top: 0, bottom: window.innerHeight } as DOMRect); } -export function getFixedTop(placeholderRect: DOMRect, targetRect: DOMRect, offsetTop: number) { - if (offsetTop !== undefined && targetRect.top > placeholderRect.top - offsetTop) { +export function getFixedTop(placeholderRect: DOMRect, targetRect: DOMRect, offsetTop?: number) { + if ( + offsetTop !== undefined && + Math.round(targetRect.top) > Math.round(placeholderRect.top) - offsetTop + ) { return `${offsetTop + targetRect.top}px`; } return undefined; @@ -19,9 +22,12 @@ export function getFixedTop(placeholderRect: DOMRect, targetRect: DOMRect, offse export function getFixedBottom( placeholderRect: DOMRect, targetRect: DOMRect, - offsetBottom: number, + offsetBottom?: number, ) { - if (offsetBottom !== undefined && targetRect.bottom < placeholderRect.bottom + offsetBottom) { + if ( + offsetBottom !== undefined && + Math.round(targetRect.bottom) < Math.round(placeholderRect.bottom) + offsetBottom + ) { const targetBottomOffset = window.innerHeight - targetRect.bottom; return `${offsetBottom + targetBottomOffset}px`; } @@ -29,7 +35,7 @@ export function getFixedBottom( } // ======================== Observer ======================== -const TRIGGER_EVENTS = [ +const TRIGGER_EVENTS: (keyof WindowEventMap)[] = [ 'resize', 'scroll', 'touchstart', diff --git a/components/alert/index.tsx b/components/alert/index.tsx index f3dead2ba5..9cd8660c99 100644 --- a/components/alert/index.tsx +++ b/components/alert/index.tsx @@ -70,7 +70,7 @@ const Alert = defineComponent({ props: alertProps(), setup(props, { slots, emit, attrs, expose }) { const { prefixCls, direction } = useConfigInject('alert', props); - const [wrapSSR, hashId] = useStyle(prefixCls); + const [wrapSSR, hashId, cssVarCls] = useStyle(prefixCls); const closing = shallowRef(false); const closed = shallowRef(false); const alertNode = shallowRef(); @@ -134,6 +134,7 @@ const Alert = defineComponent({ [`${prefixClsValue}-closable`]: closable, [`${prefixClsValue}-rtl`]: direction.value === 'rtl', [hashId.value]: true, + [cssVarCls.value]: true, }); const closeIcon = closable ? ( diff --git a/components/alert/style/index.ts b/components/alert/style/index.ts index 172674fe57..ffb6f94f39 100644 --- a/components/alert/style/index.ts +++ b/components/alert/style/index.ts @@ -1,13 +1,29 @@ -import type { CSSInterpolation, CSSObject } from '../../_util/cssinjs'; -import type { FullToken, GenerateStyle } from '../../theme/internal'; -import { genComponentStyleHook, mergeToken } from '../../theme/internal'; +import { CSSObject, unit } from '../../_util/cssinjs'; +import { FullToken, GenerateStyle, genStyleHooks, GetDefaultToken } from '../../theme/internal'; import { resetComponent } from '../../style'; - -export interface ComponentToken {} +import { CSSProperties } from 'vue'; + +export interface ComponentToken { + // Component token here + /** + * @desc 默认内间距 + * @descEN Default padding + */ + defaultPadding: CSSProperties['padding']; + /** + * @desc 带有描述的内间距 + * @descEN Padding with description + */ + withDescriptionPadding: CSSProperties['padding']; + /** + * @desc 带有描述时的图标尺寸 + * @descEN Icon size with description + */ + withDescriptionIconSize: number; +} type AlertToken = FullToken<'Alert'> & { - alertIconSizeLG: number; - alertPaddingHorizontal: number; + // Custom token here }; const genAlertTypeStyle = ( @@ -17,8 +33,8 @@ const genAlertTypeStyle = ( token: AlertToken, alertCls: string, ): CSSObject => ({ - backgroundColor: bgColor, - border: `${token.lineWidth}px ${token.lineType} ${borderColor}`, + background: bgColor, + border: `${unit(token.lineWidth)} ${token.lineType} ${borderColor}`, [`${alertCls}-icon`]: { color: iconColor, }, @@ -35,12 +51,11 @@ export const genBaseStyle: GenerateStyle = (token: AlertToken): CSSO lineHeight, borderRadiusLG: borderRadius, motionEaseInOutCirc, - alertIconSizeLG, + withDescriptionIconSize, colorText, - paddingContentVerticalSM, - alertPaddingHorizontal, - paddingMD, - paddingContentHorizontalLG, + colorTextHeading, + withDescriptionPadding, + defaultPadding, } = token; return { @@ -49,7 +64,7 @@ export const genBaseStyle: GenerateStyle = (token: AlertToken): CSSO position: 'relative', display: 'flex', alignItems: 'center', - padding: `${paddingContentVerticalSM}px ${alertPaddingHorizontal}px`, // Fixed horizontal padding here. + padding: defaultPadding, wordWrap: 'break-word', borderRadius, @@ -67,14 +82,14 @@ export const genBaseStyle: GenerateStyle = (token: AlertToken): CSSO lineHeight: 0, }, - [`&-description`]: { + '&-description': { display: 'none', fontSize, lineHeight, }, '&-message': { - color: colorText, + color: colorTextHeading, }, [`&${componentCls}-motion-leave`]: { @@ -96,24 +111,23 @@ export const genBaseStyle: GenerateStyle = (token: AlertToken): CSSO [`${componentCls}-with-description`]: { alignItems: 'flex-start', - paddingInline: paddingContentHorizontalLG, - paddingBlock: paddingMD, - + padding: withDescriptionPadding, [`${componentCls}-icon`]: { marginInlineEnd: marginSM, - fontSize: alertIconSizeLG, + fontSize: withDescriptionIconSize, lineHeight: 0, }, [`${componentCls}-message`]: { display: 'block', marginBottom: marginXS, - color: colorText, + color: colorTextHeading, fontSize: fontSizeLG, }, [`${componentCls}-description`]: { display: 'block', + color: colorText, }, }, @@ -187,7 +201,7 @@ export const genActionStyle: GenerateStyle = (token: AlertToken): CS return { [componentCls]: { - [`&-action`]: { + '&-action': { marginInlineStart: marginXS, }, @@ -196,7 +210,7 @@ export const genActionStyle: GenerateStyle = (token: AlertToken): CS padding: 0, overflow: 'hidden', fontSize: fontSizeIcon, - lineHeight: `${fontSizeIcon}px`, + lineHeight: unit(fontSizeIcon), backgroundColor: 'transparent', border: 'none', outline: 'none', @@ -222,19 +236,17 @@ export const genActionStyle: GenerateStyle = (token: AlertToken): CS }; }; -export const genAlertStyle: GenerateStyle = (token: AlertToken): CSSInterpolation => [ - genBaseStyle(token), - genTypeStyle(token), - genActionStyle(token), -]; - -export default genComponentStyleHook('Alert', token => { - const { fontSizeHeading3 } = token; - - const alertToken = mergeToken(token, { - alertIconSizeLG: fontSizeHeading3, - alertPaddingHorizontal: 12, // Fixed value here. - }); +export const prepareComponentToken: GetDefaultToken<'Alert'> = token => { + const paddingHorizontal = 12; // Fixed value here. + return { + withDescriptionIconSize: token.fontSizeHeading3, + defaultPadding: `${token.paddingContentVerticalSM}px ${paddingHorizontal}px`, + withDescriptionPadding: `${token.paddingMD}px ${token.paddingContentHorizontalLG}px`, + }; +}; - return [genAlertStyle(alertToken)]; -}); +export default genStyleHooks( + 'Alert', + token => [genBaseStyle(token), genTypeStyle(token), genActionStyle(token)], + prepareComponentToken, +); diff --git a/components/anchor/Anchor.tsx b/components/anchor/Anchor.tsx index 60f9e243d1..d727e3d40b 100644 --- a/components/anchor/Anchor.tsx +++ b/components/anchor/Anchor.tsx @@ -23,6 +23,7 @@ import AnchorLink from './AnchorLink'; import PropTypes from '../_util/vue-types'; import devWarning from '../vc-util/devWarning'; import { arrayType } from '../_util/type'; +import useCSSVarCls from '../config-provider/hooks/useCssVarCls'; export type AnchorDirection = 'vertical' | 'horizontal'; @@ -39,8 +40,7 @@ function getOffsetTop(element: HTMLElement, container: AnchorContainer): number if (rect.width || rect.height) { if (container === window) { - container = element.ownerDocument!.documentElement!; - return rect.top - container.clientTop; + return rect.top - element.ownerDocument!.documentElement!.clientTop; } return rect.top - (container as HTMLElement).getBoundingClientRect().top; } @@ -70,6 +70,7 @@ export const anchorProps = () => ({ targetOffset: Number, items: arrayType(), direction: PropTypes.oneOf(['vertical', 'horizontal'] as AnchorDirection[]).def('vertical'), + replace: Boolean, onChange: Function as PropType<(currentActiveLink: string) => void>, onClick: Function as PropType<(e: MouseEvent, link: { title: any; href: string }) => void>, }); @@ -91,7 +92,7 @@ export default defineComponent({ setup(props, { emit, attrs, slots, expose }) { const { prefixCls, getTargetContainer, direction } = useConfigInject('anchor', props); const anchorDirection = computed(() => props.direction ?? 'vertical'); - + const rootCls = useCSSVarCls(prefixCls); if (process.env.NODE_ENV !== 'production') { devWarning( props.items && typeof slots.default !== 'function', @@ -133,7 +134,7 @@ export default defineComponent({ const target = document.getElementById(sharpLinkMatch[1]); if (target) { const top = getOffsetTop(target, container); - if (top < offsetTop + bounds) { + if (top <= offsetTop + bounds) { linkSections.push({ link, top, @@ -170,7 +171,7 @@ export default defineComponent({ } const container = getContainer.value(); - const scrollTop = getScroll(container, true); + const scrollTop = getScroll(container); const eleOffsetTop = getOffsetTop(targetElement, container); let y = scrollTop + eleOffsetTop; y -= targetOffset !== undefined ? targetOffset : offsetTop || 0; @@ -277,6 +278,7 @@ export default defineComponent({ title={title} customTitleProps={option} v-slots={{ customTitle: slots.customTitle }} + replace={props.replace} > {anchorDirection.value === 'vertical' ? createNestedLink(children) : null} @@ -284,7 +286,7 @@ export default defineComponent({ }) : null; - const [wrapSSR, hashId] = useStyle(prefixCls); + const [wrapSSR, hashId, cssVarCls] = useStyle(prefixCls, rootCls); return () => { const { offsetTop, affix, showInkInFixed } = props; @@ -296,6 +298,8 @@ export default defineComponent({ const wrapperClass = classNames(hashId.value, props.wrapperClass, `${pre}-wrapper`, { [`${pre}-wrapper-horizontal`]: anchorDirection.value === 'horizontal', [`${pre}-rtl`]: direction.value === 'rtl', + [rootCls.value]: true, + [cssVarCls.value]: true, }); const anchorClass = classNames(pre, { diff --git a/components/anchor/AnchorLink.tsx b/components/anchor/AnchorLink.tsx index 5c5afa8737..c45fc7ad5c 100644 --- a/components/anchor/AnchorLink.tsx +++ b/components/anchor/AnchorLink.tsx @@ -13,6 +13,7 @@ export const anchorLinkProps = () => ({ href: String, title: anyType VueNode)>(), target: String, + replace: Boolean, /* private use */ customTitleProps: objectType(), }); @@ -53,6 +54,10 @@ export default defineComponent({ const { href } = props; contextHandleClick(e, { title: mergedTitle, href }); scrollTo(href); + if (props.replace) { + e.preventDefault(); + window.location.replace(href); + } }; watch( diff --git a/components/anchor/style/index.ts b/components/anchor/style/index.ts index 119055aac8..387bf26368 100644 --- a/components/anchor/style/index.ts +++ b/components/anchor/style/index.ts @@ -1,21 +1,55 @@ -import type { CSSObject } from '../../_util/cssinjs'; -import type { FullToken, GenerateStyle } from '../../theme/internal'; -import { genComponentStyleHook, mergeToken } from '../../theme/internal'; +import { unit } from '../../_util/cssinjs'; +import { + FullToken, + GenerateStyle, + genStyleHooks, + GetDefaultToken, + mergeToken, +} from '../../theme/internal'; import { resetComponent, textEllipsis } from '../../style'; -export interface ComponentToken {} +export interface ComponentToken { + /** + * @desc 链接纵向内间距 + * @descEN Vertical padding of link + */ + linkPaddingBlock: number; + /** + * @desc 链接横向内间距 + * @descEN Horizontal padding of link + */ + linkPaddingInlineStart: number; +} +/** + * @desc Anchor 组件的 Token + * @descEN Token for Anchor component + */ interface AnchorToken extends FullToken<'Anchor'> { + /** + * @desc 容器块偏移量 + * @descEN Holder block offset + */ holderOffsetBlock: number; - anchorPaddingBlock: number; - anchorPaddingBlockSecondary: number; - anchorPaddingInline: number; - anchorBallSize: number; - anchorTitleBlock: number; + /** + * @desc 次级锚点块内间距 + * @descEN Secondary anchor block padding + */ + anchorPaddingBlockSecondary: number | string; + /** + * @desc 锚点球大小 + * @descEN Anchor ball size + */ + anchorBallSize: number | string; + /** + * @desc 锚点标题块 + * @descEN Anchor title block + */ + anchorTitleBlock: number | string; } // ============================== Shared ============================== -const genSharedAnchorStyle: GenerateStyle = (token): CSSObject => { +const genSharedAnchorStyle: GenerateStyle = token => { const { componentCls, holderOffsetBlock, @@ -24,26 +58,25 @@ const genSharedAnchorStyle: GenerateStyle = (token): CSSObject => { colorPrimary, lineType, colorSplit, + calc, } = token; return { [`${componentCls}-wrapper`]: { - marginBlockStart: -holderOffsetBlock, + marginBlockStart: calc(holderOffsetBlock).mul(-1).equal(), paddingBlockStart: holderOffsetBlock, // delete overflow: auto // overflow: 'auto', - backgroundColor: 'transparent', - [componentCls]: { ...resetComponent(token), position: 'relative', paddingInlineStart: lineWidthBold, [`${componentCls}-link`]: { - paddingBlock: token.anchorPaddingBlock, - paddingInline: `${token.anchorPaddingInline}px 0`, + paddingBlock: token.linkPaddingBlock, + paddingInline: `${unit(token.linkPaddingInlineStart)} 0`, '&-title': { ...textEllipsis, @@ -73,28 +106,21 @@ const genSharedAnchorStyle: GenerateStyle = (token): CSSObject => { [componentCls]: { '&::before': { position: 'absolute', - left: { - _skip_check_: true, - value: 0, - }, + insetInlineStart: 0, top: 0, height: '100%', - borderInlineStart: `${lineWidthBold}px ${lineType} ${colorSplit}`, + borderInlineStart: `${unit(lineWidthBold)} ${lineType} ${colorSplit}`, content: '" "', }, [`${componentCls}-ink`]: { position: 'absolute', - left: { - _skip_check_: true, - value: 0, - }, + insetInlineStart: 0, display: 'none', transform: 'translateY(-50%)', transition: `top ${motionDurationSlow} ease-in-out`, width: lineWidthBold, backgroundColor: colorPrimary, - [`&${componentCls}-ink-visible`]: { display: 'inline-block', }, @@ -109,7 +135,7 @@ const genSharedAnchorStyle: GenerateStyle = (token): CSSObject => { }; }; -const genSharedAnchorHorizontalStyle: GenerateStyle = (token): CSSObject => { +const genSharedAnchorHorizontalStyle: GenerateStyle = token => { const { componentCls, motionDurationSlow, lineWidthBold, colorPrimary } = token; return { @@ -127,7 +153,7 @@ const genSharedAnchorHorizontalStyle: GenerateStyle = (token): CSSO value: 0, }, bottom: 0, - borderBottom: `1px ${token.lineType} ${token.colorSplit}`, + borderBottom: `${unit(token.lineWidth)} ${token.lineType} ${token.colorSplit}`, content: '" "', }, @@ -157,17 +183,23 @@ const genSharedAnchorHorizontalStyle: GenerateStyle = (token): CSSO }; }; -// ============================== Export ============================== -export default genComponentStyleHook('Anchor', token => { - const { fontSize, fontSizeLG, padding, paddingXXS } = token; - - const anchorToken = mergeToken(token, { - holderOffsetBlock: paddingXXS, - anchorPaddingBlock: paddingXXS, - anchorPaddingBlockSecondary: paddingXXS / 2, - anchorPaddingInline: padding, - anchorTitleBlock: (fontSize / 14) * 3, - anchorBallSize: fontSizeLG / 2, - }); - return [genSharedAnchorStyle(anchorToken), genSharedAnchorHorizontalStyle(anchorToken)]; +export const prepareComponentToken: GetDefaultToken<'Anchor'> = token => ({ + linkPaddingBlock: token.paddingXXS, + linkPaddingInlineStart: token.padding, }); + +// ============================== Export ============================== +export default genStyleHooks( + 'Anchor', + token => { + const { fontSize, fontSizeLG, paddingXXS, calc } = token; + const anchorToken = mergeToken(token, { + holderOffsetBlock: paddingXXS, + anchorPaddingBlockSecondary: calc(paddingXXS).div(2).equal(), + anchorTitleBlock: calc(fontSize).div(14).mul(3).equal(), + anchorBallSize: calc(fontSizeLG).div(2).equal(), + }); + return [genSharedAnchorStyle(anchorToken), genSharedAnchorHorizontalStyle(anchorToken)]; + }, + prepareComponentToken, +); diff --git a/components/badge/style/index.ts b/components/badge/style/index.ts index 7589799b61..aa7e1a2ef5 100644 --- a/components/badge/style/index.ts +++ b/components/badge/style/index.ts @@ -4,6 +4,8 @@ import type { FullToken, GenerateStyle } from '../../theme/internal'; import { genComponentStyleHook, mergeToken } from '../../theme/internal'; import { genPresetColor, resetComponent } from '../../style'; +export interface ComponentToken {} + interface BadgeToken extends FullToken<'Badge'> { badgeFontHeight: number; badgeZIndex: number | string; diff --git a/components/breadcrumb/style/index.ts b/components/breadcrumb/style/index.ts index 9d3b24d9cf..d19010fba7 100644 --- a/components/breadcrumb/style/index.ts +++ b/components/breadcrumb/style/index.ts @@ -3,6 +3,8 @@ import type { FullToken, GenerateStyle } from '../../theme/internal'; import { genComponentStyleHook, mergeToken } from '../../theme/internal'; import { genFocusStyle, resetComponent } from '../../style'; +export interface ComponentToken {} + interface BreadcrumbToken extends FullToken<'Breadcrumb'> { breadcrumbBaseColor: string; breadcrumbFontSize: number; diff --git a/components/button/button-group.tsx b/components/button/button-group.tsx index 1403382652..a97f14ad99 100644 --- a/components/button/button-group.tsx +++ b/components/button/button-group.tsx @@ -45,7 +45,11 @@ export default defineComponent({ break; default: // eslint-disable-next-line no-console - devWarning(!size, 'Button.Group', 'Invalid prop `size`.'); + devWarning( + !size || ['large', 'small', 'middle'].includes(size), + 'Button.Group', + 'Invalid prop `size`.', + ); } return { [`${prefixCls.value}`]: true, diff --git a/components/button/button.tsx b/components/button/button.tsx index 32fdf9e21d..e71f278053 100644 --- a/components/button/button.tsx +++ b/components/button/button.tsx @@ -45,7 +45,7 @@ export default defineComponent({ // emits: ['click', 'mousedown'], setup(props, { slots, attrs, emit, expose }) { const { prefixCls, autoInsertSpaceInButton, direction, size } = useConfigInject('btn', props); - const [wrapSSR, hashId] = useStyle(prefixCls); + const [wrapCSSVar, hashId, cssVarCls] = useStyle(prefixCls); const groupSizeContext = GroupSizeContext.useInject(); const disabledContext = useInjectDisabled(); const mergedDisabled = computed(() => props.disabled ?? disabledContext.value); @@ -95,6 +95,7 @@ export default defineComponent({ compactItemClassnames.value, { [hashId.value]: true, + [cssVarCls.value]: true, [`${pre}`]: true, [`${pre}-${shape}`]: shape !== 'default' && shape, [`${pre}-${type}`]: type, @@ -216,7 +217,7 @@ export default defineComponent({ ); if (href !== undefined) { - return wrapSSR( + return wrapCSSVar( {iconNode} {kids} @@ -239,7 +240,7 @@ export default defineComponent({ ); } - return wrapSSR(buttonNode); + return wrapCSSVar(buttonNode); }; }, }); diff --git a/components/button/style/compactCmp.ts b/components/button/style/compactCmp.ts new file mode 100644 index 0000000000..c19b37f7b2 --- /dev/null +++ b/components/button/style/compactCmp.ts @@ -0,0 +1,72 @@ +// Style as inline component +import type { ButtonToken } from './token'; +import { prepareComponentToken, prepareToken } from './token'; +import { genCompactItemStyle } from '../../style/compact-item'; +import { genCompactItemVerticalStyle } from '../../style/compact-item-vertical'; +import type { GenerateStyle } from '../../theme/internal'; +import { genSubStyleComponent } from '../../theme/internal'; +import type { CSSObject } from '../../_util/cssinjs'; +import { unit } from '../../_util/cssinjs'; + +const genButtonCompactStyle: GenerateStyle = token => { + const { componentCls, calc } = token; + + return { + [componentCls]: { + // Special styles for Primary Button + [`&-compact-item${componentCls}-primary`]: { + [`&:not([disabled]) + ${componentCls}-compact-item${componentCls}-primary:not([disabled])`]: + { + position: 'relative', + + '&:before': { + position: 'absolute', + top: calc(token.lineWidth).mul(-1).equal(), + insetInlineStart: calc(token.lineWidth).mul(-1).equal(), + display: 'inline-block', + width: token.lineWidth, + height: `calc(100% + ${unit(token.lineWidth)} * 2)`, + backgroundColor: token.colorPrimaryHover, + content: '""', + }, + }, + }, + // Special styles for Primary Button + '&-compact-vertical-item': { + [`&${componentCls}-primary`]: { + [`&:not([disabled]) + ${componentCls}-compact-vertical-item${componentCls}-primary:not([disabled])`]: + { + position: 'relative', + + '&:before': { + position: 'absolute', + top: calc(token.lineWidth).mul(-1).equal(), + insetInlineStart: calc(token.lineWidth).mul(-1).equal(), + display: 'inline-block', + width: `calc(100% + ${unit(token.lineWidth)} * 2)`, + height: token.lineWidth, + backgroundColor: token.colorPrimaryHover, + content: '""', + }, + }, + }, + }, + }, + }; +}; + +// ============================== Export ============================== +export default genSubStyleComponent( + ['Button', 'compact'], + token => { + const buttonToken = prepareToken(token); + + return [ + // Space Compact + genCompactItemStyle(buttonToken), + genCompactItemVerticalStyle(buttonToken), + genButtonCompactStyle(buttonToken), + ] as CSSObject[]; + }, + prepareComponentToken, +); diff --git a/components/button/style/group.ts b/components/button/style/group.ts index 0bc094bc65..066d3d7119 100644 --- a/components/button/style/group.ts +++ b/components/button/style/group.ts @@ -1,4 +1,5 @@ -import type { ButtonToken } from '.'; +import type { CSSObject } from '../../_util/cssinjs'; +import type { ButtonToken } from './token'; import type { GenerateStyle } from '../../theme/internal'; const genButtonBorderStyle = (buttonTypeCls: string, borderColor: string) => ({ @@ -22,8 +23,8 @@ const genButtonBorderStyle = (buttonTypeCls: string, borderColor: string) => ({ }, }); -const genGroupStyle: GenerateStyle = token => { - const { componentCls, fontSize, lineWidth, colorPrimaryHover, colorErrorHover } = token; +const genGroupStyle: GenerateStyle = token => { + const { componentCls, fontSize, lineWidth, groupBorderColor, colorErrorHover } = token; return { [`${componentCls}-group`]: [ @@ -41,7 +42,7 @@ const genGroupStyle: GenerateStyle = token => { }, '&:not(:first-child)': { - marginInlineStart: -lineWidth, + marginInlineStart: token.calc(lineWidth).mul(-1).equal(), [`&, & > ${componentCls}`]: { borderStartStartRadius: 0, @@ -71,7 +72,7 @@ const genGroupStyle: GenerateStyle = token => { }, // Border Color - genButtonBorderStyle(`${componentCls}-primary`, colorPrimaryHover), + genButtonBorderStyle(`${componentCls}-primary`, groupBorderColor), genButtonBorderStyle(`${componentCls}-danger`, colorErrorHover), ], }; diff --git a/components/button/style/index.ts b/components/button/style/index.ts index 20dfe069b6..d2aa11c1bd 100644 --- a/components/button/style/index.ts +++ b/components/button/style/index.ts @@ -1,51 +1,59 @@ -import type { CSSInterpolation, CSSObject } from '../../_util/cssinjs'; -import type { FullToken, GenerateStyle } from '../../theme/internal'; -import { genComponentStyleHook, mergeToken } from '../../theme/internal'; -import genGroupStyle from './group'; -import { genFocusStyle } from '../../style'; -import { genCompactItemStyle } from '../../style/compact-item'; -import { genCompactItemVerticalStyle } from '../../style/compact-item-vertical'; +import type { CSSObject } from '../../_util/cssinjs'; +import { unit } from '../../_util/cssinjs'; -/** Component only token. Which will handle additional calculation of alias token */ -export interface ComponentToken {} +import { genFocusStyle } from '../../style'; +import type { GenerateStyle } from '../../theme/internal'; +import { genStyleHooks, mergeToken } from '../../theme/internal'; +import genGroupStyle from './group'; +import type { ButtonToken, ComponentToken } from './token'; +import { prepareComponentToken, prepareToken } from './token'; -export interface ButtonToken extends FullToken<'Button'> { - // FIXME: should be removed - colorOutlineDefault: string; - buttonPaddingHorizontal: number; -} +export type { ComponentToken }; // ============================== Shared ============================== const genSharedButtonStyle: GenerateStyle = (token): CSSObject => { - const { componentCls, iconCls } = token; + const { componentCls, iconCls, fontWeight } = token; return { [componentCls]: { outline: 'none', position: 'relative', display: 'inline-block', - fontWeight: 400, + fontWeight, whiteSpace: 'nowrap', textAlign: 'center', backgroundImage: 'none', - backgroundColor: 'transparent', - border: `${token.lineWidth}px ${token.lineType} transparent`, + background: 'transparent', + border: `${unit(token.lineWidth)} ${token.lineType} transparent`, cursor: 'pointer', transition: `all ${token.motionDurationMid} ${token.motionEaseInOut}`, userSelect: 'none', touchAction: 'manipulation', - lineHeight: token.lineHeight, color: token.colorText, + '&:disabled > *': { + pointerEvents: 'none', + }, + '> span': { display: 'inline-block', }, + [`${componentCls}-icon`]: { + lineHeight: 0, + }, + // Leave a space between icon and text. [`> ${iconCls} + span, > span + ${iconCls}`]: { marginInlineStart: token.marginXS, }, + [`&:not(${componentCls}-icon-only) > ${componentCls}-icon`]: { + [`&${componentCls}-loading-icon, &:not(:last-child)`]: { + marginInlineEnd: token.marginXS, + }, + }, + '> a': { color: 'currentColor', }, @@ -54,54 +62,29 @@ const genSharedButtonStyle: GenerateStyle = (token): CSS ...genFocusStyle(token), }, + [`&${componentCls}-two-chinese-chars::first-letter`]: { + letterSpacing: '0.34em', + }, + + [`&${componentCls}-two-chinese-chars > *:not(${iconCls})`]: { + marginInlineEnd: '-0.34em', + letterSpacing: '0.34em', + }, + // make `btn-icon-only` not too narrow [`&-icon-only${componentCls}-compact-item`]: { flex: 'none', }, - // Special styles for Primary Button - [`&-compact-item${componentCls}-primary`]: { - [`&:not([disabled]) + ${componentCls}-compact-item${componentCls}-primary:not([disabled])`]: - { - position: 'relative', - - '&:before': { - position: 'absolute', - top: -token.lineWidth, - insetInlineStart: -token.lineWidth, - display: 'inline-block', - width: token.lineWidth, - height: `calc(100% + ${token.lineWidth * 2}px)`, - backgroundColor: token.colorPrimaryHover, - content: '""', - }, - }, - }, - // Special styles for Primary Button - '&-compact-vertical-item': { - [`&${componentCls}-primary`]: { - [`&:not([disabled]) + ${componentCls}-compact-vertical-item${componentCls}-primary:not([disabled])`]: - { - position: 'relative', - - '&:before': { - position: 'absolute', - top: -token.lineWidth, - insetInlineStart: -token.lineWidth, - display: 'inline-block', - width: `calc(100% + ${token.lineWidth * 2}px)`, - height: token.lineWidth, - backgroundColor: token.colorPrimaryHover, - content: '""', - }, - }, - }, - }, }, - }; + } as CSSObject; }; -const genHoverActiveButtonStyle = (hoverStyle: CSSObject, activeStyle: CSSObject): CSSObject => ({ - '&:not(:disabled)': { +const genHoverActiveButtonStyle = ( + btnCls: string, + hoverStyle: CSSObject, + activeStyle: CSSObject, +): CSSObject => ({ + [`&:not(:disabled):not(${btnCls}-disabled)`]: { '&:hover': hoverStyle, '&:active': activeStyle, }, @@ -117,21 +100,22 @@ 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 =============================== const genDisabledStyle: GenerateStyle = token => ({ cursor: 'not-allowed', - borderColor: token.colorBorder, + borderColor: token.borderColorDisabled, color: token.colorTextDisabled, - backgroundColor: token.colorBgContainerDisabled, + background: token.colorBgContainerDisabled, boxShadow: 'none', }); const genGhostButtonStyle = ( btnCls: string, + background: string, textColor: string | false, borderColor: string | false, textColorDisabled: string | false, @@ -141,17 +125,18 @@ const genGhostButtonStyle = ( ): CSSObject => ({ [`&${btnCls}-background-ghost`]: { color: textColor || undefined, - backgroundColor: 'transparent', + background, borderColor: borderColor || undefined, boxShadow: 'none', ...genHoverActiveButtonStyle( + btnCls, { - backgroundColor: 'transparent', + background, ...hoverStyle, }, { - backgroundColor: 'transparent', + background, ...activeStyle, }, ), @@ -165,7 +150,7 @@ const genGhostButtonStyle = ( }); const genSolidDisabledButtonStyle: GenerateStyle = token => ({ - '&:disabled': { + [`&:disabled, &${token.componentCls}-disabled`]: { ...genDisabledStyle(token), }, }); @@ -175,7 +160,7 @@ const genSolidButtonStyle: GenerateStyle = token => ({ }); const genPureDisabledButtonStyle: GenerateStyle = token => ({ - '&:disabled': { + [`&:disabled, &${token.componentCls}-disabled`]: { cursor: 'not-allowed', color: token.colorTextDisabled, }, @@ -185,12 +170,14 @@ const genPureDisabledButtonStyle: GenerateStyle = token const genDefaultButtonStyle: GenerateStyle = token => ({ ...genSolidButtonStyle(token), - backgroundColor: token.colorBgContainer, - borderColor: token.colorBorder, + background: token.defaultBg, + borderColor: token.defaultBorderColor, + color: token.defaultColor, - boxShadow: `0 ${token.controlOutlineWidth}px 0 ${token.controlTmpOutline}`, + boxShadow: token.defaultShadow, ...genHoverActiveButtonStyle( + token.componentCls, { color: token.colorPrimaryHover, borderColor: token.colorPrimaryHover, @@ -203,8 +190,9 @@ const genDefaultButtonStyle: GenerateStyle = token => ({ ...genGhostButtonStyle( token.componentCls, - token.colorBgContainer, - token.colorBgContainer, + token.ghostBg, + token.defaultGhostColor, + token.defaultGhostBorderColor, token.colorTextDisabled, token.colorBorder, ), @@ -214,6 +202,7 @@ const genDefaultButtonStyle: GenerateStyle = token => ({ borderColor: token.colorError, ...genHoverActiveButtonStyle( + token.componentCls, { color: token.colorErrorHover, borderColor: token.colorErrorBorderHover, @@ -226,6 +215,7 @@ const genDefaultButtonStyle: GenerateStyle = token => ({ ...genGhostButtonStyle( token.componentCls, + token.ghostBg, token.colorError, token.colorError, token.colorTextDisabled, @@ -239,24 +229,26 @@ const genDefaultButtonStyle: GenerateStyle = token => ({ const genPrimaryButtonStyle: GenerateStyle = token => ({ ...genSolidButtonStyle(token), - color: token.colorTextLightSolid, - backgroundColor: token.colorPrimary, + color: token.primaryColor, + background: token.colorPrimary, - boxShadow: `0 ${token.controlOutlineWidth}px 0 ${token.controlOutline}`, + boxShadow: token.primaryShadow, ...genHoverActiveButtonStyle( + token.componentCls, { color: token.colorTextLightSolid, - backgroundColor: token.colorPrimaryHover, + background: token.colorPrimaryHover, }, { color: token.colorTextLightSolid, - backgroundColor: token.colorPrimaryActive, + background: token.colorPrimaryActive, }, ), ...genGhostButtonStyle( token.componentCls, + token.ghostBg, token.colorPrimary, token.colorPrimary, token.colorTextDisabled, @@ -272,20 +264,23 @@ const genPrimaryButtonStyle: GenerateStyle = token => ({ ), [`&${token.componentCls}-dangerous`]: { - backgroundColor: token.colorError, - boxShadow: `0 ${token.controlOutlineWidth}px 0 ${token.colorErrorOutline}`, + background: token.colorError, + boxShadow: token.dangerShadow, + color: token.dangerColor, ...genHoverActiveButtonStyle( + token.componentCls, { - backgroundColor: token.colorErrorHover, + background: token.colorErrorHover, }, { - backgroundColor: token.colorErrorActive, + background: token.colorErrorActive, }, ), ...genGhostButtonStyle( token.componentCls, + token.ghostBg, token.colorError, token.colorError, token.colorTextDisabled, @@ -314,8 +309,10 @@ const genLinkButtonStyle: GenerateStyle = token => ({ color: token.colorLink, ...genHoverActiveButtonStyle( + token.componentCls, { color: token.colorLinkHover, + background: token.linkHoverBg, }, { color: token.colorLinkActive, @@ -328,6 +325,7 @@ const genLinkButtonStyle: GenerateStyle = token => ({ color: token.colorError, ...genHoverActiveButtonStyle( + token.componentCls, { color: token.colorErrorHover, }, @@ -343,13 +341,14 @@ const genLinkButtonStyle: GenerateStyle = token => ({ // Type: Text const genTextButtonStyle: GenerateStyle = token => ({ ...genHoverActiveButtonStyle( + token.componentCls, { color: token.colorText, - backgroundColor: token.colorBgTextHover, + background: token.textHoverBg, }, { color: token.colorText, - backgroundColor: token.colorBgTextActive, + background: token.colorBgTextActive, }, ), @@ -360,26 +359,19 @@ const genTextButtonStyle: GenerateStyle = token => ({ ...genPureDisabledButtonStyle(token), ...genHoverActiveButtonStyle( + token.componentCls, { color: token.colorErrorHover, - backgroundColor: token.colorErrorBg, + background: token.colorErrorBg, }, { color: token.colorErrorHover, - backgroundColor: token.colorErrorBg, + background: token.colorErrorBg, }, ), }, }); -// Href and Disabled -const genDisabledButtonStyle: GenerateStyle = token => ({ - ...genDisabledStyle(token), - [`&${token.componentCls}:hover`]: { - ...genDisabledStyle(token), - }, -}); - const genTypeButtonStyle: GenerateStyle = token => { const { componentCls } = token; @@ -389,26 +381,30 @@ const genTypeButtonStyle: GenerateStyle = token => { [`${componentCls}-dashed`]: genDashedButtonStyle(token), [`${componentCls}-link`]: genLinkButtonStyle(token), [`${componentCls}-text`]: genTextButtonStyle(token), - [`${componentCls}-disabled`]: genDisabledButtonStyle(token), + [`${componentCls}-ghost`]: genGhostButtonStyle( + token.componentCls, + token.ghostBg, + token.colorBgContainer, + token.colorBgContainer, + token.colorTextDisabled, + token.colorBorder, + ), }; }; // =============================== Size =============================== -const genSizeButtonStyle = (token: ButtonToken, sizePrefixCls: string = ''): CSSInterpolation => { +const genSizeButtonStyle = (token: ButtonToken, sizePrefixCls: string = '') => { const { componentCls, - iconCls, controlHeight, fontSize, lineHeight, - lineWidth, borderRadius, buttonPaddingHorizontal, + iconCls, + buttonPaddingVertical, } = token; - const paddingVertical = Math.max(0, (controlHeight - fontSize * lineHeight) / 2 - lineWidth); - const paddingHorizontal = buttonPaddingHorizontal - lineWidth; - const iconOnlyCls = `${componentCls}-icon-only`; return [ @@ -416,8 +412,9 @@ const genSizeButtonStyle = (token: ButtonToken, sizePrefixCls: string = ''): CSS { [`${componentCls}${sizePrefixCls}`]: { fontSize, + lineHeight, height: controlHeight, - padding: `${paddingVertical}px ${paddingHorizontal}px`, + padding: `${unit(buttonPaddingVertical!)} ${unit(buttonPaddingHorizontal!)}`, borderRadius, [`&${iconOnlyCls}`]: { @@ -427,8 +424,8 @@ const genSizeButtonStyle = (token: ButtonToken, sizePrefixCls: string = ''): CSS [`&${componentCls}-round`]: { width: 'auto', }, - '> span': { - transform: 'scale(1.143)', // 14px -> 16px + [iconCls]: { + fontSize: token.buttonIconOnlyFontSize, }, }, @@ -441,10 +438,6 @@ const genSizeButtonStyle = (token: ButtonToken, sizePrefixCls: string = ''): CSS [`${componentCls}-loading-icon`]: { transition: `width ${token.motionDurationSlow} ${token.motionEaseInOut}, opacity ${token.motionDurationSlow} ${token.motionEaseInOut}`, }, - - [`&:not(${iconOnlyCls}) ${componentCls}-loading-icon > ${iconCls}`]: { - marginInlineEnd: token.marginXS, - }, }, }, @@ -458,14 +451,24 @@ const genSizeButtonStyle = (token: ButtonToken, sizePrefixCls: string = ''): CSS ]; }; -const genSizeBaseButtonStyle: GenerateStyle = token => genSizeButtonStyle(token); +const genSizeBaseButtonStyle: GenerateStyle = token => + genSizeButtonStyle( + mergeToken(token, { + fontSize: token.contentFontSize, + lineHeight: token.contentLineHeight, + }), + ); const genSizeSmallButtonStyle: GenerateStyle = token => { const smallToken = mergeToken(token, { controlHeight: token.controlHeightSM, + fontSize: token.contentFontSizeSM, + lineHeight: token.contentLineHeightSM, padding: token.paddingXS, - buttonPaddingHorizontal: 8, // Fixed padding + buttonPaddingHorizontal: token.paddingInlineSM, + buttonPaddingVertical: token.paddingBlockSM, borderRadius: token.borderRadiusSM, + buttonIconOnlyFontSize: token.onlyIconSizeSM, }); return genSizeButtonStyle(smallToken, `${token.componentCls}-sm`); @@ -474,8 +477,12 @@ const genSizeSmallButtonStyle: GenerateStyle = token => { const genSizeLargeButtonStyle: GenerateStyle = token => { const largeToken = mergeToken(token, { controlHeight: token.controlHeightLG, - fontSize: token.fontSizeLG, + fontSize: token.contentFontSizeLG, + lineHeight: token.contentLineHeightLG, + buttonPaddingHorizontal: token.paddingInlineLG, + buttonPaddingVertical: token.paddingBlockLG, borderRadius: token.borderRadiusLG, + buttonIconOnlyFontSize: token.onlyIconSizeLG, }); return genSizeButtonStyle(largeToken, `${token.componentCls}-lg`); @@ -493,33 +500,37 @@ const genBlockButtonStyle: GenerateStyle = token => { }; // ============================== Export ============================== -export default genComponentStyleHook('Button', token => { - const { controlTmpOutline, paddingContentHorizontal } = token; - const buttonToken = mergeToken(token, { - colorOutlineDefault: controlTmpOutline, - buttonPaddingHorizontal: paddingContentHorizontal, - }); +export default genStyleHooks( + 'Button', + token => { + const buttonToken = prepareToken(token); - return [ - // Shared - genSharedButtonStyle(buttonToken), + return [ + // Shared + genSharedButtonStyle(buttonToken), - // Size - genSizeSmallButtonStyle(buttonToken), - genSizeBaseButtonStyle(buttonToken), - genSizeLargeButtonStyle(buttonToken), + // Size + genSizeSmallButtonStyle(buttonToken), + genSizeBaseButtonStyle(buttonToken), + genSizeLargeButtonStyle(buttonToken), - // Block - genBlockButtonStyle(buttonToken), + // Block + genBlockButtonStyle(buttonToken), - // Group (type, ghost, danger, disabled, loading) - genTypeButtonStyle(buttonToken), + // Group (type, ghost, danger, loading) + genTypeButtonStyle(buttonToken), - // Button Group - genGroupStyle(buttonToken), - - // Space Compact - genCompactItemStyle(token, { focus: false }), - genCompactItemVerticalStyle(token), - ]; -}); + // Button Group + genGroupStyle(buttonToken), + ]; + }, + prepareComponentToken, + { + unitless: { + fontWeight: true, + contentLineHeight: true, + contentLineHeightSM: true, + contentLineHeightLG: true, + }, + }, +); diff --git a/components/button/style/token.ts b/components/button/style/token.ts new file mode 100644 index 0000000000..c075a55d5a --- /dev/null +++ b/components/button/style/token.ts @@ -0,0 +1,234 @@ +import type { CSSProperties } from 'vue'; +import type { FullToken, GetDefaultToken } from '../../theme/internal'; +import { getLineHeight, mergeToken } from '../../theme/internal'; +import type { GenStyleFn } from '../../theme/util/genComponentStyleHook'; + +/** Component only token. Which will handle additional calculation of alias token */ +export interface ComponentToken { + /** + * @desc 文字字重 + * @descEN Font weight of text + */ + fontWeight: CSSProperties['fontWeight']; + /** + * @desc 默认按钮阴影 + * @descEN Shadow of default button + */ + defaultShadow: string; + /** + * @desc 主要按钮阴影 + * @descEN Shadow of primary button + */ + primaryShadow: string; + /** + * @desc 危险按钮阴影 + * @descEN Shadow of danger button + */ + dangerShadow: string; + /** + * @desc 主要按钮文本颜色 + * @descEN Text color of primary button + */ + primaryColor: string; + /** + * @desc 默认按钮文本颜色 + * @descEN Text color of default button + */ + defaultColor: string; + /** + * @desc 默认按钮背景色 + * @descEN Background color of default button + */ + defaultBg: string; + /** + * @desc 默认按钮边框颜色 + * @descEN Border color of default button + */ + defaultBorderColor: string; + /** + * @desc 危险按钮文本颜色 + * @descEN Text color of danger button + */ + dangerColor: string; + /** + * @desc 禁用状态边框颜色 + * @descEN Border color of disabled button + */ + borderColorDisabled: string; + /** + * @desc 默认幽灵按钮文本颜色 + * @descEN Text color of default ghost button + */ + defaultGhostColor: string; + /** + * @desc 幽灵按钮背景色 + * @descEN Background color of ghost button + */ + ghostBg: string; + /** + * @desc 默认幽灵按钮边框颜色 + * @descEN Border color of default ghost button + */ + defaultGhostBorderColor: string; + /** + * @desc 按钮横向内间距 + * @descEN Horizontal padding of button + */ + paddingInline: CSSProperties['paddingInline']; + /** + * @desc 大号按钮横向内间距 + * @descEN Horizontal padding of large button + */ + paddingInlineLG: CSSProperties['paddingInline']; + /** + * @desc 小号按钮横向内间距 + * @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 + */ + onlyIconSize: number; + /** + * @desc 大号只有图标的按钮图标尺寸 + * @descEN Icon size of large button which only contains icon + */ + onlyIconSizeLG: number; + /** + * @desc 小号只有图标的按钮图标尺寸 + * @descEN Icon size of small button which only contains icon + */ + onlyIconSizeSM: number; + /** + * @desc 按钮组边框颜色 + * @descEN Border color of button group + */ + groupBorderColor: string; + /** + * @desc 链接按钮悬浮态背景色 + * @descEN Background color of link button when hover + */ + linkHoverBg: string; + /** + * @desc 文本按钮悬浮态背景色 + * @descEN Background color of text button when hover + */ + textHoverBg: string; + /** + * @desc 按钮内容字体大小 + * @descEN Font size of button content + */ + contentFontSize: number; + /** + * @desc 大号按钮内容字体大小 + * @descEN Font size of large button content + */ + contentFontSizeLG: number; + /** + * @desc 小号按钮内容字体大小 + * @descEN Font size of small button content + */ + contentFontSizeSM: number; + /** + * @desc 按钮内容字体行高 + * @descEN Line height of button content + */ + contentLineHeight: number; + /** + * @desc 大号按钮内容字体行高 + * @descEN Line height of large button content + */ + contentLineHeightLG: number; + /** + * @desc 小号按钮内容字体行高 + * @descEN Line height of small button content + */ + contentLineHeightSM: number; +} + +export interface ButtonToken extends FullToken<'Button'> { + buttonPaddingHorizontal: CSSProperties['paddingInline']; + buttonPaddingVertical: CSSProperties['paddingBlock']; + buttonIconOnlyFontSize: number; +} + +export const prepareToken: (token: Parameters>[0]) => ButtonToken = token => { + const { paddingInline, onlyIconSize, paddingBlock } = token; + + const buttonToken = mergeToken(token, { + buttonPaddingHorizontal: paddingInline, + buttonPaddingVertical: paddingBlock, + buttonIconOnlyFontSize: onlyIconSize, + }); + + return buttonToken; +}; + +export const prepareComponentToken: GetDefaultToken<'Button'> = token => { + const contentFontSize = token.contentFontSize ?? token.fontSize; + const contentFontSizeSM = token.contentFontSizeSM ?? token.fontSize; + const contentFontSizeLG = token.contentFontSizeLG ?? token.fontSizeLG; + const contentLineHeight = token.contentLineHeight ?? getLineHeight(contentFontSize); + const contentLineHeightSM = token.contentLineHeightSM ?? getLineHeight(contentFontSizeSM); + const contentLineHeightLG = token.contentLineHeightLG ?? getLineHeight(contentFontSizeLG); + + 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, + 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, + contentLineHeight, + contentLineHeightSM, + contentLineHeightLG, + paddingBlock: Math.max( + (token.controlHeight - contentFontSize * contentLineHeight) / 2 - token.lineWidth, + 0, + ), + paddingBlockSM: Math.max( + (token.controlHeightSM - contentFontSizeSM * contentLineHeightSM) / 2 - token.lineWidth, + 0, + ), + paddingBlockLG: Math.max( + (token.controlHeightLG - contentFontSizeLG * contentLineHeightLG) / 2 - token.lineWidth, + 0, + ), + }; +}; diff --git a/components/config-provider/context.ts b/components/config-provider/context.ts index f219075952..cf69fc73b9 100644 --- a/components/config-provider/context.ts +++ b/components/config-provider/context.ts @@ -57,6 +57,18 @@ 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; + } + | boolean; } export const configProviderProps = () => ({ diff --git a/components/config-provider/hooks/useCssVarCls.ts b/components/config-provider/hooks/useCssVarCls.ts new file mode 100644 index 0000000000..b3e60fd8c1 --- /dev/null +++ b/components/config-provider/hooks/useCssVarCls.ts @@ -0,0 +1,16 @@ +import { useToken } from '../../theme/internal'; +import type { Ref } from 'vue'; +import { computed } from 'vue'; + +/** + * This hook is only for cssVar to add root className for components. + * If root ClassName is needed, this hook could be refactored with `-root` + * @param prefixCls + */ +const useCSSVarCls = (prefixCls: Ref) => { + const [, , , , cssVar] = useToken(); + + return computed(() => (cssVar.value ? `${prefixCls.value}-css-var` : '')); +}; + +export default useCSSVarCls; diff --git a/components/config-provider/hooks/useSize.ts b/components/config-provider/hooks/useSize.ts new file mode 100644 index 0000000000..85fac8d982 --- /dev/null +++ b/components/config-provider/hooks/useSize.ts @@ -0,0 +1,32 @@ +import type { SizeType } from '../SizeContext'; +import { useInjectSize } from '../SizeContext'; +import type { Ref } from 'vue'; +import { computed, shallowRef, watch } from 'vue'; + +const useSize = (customSize?: T | ((ctxSize: SizeType) => T)): Ref => { + const size = useInjectSize(); + + const mergedSize = shallowRef(null); + + watch( + computed(() => { + return [customSize, size.value]; + }), + () => { + if (!customSize) { + mergedSize.value = size.value as T; + } + if (typeof customSize === 'string') { + mergedSize.value = customSize ?? (size.value as T); + } + if (customSize instanceof Function) { + mergedSize.value = customSize(size.value) as T; + } + }, + { immediate: true }, + ); + + return mergedSize; +}; + +export default useSize; diff --git a/components/config-provider/hooks/useTheme.ts b/components/config-provider/hooks/useTheme.ts index 0ed451193c..027a1a6941 100644 --- a/components/config-provider/hooks/useTheme.ts +++ b/components/config-provider/hooks/useTheme.ts @@ -2,13 +2,26 @@ import type { ThemeConfig } from '../context'; import { defaultConfig } from '../../theme/internal'; import type { Ref } from 'vue'; import { computed } from 'vue'; - +import devWarning from '../../vc-util/warning'; +const themeKey = 'antdvtheme'; export default function useTheme(theme?: Ref, parentTheme?: Ref) { const themeConfig = computed(() => theme?.value || {}); const parentThemeConfig = computed(() => themeConfig.value.inherit === false || !parentTheme?.value ? defaultConfig : parentTheme.value, ); + if (process.env.NODE_ENV !== 'production') { + const cssVarEnabled = themeConfig.value.cssVar || parentThemeConfig.value.cssVar; + const validKey = !!( + (typeof themeConfig.value.cssVar === 'object' && themeConfig.value.cssVar?.key) || + themeKey + ); + devWarning( + !cssVarEnabled || validKey, + '[Ant Design Vue ConfigProvider] Missing key in `cssVar` config. Please set `cssVar.key` manually in each ConfigProvider inside `cssVar` enabled ConfigProvider.', + ); + } + const mergedTheme = computed(() => { if (!theme?.value) { return parentTheme?.value; @@ -26,6 +39,17 @@ export default function useTheme(theme?: Ref, parentTheme?: Ref, parentTheme?: Ref { + return 'themekey' + uid++; +}; + +export default useThemeKey; diff --git a/components/config-provider/index.tsx b/components/config-provider/index.tsx index 5b09ec16b8..5167540fc8 100644 --- a/components/config-provider/index.tsx +++ b/components/config-provider/index.tsx @@ -15,7 +15,7 @@ import type { ValidateMessages } from '../form/interface'; import useStyle from './style'; import useTheme from './hooks/useTheme'; import defaultSeedToken from '../theme/themes/seed'; -import type { ConfigProviderInnerProps, ConfigProviderProps, Theme } from './context'; +import type { ConfigProviderInnerProps, ConfigProviderProps, Theme, ThemeConfig } from './context'; import { useConfigContextProvider, useConfigContextInject, @@ -26,7 +26,7 @@ import { import { useProviderSize } from './SizeContext'; import { useProviderDisabled } from './DisabledContext'; import { createTheme } from '../_util/cssinjs'; -import { DesignTokenProvider } from '../theme/internal'; +import { defaultTheme, DesignTokenProvider } from '../theme/context'; export type { ConfigProviderProps, @@ -226,19 +226,47 @@ const ConfigProvider = defineComponent({ // ================================ Dynamic theme ================================ const memoTheme = computed(() => { - const { algorithm, token, ...rest } = mergedTheme.value || {}; + const { algorithm, token, components, cssVar, ...rest } = mergedTheme.value || {}; const themeObj = algorithm && (!Array.isArray(algorithm) || algorithm.length > 0) ? createTheme(algorithm) - : undefined; + : defaultTheme; + + const parsedComponents: any = {}; + Object.entries(components || {}).forEach(([componentName, componentToken]) => { + const parsedToken: typeof componentToken & { theme?: typeof defaultTheme } = { + ...componentToken, + }; + if ('algorithm' in parsedToken) { + if (parsedToken.algorithm === true) { + parsedToken.theme = themeObj; + } else if ( + Array.isArray(parsedToken.algorithm) || + typeof parsedToken.algorithm === 'function' + ) { + parsedToken.theme = createTheme(parsedToken.algorithm as any); + } + delete parsedToken.algorithm; + } + parsedComponents[componentName] = parsedToken; + }); + + const mergedToken = { + ...defaultSeedToken, + ...token, + }; + return { ...rest, theme: themeObj, - token: { - ...defaultSeedToken, - ...token, + token: mergedToken, + components: parsedComponents, + override: { + override: mergedToken, + ...parsedComponents, }, + cssVar: cssVar as Exclude, }; }); const validateMessagesRef = computed(() => { diff --git a/components/config-provider/style/index.ts b/components/config-provider/style/index.ts index 77ed478a06..c3533b2789 100644 --- a/components/config-provider/style/index.ts +++ b/components/config-provider/style/index.ts @@ -1,3 +1,4 @@ +import type { CSSObject } from '../../_util/cssinjs'; import { useStyleRegister } from '../../_util/cssinjs'; import { resetIcon } from '../../style'; import { useToken } from '../../theme/internal'; @@ -13,16 +14,17 @@ const useStyle = (iconPrefixCls: Ref) => { hashId: '', path: ['ant-design-icons', iconPrefixCls.value], })), - () => [ - { - [`.${iconPrefixCls.value}`]: { - ...resetIcon(), - [`.${iconPrefixCls.value} .${iconPrefixCls.value}-icon`]: { - display: 'block', + () => + [ + { + [`.${iconPrefixCls.value}`]: { + ...resetIcon(), + [`.${iconPrefixCls.value} .${iconPrefixCls.value}-icon`]: { + display: 'block', + }, }, }, - }, - ], + ] as CSSObject[], ); }; diff --git a/components/date-picker/style/index.ts b/components/date-picker/style/index.ts index 4e92852fd3..5675e67362 100644 --- a/components/date-picker/style/index.ts +++ b/components/date-picker/style/index.ts @@ -22,6 +22,8 @@ import type { TokenWithCommonCls } from '../../theme/util/genComponentStyleHook' import { resetComponent, roundedArrow, textEllipsis } from '../../style'; import { genCompactItemStyle } from '../../style/compact-item'; +export interface ComponentToken {} + export interface ComponentToken { presetsWidth: number; presetsMaxWidth: number; diff --git a/components/descriptions/style/index.ts b/components/descriptions/style/index.ts index 037edb550c..c4289b7de9 100644 --- a/components/descriptions/style/index.ts +++ b/components/descriptions/style/index.ts @@ -3,6 +3,8 @@ import type { FullToken, GenerateStyle } from '../../theme/internal'; import { genComponentStyleHook, mergeToken } from '../../theme/internal'; import { resetComponent, textEllipsis } from '../../style'; +export interface ComponentToken {} + interface DescriptionsToken extends FullToken<'Descriptions'> { descriptionsTitleMarginBottom: number; descriptionsExtraColor: string; diff --git a/components/float-button/BackTop.tsx b/components/float-button/BackTop.tsx index 7229059752..38dbee0e72 100644 --- a/components/float-button/BackTop.tsx +++ b/components/float-button/BackTop.tsx @@ -60,7 +60,7 @@ const BackTop = defineComponent({ const handleScroll = throttleByAnimationFrame((e: Event | { target: any }) => { const { visibilityHeight } = props; - const scrollTop = getScroll(e.target, true); + const scrollTop = getScroll(e.target); state.visible = scrollTop >= visibilityHeight; }); diff --git a/components/form/style/index.ts b/components/form/style/index.ts index dad4aa62f0..394a3db899 100644 --- a/components/form/style/index.ts +++ b/components/form/style/index.ts @@ -5,6 +5,8 @@ import { genComponentStyleHook, mergeToken } from '../../theme/internal'; import { resetComponent } from '../../style'; import genFormValidateMotionStyle from './explain'; +export interface ComponentToken {} + export interface FormToken extends FullToken<'Form'> { formItemCls: string; rootPrefixCls: string; diff --git a/components/grid/style/index.ts b/components/grid/style/index.ts index 923e01837f..d9df3e2fa8 100644 --- a/components/grid/style/index.ts +++ b/components/grid/style/index.ts @@ -2,6 +2,8 @@ import type { CSSObject } from '../../_util/cssinjs'; import type { FullToken, GenerateStyle } from '../../theme/internal'; import { genComponentStyleHook, mergeToken } from '../../theme/internal'; +export interface ComponentToken {} + interface GridRowToken extends FullToken<'Grid'> {} interface GridColToken extends FullToken<'Grid'> { diff --git a/components/index.ts b/components/index.ts index 3f86413a3a..d9d945c939 100644 --- a/components/index.ts +++ b/components/index.ts @@ -2,7 +2,7 @@ import type { App } from 'vue'; import * as components from './components'; import { default as version } from './version'; -import cssinjs from './_util/cssinjs'; +import * as cssinjs from './_util/cssinjs'; export * from './components'; export * from './_util/cssinjs'; diff --git a/components/input/style/index.ts b/components/input/style/index.ts index 7653f78707..dec232d664 100644 --- a/components/input/style/index.ts +++ b/components/input/style/index.ts @@ -5,6 +5,8 @@ import type { GlobalToken } from '../../theme/interface'; import { clearFix, resetComponent } from '../../style'; import { genCompactItemStyle } from '../../style/compact-item'; +export interface ComponentToken {} + export type InputToken> = T & { inputAffixPadding: number; inputPaddingVertical: number; diff --git a/components/page-header/style/index.ts b/components/page-header/style/index.ts index 2ed3537790..99717577df 100644 --- a/components/page-header/style/index.ts +++ b/components/page-header/style/index.ts @@ -4,6 +4,8 @@ import { genComponentStyleHook, mergeToken } from '../../theme/internal'; import { resetComponent, textEllipsis } from '../../style'; import { operationUnit } from '../../style'; +export interface ComponentToken {} + interface PageHeaderToken extends FullToken<'PageHeader'> { pageHeaderPadding: number; pageHeaderPaddingVertical: number; diff --git a/components/pagination/style/index.tsx b/components/pagination/style/index.tsx index 9f94de9abf..56d6201343 100644 --- a/components/pagination/style/index.tsx +++ b/components/pagination/style/index.tsx @@ -9,6 +9,8 @@ import type { FullToken, GenerateStyle } from '../../theme/internal'; import { genComponentStyleHook, mergeToken } from '../../theme/internal'; import { genFocusOutline, genFocusStyle, resetComponent } from '../../style'; +export interface ComponentToken {} + interface PaginationToken extends InputToken> { paginationItemSize: number; paginationFontFamily: string; diff --git a/components/statistic/style/index.tsx b/components/statistic/style/index.tsx index d70e31b0c2..b180819264 100644 --- a/components/statistic/style/index.tsx +++ b/components/statistic/style/index.tsx @@ -3,6 +3,8 @@ import type { FullToken, GenerateStyle } from '../../theme/internal'; import { genComponentStyleHook, mergeToken } from '../../theme/internal'; import { resetComponent } from '../../style'; +export interface ComponentToken {} + interface StatisticToken extends FullToken<'Statistic'> { statisticTitleFontSize: number; statisticContentFontSize: number; diff --git a/components/switch/demo/basic.vue b/components/switch/demo/basic.vue index af7d01376c..076d7b4ea1 100644 --- a/components/switch/demo/basic.vue +++ b/components/switch/demo/basic.vue @@ -19,7 +19,8 @@ The most basic usage. + diff --git a/components/switch/demo/size.vue b/components/switch/demo/size.vue index 7bc9687582..145345d71b 100644 --- a/components/switch/demo/size.vue +++ b/components/switch/demo/size.vue @@ -27,6 +27,6 @@ title: import { reactive } from 'vue'; const state = reactive({ checked1: true, - checked2: false, + checked2: true, }); diff --git a/components/switch/demo/text.vue b/components/switch/demo/text.vue index e1e8a64504..5173471343 100644 --- a/components/switch/demo/text.vue +++ b/components/switch/demo/text.vue @@ -18,7 +18,7 @@ With text and icon.