diff --git a/components/_util/colors.ts b/components/_util/colors.ts index bb3c2ca2c..7d87054c2 100644 --- a/components/_util/colors.ts +++ b/components/_util/colors.ts @@ -1,23 +1,34 @@ -import type { ElementOf } from './type'; -import { tuple } from './type'; +import type { PresetColorKey } from '../theme/interface'; +import { PresetColors } from '../theme/interface'; -export const PresetStatusColorTypes = tuple('success', 'processing', 'error', 'default', 'warning'); +type InverseColor = `${PresetColorKey}-inverse`; +const inverseColors = PresetColors.map(color => `${color}-inverse`); -export const PresetColorTypes = tuple( - 'pink', - 'red', - 'yellow', - 'orange', - 'cyan', - 'green', - 'blue', - 'purple', - 'geekblue', - 'magenta', - 'volcano', - 'gold', - 'lime', -); +export const PresetStatusColorTypes = [ + 'success', + 'processing', + 'error', + 'default', + 'warning', +] as const; -export type PresetColorType = ElementOf; -export type PresetStatusColorType = ElementOf; +export type PresetColorType = PresetColorKey | InverseColor; + +export type PresetStatusColorType = typeof PresetStatusColorTypes[number]; + +/** + * determine if the color keyword belongs to the `Ant Design` {@link PresetColors}. + * @param color color to be judged + * @param includeInverse whether to include reversed colors + */ +export function isPresetColor(color?: any, includeInverse = true) { + if (includeInverse) { + return [...inverseColors, ...PresetColors].includes(color); + } + + return PresetColors.includes(color); +} + +export function isPresetStatusColor(color?: any): color is PresetStatusColorType { + return PresetStatusColorTypes.includes(color); +} diff --git a/components/_util/type.ts b/components/_util/type.ts index b2e14f33a..1a18abf5a 100644 --- a/components/_util/type.ts +++ b/components/_util/type.ts @@ -14,7 +14,7 @@ export type ElementOf = T extends (infer E)[] ? E : T extends readonly (infer /** * https://github.com/Microsoft/TypeScript/issues/29729 */ -export type LiteralUnion = T | (U & {}); +export type LiteralUnion = T | (string & {}); export type Data = Record; diff --git a/components/badge/Badge.tsx b/components/badge/Badge.tsx index 0464f7b6c..74f9e638a 100644 --- a/components/badge/Badge.tsx +++ b/components/badge/Badge.tsx @@ -7,10 +7,13 @@ import { getTransitionProps, Transition } from '../_util/transition'; import type { ExtractPropTypes, CSSProperties, PropType } from 'vue'; import { defineComponent, computed, ref, watch } from 'vue'; import Ribbon from './Ribbon'; -import { isPresetColor } from './utils'; import useConfigInject from '../_util/hooks/useConfigInject'; import isNumeric from '../_util/isNumeric'; +import useStyle from './style'; +import type { PresetColorKey } from '../theme/interface'; +import type { LiteralUnion } from '../_util/type'; import type { PresetStatusColorType } from '../_util/colors'; +import { isPresetColor } from '../_util/colors'; export const badgeProps = () => ({ /** Number to show in badge */ @@ -24,7 +27,7 @@ export const badgeProps = () => ({ scrollNumberPrefixCls: String, status: { type: String as PropType }, size: { type: String as PropType<'default' | 'small'>, default: 'default' }, - color: String, + color: String as PropType>, text: PropTypes.any, offset: Array as unknown as PropType<[number | string, number | string]>, numberStyle: { type: Object as PropType, default: undefined as CSSProperties }, @@ -42,6 +45,7 @@ export default defineComponent({ slots: ['text', 'count'], setup(props, { slots, attrs }) { const { prefixCls, direction } = useConfigInject('badge', props); + const [wrapSSR, hashId] = useStyle(prefixCls); // ================================ Misc ================================ const numberedDisplayCount = computed(() => { @@ -52,15 +56,16 @@ export default defineComponent({ ) as string | number | null; }); - const hasStatus = computed( - () => - (props.status !== null && props.status !== undefined) || - (props.color !== null && props.color !== undefined), - ); - const isZero = computed( () => numberedDisplayCount.value === '0' || numberedDisplayCount.value === 0, ); + const ignoreCount = computed(() => props.count === null || (isZero.value && !props.showZero)); + const hasStatus = computed( + () => + ((props.status !== null && props.status !== undefined) || + (props.color !== null && props.color !== undefined)) && + ignoreCount.value, + ); const showAsDot = computed(() => props.dot && !isZero.value); @@ -92,17 +97,18 @@ export default defineComponent({ }, { immediate: true }, ); - + // InternalColor + const isInternalColor = computed(() => isPresetColor(props.color, false)); // Shared styles const statusCls = computed(() => ({ [`${prefixCls.value}-status-dot`]: hasStatus.value, [`${prefixCls.value}-status-${props.status}`]: !!props.status, - [`${prefixCls.value}-status-${props.color}`]: isPresetColor(props.color), + [`${prefixCls.value}-status-${props.color}`]: isInternalColor.value, })); const statusStyle = computed(() => { - if (props.color && !isPresetColor(props.color)) { - return { background: props.color }; + if (props.color && !isInternalColor.value) { + return { background: props.color, color: props.color }; } else { return {}; } @@ -115,7 +121,7 @@ export default defineComponent({ [`${prefixCls.value}-multiple-words`]: !isDotRef.value && displayCount.value && displayCount.value.toString().length > 1, [`${prefixCls.value}-status-${props.status}`]: !!props.status, - [`${prefixCls.value}-status-${props.color}`]: isPresetColor(props.color), + [`${prefixCls.value}-status-${props.color}`]: isInternalColor.value, })); return () => { @@ -179,18 +185,19 @@ export default defineComponent({ [`${pre}-rtl`]: direction.value === 'rtl', }, attrs.class, + hashId.value, ); // if (!children && hasStatus.value) { const statusTextColor = mergedStyle.color; - return ( + return wrapSSR( {text} - + , ); } @@ -198,12 +205,12 @@ export default defineComponent({ appear: false, }); let scrollNumberStyle: CSSProperties = { ...mergedStyle, ...(props.numberStyle as object) }; - if (color && !isPresetColor(color)) { + if (color && !isInternalColor.value) { scrollNumberStyle = scrollNumberStyle || {}; scrollNumberStyle.background = color; } - return ( + return wrapSSR( {children} @@ -221,7 +228,7 @@ export default defineComponent({ {statusTextNode} - + , ); }; }, diff --git a/components/badge/Ribbon.tsx b/components/badge/Ribbon.tsx index 0d6c9cc30..103d315dc 100644 --- a/components/badge/Ribbon.tsx +++ b/components/badge/Ribbon.tsx @@ -1,6 +1,7 @@ import type { LiteralUnion } from '../_util/type'; import type { PresetColorType } from '../_util/colors'; -import { isPresetColor } from './utils'; +import useStyle from './style'; +import { isPresetColor } from '../_util/colors'; import type { CSSProperties, PropType, ExtractPropTypes } from 'vue'; import { defineComponent, computed } from 'vue'; import PropTypes from '../_util/vue-types'; @@ -8,7 +9,7 @@ import useConfigInject from '../_util/hooks/useConfigInject'; export const ribbonProps = () => ({ prefix: String, - color: { type: String as PropType> }, + color: { type: String as PropType> }, text: PropTypes.any, placement: { type: String as PropType<'start' | 'end'>, default: 'end' }, }); @@ -23,7 +24,8 @@ export default defineComponent({ slots: ['text'], setup(props, { attrs, slots }) { const { prefixCls, direction } = useConfigInject('ribbon', props); - const colorInPreset = computed(() => isPresetColor(props.color)); + const [wrapSSR, hashId] = useStyle(prefixCls); + const colorInPreset = computed(() => isPresetColor(props.color, false)); const ribbonCls = computed(() => [ prefixCls.value, `${prefixCls.value}-placement-${props.placement}`, @@ -40,17 +42,17 @@ export default defineComponent({ colorStyle.background = props.color; cornerColorStyle.color = props.color; } - return ( -
+ return wrapSSR( +
{slots.default?.()}
{props.text || slots.text?.()}
-
+
, ); }; }, diff --git a/components/badge/style/index.less b/components/badge/style/index.less deleted file mode 100644 index 8059ae581..000000000 --- a/components/badge/style/index.less +++ /dev/null @@ -1,281 +0,0 @@ -@import '../../style/themes/index'; -@import '../../style/mixins/index'; - -@badge-prefix-cls: ~'@{ant-prefix}-badge'; -@number-prefix-cls: ~'@{ant-prefix}-scroll-number'; - -.@{badge-prefix-cls} { - .reset-component(); - - position: relative; - display: inline-block; - line-height: 1; - - &-count { - z-index: @zindex-badge; - min-width: @badge-height; - height: @badge-height; - padding: 0 6px; - color: @badge-text-color; - font-weight: @badge-font-weight; - font-size: @badge-font-size; - line-height: @badge-height; - white-space: nowrap; - text-align: center; - background: @badge-color; - border-radius: (@badge-height / 2); - box-shadow: 0 0 0 1px @shadow-color-inverse; - - a, - a:hover { - color: @badge-text-color; - } - } - - &-count-sm { - min-width: @badge-height-sm; - height: @badge-height-sm; - padding: 0; - font-size: @badge-font-size-sm; - line-height: @badge-height-sm; - border-radius: (@badge-height-sm / 2); - } - - &-multiple-words { - padding: 0 8px; - } - - &-dot { - z-index: @zindex-badge; - width: @badge-dot-size; - min-width: @badge-dot-size; - height: @badge-dot-size; - background: @highlight-color; - border-radius: 100%; - box-shadow: 0 0 0 1px @shadow-color-inverse; - } - - // Tricky way to resolve https://github.com/ant-design/ant-design/issues/30088 - &-dot.@{number-prefix-cls} { - transition: background 1.5s; - } - - &-count, - &-dot, - .@{number-prefix-cls}-custom-component { - position: absolute; - top: 0; - right: 0; - transform: translate(50%, -50%); - transform-origin: 100% 0%; - - &.@{iconfont-css-prefix}-spin { - animation: antBadgeLoadingCircle 1s infinite linear; - } - } - - &-status { - line-height: inherit; - vertical-align: baseline; - - &-dot { - position: relative; - top: -1px; - display: inline-block; - width: @badge-status-size; - height: @badge-status-size; - vertical-align: middle; - border-radius: 50%; - } - - &-success { - background-color: @success-color; - } - - &-processing { - position: relative; - background-color: @processing-color; - - &::after { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - border: 1px solid @processing-color; - border-radius: 50%; - animation: antStatusProcessing 1.2s infinite ease-in-out; - content: ''; - } - } - - &-default { - background-color: @normal-color; - } - - &-error { - background-color: @error-color; - } - - &-warning { - background-color: @warning-color; - } - - // mixin to iterate over colors and create CSS class for each one - .make-color-classes(@i: length(@preset-colors)) when (@i > 0) { - .make-color-classes(@i - 1); - @color: extract(@preset-colors, @i); - @darkColor: '@{color}-6'; - &-@{color} { - background: @@darkColor; - } - } - .make-color-classes(); - - &-text { - margin-left: 8px; - color: @text-color; - font-size: @font-size-base; - } - } - - &-zoom-appear, - &-zoom-enter { - animation: antZoomBadgeIn @animation-duration-slow @ease-out-back; - animation-fill-mode: both; - } - - &-zoom-leave { - animation: antZoomBadgeOut @animation-duration-slow @ease-in-back; - animation-fill-mode: both; - } - - &-not-a-wrapper { - .@{badge-prefix-cls}-zoom-appear, - .@{badge-prefix-cls}-zoom-enter { - animation: antNoWrapperZoomBadgeIn @animation-duration-slow @ease-out-back; - } - - .@{badge-prefix-cls}-zoom-leave { - animation: antNoWrapperZoomBadgeOut @animation-duration-slow @ease-in-back; - } - - &:not(.@{badge-prefix-cls}-status) { - vertical-align: middle; - } - - .@{number-prefix-cls}-custom-component, - .@{badge-prefix-cls}-count { - transform: none; - } - - .@{number-prefix-cls}-custom-component, - .@{number-prefix-cls} { - position: relative; - top: auto; - display: block; - transform-origin: 50% 50%; - } - } -} - -@keyframes antStatusProcessing { - 0% { - transform: scale(0.8); - opacity: 0.5; - } - - 100% { - transform: scale(2.4); - opacity: 0; - } -} - -// Safari will blink with transform when inner element has absolute style. -.safari-fix-motion() { - /* stylelint-disable property-no-vendor-prefix */ - -webkit-transform-style: preserve-3d; - -webkit-backface-visibility: hidden; - /* stylelint-enable property-no-vendor-prefix */ -} - -.@{number-prefix-cls} { - overflow: hidden; - direction: ltr; - - &-only { - position: relative; - display: inline-block; - height: @badge-height; - transition: all @animation-duration-slow @ease-in-out; - .safari-fix-motion; - - > p.@{number-prefix-cls}-only-unit { - height: @badge-height; - margin: 0; - .safari-fix-motion; - } - } - - &-symbol { - vertical-align: top; - } -} - -@keyframes antZoomBadgeIn { - 0% { - transform: scale(0) translate(50%, -50%); - opacity: 0; - } - - 100% { - transform: scale(1) translate(50%, -50%); - } -} - -@keyframes antZoomBadgeOut { - 0% { - transform: scale(1) translate(50%, -50%); - } - - 100% { - transform: scale(0) translate(50%, -50%); - opacity: 0; - } -} - -@keyframes antNoWrapperZoomBadgeIn { - 0% { - transform: scale(0); - opacity: 0; - } - - 100% { - transform: scale(1); - } -} - -@keyframes antNoWrapperZoomBadgeOut { - 0% { - transform: scale(1); - } - - 100% { - transform: scale(0); - opacity: 0; - } -} - -@keyframes antBadgeLoadingCircle { - 0% { - transform-origin: 50%; - } - - 100% { - transform: translate(50%, -50%) rotate(360deg); - transform-origin: 50%; - } -} - -@import './ribbon'; -@import './rtl'; diff --git a/components/badge/style/index.ts b/components/badge/style/index.ts new file mode 100644 index 000000000..069b93a6d --- /dev/null +++ b/components/badge/style/index.ts @@ -0,0 +1,376 @@ +import type { CSSObject } from '../../_util/cssinjs'; +import { Keyframes } from '../../_util/cssinjs'; +import type { FullToken, GenerateStyle } from '../../theme/internal'; +import { genComponentStyleHook, mergeToken } from '../../theme/internal'; +import { genPresetColor, resetComponent } from '../../_style'; + +interface BadgeToken extends FullToken<'Badge'> { + badgeFontHeight: number; + badgeZIndex: number | string; + badgeHeight: number; + badgeHeightSm: number; + badgeTextColor: string; + badgeFontWeight: string; + badgeFontSize: number; + badgeColor: string; + badgeColorHover: string; + badgeDotSize: number; + badgeFontSizeSm: number; + badgeStatusSize: number; + badgeShadowSize: number; + badgeShadowColor: string; + badgeProcessingDuration: string; + badgeRibbonOffset: number; + badgeRibbonCornerTransform: string; + badgeRibbonCornerFilter: string; +} + +const antStatusProcessing = new Keyframes('antStatusProcessing', { + '0%': { transform: 'scale(0.8)', opacity: 0.5 }, + '100%': { transform: 'scale(2.4)', opacity: 0 }, +}); + +const antZoomBadgeIn = new Keyframes('antZoomBadgeIn', { + '0%': { transform: 'scale(0) translate(50%, -50%)', opacity: 0 }, + '100%': { transform: 'scale(1) translate(50%, -50%)' }, +}); + +const antZoomBadgeOut = new Keyframes('antZoomBadgeOut', { + '0%': { transform: 'scale(1) translate(50%, -50%)' }, + '100%': { transform: 'scale(0) translate(50%, -50%)', opacity: 0 }, +}); + +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%': { + transform: 'translate(50%, -50%) rotate(360deg)', + transformOrigin: '50%', + }, +}); + +const genSharedBadgeStyle: GenerateStyle = (token: BadgeToken): CSSObject => { + const { + componentCls, + iconCls, + antCls, + badgeFontHeight, + badgeShadowSize, + badgeHeightSm, + motionDurationSlow, + badgeStatusSize, + marginXS, + badgeRibbonOffset, + } = token; + const numberPrefixCls = `${antCls}-scroll-number`; + const ribbonPrefixCls = `${antCls}-ribbon`; + const ribbonWrapperPrefixCls = `${antCls}-ribbon-wrapper`; + + const statusPreset = genPresetColor(token, (colorKey, { darkColor }) => ({ + [`${componentCls}-status-${colorKey}`]: { + background: darkColor, + }, + })); + + const statusRibbonPreset = genPresetColor(token, (colorKey, { darkColor }) => ({ + [`&${ribbonPrefixCls}-color-${colorKey}`]: { + background: darkColor, + color: darkColor, + }, + })); + + return { + [componentCls]: { + ...resetComponent(token), + position: 'relative', + display: 'inline-block', + width: 'fit-content', + lineHeight: 1, + + [`${componentCls}-count`]: { + zIndex: token.badgeZIndex, + minWidth: token.badgeHeight, + height: token.badgeHeight, + color: token.badgeTextColor, + fontWeight: token.badgeFontWeight, + fontSize: token.badgeFontSize, + lineHeight: `${token.badgeHeight}px`, + whiteSpace: 'nowrap', + textAlign: 'center', + background: token.badgeColor, + borderRadius: token.badgeHeight / 2, + boxShadow: `0 0 0 ${badgeShadowSize}px ${token.badgeShadowColor}`, + transition: `background ${token.motionDurationMid}`, + + a: { + color: token.badgeTextColor, + }, + 'a:hover': { + color: token.badgeTextColor, + }, + + 'a:hover &': { + background: token.badgeColorHover, + }, + }, + [`${componentCls}-count-sm`]: { + minWidth: badgeHeightSm, + height: badgeHeightSm, + fontSize: token.badgeFontSizeSm, + lineHeight: `${badgeHeightSm}px`, + borderRadius: badgeHeightSm / 2, + }, + + [`${componentCls}-multiple-words`]: { + padding: `0 ${token.paddingXS}px`, + }, + + [`${componentCls}-dot`]: { + zIndex: token.badgeZIndex, + width: token.badgeDotSize, + minWidth: token.badgeDotSize, + height: token.badgeDotSize, + background: token.badgeColor, + borderRadius: '100%', + boxShadow: `0 0 0 ${badgeShadowSize}px ${token.badgeShadowColor}`, + }, + [`${componentCls}-dot${numberPrefixCls}`]: { + transition: `background ${motionDurationSlow}`, + }, + [`${componentCls}-count, ${componentCls}-dot, ${numberPrefixCls}-custom-component`]: { + position: 'absolute', + top: 0, + insetInlineEnd: 0, + transform: 'translate(50%, -50%)', + transformOrigin: '100% 0%', + [`${iconCls}-spin`]: { + animationName: antBadgeLoadingCircle, + animationDuration: token.motionDurationMid, + animationIterationCount: 'infinite', + animationTimingFunction: 'linear', + }, + }, + [`&${componentCls}-status`]: { + lineHeight: 'inherit', + verticalAlign: 'baseline', + + [`${componentCls}-status-dot`]: { + position: 'relative', + top: -1, // Magic number, but seems better experience + display: 'inline-block', + width: badgeStatusSize, + height: badgeStatusSize, + verticalAlign: 'middle', + borderRadius: '50%', + }, + + [`${componentCls}-status-success`]: { + backgroundColor: token.colorSuccess, + }, + [`${componentCls}-status-processing`]: { + position: 'relative', + color: token.colorPrimary, + backgroundColor: token.colorPrimary, + + '&::after': { + position: 'absolute', + top: 0, + insetInlineStart: 0, + width: '100%', + height: '100%', + borderWidth: badgeShadowSize, + borderStyle: 'solid', + borderColor: 'inherit', + borderRadius: '50%', + animationName: antStatusProcessing, + animationDuration: token.badgeProcessingDuration, + animationIterationCount: 'infinite', + animationTimingFunction: 'ease-in-out', + content: '""', + }, + }, + [`${componentCls}-status-default`]: { + backgroundColor: token.colorTextPlaceholder, + }, + + [`${componentCls}-status-error`]: { + backgroundColor: token.colorError, + }, + + [`${componentCls}-status-warning`]: { + backgroundColor: token.colorWarning, + }, + ...statusPreset, + [`${componentCls}-status-text`]: { + marginInlineStart: marginXS, + color: token.colorText, + fontSize: token.fontSize, + }, + }, + [`${componentCls}-zoom-appear, ${componentCls}-zoom-enter`]: { + animationName: antZoomBadgeIn, + animationDuration: token.motionDurationSlow, + animationTimingFunction: token.motionEaseOutBack, + animationFillMode: 'both', + }, + [`${componentCls}-zoom-leave`]: { + animationName: antZoomBadgeOut, + animationDuration: token.motionDurationSlow, + animationTimingFunction: token.motionEaseOutBack, + animationFillMode: 'both', + }, + [`&${componentCls}-not-a-wrapper`]: { + [`${componentCls}-zoom-appear, ${componentCls}-zoom-enter`]: { + animationName: antNoWrapperZoomBadgeIn, + animationDuration: token.motionDurationSlow, + animationTimingFunction: token.motionEaseOutBack, + }, + + [`${componentCls}-zoom-leave`]: { + animationName: antNoWrapperZoomBadgeOut, + animationDuration: token.motionDurationSlow, + animationTimingFunction: token.motionEaseOutBack, + }, + [`&:not(${componentCls}-status)`]: { + verticalAlign: 'middle', + }, + [`${numberPrefixCls}-custom-component, ${componentCls}-count`]: { + transform: 'none', + }, + [`${numberPrefixCls}-custom-component, ${numberPrefixCls}`]: { + position: 'relative', + top: 'auto', + display: 'block', + transformOrigin: '50% 50%', + }, + }, + [`${numberPrefixCls}`]: { + overflow: 'hidden', + [`${numberPrefixCls}-only`]: { + position: 'relative', + display: 'inline-block', + height: token.badgeHeight, + transition: `all ${token.motionDurationSlow} ${token.motionEaseOutBack}`, + WebkitTransformStyle: 'preserve-3d', + WebkitBackfaceVisibility: 'hidden', + [`> p${numberPrefixCls}-only-unit`]: { + height: token.badgeHeight, + margin: 0, + WebkitTransformStyle: 'preserve-3d', + WebkitBackfaceVisibility: 'hidden', + }, + }, + [`${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, + height: badgeFontHeight, + 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; + + const badgeFontHeight = Math.round(fontSize * lineHeight); + const badgeShadowSize = lineWidth; + const badgeZIndex = 'auto'; + const badgeHeight = badgeFontHeight - 2 * badgeShadowSize; + const badgeTextColor = token.colorBgContainer; + const badgeFontWeight = 'normal'; + const badgeFontSize = fontSizeSM; + 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, + + // Follow token just by Design. Not related with token + badgeRibbonCornerTransform: 'scaleY(0.75)', + badgeRibbonCornerFilter: `brightness(75%)`, + }); + + return [genSharedBadgeStyle(badgeToken)]; +}); diff --git a/components/badge/style/index.tsx b/components/badge/style/index.tsx deleted file mode 100644 index 3a3ab0de5..000000000 --- a/components/badge/style/index.tsx +++ /dev/null @@ -1,2 +0,0 @@ -import '../../style/index.less'; -import './index.less'; diff --git a/components/badge/style/ribbon.less b/components/badge/style/ribbon.less deleted file mode 100644 index 6a6e366bf..000000000 --- a/components/badge/style/ribbon.less +++ /dev/null @@ -1,81 +0,0 @@ -@import '../../style/themes/index'; -@import '../../style/mixins/index'; - -@ribbon-prefix-cls: ~'@{ant-prefix}-ribbon'; -@ribbon-wrapper-prefix-cls: ~'@{ant-prefix}-ribbon-wrapper'; - -.@{ribbon-wrapper-prefix-cls} { - position: relative; -} - -.@{ribbon-prefix-cls} { - .reset-component(); - - position: absolute; - top: 8px; - height: 22px; - padding: 0 8px; - color: @badge-text-color; - line-height: 22px; - white-space: nowrap; - background-color: @primary-color; - border-radius: @border-radius-sm; - - &-text { - color: @white; - } - - &-corner { - position: absolute; - top: 100%; - width: 8px; - height: 8px; - color: currentcolor; - border: 4px solid; - transform: scaleY(0.75); - transform-origin: top; - // If not support IE 11, use filter: brightness(75%) instead - &::after { - position: absolute; - top: -4px; - left: -4px; - width: inherit; - height: inherit; - color: rgba(0, 0, 0, 0.25); - border: inherit; - content: ''; - } - } - - // colors - // mixin to iterate over colors and create CSS class for each one - .make-color-classes(@i: length(@preset-colors)) when (@i > 0) { - .make-color-classes(@i - 1); - @color: extract(@preset-colors, @i); - @darkColor: '@{color}-6'; - &-color-@{color} { - color: @@darkColor; - background: @@darkColor; - } - } - .make-color-classes(); - - // placement - &.@{ribbon-prefix-cls}-placement-end { - right: -8px; - border-bottom-right-radius: 0; - .@{ribbon-prefix-cls}-corner { - right: 0; - border-color: currentcolor transparent transparent currentcolor; - } - } - - &.@{ribbon-prefix-cls}-placement-start { - left: -8px; - border-bottom-left-radius: 0; - .@{ribbon-prefix-cls}-corner { - left: 0; - border-color: currentcolor currentcolor transparent transparent; - } - } -} diff --git a/components/badge/style/rtl.less b/components/badge/style/rtl.less deleted file mode 100644 index 276a6ef6e..000000000 --- a/components/badge/style/rtl.less +++ /dev/null @@ -1,100 +0,0 @@ -.@{badge-prefix-cls} { - &-rtl { - direction: rtl; - } - - &:not(&-not-a-wrapper) &-count, - &:not(&-not-a-wrapper) &-dot, - &:not(&-not-a-wrapper) .@{number-prefix-cls}-custom-component { - .@{badge-prefix-cls}-rtl& { - right: auto; - left: 0; - direction: ltr; - transform: translate(-50%, -50%); - transform-origin: 0% 0%; - } - } - - &-rtl&:not(&-not-a-wrapper) .@{number-prefix-cls}-custom-component { - right: auto; - left: 0; - transform: translate(-50%, -50%); - transform-origin: 0% 0%; - } - - &-status { - &-text { - .@{badge-prefix-cls}-rtl & { - margin-right: 8px; - margin-left: 0; - } - } - } - - &:not(&-not-a-wrapper).@{badge-prefix-cls}-rtl { - .@{badge-prefix-cls}-zoom-appear, - .@{badge-prefix-cls}-zoom-enter { - animation-name: antZoomBadgeInRtl; - } - - .@{badge-prefix-cls}-zoom-leave { - animation-name: antZoomBadgeOutRtl; - } - } -} - -.@{ribbon-prefix-cls}-rtl { - direction: rtl; - &.@{ribbon-prefix-cls}-placement-end { - right: unset; - left: -8px; - border-bottom-right-radius: @border-radius-sm; - border-bottom-left-radius: 0; - .@{ribbon-prefix-cls}-corner { - right: unset; - left: 0; - border-color: currentcolor currentcolor transparent transparent; - - &::after { - border-color: currentcolor currentcolor transparent transparent; - } - } - } - &.@{ribbon-prefix-cls}-placement-start { - right: -8px; - left: unset; - border-bottom-right-radius: 0; - border-bottom-left-radius: @border-radius-sm; - .@{ribbon-prefix-cls}-corner { - right: 0; - left: unset; - border-color: currentcolor transparent transparent currentcolor; - - &::after { - border-color: currentcolor transparent transparent currentcolor; - } - } - } -} - -@keyframes antZoomBadgeInRtl { - 0% { - transform: scale(0) translate(-50%, -50%); - opacity: 0; - } - - 100% { - transform: scale(1) translate(-50%, -50%); - } -} - -@keyframes antZoomBadgeOutRtl { - 0% { - transform: scale(1) translate(-50%, -50%); - } - - 100% { - transform: scale(0) translate(-50%, -50%); - opacity: 0; - } -} diff --git a/components/badge/utils.ts b/components/badge/utils.ts deleted file mode 100644 index 21bebac2e..000000000 --- a/components/badge/utils.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { PresetColorTypes } from '../_util/colors'; - -export function isPresetColor(color?: string): boolean { - return (PresetColorTypes as any[]).indexOf(color) !== -1; -} diff --git a/components/style.ts b/components/style.ts index 232f4eb7d..611c5554f 100644 --- a/components/style.ts +++ b/components/style.ts @@ -7,7 +7,7 @@ import './tag/style'; import './rate/style'; import './pagination/style'; // import './avatar/style'; -import './badge/style'; +// import './badge/style'; import './tabs/style'; import './input/style'; import './tooltip/style'; diff --git a/components/tag/index.tsx b/components/tag/index.tsx index 7ad896a99..6021b2dc8 100644 --- a/components/tag/index.tsx +++ b/components/tag/index.tsx @@ -5,18 +5,15 @@ import PropTypes from '../_util/vue-types'; import CloseOutlined from '@ant-design/icons-vue/CloseOutlined'; import Wave from '../_util/wave'; import type { PresetColorType, PresetStatusColorType } from '../_util/colors'; -import { PresetColorTypes, PresetStatusColorTypes } from '../_util/colors'; +import { isPresetColor, isPresetStatusColor } from '../_util/colors'; import type { LiteralUnion } from '../_util/type'; import CheckableTag from './CheckableTag'; import useConfigInject from '../_util/hooks/useConfigInject'; -const PresetColorRegex = new RegExp(`^(${PresetColorTypes.join('|')})(-inverse)?$`); -const PresetStatusColorRegex = new RegExp(`^(${PresetStatusColorTypes.join('|')})$`); - export const tagProps = () => ({ prefixCls: String, color: { - type: String as PropType>, + type: String as PropType>, }, closable: { type: Boolean, default: false }, closeIcon: PropTypes.any, @@ -60,18 +57,22 @@ const Tag = defineComponent({ } }; - const isPresetColor = computed(() => { - const { color } = props; - if (!color) { - return false; - } - return PresetColorRegex.test(color) || PresetStatusColorRegex.test(color); - }); + // const isPresetColor = computed(() => { + // const { color } = props; + // if (!color) { + // return false; + // } + // return PresetColorRegex.test(color) || PresetStatusColorRegex.test(color); + // }); + + const isInternalColor = computed( + () => isPresetColor(props.color) || isPresetStatusColor(props.color), + ); const tagClassName = computed(() => classNames(prefixCls.value, { - [`${prefixCls.value}-${props.color}`]: isPresetColor.value, - [`${prefixCls.value}-has-color`]: props.color && !isPresetColor.value, + [`${prefixCls.value}-${props.color}`]: isInternalColor.value, + [`${prefixCls.value}-has-color`]: props.color && !isInternalColor.value, [`${prefixCls.value}-hidden`]: !visible.value, [`${prefixCls.value}-rtl`]: direction.value === 'rtl', }), @@ -99,7 +100,7 @@ const Tag = defineComponent({ }; const tagStyle = { - backgroundColor: color && !isPresetColor.value ? color : undefined, + backgroundColor: color && !isInternalColor.value ? color : undefined, }; const iconNode = icon || null; diff --git a/components/tooltip/Tooltip.tsx b/components/tooltip/Tooltip.tsx index dcddcc64e..477aaf154 100644 --- a/components/tooltip/Tooltip.tsx +++ b/components/tooltip/Tooltip.tsx @@ -1,9 +1,8 @@ -import type { ExtractPropTypes, CSSProperties } from 'vue'; +import type { ExtractPropTypes } from 'vue'; import { computed, watch, defineComponent, onMounted, ref } from 'vue'; import VcTooltip from '../vc-tooltip'; import classNames from '../_util/classNames'; import PropTypes from '../_util/vue-types'; -import { PresetColorTypes } from '../_util/colors'; import warning from '../_util/warning'; import { getStyle, filterEmpty, isValidElement, initDefaultProps } from '../_util/props-util'; import { cloneElement } from '../_util/vnode'; @@ -13,6 +12,7 @@ import useConfigInject from '../_util/hooks/useConfigInject'; import getPlacements from './placements'; import firstNotUndefined from '../_util/firstNotUndefined'; import raf from '../_util/raf'; +import { parseColor } from './util'; export type { AdjustOverflow, PlacementsConfig } from './placements'; // https://github.com/react-component/tooltip @@ -39,8 +39,6 @@ const splitObject = (obj: any, keys: string[]) => { return { picked, omitted }; }; -const PresetColorRegex = new RegExp(`^(${PresetColorTypes.join('|')})(-inverse)?$`); - export const tooltipProps = () => ({ ...abstractTooltipProps(), title: PropTypes.any, @@ -76,7 +74,7 @@ export default defineComponent({ slots: ['title'], // emits: ['update:visible', 'visibleChange'], setup(props, { slots, emit, attrs, expose }) { - const { prefixCls, getPopupContainer } = useConfigInject('tooltip', props); + const { prefixCls, getPopupContainer, direction } = useConfigInject('tooltip', props); const visible = ref(firstNotUndefined([props.visible, props.defaultVisible])); @@ -216,9 +214,9 @@ export default defineComponent({ } domNode.style.transformOrigin = `${transformOrigin.left} ${transformOrigin.top}`; }; - + const colorInfo = computed(() => parseColor(prefixCls.value, props.color)); return () => { - const { openClassName, color, overlayClassName } = props; + const { openClassName, overlayClassName } = props; let children = filterEmpty(slots.default?.()) ?? null; children = children.length === 1 ? children[0] : children; @@ -237,15 +235,19 @@ export default defineComponent({ [openClassName || `${prefixCls.value}-open`]: true, [child.props && child.props.class]: child.props && child.props.class, }); - const customOverlayClassName = classNames(overlayClassName, { - [`${prefixCls.value}-${color}`]: color && PresetColorRegex.test(color), - }); - let formattedOverlayInnerStyle: CSSProperties; - let arrowContentStyle: CSSProperties; - if (color && !PresetColorRegex.test(color)) { - formattedOverlayInnerStyle = { backgroundColor: color }; - arrowContentStyle = { '--antd-arrow-background-color': color }; - } + const customOverlayClassName = classNames( + overlayClassName, + { + [`${prefixCls.value}-rtl`]: direction.value === 'rtl', + }, + + colorInfo.value.className, + ); + const formattedOverlayInnerStyle = { + ...props.overlayInnerStyle, + ...colorInfo.value.overlayStyle, + }; + const arrowContentStyle = colorInfo.value.arrowStyle; const vcTooltipProps = { ...attrs, ...(props as TooltipProps), diff --git a/components/tooltip/abstractTooltipProps.ts b/components/tooltip/abstractTooltipProps.ts index 7d20ed2d6..e3b096e1f 100644 --- a/components/tooltip/abstractTooltipProps.ts +++ b/components/tooltip/abstractTooltipProps.ts @@ -2,7 +2,8 @@ import type { CSSProperties, PropType } from 'vue'; import type { AlignType, BuildInPlacements } from '../vc-trigger/interface'; import type { AdjustOverflow } from './placements'; export type TriggerType = 'hover' | 'focus' | 'click' | 'contextmenu'; - +import type { PresetColorType } from '../_util/colors'; +import type { LiteralUnion } from '../_util/type'; export type TooltipPlacement = | 'top' | 'left' @@ -22,9 +23,13 @@ export default () => ({ visible: { type: Boolean, default: undefined }, defaultVisible: { type: Boolean, default: undefined }, placement: String as PropType, - color: String, + color: String as PropType>, transitionName: String, overlayStyle: { type: Object as PropType, default: undefined as CSSProperties }, + overlayInnerStyle: { + type: Object as PropType, + default: undefined as CSSProperties, + }, overlayClassName: String, openClassName: String, prefixCls: String, diff --git a/components/tooltip/util.ts b/components/tooltip/util.ts new file mode 100644 index 000000000..82a4743d8 --- /dev/null +++ b/components/tooltip/util.ts @@ -0,0 +1,22 @@ +import type { CSSProperties } from 'vue'; +import classNames from '../_util/classNames'; +import { isPresetColor } from '../_util/colors'; + +export function parseColor(prefixCls: string, color?: string) { + const isInternalColor = isPresetColor(color); + + const className = classNames({ + [`${prefixCls}-${color}`]: color && isInternalColor, + }); + + const overlayStyle: CSSProperties = {}; + const arrowStyle: CSSProperties = {}; + + if (color && !isInternalColor) { + overlayStyle.background = color; + // @ts-ignore + arrowStyle['--antd-arrow-background-color'] = color; + } + + return { className, overlayStyle, arrowStyle }; +}