diff --git a/components/badge/ScrollNumber.tsx b/components/badge/ScrollNumber.tsx index 8289038a5f1a..6d432772e678 100644 --- a/components/badge/ScrollNumber.tsx +++ b/components/badge/ScrollNumber.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { useState } from 'react'; import classNames from 'classnames'; import { ConfigContext } from '../config-provider'; import { cloneElement } from '../_util/reactNode'; @@ -63,10 +64,10 @@ const ScrollNumber: React.FC = ({ onAnimated = () => {}, ...restProps }) => { - const [animateStarted, setAnimateStarted] = React.useState(true); - const [count, setCount] = React.useState(customizeCount); - const [prevCount, setPrevCount] = React.useState(customizeCount); - const [lastCount, setLastCount] = React.useState(customizeCount); + const [animateStarted, setAnimateStarted] = useState(true); + const [count, setCount] = useState(customizeCount); + const [prevCount, setPrevCount] = useState(customizeCount); + const [lastCount, setLastCount] = useState(customizeCount); const { getPrefixCls } = React.useContext(ConfigContext); const prefixCls = getPrefixCls('scroll-number', customizePrefixCls); @@ -154,15 +155,12 @@ const ScrollNumber: React.FC = ({ ); }; - const numberNodeRef = React.useRef(null); - if (show) { - numberNodeRef.current = - count && Number(count) % 1 === 0 - ? getNumberArray(count) - .map((num, i) => renderCurrentNumber(num, i)) - .reverse() - : count; - } + const numberNode = + count && Number(count) % 1 === 0 + ? getNumberArray(count) + .map((num, i) => renderCurrentNumber(num, i)) + .reverse() + : count; // allow specify the border // mock border-color by box-shadow for compatible with old usage: @@ -178,7 +176,7 @@ const ScrollNumber: React.FC = ({ className: classNames(`${prefixCls}-custom-component`, oriProps?.className), })); } - return React.createElement(component as any, newProps, numberNodeRef.current); + return React.createElement(component as any, newProps, numberNode); }; export default ScrollNumber; diff --git a/components/badge/__tests__/__snapshots__/demo.test.js.snap b/components/badge/__tests__/__snapshots__/demo.test.js.snap index 2156a37ac0a9..99796ec9d921 100644 --- a/components/badge/__tests__/__snapshots__/demo.test.js.snap +++ b/components/badge/__tests__/__snapshots__/demo.test.js.snap @@ -994,6 +994,24 @@ exports[`renders ./components/badge/demo/no-wrapper.md correctly 1`] = `
+
+ +
( - - - - - -); +import { Badge, Space, Switch } from 'antd'; + +const Demo = () => { + const [show, setShow] = React.useState(true); + + return ( + + { + setShow(!show); + }} + /> + + + + + ); +}; ReactDOM.render(, mountNode); ``` diff --git a/components/badge/index.tsx b/components/badge/index.tsx index 97a0826fa092..de32d56a0464 100644 --- a/components/badge/index.tsx +++ b/components/badge/index.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { useMemo, useRef } from 'react'; import CSSMotion from 'rc-motion'; import classNames from 'classnames'; import ScrollNumber from './ScrollNumber'; @@ -68,21 +69,28 @@ const Badge: CompoundedComponent = ({ const showAsDot = (dot && !isZero) || hasStatus; - const displayCount = showAsDot ? '' : numberedDisplayCount; + const mergedCount = showAsDot ? '' : numberedDisplayCount; - const isHidden = React.useMemo(() => { - const isEmpty = displayCount === null || displayCount === undefined || displayCount === ''; + const isHidden = useMemo(() => { + const isEmpty = mergedCount === null || mergedCount === undefined || mergedCount === ''; return (isEmpty || (isZero && !showZero)) && !showAsDot; - }, [displayCount, isZero, showZero, showAsDot]); + }, [mergedCount, isZero, showZero, showAsDot]); + + // We need cache count since remove motion should not change count display + const displayCountRef = useRef(mergedCount); + if (!isHidden) { + displayCountRef.current = mergedCount; + } + const displayCount = displayCountRef.current; // We will cache the dot status to avoid shaking on leaved motion - const isDotRef = React.useRef(showAsDot); + const isDotRef = useRef(showAsDot); if (!isHidden) { isDotRef.current = showAsDot; } // =============================== Styles =============================== - const mergedStyle = React.useMemo(() => { + const mergedStyle = useMemo(() => { if (!offset) { return { ...style }; } @@ -173,7 +181,7 @@ const Badge: CompoundedComponent = ({ [`${prefixCls}-count`]: !isDot, [`${prefixCls}-count-sm`]: size === 'small', [`${prefixCls}-multiple-words`]: - !isDot && count && count.toString && count.toString().length > 1, + !isDot && displayCount && displayCount?.toString().length > 1, [`${prefixCls}-status-${status}`]: !!status, [`${prefixCls}-status-${color}`]: isPresetColor(color), }); diff --git a/components/badge/style/index.less b/components/badge/style/index.less index 1615050ecc29..5773d2e500d1 100644 --- a/components/badge/style/index.less +++ b/components/badge/style/index.less @@ -124,16 +124,25 @@ &-zoom-appear, &-zoom-enter { - animation: antZoomBadgeIn 0.3s @ease-out-back; + animation: antZoomBadgeIn @animation-duration-slow @ease-out-back; animation-fill-mode: both; } &-zoom-leave { - animation: antZoomBadgeOut 0.3s @ease-in-back; + 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; } @@ -142,6 +151,7 @@ position: relative; top: auto; display: block; + transform-origin: 50% 50%; } .@{badge-prefix-cls}-count { @@ -166,7 +176,7 @@ &-only { display: inline-block; height: @badge-height; - transition: all 0.3s @ease-in-out; + transition: all @animation-duration-slow @ease-in-out; > p.@{number-prefix-cls}-only-unit { height: @badge-height; margin: 0; @@ -198,5 +208,25 @@ } } +@keyframes antNoWrapperZoomBadgeIn { + 0% { + transform: scale(0); + opacity: 0; + } + 100% { + transform: scale(1); + } +} + +@keyframes antNoWrapperZoomBadgeOut { + 0% { + transform: scale(1); + } + 100% { + transform: scale(0); + opacity: 0; + } +} + @import './ribbon'; @import './rtl';