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`; - } +export const extract: ExtractStyle`; +} 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/avatar/Avatar.tsx b/components/avatar/Avatar.tsx index 5bd4f17e0e..7cba88f3ad 100644 --- a/components/avatar/Avatar.tsx +++ b/components/avatar/Avatar.tsx @@ -12,6 +12,7 @@ import ResizeObserver from '../vc-resize-observer'; import eagerComputed from '../_util/eagerComputed'; import useStyle from './style'; import { useAvatarInjectContext } from './AvatarContext'; +import useCSSVarCls from '../config-provider/hooks/useCssVarCls'; export type AvatarSize = 'large' | 'small' | 'default' | number | ScreenSizeMap; @@ -55,7 +56,8 @@ const Avatar = defineComponent({ const avatarNodeRef = shallowRef(null); const { prefixCls } = useConfigInject('avatar', props); - const [wrapSSR, hashId] = useStyle(prefixCls); + const rootCls = useCSSVarCls(prefixCls); + const [wrapSSR, hashId, cssVarCls] = useStyle(prefixCls, rootCls); const avatarCtx = useAvatarInjectContext(); const size = computed(() => { return props.size === 'default' ? avatarCtx.size : props.size; @@ -146,6 +148,8 @@ const Avatar = defineComponent({ [`${pre}-${mergeShape}`]: true, [`${pre}-image`]: src && isImgExist.value, [`${pre}-icon`]: icon, + [cssVarCls.value]: true, + [rootCls.value]: true, [hashId.value]: true, }; @@ -175,7 +179,7 @@ const Avatar = defineComponent({ } else if (icon) { childrenToRender = icon; } else if (isMounted.value || scale.value !== 1) { - const transformString = `scale(${scale.value}) translateX(-50%)`; + const transformString = `scale(${scale.value})`; const childrenStyle: CSSProperties = { msTransform: transformString, WebkitTransform: transformString, diff --git a/components/avatar/Group.tsx b/components/avatar/Group.tsx index c60f6411b4..75971b9dae 100644 --- a/components/avatar/Group.tsx +++ b/components/avatar/Group.tsx @@ -6,6 +6,7 @@ import type { PropType, ExtractPropTypes, CSSProperties } from 'vue'; import { computed, defineComponent, watchEffect } from 'vue'; import { flattenChildren, getPropsSlot } from '../_util/props-util'; import useConfigInject from '../config-provider/hooks/useConfigInject'; +import useCSSVarCls from '../config-provider/hooks/useCssVarCls'; import useStyle from './style'; import { useAvatarProviderContext } from './AvatarContext'; @@ -36,7 +37,8 @@ const Group = defineComponent({ setup(props, { slots, attrs }) { const { prefixCls, direction } = useConfigInject('avatar', props); const groupPrefixCls = computed(() => `${prefixCls.value}-group`); - const [wrapSSR, hashId] = useStyle(prefixCls); + const rootCls = useCSSVarCls(prefixCls); + const [wrapSSR, hashId, cssVarCls] = useStyle(prefixCls, rootCls); watchEffect(() => { const context = { size: props.size, shape: props.shape }; useAvatarProviderContext(context); @@ -54,6 +56,8 @@ const Group = defineComponent({ [groupPrefixCls.value]: true, [`${groupPrefixCls.value}-rtl`]: direction.value === 'rtl', [`${attrs.class}`]: !!attrs.class, + [cssVarCls.value]: true, + [rootCls.value]: true, [hashId.value]: true, }; diff --git a/components/avatar/style/index.ts b/components/avatar/style/index.ts index 6d8201add1..5bcd696853 100644 --- a/components/avatar/style/index.ts +++ b/components/avatar/style/index.ts @@ -1,12 +1,14 @@ +import { unit } from '../../_util/cssinjs'; import type { CSSObject } from '../../_util/cssinjs'; -import type { FullToken, GenerateStyle } from '../../theme/internal'; -import { genComponentStyleHook, mergeToken } from '../../theme/internal'; + import { resetComponent } from '../../style'; +import type { FullToken, GenerateStyle, GetDefaultToken } from '../../theme/internal'; +import { genStyleHooks, mergeToken } from '../../theme/internal'; export interface ComponentToken { /** - * @desc 头像背景色 - * @descEN Background color of Avatar + * @desc 头像尺寸 + * @descEN Size of Avatar */ containerSize: number; /** @@ -51,10 +53,14 @@ export interface ComponentToken { groupBorderColor: string; } +/** + * @desc Avatar 组件的 Token + * @descEN Token for Avatar component + */ type AvatarToken = FullToken<'avatar'> & { + avatarBgColor: string; avatarBg: string; avatarColor: string; - avatarBgColor: string; }; const genBaseStyle: GenerateStyle = token => { @@ -81,22 +87,12 @@ const genBaseStyle: GenerateStyle = token => { const avatarSizeStyle = (size: number, fontSize: number, radius: number): CSSObject => ({ width: size, height: size, - lineHeight: `${size - lineWidth * 2}px`, borderRadius: '50%', [`&${componentCls}-square`]: { borderRadius: radius, }, - [`${componentCls}-string`]: { - position: 'absolute', - left: { - _skip_check_: true, - value: '50%', - }, - transformOrigin: '0 center', - }, - [`&${componentCls}-icon`]: { fontSize, [`> ${iconCls}`]: { @@ -109,16 +105,18 @@ const genBaseStyle: GenerateStyle = token => { [componentCls]: { ...resetComponent(token), position: 'relative', - display: 'inline-block', + display: 'inline-flex', + justifyContent: 'center', + alignItems: 'center', overflow: 'hidden', color: avatarColor, whiteSpace: 'nowrap', textAlign: 'center', verticalAlign: 'middle', background: avatarBg, - border: `${lineWidth}px ${lineType} transparent`, + border: `${unit(lineWidth)} ${lineType} transparent`, - [`&-image`]: { + '&-image': { background: 'transparent', }, @@ -128,11 +126,11 @@ const genBaseStyle: GenerateStyle = token => { ...avatarSizeStyle(containerSize, textFontSize, borderRadius), - [`&-lg`]: { + '&-lg': { ...avatarSizeStyle(containerSizeLG, textFontSizeLG, borderRadiusLG), }, - [`&-sm`]: { + '&-sm': { ...avatarSizeStyle(containerSizeSM, textFontSizeSM, borderRadiusSM), }, @@ -153,11 +151,11 @@ const genGroupStyle: GenerateStyle = token => { [`${componentCls}-group`]: { display: 'inline-flex', - [`${componentCls}`]: { + [componentCls]: { borderColor: groupBorderColor, }, - [`> *:not(:first-child)`]: { + '> *:not(:first-child)': { marginInlineStart: groupOverlapping, }, }, @@ -169,7 +167,33 @@ const genGroupStyle: GenerateStyle = token => { }; }; -export default genComponentStyleHook( +export const prepareComponentToken: GetDefaultToken<'avatar'> = token => { + const { + controlHeight, + controlHeightLG, + controlHeightSM, + fontSize, + fontSizeLG, + fontSizeXL, + fontSizeHeading3, + marginXS, + marginXXS, + colorBorderBg, + } = token; + return { + containerSize: controlHeight, + containerSizeLG: controlHeightLG, + containerSizeSM: controlHeightSM, + textFontSize: Math.round((fontSizeLG + fontSizeXL) / 2), + textFontSizeLG: fontSizeHeading3, + textFontSizeSM: fontSize, + groupSpace: marginXXS, + groupOverlapping: -marginXS, + groupBorderColor: colorBorderBg, + }; +}; + +export default genStyleHooks( 'Avatar', token => { const { colorTextLightSolid, colorTextPlaceholder } = token; @@ -179,33 +203,5 @@ export default genComponentStyleHook( }); return [genBaseStyle(avatarToken), genGroupStyle(avatarToken)]; }, - token => { - const { - controlHeight, - controlHeightLG, - controlHeightSM, - - fontSize, - fontSizeLG, - fontSizeXL, - fontSizeHeading3, - - marginXS, - marginXXS, - colorBorderBg, - } = token; - return { - containerSize: controlHeight, - containerSizeLG: controlHeightLG, - containerSizeSM: controlHeightSM, - - textFontSize: Math.round((fontSizeLG + fontSizeXL) / 2), - textFontSizeLG: fontSizeHeading3, - textFontSizeSM: fontSize, - - groupSpace: marginXXS, - groupOverlapping: -marginXS, - groupBorderColor: colorBorderBg, - }; - }, + prepareComponentToken, ); diff --git a/components/badge/Badge.tsx b/components/badge/Badge.tsx index a0363732f4..a2577564b2 100644 --- a/components/badge/Badge.tsx +++ b/components/badge/Badge.tsx @@ -9,6 +9,7 @@ import { defineComponent, computed, ref, watch, Transition } from 'vue'; import Ribbon from './Ribbon'; import useConfigInject from '../config-provider/hooks/useConfigInject'; import isNumeric from '../_util/isNumeric'; +import useCSSVarCls from '../config-provider/hooks/useCssVarCls'; import useStyle from './style'; import type { PresetColorKey } from '../theme/interface'; import type { LiteralUnion, CustomSlotsType } from '../_util/type'; @@ -49,7 +50,8 @@ export default defineComponent({ }>, setup(props, { slots, attrs }) { const { prefixCls, direction } = useConfigInject('badge', props); - const [wrapSSR, hashId] = useStyle(prefixCls); + const rootCls = useCSSVarCls(prefixCls); + const [wrapSSR, hashId, cssVarCls] = useStyle(prefixCls, rootCls); // ================================ Misc ================================ const numberedDisplayCount = computed(() => { @@ -189,6 +191,8 @@ export default defineComponent({ [`${pre}-rtl`]: direction.value === 'rtl', }, attrs.class, + cssVarCls.value, + rootCls.value, hashId.value, ); diff --git a/components/badge/Ribbon.tsx b/components/badge/Ribbon.tsx index 6725d833b2..6426f612da 100644 --- a/components/badge/Ribbon.tsx +++ b/components/badge/Ribbon.tsx @@ -1,6 +1,7 @@ import type { CustomSlotsType, LiteralUnion } from '../_util/type'; import type { PresetColorType } from '../_util/colors'; -import useStyle from './style'; +import useCSSVarCls from '../config-provider/hooks/useCssVarCls'; +import useStyle from './style/ribbon'; import { isPresetColor } from '../_util/colors'; import type { CSSProperties, PropType, ExtractPropTypes } from 'vue'; import { defineComponent, computed } from 'vue'; @@ -27,7 +28,8 @@ export default defineComponent({ }>, setup(props, { attrs, slots }) { const { prefixCls, direction } = useConfigInject('ribbon', props); - const [wrapSSR, hashId] = useStyle(prefixCls); + const rootCls = useCSSVarCls(prefixCls); + const [wrapSSR, hashId, cssVarCls] = useStyle(prefixCls, rootCls); const colorInPreset = computed(() => isPresetColor(props.color, false)); const ribbonCls = computed(() => [ prefixCls.value, @@ -46,7 +48,10 @@ export default defineComponent({ cornerColorStyle.color = props.color; } return wrapSSR( -
+
{slots.default?.()}
{ +import { resetComponent } from '../../style'; +import type { FullToken, GenerateStyle, GenStyleFn, GetDefaultToken } from '../../theme/internal'; +import { genPresetColor, genStyleHooks, mergeToken } from '../../theme/internal'; + +/** Component only token. Which will handle additional calculation of alias token */ +export interface ComponentToken { + // Component token here + /** + * @desc 徽标 z-index + * @descEN z-index of badge + */ + indicatorZIndex: number | string; + /** + * @desc 徽标高度 + * @descEN Height of badge + */ + indicatorHeight: number | string; + /** + * @desc 小号徽标高度 + * @descEN Height of small badge + */ + indicatorHeightSM: number | string; + /** + * @desc 点状徽标尺寸 + * @descEN Size of dot badge + */ + dotSize: number; + /** + * @desc 徽标文本尺寸 + * @descEN Font size of badge text + */ + textFontSize: number; + /** + * @desc 小号徽标文本尺寸 + * @descEN Font size of small badge text + */ + textFontSizeSM: number; + /** + * @desc 徽标文本粗细 + * @descEN Font weight of badge text + */ + textFontWeight: number | string; + /** + * @desc 状态徽标尺寸 + * @descEN Size of status badge + */ + statusSize: number; +} + +/** + * @desc Badge 组件的 Token + * @descEN Token for Badge component + */ +export interface BadgeToken extends FullToken<'badge'> { + /** + * @desc 徽标字体高度 + * @descEN Font height of badge + */ badgeFontHeight: number; - badgeZIndex: number | string; - badgeHeight: number; - badgeHeightSm: number; + /** + * @desc 徽标文本颜色 + * @descEN Text color of badge + */ badgeTextColor: string; - badgeFontWeight: string; - badgeFontSize: number; + /** + * @desc 徽标颜色 + * @descEN Color of badge + */ badgeColor: string; + /** + * @desc 徽标悬停颜色 + * @descEN Hover color of badge + */ badgeColorHover: string; - badgeDotSize: number; - badgeFontSizeSm: number; - badgeStatusSize: number; + /** + * @desc 徽标阴影尺寸 + * @descEN Shadow size of badge + */ badgeShadowSize: number; + /** + * @desc 徽标阴影颜色 + * @descEN Shadow color of badge + */ badgeShadowColor: string; + /** + * @desc 徽标处理持续时间 + * @descEN Processing duration of badge + */ badgeProcessingDuration: string; + /** + * @desc 徽标丝带偏移量 + * @descEN Ribbon offset of badge + */ badgeRibbonOffset: number; + /** + * @desc 徽标丝带角变换 + * @descEN Ribbon corner transform of badge + */ badgeRibbonCornerTransform: string; + /** + * @desc 徽标丝带角滤镜 + * @descEN Ribbon corner filter of badge + */ badgeRibbonCornerFilter: string; } @@ -44,10 +125,12 @@ const antNoWrapperZoomBadgeIn = new Keyframes('antNoWrapperZoomBadgeIn', { '0%': { transform: 'scale(0)', opacity: 0 }, '100%': { transform: 'scale(1)' }, }); + const antNoWrapperZoomBadgeOut = new Keyframes('antNoWrapperZoomBadgeOut', { '0%': { transform: 'scale(1)' }, '100%': { transform: 'scale(0)', opacity: 0 }, }); + const antBadgeLoadingCircle = new Keyframes('antBadgeLoadingCircle', { '0%': { transformOrigin: '50%' }, '100%': { @@ -56,22 +139,23 @@ const antBadgeLoadingCircle = new Keyframes('antBadgeLoadingCircle', { }, }); -const genSharedBadgeStyle: GenerateStyle = (token: BadgeToken): CSSObject => { +const genSharedBadgeStyle: GenerateStyle = token => { const { componentCls, iconCls, antCls, - badgeFontHeight, badgeShadowSize, - badgeHeightSm, - motionDurationSlow, - badgeStatusSize, + textFontSize, + textFontSizeSM, + statusSize, + dotSize, + textFontWeight, + indicatorHeight, + indicatorHeightSM, marginXS, - badgeRibbonOffset, + calc, } = token; const numberPrefixCls = `${antCls}-scroll-number`; - const ribbonPrefixCls = `${antCls}-ribbon`; - const ribbonWrapperPrefixCls = `${antCls}-ribbon-wrapper`; const colorPreset = genPresetColor(token, (colorKey, { darkColor }) => ({ [`&${componentCls} ${componentCls}-color-${colorKey}`]: { @@ -79,13 +163,9 @@ const genSharedBadgeStyle: GenerateStyle = (token: BadgeToken): CSSO [`&:not(${componentCls}-count)`]: { color: darkColor, }, - }, - })); - - const statusRibbonPreset = genPresetColor(token, (colorKey, { darkColor }) => ({ - [`&${ribbonPrefixCls}-color-${colorKey}`]: { - background: darkColor, - color: darkColor, + 'a:hover &': { + background: darkColor, + }, }, })); @@ -98,18 +178,20 @@ const genSharedBadgeStyle: GenerateStyle = (token: BadgeToken): CSSO lineHeight: 1, [`${componentCls}-count`]: { - zIndex: token.badgeZIndex, - minWidth: token.badgeHeight, - height: token.badgeHeight, + display: 'inline-flex', + justifyContent: 'center', + zIndex: token.indicatorZIndex, + minWidth: indicatorHeight, + height: indicatorHeight, color: token.badgeTextColor, - fontWeight: token.badgeFontWeight, - fontSize: token.badgeFontSize, - lineHeight: `${token.badgeHeight}px`, + fontWeight: textFontWeight, + fontSize: textFontSize, + lineHeight: unit(indicatorHeight), whiteSpace: 'nowrap', textAlign: 'center', background: token.badgeColor, - borderRadius: token.badgeHeight / 2, - boxShadow: `0 0 0 ${badgeShadowSize}px ${token.badgeShadowColor}`, + borderRadius: calc(indicatorHeight).div(2).equal(), + boxShadow: `0 0 0 ${unit(badgeShadowSize)} ${token.badgeShadowColor}`, transition: `background ${token.motionDurationMid}`, a: { @@ -124,28 +206,29 @@ const genSharedBadgeStyle: GenerateStyle = (token: BadgeToken): CSSO }, }, [`${componentCls}-count-sm`]: { - minWidth: badgeHeightSm, - height: badgeHeightSm, - fontSize: token.badgeFontSizeSm, - lineHeight: `${badgeHeightSm}px`, - borderRadius: badgeHeightSm / 2, + minWidth: indicatorHeightSM, + height: indicatorHeightSM, + fontSize: textFontSizeSM, + lineHeight: unit(indicatorHeightSM), + borderRadius: calc(indicatorHeightSM).div(2).equal(), }, [`${componentCls}-multiple-words`]: { - padding: `0 ${token.paddingXS}px`, + padding: `0 ${unit(token.paddingXS)}`, + + bdi: { + unicodeBidi: 'plaintext', + }, }, [`${componentCls}-dot`]: { - zIndex: token.badgeZIndex, - width: token.badgeDotSize, - minWidth: token.badgeDotSize, - height: token.badgeDotSize, + zIndex: token.indicatorZIndex, + width: dotSize, + minWidth: dotSize, + height: dotSize, background: token.badgeColor, borderRadius: '100%', - boxShadow: `0 0 0 ${badgeShadowSize}px ${token.badgeShadowColor}`, - }, - [`${componentCls}-dot${numberPrefixCls}`]: { - transition: `background ${motionDurationSlow}`, + boxShadow: `0 0 0 ${unit(badgeShadowSize)} ${token.badgeShadowColor}`, }, [`${componentCls}-count, ${componentCls}-dot, ${numberPrefixCls}-custom-component`]: { position: 'absolute', @@ -168,8 +251,8 @@ const genSharedBadgeStyle: GenerateStyle = (token: BadgeToken): CSSO position: 'relative', top: -1, // Magic number, but seems better experience display: 'inline-block', - width: badgeStatusSize, - height: badgeStatusSize, + width: statusSize, + height: statusSize, verticalAlign: 'middle', borderRadius: '50%', }, @@ -179,8 +262,9 @@ const genSharedBadgeStyle: GenerateStyle = (token: BadgeToken): CSSO }, [`${componentCls}-status-processing`]: { overflow: 'visible', - color: token.colorPrimary, - backgroundColor: token.colorPrimary, + color: token.colorInfo, + backgroundColor: token.colorInfo, + borderColor: 'currentcolor', '&::after': { position: 'absolute', @@ -254,118 +338,56 @@ const genSharedBadgeStyle: GenerateStyle = (token: BadgeToken): CSSO transformOrigin: '50% 50%', }, }, - [`${numberPrefixCls}`]: { + [numberPrefixCls]: { overflow: 'hidden', + transition: `all ${token.motionDurationMid} ${token.motionEaseOutBack}`, [`${numberPrefixCls}-only`]: { position: 'relative', display: 'inline-block', - height: token.badgeHeight, + height: indicatorHeight, transition: `all ${token.motionDurationSlow} ${token.motionEaseOutBack}`, WebkitTransformStyle: 'preserve-3d', WebkitBackfaceVisibility: 'hidden', [`> p${numberPrefixCls}-only-unit`]: { - height: token.badgeHeight, + height: indicatorHeight, margin: 0, WebkitTransformStyle: 'preserve-3d', WebkitBackfaceVisibility: 'hidden', }, }, - [`${numberPrefixCls}-symbol`]: { verticalAlign: 'top' }, + [`${numberPrefixCls}-symbol`]: { + verticalAlign: 'top', + }, }, // ====================== RTL ======================= '&-rtl': { direction: 'rtl', - [`${componentCls}-count, ${componentCls}-dot, ${numberPrefixCls}-custom-component`]: { transform: 'translate(-50%, -50%)', }, }, }, - [`${ribbonWrapperPrefixCls}`]: { position: 'relative' }, - [`${ribbonPrefixCls}`]: { - ...resetComponent(token), - position: 'absolute', - top: marginXS, - padding: `0 ${token.paddingXS}px`, - color: token.colorPrimary, - lineHeight: `${badgeFontHeight}px`, - whiteSpace: 'nowrap', - backgroundColor: token.colorPrimary, - borderRadius: token.borderRadiusSM, - [`${ribbonPrefixCls}-text`]: { color: token.colorTextLightSolid }, - [`${ribbonPrefixCls}-corner`]: { - position: 'absolute', - top: '100%', - width: badgeRibbonOffset, - height: badgeRibbonOffset, - color: 'currentcolor', - border: `${badgeRibbonOffset / 2}px solid`, - transform: token.badgeRibbonCornerTransform, - transformOrigin: 'top', - filter: token.badgeRibbonCornerFilter, - }, - ...statusRibbonPreset, - [`&${ribbonPrefixCls}-placement-end`]: { - insetInlineEnd: -badgeRibbonOffset, - borderEndEndRadius: 0, - [`${ribbonPrefixCls}-corner`]: { - insetInlineEnd: 0, - borderInlineEndColor: 'transparent', - borderBlockEndColor: 'transparent', - }, - }, - [`&${ribbonPrefixCls}-placement-start`]: { - insetInlineStart: -badgeRibbonOffset, - borderEndStartRadius: 0, - [`${ribbonPrefixCls}-corner`]: { - insetInlineStart: 0, - borderBlockEndColor: 'transparent', - borderInlineStartColor: 'transparent', - }, - }, - - // ====================== RTL ======================= - '&-rtl': { - direction: 'rtl', - }, - }, }; }; // ============================== Export ============================== -export default genComponentStyleHook('Badge', token => { - const { fontSize, lineHeight, fontSizeSM, lineWidth, marginXS, colorBorderBg } = token; +export const prepareToken: (token: Parameters>[0]) => BadgeToken = token => { + const { fontHeight, lineWidth, marginXS, colorBorderBg } = token; - const badgeFontHeight = Math.round(fontSize * lineHeight); + const badgeFontHeight = fontHeight; const badgeShadowSize = lineWidth; - const badgeZIndex = 'auto'; - const badgeHeight = badgeFontHeight - 2 * badgeShadowSize; - const badgeTextColor = token.colorBgContainer; - const badgeFontWeight = 'normal'; - const badgeFontSize = fontSizeSM; + const badgeTextColor = token.colorTextLightSolid; const badgeColor = token.colorError; const badgeColorHover = token.colorErrorHover; - const badgeHeightSm = fontSize; - const badgeDotSize = fontSizeSM / 2; - const badgeFontSizeSm = fontSizeSM; - const badgeStatusSize = fontSizeSM / 2; const badgeToken = mergeToken(token, { badgeFontHeight, badgeShadowSize, - badgeZIndex, - badgeHeight, badgeTextColor, - badgeFontWeight, - badgeFontSize, badgeColor, badgeColorHover, badgeShadowColor: colorBorderBg, - badgeHeightSm, - badgeDotSize, - badgeFontSizeSm, - badgeStatusSize, badgeProcessingDuration: '1.2s', badgeRibbonOffset: marginXS, @@ -374,5 +396,28 @@ export default genComponentStyleHook('Badge', token => { badgeRibbonCornerFilter: `brightness(75%)`, }); - return [genSharedBadgeStyle(badgeToken)]; -}); + return badgeToken; +}; + +export const prepareComponentToken: GetDefaultToken<'badge'> = token => { + const { fontSize, lineHeight, fontSizeSM, lineWidth } = token; + return { + indicatorZIndex: 'auto', + indicatorHeight: Math.round(fontSize * lineHeight) - 2 * lineWidth, + indicatorHeightSM: fontSize, + dotSize: fontSizeSM / 2, + textFontSize: fontSizeSM, + textFontSizeSM: fontSizeSM, + textFontWeight: 'normal', + statusSize: fontSizeSM / 2, + }; +}; + +export default genStyleHooks( + 'Badge', + token => { + const badgeToken = prepareToken(token); + return genSharedBadgeStyle(badgeToken); + }, + prepareComponentToken, +); diff --git a/components/badge/style/ribbon.ts b/components/badge/style/ribbon.ts new file mode 100644 index 0000000000..91698fb71b --- /dev/null +++ b/components/badge/style/ribbon.ts @@ -0,0 +1,86 @@ +import { unit } from '../../_util/cssinjs'; + +import { prepareComponentToken, prepareToken } from '.'; +import type { BadgeToken } from '.'; +import { resetComponent } from '../../style'; +import type { GenerateStyle } from '../../theme/internal'; +import { genPresetColor, genStyleHooks } from '../../theme/internal'; + +// ============================== Ribbon ============================== +const genRibbonStyle: GenerateStyle = token => { + const { antCls, badgeFontHeight, marginXS, badgeRibbonOffset, calc } = token; + const ribbonPrefixCls = `${antCls}-ribbon`; + const ribbonWrapperPrefixCls = `${antCls}-ribbon-wrapper`; + + const statusRibbonPreset = genPresetColor(token, (colorKey, { darkColor }) => ({ + [`&${ribbonPrefixCls}-color-${colorKey}`]: { + background: darkColor, + color: darkColor, + }, + })); + + return { + [ribbonWrapperPrefixCls]: { + position: 'relative', + }, + [ribbonPrefixCls]: { + ...resetComponent(token), + position: 'absolute', + top: marginXS, + padding: `0 ${unit(token.paddingXS)}`, + color: token.colorPrimary, + lineHeight: unit(badgeFontHeight), + whiteSpace: 'nowrap', + backgroundColor: token.colorPrimary, + borderRadius: token.borderRadiusSM, + [`${ribbonPrefixCls}-text`]: { + color: token.badgeTextColor, + }, + [`${ribbonPrefixCls}-corner`]: { + position: 'absolute', + top: '100%', + width: badgeRibbonOffset, + height: badgeRibbonOffset, + color: 'currentcolor', + border: `${unit(calc(badgeRibbonOffset).div(2).equal())} solid`, + transform: token.badgeRibbonCornerTransform, + transformOrigin: 'top', + filter: token.badgeRibbonCornerFilter, + }, + ...statusRibbonPreset, + [`&${ribbonPrefixCls}-placement-end`]: { + insetInlineEnd: calc(badgeRibbonOffset).mul(-1).equal(), + borderEndEndRadius: 0, + [`${ribbonPrefixCls}-corner`]: { + insetInlineEnd: 0, + borderInlineEndColor: 'transparent', + borderBlockEndColor: 'transparent', + }, + }, + [`&${ribbonPrefixCls}-placement-start`]: { + insetInlineStart: calc(badgeRibbonOffset).mul(-1).equal(), + borderEndStartRadius: 0, + [`${ribbonPrefixCls}-corner`]: { + insetInlineStart: 0, + borderBlockEndColor: 'transparent', + borderInlineStartColor: 'transparent', + }, + }, + + // ====================== RTL ======================= + '&-rtl': { + direction: 'rtl', + }, + }, + }; +}; + +// ============================== Export ============================== +export default genStyleHooks( + ['Badge', 'Ribbon'], + token => { + const badgeToken = prepareToken(token); + return genRibbonStyle(badgeToken); + }, + prepareComponentToken, +); diff --git a/components/breadcrumb/Breadcrumb.tsx b/components/breadcrumb/Breadcrumb.tsx index ad9e24b0de..845d4da539 100644 --- a/components/breadcrumb/Breadcrumb.tsx +++ b/components/breadcrumb/Breadcrumb.tsx @@ -8,6 +8,7 @@ import BreadcrumbItem from './BreadcrumbItem'; import Menu from '../menu'; import useConfigInject from '../config-provider/hooks/useConfigInject'; import useStyle from './style'; +import useCSSVarCls from '../config-provider/hooks/useCssVarCls'; import type { CustomSlotsType, VueNode } from '../_util/type'; export interface Route { @@ -65,7 +66,8 @@ export default defineComponent({ }>, setup(props, { slots, attrs }) { const { prefixCls, direction } = useConfigInject('breadcrumb', props); - const [wrapSSR, hashId] = useStyle(prefixCls); + const rootCls = useCSSVarCls(prefixCls); + const [wrapSSR, hashId, cssVarCls] = useStyle(prefixCls, rootCls); const getPath = (path: string, params: unknown) => { path = (path || '').replace(/^\//, ''); Object.keys(params).forEach(key => { @@ -158,6 +160,8 @@ export default defineComponent({ [prefixCls.value]: true, [`${prefixCls.value}-rtl`]: direction.value === 'rtl', [`${attrs.class}`]: !!attrs.class, + [rootCls.value]: true, + [cssVarCls.value]: true, [hashId.value]: true, }; diff --git a/components/breadcrumb/style/index.ts b/components/breadcrumb/style/index.ts index 9d3b24d9cf..88eb6d3a04 100644 --- a/components/breadcrumb/style/index.ts +++ b/components/breadcrumb/style/index.ts @@ -1,30 +1,61 @@ +import { unit } from '../../_util/cssinjs'; import type { CSSObject } from '../../_util/cssinjs'; -import type { FullToken, GenerateStyle } from '../../theme/internal'; -import { genComponentStyleHook, mergeToken } from '../../theme/internal'; -import { genFocusStyle, resetComponent } from '../../style'; -interface BreadcrumbToken extends FullToken<'breadcrumb'> { - breadcrumbBaseColor: string; - breadcrumbFontSize: number; - breadcrumbIconFontSize: number; - breadcrumbLinkColor: string; - breadcrumbLinkColorHover: string; - breadcrumbLastItemColor: string; - breadcrumbSeparatorMargin: number; - breadcrumbSeparatorColor: string; +import { genFocusStyle, resetComponent } from '../../style'; +import type { FullToken, GenerateStyle, GetDefaultToken } from '../../theme/internal'; +import { genStyleHooks, mergeToken } from '../../theme/internal'; + +export interface ComponentToken { + /** + * @desc 面包屑项文字颜色 + * @descEN Text color of Breadcrumb item + */ + itemColor: string; + /** + * @desc 图标大小 + * @descEN Icon size + */ + iconFontSize: number; + /** + * @desc 链接文字颜色 + * @descEN Text color of link + */ + linkColor: string; + /** + * @desc 链接文字悬浮颜色 + * @descEN Color of hovered link + */ + linkHoverColor: string; + /** + * @desc 最后一项文字颜色 + * @descEN Text color of the last item + */ + lastItemColor: string; + /** + * @desc 分隔符外间距 + * @descEN Margin of separator + */ + separatorMargin: number; + /** + * @desc 分隔符颜色 + * @descEN Color of separator + */ + separatorColor: string; } +interface BreadcrumbToken extends FullToken<'breadcrumb'> {} + const genBreadcrumbStyle: GenerateStyle = token => { - const { componentCls, iconCls } = token; + const { componentCls, iconCls, calc } = token; return { [componentCls]: { ...resetComponent(token), - color: token.breadcrumbBaseColor, - fontSize: token.breadcrumbFontSize, + color: token.itemColor, + fontSize: token.fontSize, [iconCls]: { - fontSize: token.breadcrumbIconFontSize, + fontSize: token.iconFontSize, }, ol: { @@ -36,33 +67,29 @@ const genBreadcrumbStyle: GenerateStyle = token => { }, a: { - color: token.breadcrumbLinkColor, + color: token.linkColor, transition: `color ${token.motionDurationMid}`, - padding: `0 ${token.paddingXXS}px`, + padding: `0 ${unit(token.paddingXXS)}`, borderRadius: token.borderRadiusSM, - height: token.lineHeight * token.fontSize, + height: token.fontHeight, display: 'inline-block', - marginInline: -token.marginXXS, + marginInline: calc(token.marginXXS).mul(-1).equal(), '&:hover': { - color: token.breadcrumbLinkColorHover, + color: token.linkHoverColor, backgroundColor: token.colorBgTextHover, }, ...genFocusStyle(token), }, - [`li:last-child`]: { - color: token.breadcrumbLastItemColor, - - [`&> ${componentCls}-separator`]: { - display: 'none', - }, + 'li:last-child': { + color: token.lastItemColor, }, [`${componentCls}-separator`]: { - marginInline: token.breadcrumbSeparatorMargin, - color: token.breadcrumbSeparatorColor, + marginInline: token.separatorMargin, + color: token.separatorColor, }, [`${componentCls}-link`]: { @@ -76,10 +103,10 @@ const genBreadcrumbStyle: GenerateStyle = token => { [`${componentCls}-overlay-link`]: { borderRadius: token.borderRadiusSM, - height: token.lineHeight * token.fontSize, + height: token.fontHeight, display: 'inline-block', - padding: `0 ${token.paddingXXS}px`, - marginInline: -token.marginXXS, + padding: `0 ${unit(token.paddingXXS)}`, + marginInline: calc(token.marginXXS).mul(-1).equal(), [`> ${iconCls}`]: { marginInlineStart: token.marginXXS, @@ -87,11 +114,11 @@ const genBreadcrumbStyle: GenerateStyle = token => { }, '&:hover': { - color: token.breadcrumbLinkColorHover, + color: token.linkHoverColor, backgroundColor: token.colorBgTextHover, a: { - color: token.breadcrumbLinkColorHover, + color: token.linkHoverColor, }, }, @@ -110,18 +137,22 @@ const genBreadcrumbStyle: GenerateStyle = token => { }; }; -// ============================== Export ============================== -export default genComponentStyleHook('Breadcrumb', token => { - const BreadcrumbToken = mergeToken(token, { - breadcrumbBaseColor: token.colorTextDescription, - breadcrumbFontSize: token.fontSize, - breadcrumbIconFontSize: token.fontSize, - breadcrumbLinkColor: token.colorTextDescription, - breadcrumbLinkColorHover: token.colorText, - breadcrumbLastItemColor: token.colorText, - breadcrumbSeparatorMargin: token.marginXS, - breadcrumbSeparatorColor: token.colorTextDescription, - }); - - return [genBreadcrumbStyle(BreadcrumbToken)]; +export const prepareComponentToken: GetDefaultToken<'breadcrumb'> = token => ({ + itemColor: token.colorTextDescription, + lastItemColor: token.colorText, + iconFontSize: token.fontSize, + linkColor: token.colorTextDescription, + linkHoverColor: token.colorText, + separatorColor: token.colorTextDescription, + separatorMargin: token.marginXS, }); + +// ============================== Export ============================== +export default genStyleHooks( + 'Breadcrumb', + token => { + const breadcrumbToken = mergeToken(token, {}); + return genBreadcrumbStyle(breadcrumbToken); + }, + prepareComponentToken, +); 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/card/Card.tsx b/components/card/Card.tsx index a5f83c8496..c05bdc97f7 100644 --- a/components/card/Card.tsx +++ b/components/card/Card.tsx @@ -7,6 +7,7 @@ import type { SizeType } from '../config-provider'; import isPlainObject from 'lodash-es/isPlainObject'; import useConfigInject from '../config-provider/hooks/useConfigInject'; import devWarning from '../vc-util/devWarning'; +import useCSSVarCls from '../config-provider/hooks/useCssVarCls'; import useStyle from './style'; import Skeleton from '../skeleton'; import type { CustomSlotsType } from '../_util/type'; @@ -67,7 +68,8 @@ const Card = defineComponent({ }>, setup(props, { slots, attrs }) { const { prefixCls, direction, size } = useConfigInject('card', props); - const [wrapSSR, hashId] = useStyle(prefixCls); + const rootCls = useCSSVarCls(prefixCls); + const [wrapSSR, hashId, cssVarCls] = useStyle(prefixCls, rootCls); const getAction = (actions: VNodeTypes[]) => { const actionList = actions.map((action, index) => (isVNode(action) && !isEmptyElement(action)) || !isVNode(action) ? ( @@ -112,6 +114,8 @@ const Card = defineComponent({ const pre = prefixCls.value; const classString = { [`${pre}`]: true, + [cssVarCls.value]: true, + [rootCls.value]: true, [hashId.value]: true, [`${pre}-loading`]: loading, [`${pre}-bordered`]: bordered, diff --git a/components/card/style/index.tsx b/components/card/style/index.tsx index 447bcd148f..15a418b9ea 100644 --- a/components/card/style/index.tsx +++ b/components/card/style/index.tsx @@ -1,19 +1,98 @@ +import { unit } from '../../_util/cssinjs'; import type { CSSObject } from '../../_util/cssinjs'; -import type { FullToken, GenerateStyle } from '../../theme/internal'; -import { genComponentStyleHook, mergeToken } from '../../theme/internal'; -import { clearFix, resetComponent, textEllipsis } from '../../style'; -export interface ComponentToken {} +import { clearFix, resetComponent, textEllipsis } from '../../style'; +import type { FullToken, GenerateStyle, GetDefaultToken } from '../../theme/internal'; +import { genStyleHooks, mergeToken } from '../../theme/internal'; + +export interface ComponentToken { + /** + * @desc 卡片头部背景色 + * @descEN Background color of card header + */ + headerBg: string; + /** + * @desc 卡片头部文字大小 + * @descEN Font size of card header + */ + headerFontSize: number | string; + /** + * @desc 小号卡片头部文字大小 + * @descEN Font size of small card header + */ + headerFontSizeSM: number | string; + /** + * @desc 卡片头部高度 + * @descEN Height of card header + */ + headerHeight: number | string; + /** + * @desc 小号卡片头部高度 + * @descEN Height of small card header + */ + headerHeightSM: number | string; + /** + * @desc 小号卡片内边距 + * @descEN Padding of small card body + */ + bodyPaddingSM: number; + /** + * @desc 小号卡片头部内边距 + * @descEN Padding of small card head + */ + headerPaddingSM: number; + /** + * @desc 卡片内边距 + * @descEN Padding of card body + */ + bodyPadding: number; + /** + * @desc 卡片头部内边距 + * @descEN Padding of card head + */ + headerPadding: number; + /** + * @desc 操作区背景色 + * @descEN Background color of card actions + */ + actionsBg: string; + /** + * @desc 操作区每一项的外间距 + * @descEN Margin of each item in card actions + */ + actionsLiMargin: string; + /** + * @desc 内置标签页组件下间距 + * @descEN Margin bottom of tabs component + */ + tabsMarginBottom: number; + /** + * @desc 额外区文字颜色 + * @descEN Text color of extra area + */ + extraColor: string; +} interface CardToken extends FullToken<'card'> { - cardHeadHeight: number; - cardHeadHeightSM: number; + /** + * @desc 卡片阴影 + * @descEN Shadow of card + */ cardShadow: string; + /** + * @desc 卡片头部内边距 + * @descEN Padding of card header + */ cardHeadPadding: number; - cardPaddingSM: number; + /** + * @desc 卡片基础内边距 + * @descEN Padding of base card + */ cardPaddingBase: number; - cardHeadTabsMarginBottom: number; - cardActionsLiMargin: string; + /** + * @desc 卡片操作区图标大小 + * @descEN Size of card actions icon + */ cardActionsIconSize: number; } @@ -21,21 +100,21 @@ interface CardToken extends FullToken<'card'> { // ============================== Head ============================== const genCardHeadStyle: GenerateStyle = (token): CSSObject => { - const { antCls, componentCls, cardHeadHeight, cardPaddingBase, cardHeadTabsMarginBottom } = token; + const { antCls, componentCls, headerHeight, headerPadding, tabsMarginBottom } = token; return { display: 'flex', justifyContent: 'center', flexDirection: 'column', - minHeight: cardHeadHeight, + minHeight: headerHeight, marginBottom: -1, // Fix card grid overflow bug: https://gw.alipayobjects.com/zos/rmsportal/XonYxBikwpgbqIQBeuhk.png - padding: `0 ${cardPaddingBase}px`, + padding: `0 ${unit(headerPadding)}`, color: token.colorTextHeading, fontWeight: token.fontWeightStrong, - fontSize: token.fontSizeLG, - background: 'transparent', - borderBottom: `${token.lineWidth}px ${token.lineType} ${token.colorBorderSecondary}`, - borderRadius: `${token.borderRadiusLG}px ${token.borderRadiusLG}px 0 0`, + fontSize: token.headerFontSize, + background: token.headerBg, + borderBottom: `${unit(token.lineWidth)} ${token.lineType} ${token.colorBorderSecondary}`, + borderRadius: `${unit(token.borderRadiusLG)} ${unit(token.borderRadiusLG)} 0 0`, ...clearFix(), @@ -62,13 +141,13 @@ const genCardHeadStyle: GenerateStyle = (token): CSSObject => { [`${antCls}-tabs-top`]: { clear: 'both', - marginBottom: cardHeadTabsMarginBottom, + marginBottom: tabsMarginBottom, color: token.colorText, fontWeight: 'normal', fontSize: token.fontSize, '&-bar': { - borderBottom: `${token.lineWidth}px ${token.lineType} ${token.colorBorderSecondary}`, + borderBottom: `${unit(token.lineWidth)} ${token.lineType} ${token.colorBorderSecondary}`, }, }, }; @@ -83,11 +162,11 @@ const genCardGridStyle: GenerateStyle = (token): CSSObject => { border: 0, borderRadius: 0, boxShadow: ` - ${lineWidth}px 0 0 0 ${colorBorderSecondary}, - 0 ${lineWidth}px 0 0 ${colorBorderSecondary}, - ${lineWidth}px ${lineWidth}px 0 0 ${colorBorderSecondary}, - ${lineWidth}px 0 0 0 ${colorBorderSecondary} inset, - 0 ${lineWidth}px 0 0 ${colorBorderSecondary} inset; + ${unit(lineWidth)} 0 0 0 ${colorBorderSecondary}, + 0 ${unit(lineWidth)} 0 0 ${colorBorderSecondary}, + ${unit(lineWidth)} ${unit(lineWidth)} 0 0 ${colorBorderSecondary}, + ${unit(lineWidth)} 0 0 0 ${colorBorderSecondary} inset, + 0 ${unit(lineWidth)} 0 0 ${colorBorderSecondary} inset; `, transition: `all ${token.motionDurationMid}`, @@ -101,27 +180,33 @@ const genCardGridStyle: GenerateStyle = (token): CSSObject => { // ============================== Actions ============================== const genCardActionsStyle: GenerateStyle = (token): CSSObject => { - const { componentCls, iconCls, cardActionsLiMargin, cardActionsIconSize, colorBorderSecondary } = - token; + const { + componentCls, + iconCls, + actionsLiMargin, + cardActionsIconSize, + colorBorderSecondary, + actionsBg, + } = token; return { margin: 0, padding: 0, listStyle: 'none', - background: token.colorBgContainer, - borderTop: `${token.lineWidth}px ${token.lineType} ${colorBorderSecondary}`, + background: actionsBg, + borderTop: `${unit(token.lineWidth)} ${token.lineType} ${colorBorderSecondary}`, display: 'flex', - borderRadius: `0 0 ${token.borderRadiusLG}px ${token.borderRadiusLG}px `, + borderRadius: `0 0 ${unit(token.borderRadiusLG)} ${unit(token.borderRadiusLG)}`, ...clearFix(), '&> li': { - margin: cardActionsLiMargin, + margin: actionsLiMargin, color: token.colorTextDescription, textAlign: 'center', '> span': { position: 'relative', display: 'block', - minWidth: token.cardActionsIconSize * 2, + minWidth: token.calc(token.cardActionsIconSize).mul(2).equal(), fontSize: token.fontSize, lineHeight: token.lineHeight, cursor: 'pointer', @@ -134,8 +219,8 @@ const genCardActionsStyle: GenerateStyle = (token): CSSObject => { [`a:not(${componentCls}-btn),> ${iconCls}`]: { display: 'inline-block', width: '100%', - color: token.colorTextDescription, - lineHeight: `${token.fontSize * token.lineHeight}px`, + color: token.colorIcon, + lineHeight: unit(token.fontHeight), transition: `color ${token.motionDurationMid}`, '&:hover': { @@ -145,12 +230,12 @@ const genCardActionsStyle: GenerateStyle = (token): CSSObject => { [`> ${iconCls}`]: { fontSize: cardActionsIconSize, - lineHeight: `${cardActionsIconSize * token.lineHeight}px`, + lineHeight: unit(token.calc(cardActionsIconSize).mul(token.lineHeight).equal()), }, }, '&:not(:last-child)': { - borderInlineEnd: `${token.lineWidth}px ${token.lineType} ${colorBorderSecondary}`, + borderInlineEnd: `${unit(token.lineWidth)} ${token.lineType} ${colorBorderSecondary}`, }, }, }; @@ -158,7 +243,7 @@ const genCardActionsStyle: GenerateStyle = (token): CSSObject => { // ============================== Meta ============================== const genCardMetaStyle: GenerateStyle = (token): CSSObject => ({ - margin: `-${token.marginXXS}px 0`, + margin: `${unit(token.calc(token.marginXXS).mul(-1).equal())} 0`, display: 'flex', ...clearFix(), @@ -189,11 +274,11 @@ const genCardMetaStyle: GenerateStyle = (token): CSSObject => ({ // ============================== Inner ============================== const genCardTypeInnerStyle: GenerateStyle = (token): CSSObject => { - const { componentCls, cardPaddingBase, colorFillAlter } = token; + const { componentCls, colorFillAlter, headerPadding, bodyPadding } = token; return { [`${componentCls}-head`]: { - padding: `0 ${cardPaddingBase}px`, + padding: `0 ${unit(headerPadding)}`, background: colorFillAlter, '&-title': { @@ -202,7 +287,7 @@ const genCardTypeInnerStyle: GenerateStyle = (token): CSSObject => { }, [`${componentCls}-body`]: { - padding: `${token.padding}px ${cardPaddingBase}px`, + padding: `${unit(token.padding)} ${unit(bodyPadding)}`, }, }; }; @@ -227,8 +312,9 @@ const genCardStyle: GenerateStyle = (token): CSSObject => { cardShadow, cardHeadPadding, colorBorderSecondary, - boxShadow, - cardPaddingBase, + boxShadowTertiary, + bodyPadding, + extraColor, } = token; return { @@ -240,7 +326,7 @@ const genCardStyle: GenerateStyle = (token): CSSObject => { borderRadius: token.borderRadiusLG, [`&:not(${componentCls}-bordered)`]: { - boxShadow, + boxShadow: boxShadowTertiary, }, [`${componentCls}-head`]: genCardHeadStyle(token), @@ -248,14 +334,14 @@ const genCardStyle: GenerateStyle = (token): CSSObject => { [`${componentCls}-extra`]: { // https://stackoverflow.com/a/22429853/3040605 marginInlineStart: 'auto', - color: '', + color: extraColor, fontWeight: 'normal', fontSize: token.fontSize, }, [`${componentCls}-body`]: { - padding: cardPaddingBase, - borderRadius: ` 0 0 ${token.borderRadiusLG}px ${token.borderRadiusLG}px`, + padding: bodyPadding, + borderRadius: `0 0 ${unit(token.borderRadiusLG)} ${unit(token.borderRadiusLG)}`, ...clearFix(), }, @@ -265,10 +351,7 @@ const genCardStyle: GenerateStyle = (token): CSSObject => { '> *': { display: 'block', width: '100%', - }, - - img: { - borderRadius: `${token.borderRadiusLG}px ${token.borderRadiusLG}px 0 0`, + borderRadius: `${unit(token.borderRadiusLG)} ${unit(token.borderRadiusLG)} 0 0`, }, }, @@ -278,7 +361,7 @@ const genCardStyle: GenerateStyle = (token): CSSObject => { }, [`${componentCls}-bordered`]: { - border: `${token.lineWidth}px ${token.lineType} ${colorBorderSecondary}`, + border: `${unit(token.lineWidth)} ${token.lineType} ${colorBorderSecondary}`, [`${componentCls}-cover`]: { marginTop: -1, @@ -298,20 +381,22 @@ const genCardStyle: GenerateStyle = (token): CSSObject => { }, [`${componentCls}-contain-grid`]: { + borderRadius: `${unit(token.borderRadiusLG)} ${unit(token.borderRadiusLG)} 0 0 `, [`${componentCls}-body`]: { display: 'flex', flexWrap: 'wrap', }, [`&:not(${componentCls}-loading) ${componentCls}-body`]: { - marginBlockStart: -token.lineWidth, - marginInlineStart: -token.lineWidth, + marginBlockStart: token.calc(token.lineWidth).mul(-1).equal(), + marginInlineStart: token.calc(token.lineWidth).mul(-1).equal(), padding: 0, }, }, [`${componentCls}-contain-tabs`]: { - [`> ${componentCls}-head`]: { + [`> div${componentCls}-head`]: { + minHeight: 0, [`${componentCls}-head-title, ${componentCls}-extra`]: { paddingTop: cardHeadPadding, }, @@ -330,14 +415,14 @@ const genCardStyle: GenerateStyle = (token): CSSObject => { // ============================== Size ============================== const genCardSizeStyle: GenerateStyle = (token): CSSObject => { - const { componentCls, cardPaddingSM, cardHeadHeightSM } = token; + const { componentCls, bodyPaddingSM, headerPaddingSM, headerHeightSM, headerFontSizeSM } = token; return { [`${componentCls}-small`]: { [`> ${componentCls}-head`]: { - minHeight: cardHeadHeightSM, - padding: `0 ${cardPaddingSM}px`, - fontSize: token.fontSize, + minHeight: headerHeightSM, + padding: `0 ${unit(headerPaddingSM)}`, + fontSize: headerFontSizeSM, [`> ${componentCls}-head-wrapper`]: { [`> ${componentCls}-extra`]: { @@ -347,13 +432,12 @@ const genCardSizeStyle: GenerateStyle = (token): CSSObject => { }, [`> ${componentCls}-body`]: { - padding: cardPaddingSM, + padding: bodyPaddingSM, }, }, [`${componentCls}-small${componentCls}-contain-tabs`]: { [`> ${componentCls}-head`]: { [`${componentCls}-head-title, ${componentCls}-extra`]: { - minHeight: cardHeadHeightSM, paddingTop: 0, display: 'flex', alignItems: 'center', @@ -363,25 +447,40 @@ const genCardSizeStyle: GenerateStyle = (token): CSSObject => { }; }; -// ============================== Export ============================== -export default genComponentStyleHook('Card', token => { - const cardToken = mergeToken(token, { - cardShadow: token.boxShadowCard, - cardHeadHeight: token.fontSizeLG * token.lineHeightLG + token.padding * 2, - cardHeadHeightSM: token.fontSize * token.lineHeight + token.paddingXS * 2, - cardHeadPadding: token.padding, - cardPaddingBase: token.paddingLG, - cardHeadTabsMarginBottom: -token.padding - token.lineWidth, - cardActionsLiMargin: `${token.paddingSM}px 0`, - cardActionsIconSize: token.fontSize, - cardPaddingSM: 12, // Fixed padding. - }); - - return [ - // Style - genCardStyle(cardToken), - - // Size - genCardSizeStyle(cardToken), - ]; +export const prepareComponentToken: GetDefaultToken<'card'> = token => ({ + headerBg: 'transparent', + headerFontSize: token.fontSizeLG, + headerFontSizeSM: token.fontSize, + headerHeight: token.fontSizeLG * token.lineHeightLG + token.padding * 2, + headerHeightSM: token.fontSize * token.lineHeight + token.paddingXS * 2, + actionsBg: token.colorBgContainer, + actionsLiMargin: `${token.paddingSM}px 0`, + tabsMarginBottom: -token.padding - token.lineWidth, + extraColor: token.colorText, + bodyPaddingSM: 12, // Fixed padding. + headerPaddingSM: 12, + bodyPadding: token.bodyPadding ?? token.paddingLG, + headerPadding: token.headerPadding ?? token.paddingLG, }); + +// ============================== Export ============================== +export default genStyleHooks( + 'Card', + token => { + const cardToken = mergeToken(token, { + cardShadow: token.boxShadowCard, + cardHeadPadding: token.padding, + cardPaddingBase: token.paddingLG, + cardActionsIconSize: token.fontSize, + }); + + return [ + // Style + genCardStyle(cardToken), + + // Size + genCardSizeStyle(cardToken), + ]; + }, + prepareComponentToken, +); diff --git a/components/collapse/Collapse.tsx b/components/collapse/Collapse.tsx index be6377ee6b..f2d5828127 100644 --- a/components/collapse/Collapse.tsx +++ b/components/collapse/Collapse.tsx @@ -19,6 +19,7 @@ import collapseMotion from '../_util/collapseMotion'; import type { CustomSlotsType } from '../_util/type'; // CSSINJS +import useCSSVarCls from '../config-provider/hooks/useCssVarCls'; import useStyle from './style'; type Key = number | string; @@ -63,7 +64,8 @@ export default defineComponent({ const { prefixCls, direction, rootPrefixCls } = useConfigInject('collapse', props); // style - const [wrapSSR, hashId] = useStyle(prefixCls); + const rootCls = useCSSVarCls(prefixCls); + const [wrapSSR, hashId, cssVarCls] = useStyle(prefixCls, rootCls); const iconPosition = computed(() => { const { expandIconPosition } = props; @@ -186,6 +188,8 @@ export default defineComponent({ [`${prefixCls.value}-ghost`]: !!ghost, [attrs.class as string]: !!attrs.class, }, + cssVarCls.value, + rootCls.value, hashId.value, ); return wrapSSR( diff --git a/components/collapse/style/index.tsx b/components/collapse/style/index.tsx index 3d9b433026..08000c06a4 100644 --- a/components/collapse/style/index.tsx +++ b/components/collapse/style/index.tsx @@ -1,26 +1,63 @@ -import { genCollapseMotion } from '../../style/motion'; -import type { FullToken, GenerateStyle } from '../../theme/internal'; -import { genComponentStyleHook, mergeToken } from '../../theme/internal'; -import { resetComponent, resetIcon } from '../../style'; +import type { CSSProperties } from 'vue'; +import { unit } from '../../_util/cssinjs'; -export interface ComponentToken {} +import { genFocusStyle, resetComponent, resetIcon } from '../../style'; +import { genCollapseMotion } from '../../style/motion'; +import type { FullToken, GenerateStyle, GetDefaultToken } from '../../theme/internal'; +import { genStyleHooks, mergeToken } from '../../theme/internal'; + +/** Component only token. Which will handle additional calculation of alias token */ +export interface ComponentToken { + // Component token here + /** + * @desc 折叠面板头部内边距 + * @descEN Padding of header + */ + headerPadding: CSSProperties['padding']; + /** + * @desc 折叠面板头部背景 + * @descEN Background of header + */ + headerBg: string; + /** + * @desc 折叠面板内容内边距 + * @descEN Padding of content + */ + contentPadding: CSSProperties['padding']; + /** + * @desc 折叠面板内容背景 + * @descEN Background of content + */ + contentBg: string; +} type CollapseToken = FullToken<'collapse'> & { - collapseContentBg: string; - collapseHeaderBg: string; - collapseHeaderPadding: string; + /** + * @desc 小号折叠面板头部内边距 + * @descEN Padding of small header + */ + collapseHeaderPaddingSM: string; + /** + * @desc 大号折叠面板头部内边距 + * @descEN Padding of large header + */ + collapseHeaderPaddingLG: string; + /** + * @desc 折叠面板边框圆角 + * @descEN Border radius of collapse panel + */ collapsePanelBorderRadius: number; - collapseContentPaddingHorizontal: number; }; export const genBaseStyle: GenerateStyle = token => { const { componentCls, - collapseContentBg, + contentBg, padding, - collapseContentPaddingHorizontal, - collapseHeaderBg, - collapseHeaderPadding, + headerBg, + headerPadding, + collapseHeaderPaddingSM, + collapseHeaderPaddingLG, collapsePanelBorderRadius, lineWidth, @@ -29,35 +66,52 @@ export const genBaseStyle: GenerateStyle = token => { colorText, colorTextHeading, colorTextDisabled, - fontSize, + fontSizeLG, lineHeight, + lineHeightLG, marginSM, paddingSM, + paddingLG, + paddingXS, motionDurationSlow, fontSizeIcon, + contentPadding, + fontHeight, + fontHeightLG, } = token; - const borderBase = `${lineWidth}px ${lineType} ${colorBorder}`; + const borderBase = `${unit(lineWidth)} ${lineType} ${colorBorder}`; return { [componentCls]: { ...resetComponent(token), - backgroundColor: collapseHeaderBg, + backgroundColor: headerBg, border: borderBase, - borderBottom: 0, - borderRadius: `${collapsePanelBorderRadius}px`, + borderRadius: collapsePanelBorderRadius, - [`&-rtl`]: { + '&-rtl': { direction: 'rtl', }, [`&> ${componentCls}-item`]: { borderBottom: borderBase, - [`&:last-child`]: { + '&:first-child': { + [` + &, + &> ${componentCls}-header`]: { + borderRadius: `${unit(collapsePanelBorderRadius)} ${unit( + collapsePanelBorderRadius, + )} 0 0`, + }, + }, + + '&:last-child': { [` &, &> ${componentCls}-header`]: { - borderRadius: `0 0 ${collapsePanelBorderRadius}px ${collapsePanelBorderRadius}px`, + borderRadius: `0 0 ${unit(collapsePanelBorderRadius)} ${unit( + collapsePanelBorderRadius, + )}`, }, }, @@ -66,23 +120,20 @@ export const genBaseStyle: GenerateStyle = token => { display: 'flex', flexWrap: 'nowrap', alignItems: 'flex-start', - padding: collapseHeaderPadding, + padding: headerPadding, color: colorTextHeading, lineHeight, cursor: 'pointer', transition: `all ${motionDurationSlow}, visibility 0s`, + ...genFocusStyle(token), [`> ${componentCls}-header-text`]: { flex: 'auto', }, - '&:focus': { - outline: 'none', - }, - //>>>>> Arrow [`${componentCls}-expand-icon`]: { - height: fontSize * lineHeight, + height: fontHeight, display: 'flex', alignItems: 'center', paddingInlineEnd: marginSM, @@ -91,7 +142,9 @@ export const genBaseStyle: GenerateStyle = token => { [`${componentCls}-arrow`]: { ...resetIcon(), fontSize: fontSizeIcon, - + // when `transform: rotate()` is applied to icon's root element + transition: `transform ${motionDurationSlow}`, + // when `transform: rotate()` is applied to icon's child element svg: { transition: `transform ${motionDurationSlow}`, }, @@ -103,50 +156,79 @@ export const genBaseStyle: GenerateStyle = token => { }, }, - [`${componentCls}-header-collapsible-only`]: { + [`${componentCls}-collapsible-header`]: { cursor: 'default', - [`${componentCls}-header-text`]: { flex: 'none', cursor: 'pointer', }, - [`${componentCls}-expand-icon`]: { - cursor: 'pointer', - }, }, - [`${componentCls}-icon-collapsible-only`]: { - cursor: 'default', + [`${componentCls}-collapsible-icon`]: { + cursor: 'unset', [`${componentCls}-expand-icon`]: { cursor: 'pointer', }, }, - - [`&${componentCls}-no-arrow`]: { - [`> ${componentCls}-header`]: { - paddingInlineStart: paddingSM, - }, - }, }, [`${componentCls}-content`]: { color: colorText, - backgroundColor: collapseContentBg, + backgroundColor: contentBg, borderTop: borderBase, [`&> ${componentCls}-content-box`]: { - padding: `${padding}px ${collapseContentPaddingHorizontal}px`, + padding: contentPadding, }, - [`&-hidden`]: { + '&-hidden': { display: 'none', }, }, + '&-small': { + [`> ${componentCls}-item`]: { + [`> ${componentCls}-header`]: { + padding: collapseHeaderPaddingSM, + paddingInlineStart: paddingXS, + + [`> ${componentCls}-expand-icon`]: { + // Arrow offset + marginInlineStart: token.calc(paddingSM).sub(paddingXS).equal(), + }, + }, + [`> ${componentCls}-content> ${componentCls}-content-box`]: { + padding: paddingSM, + }, + }, + }, + + '&-large': { + [`> ${componentCls}-item`]: { + fontSize: fontSizeLG, + lineHeight: lineHeightLG, + [`> ${componentCls}-header`]: { + padding: collapseHeaderPaddingLG, + paddingInlineStart: padding, + + [`> ${componentCls}-expand-icon`]: { + height: fontHeightLG, + // Arrow offset + marginInlineStart: token.calc(paddingLG).sub(padding).equal(), + }, + }, + [`> ${componentCls}-content> ${componentCls}-content-box`]: { + padding: paddingLG, + }, + }, + }, + [`${componentCls}-item:last-child`]: { + borderBottom: 0, + [`> ${componentCls}-content`]: { - borderRadius: `0 0 ${collapsePanelBorderRadius}px ${collapsePanelBorderRadius}px`, + borderRadius: `0 0 ${unit(collapsePanelBorderRadius)} ${unit(collapsePanelBorderRadius)}`, }, }, @@ -179,7 +261,7 @@ export const genBaseStyle: GenerateStyle = token => { const genArrowStyle: GenerateStyle = token => { const { componentCls } = token; - const fixedSelector = `> ${componentCls}-item> ${componentCls}-header ${componentCls}-arrow svg`; + const fixedSelector = `> ${componentCls}-item> ${componentCls}-header ${componentCls}-arrow`; return { [`${componentCls}-rtl`]: { @@ -193,7 +275,7 @@ const genArrowStyle: GenerateStyle = token => { const genBorderlessStyle: GenerateStyle = token => { const { componentCls, - collapseHeaderBg, + headerBg, paddingXXS, colorBorder, @@ -201,7 +283,7 @@ const genBorderlessStyle: GenerateStyle = token => { return { [`${componentCls}-borderless`]: { - backgroundColor: collapseHeaderBg, + backgroundColor: headerBg, border: 0, [`> ${componentCls}-item`]: { @@ -252,20 +334,29 @@ const genGhostStyle: GenerateStyle = token => { }; }; -export default genComponentStyleHook('Collapse', token => { - const collapseToken = mergeToken(token, { - collapseContentBg: token.colorBgContainer, - collapseHeaderBg: token.colorFillAlter, - collapseHeaderPadding: `${token.paddingSM}px ${token.padding}px`, - collapsePanelBorderRadius: token.borderRadiusLG, - collapseContentPaddingHorizontal: 16, // Fixed value - }); - - return [ - genBaseStyle(collapseToken), - genBorderlessStyle(collapseToken), - genGhostStyle(collapseToken), - genArrowStyle(collapseToken), - genCollapseMotion(collapseToken), - ]; +export const prepareComponentToken: GetDefaultToken<'collapse'> = token => ({ + headerPadding: `${token.paddingSM}px ${token.padding}px`, + headerBg: token.colorFillAlter, + contentPadding: `${token.padding}px 16px`, // Fixed Value + contentBg: token.colorBgContainer, }); + +export default genStyleHooks( + 'Collapse', + token => { + const collapseToken = mergeToken(token, { + collapseHeaderPaddingSM: `${unit(token.paddingXS)} ${unit(token.paddingSM)}`, + collapseHeaderPaddingLG: `${unit(token.padding)} ${unit(token.paddingLG)}`, + collapsePanelBorderRadius: token.borderRadiusLG, + }); + + return [ + genBaseStyle(collapseToken), + genBorderlessStyle(collapseToken), + genGhostStyle(collapseToken), + genArrowStyle(collapseToken), + genCollapseMotion(collapseToken), + ]; + }, + prepareComponentToken, +); 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..ca99988a7c 100644 --- a/components/config-provider/hooks/useTheme.ts +++ b/components/config-provider/hooks/useTheme.ts @@ -2,13 +2,27 @@ 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 +40,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/divider/index.tsx b/components/divider/index.tsx index 2eedacac72..d246c08ffa 100644 --- a/components/divider/index.tsx +++ b/components/divider/index.tsx @@ -3,6 +3,7 @@ import type { ExtractPropTypes, PropType } from 'vue'; import { computed, defineComponent } from 'vue'; import { withInstall } from '../_util/type'; import useConfigInject from '../config-provider/hooks/useConfigInject'; +import useCSSVarCls from '../config-provider/hooks/useCssVarCls'; import useStyle from './style'; export const dividerProps = () => ({ @@ -34,7 +35,8 @@ const Divider = defineComponent({ props: dividerProps(), setup(props, { slots, attrs }) { const { prefixCls: prefixClsRef, direction } = useConfigInject('divider', props); - const [wrapSSR, hashId] = useStyle(prefixClsRef); + const rootCls = useCSSVarCls(prefixClsRef); + const [wrapSSR, hashId, cssVarCls] = useStyle(prefixClsRef, rootCls); const hasCustomMarginLeft = computed( () => props.orientation === 'left' && props.orientationMargin != null, ); @@ -46,6 +48,8 @@ const Divider = defineComponent({ const prefixCls = prefixClsRef.value; return { [prefixCls]: true, + [cssVarCls.value]: true, + [rootCls.value]: true, [hashId.value]: !!hashId.value, [`${prefixCls}-${type}`]: true, [`${prefixCls}-dashed`]: !!dashed, diff --git a/components/divider/style/index.ts b/components/divider/style/index.ts index 6dc979b047..c96b840ee7 100644 --- a/components/divider/style/index.ts +++ b/components/divider/style/index.ts @@ -1,27 +1,68 @@ +import type { CSSProperties } from 'vue'; 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 { resetComponent } from '../../style'; +import type { FullToken, GenerateStyle, GetDefaultToken } from '../../theme/internal'; +import { genStyleHooks, mergeToken } from '../../theme/internal'; /** Component only token. Which will handle additional calculation of alias token */ export interface ComponentToken { - sizePaddingEdgeHorizontal: number; + /** + * @desc 文本横向内间距 + * @descEN Horizontal padding of text + */ + textPaddingInline: CSSProperties['paddingInline']; + /** + * @desc 文本与边缘距离,取值 0 〜 1 + * @descEN Distance between text and edge, which should be a number between 0 and 1. + */ + orientationMargin: number; + /** + * @desc 纵向分割线的横向外间距 + * @descEN Horizontal margin of vertical Divider + */ + verticalMarginInline: CSSProperties['marginInline']; } +/** + * @desc Divider 组件的 Token + * @descEN Token for Divider component + */ interface DividerToken extends FullToken<'divider'> { - dividerVerticalGutterMargin: number; - dividerHorizontalWithTextGutterMargin: number; - dividerHorizontalGutterMargin: number; + /** + * @desc 尺寸边距 + * @descEN Size padding edge horizontal + */ + sizePaddingEdgeHorizontal: number | string; + /** + * @desc 带文本的水平分割线的外边距 + * @descEN Horizontal margin of divider with text + */ + dividerHorizontalWithTextGutterMargin: number | string; + /** + * @desc 水平分割线的外边距 + * @descEN Horizontal margin of divider + */ + dividerHorizontalGutterMargin: number | string; } // ============================== Shared ============================== const genSharedDividerStyle: GenerateStyle = (token): CSSObject => { - const { componentCls, sizePaddingEdgeHorizontal, colorSplit, lineWidth } = token; + const { + componentCls, + sizePaddingEdgeHorizontal, + colorSplit, + lineWidth, + textPaddingInline, + orientationMargin, + verticalMarginInline, + } = token; return { [componentCls]: { ...resetComponent(token), - borderBlockStart: `${lineWidth}px solid ${colorSplit}`, + borderBlockStart: `${unit(lineWidth)} solid ${colorSplit}`, // vertical '&-vertical': { @@ -29,10 +70,11 @@ const genSharedDividerStyle: GenerateStyle = (token): CSSObject => top: '-0.06em', display: 'inline-block', height: '0.9em', - margin: `0 ${token.dividerVerticalGutterMargin}px`, + marginInline: verticalMarginInline, + marginBlock: 0, verticalAlign: 'middle', borderTop: 0, - borderInlineStart: `${lineWidth}px solid ${colorSplit}`, + borderInlineStart: `${unit(lineWidth)} solid ${colorSplit}`, }, '&-horizontal': { @@ -40,13 +82,13 @@ const genSharedDividerStyle: GenerateStyle = (token): CSSObject => clear: 'both', width: '100%', minWidth: '100%', // Fix https://github.com/ant-design/ant-design/issues/10914 - margin: `${token.dividerHorizontalGutterMargin}px 0`, + margin: `${unit(token.dividerHorizontalGutterMargin)} 0`, }, [`&-horizontal${componentCls}-with-text`]: { display: 'flex', alignItems: 'center', - margin: `${token.dividerHorizontalWithTextGutterMargin}px 0`, + margin: `${unit(token.dividerHorizontalWithTextGutterMargin)} 0`, color: token.colorTextHeading, fontWeight: 500, fontSize: token.fontSizeLG, @@ -57,7 +99,7 @@ const genSharedDividerStyle: GenerateStyle = (token): CSSObject => '&::before, &::after': { position: 'relative', width: '50%', - borderBlockStart: `${lineWidth}px solid transparent`, + borderBlockStart: `${unit(lineWidth)} solid transparent`, // Chrome not accept `inherit` in `border-top` borderBlockStartColor: 'inherit', borderBlockEnd: 0, @@ -68,34 +110,33 @@ const genSharedDividerStyle: GenerateStyle = (token): CSSObject => [`&-horizontal${componentCls}-with-text-left`]: { '&::before': { - width: '5%', + width: `calc(${orientationMargin} * 100%)`, }, - '&::after': { - width: '95%', + width: `calc(100% - ${orientationMargin} * 100%)`, }, }, [`&-horizontal${componentCls}-with-text-right`]: { '&::before': { - width: '95%', + width: `calc(100% - ${orientationMargin} * 100%)`, }, - '&::after': { - width: '5%', + width: `calc(${orientationMargin} * 100%)`, }, }, [`${componentCls}-inner-text`]: { display: 'inline-block', - padding: '0 1em', + paddingBlock: 0, + paddingInline: textPaddingInline, }, '&-dashed': { background: 'none', borderColor: colorSplit, borderStyle: 'dashed', - borderWidth: `${lineWidth}px 0 0`, + borderWidth: `${unit(lineWidth)} 0 0`, }, [`&-horizontal${componentCls}-with-text${componentCls}-dashed`]: { @@ -111,6 +152,26 @@ const genSharedDividerStyle: GenerateStyle = (token): CSSObject => borderBlockEnd: 0, }, + '&-dotted': { + background: 'none', + borderColor: colorSplit, + borderStyle: 'dotted', + borderWidth: `${unit(lineWidth)} 0 0`, + }, + + [`&-horizontal${componentCls}-with-text${componentCls}-dotted`]: { + '&::before, &::after': { + borderStyle: 'dotted none none', + }, + }, + + [`&-vertical${componentCls}-dotted`]: { + borderInlineStartWidth: lineWidth, + borderInlineEnd: 0, + borderBlockStart: 0, + borderBlockEnd: 0, + }, + [`&-plain${componentCls}-with-text`]: { color: token.colorText, fontWeight: 'normal', @@ -150,18 +211,27 @@ const genSharedDividerStyle: GenerateStyle = (token): CSSObject => }; }; +export const prepareComponentToken: GetDefaultToken<'divider'> = token => ({ + textPaddingInline: '1em', + orientationMargin: 0.05, + verticalMarginInline: token.marginXS, +}); + // ============================== Export ============================== -export default genComponentStyleHook( +export default genStyleHooks( 'Divider', token => { const dividerToken = mergeToken(token, { - dividerVerticalGutterMargin: token.marginXS, dividerHorizontalWithTextGutterMargin: token.margin, dividerHorizontalGutterMargin: token.marginLG, + sizePaddingEdgeHorizontal: 0, }); return [genSharedDividerStyle(dividerToken)]; }, + prepareComponentToken, { - sizePaddingEdgeHorizontal: 0, + unitless: { + orientationMargin: true, + }, }, ); 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/list/demo/simple.vue b/components/list/demo/simple.vue index 9ab30d009b..017ad7707b 100644 --- a/components/list/demo/simple.vue +++ b/components/list/demo/simple.vue @@ -33,7 +33,7 @@ Customizing the header and footer of list by setting `header` and `footer` prope
Footer
-

Default Size

+

Default Size