diff --git a/components/badge/Badge.tsx b/components/badge/Badge.tsx index 4a3b263de..49c957e3a 100644 --- a/components/badge/Badge.tsx +++ b/components/badge/Badge.tsx @@ -1,27 +1,39 @@ import PropTypes from '../_util/vue-types'; import ScrollNumber from './ScrollNumber'; import classNames from '../_util/classNames'; -import { initDefaultProps, getComponent, getSlot } from '../_util/props-util'; +import { getPropsSlot, flattenChildren } from '../_util/props-util'; import { cloneElement } from '../_util/vnode'; import { getTransitionProps, Transition } from '../_util/transition'; import isNumeric from '../_util/isNumeric'; import { defaultConfigProvider } from '../config-provider'; -import { inject, defineComponent, CSSProperties, VNode, App, Plugin } from 'vue'; +import { + inject, + defineComponent, + ExtractPropTypes, + CSSProperties, + VNode, + App, + Plugin, + reactive, + computed, +} from 'vue'; import { tuple } from '../_util/type'; import Ribbon from './Ribbon'; import { isPresetColor } from './utils'; -const BadgeProps = { +export const badgeProps = { /** Number to show in badge */ count: PropTypes.VNodeChild, showZero: PropTypes.looseBool, /** Max count to show */ - overflowCount: PropTypes.number, + overflowCount: PropTypes.number.def(99), /** whether to show red dot without number */ dot: PropTypes.looseBool, prefixCls: PropTypes.string, scrollNumberPrefixCls: PropTypes.string, status: PropTypes.oneOf(tuple('success', 'processing', 'default', 'error', 'warning')), + // sync antd@4.6.0 + size: PropTypes.oneOf(tuple('default', 'small')).def('default'), color: PropTypes.string, text: PropTypes.VNodeChild, offset: PropTypes.arrayOf(PropTypes.oneOfType([String, Number])), @@ -29,48 +41,44 @@ const BadgeProps = { title: PropTypes.string, }; +export type BadgeProps = Partial>; + const Badge = defineComponent({ name: 'ABadge', Ribbon, - props: initDefaultProps(BadgeProps, { - showZero: false, - dot: false, - overflowCount: 99, - }) as typeof BadgeProps, - setup() { - return { - configProvider: inject('configProvider', defaultConfigProvider), + props: badgeProps, + setup(props, { slots }) { + const configProvider = inject('configProvider', defaultConfigProvider); + const state = reactive({ badgeCount: undefined, - }; - }, - methods: { - getNumberedDispayCount() { - const { overflowCount } = this.$props; - const count = this.badgeCount; + }); + + const getNumberedDispayCount = () => { + const { overflowCount } = props; + const count = state.badgeCount; const displayCount = count > overflowCount ? `${overflowCount}+` : count; return displayCount; - }, + }; - getDispayCount() { - const isDot = this.isDot(); + const getDispayCount = computed(() => { // dot mode don't need count - if (isDot) { + if (isDot.value) { return ''; } - return this.getNumberedDispayCount(); - }, + return getNumberedDispayCount(); + }); - getScrollNumberTitle() { - const { title } = this.$props; - const count = this.badgeCount; + const getScrollNumberTitle = () => { + const { title } = props; + const count = state.badgeCount; if (title) { return title; } return typeof count === 'string' || typeof count === 'number' ? count : undefined; - }, + }; - getStyleWithOffset() { - const { offset, numberStyle } = this.$props; + const getStyleWithOffset = () => { + const { offset, numberStyle } = props; return offset ? { right: `${-parseInt(offset[0] as string, 10)}px`, @@ -78,47 +86,49 @@ const Badge = defineComponent({ ...numberStyle, } : { ...numberStyle }; - }, - getBadgeClassName(prefixCls: string, children: VNode[]) { - const hasStatus = this.hasStatus(); + }; + + const hasStatus = computed(() => { + const { status, color } = props; + return !!status || !!color; + }); + + const isZero = computed(() => { + const numberedDispayCount = getNumberedDispayCount(); + return numberedDispayCount === '0' || numberedDispayCount === 0; + }); + + const isDot = computed(() => { + const { dot } = props; + return (dot && !isZero.value) || hasStatus.value; + }); + + const isHidden = computed(() => { + const { showZero } = props; + const isEmpty = + getDispayCount.value === null || + getDispayCount.value === undefined || + getDispayCount.value === ''; + return (isEmpty || (isZero.value && !showZero)) && !isDot.value; + }); + + const renderStatusText = (prefixCls: string) => { + const text = getPropsSlot(slots, props, 'text'); + const hidden = isHidden.value; + return hidden || !text ? null : {text}; + }; + + const getBadgeClassName = (prefixCls: string, children: VNode[]) => { + const status = hasStatus.value; return classNames(prefixCls, { - [`${prefixCls}-status`]: hasStatus, - [`${prefixCls}-dot-status`]: hasStatus && this.dot && !this.isZero(), + [`${prefixCls}-status`]: status, + [`${prefixCls}-dot-status`]: status && props.dot && !isZero.value, [`${prefixCls}-not-a-wrapper`]: !children.length, }); - }, - hasStatus() { - const { status, color } = this.$props; - return !!status || !!color; - }, - isZero() { - const numberedDispayCount = this.getNumberedDispayCount(); - return numberedDispayCount === '0' || numberedDispayCount === 0; - }, + }; - isDot() { - const { dot } = this.$props; - const isZero = this.isZero(); - return (dot && !isZero) || this.hasStatus(); - }, - - isHidden() { - const { showZero } = this.$props; - const displayCount = this.getDispayCount(); - const isZero = this.isZero(); - const isDot = this.isDot(); - const isEmpty = displayCount === null || displayCount === undefined || displayCount === ''; - return (isEmpty || (isZero && !showZero)) && !isDot; - }, - - renderStatusText(prefixCls: string) { - const text = getComponent(this, 'text'); - const hidden = this.isHidden(); - return hidden || !text ? null : {text}; - }, - - renderDispayComponent() { - const count = this.badgeCount; + const renderDispayComponent = () => { + const count = state.badgeCount; const customNode = count; if (!customNode || typeof customNode !== 'object') { return undefined; @@ -126,103 +136,102 @@ const Badge = defineComponent({ return cloneElement( customNode, { - style: this.getStyleWithOffset(), + style: getStyleWithOffset(), }, false, ); - }, + }; - renderBadgeNumber(prefixCls: string, scrollNumberPrefixCls: string) { - const { status, color } = this.$props; - const count = this.badgeCount; - const displayCount = this.getDispayCount(); - const isDot = this.isDot(); - const hidden = this.isHidden(); + const renderBadgeNumber = (prefixCls: string, scrollNumberPrefixCls: string) => { + const { status, color, size } = props; + const count = state.badgeCount; + const displayCount = getDispayCount.value; const scrollNumberCls = { - [`${prefixCls}-dot`]: isDot, - [`${prefixCls}-count`]: !isDot, + [`${prefixCls}-dot`]: isDot.value, + [`${prefixCls}-count`]: !isDot.value, + [`${prefixCls}-count-sm`]: size === 'small', [`${prefixCls}-multiple-words`]: - !isDot && count && count.toString && count.toString().length > 1, + !isDot.value && count && count.toString && count.toString().length > 1, [`${prefixCls}-status-${status}`]: !!status, [`${prefixCls}-status-${color}`]: isPresetColor(color), }; - let statusStyle = this.getStyleWithOffset(); + let statusStyle = getStyleWithOffset(); if (color && !isPresetColor(color)) { statusStyle = statusStyle || {}; statusStyle.background = color; } - return hidden ? null : ( + return isHidden.value ? null : ( ); - }, - }, + }; - render() { - const { - prefixCls: customizePrefixCls, - scrollNumberPrefixCls: customizeScrollNumberPrefixCls, - status, - color, - } = this; + return () => { + const { + prefixCls: customizePrefixCls, + scrollNumberPrefixCls: customizeScrollNumberPrefixCls, + status, + color, + } = props; - const text = getComponent(this, 'text'); - const getPrefixCls = this.configProvider.getPrefixCls; - const prefixCls = getPrefixCls('badge', customizePrefixCls); - const scrollNumberPrefixCls = getPrefixCls('scroll-number', customizeScrollNumberPrefixCls); + const text = getPropsSlot(slots, props, 'text'); + const getPrefixCls = configProvider.getPrefixCls; + const prefixCls = getPrefixCls('badge', customizePrefixCls); + const scrollNumberPrefixCls = getPrefixCls('scroll-number', customizeScrollNumberPrefixCls); - const children = getSlot(this); - let count = getComponent(this, 'count'); - if (Array.isArray(count)) { - count = count[0]; - } - this.badgeCount = count; - const scrollNumber = this.renderBadgeNumber(prefixCls, scrollNumberPrefixCls); - const statusText = this.renderStatusText(prefixCls); - const statusCls = classNames({ - [`${prefixCls}-status-dot`]: this.hasStatus(), - [`${prefixCls}-status-${status}`]: !!status, - [`${prefixCls}-status-${color}`]: isPresetColor(color), - }); - const statusStyle: CSSProperties = {}; - if (color && !isPresetColor(color)) { - statusStyle.background = color; - } - // - if (!children.length && this.hasStatus()) { - const styleWithOffset = this.getStyleWithOffset(); - const statusTextColor = styleWithOffset && styleWithOffset.color; - return ( - - - - {text} + const children = flattenChildren(slots.default?.()); + let count = getPropsSlot(slots, props, 'count'); + if (Array.isArray(count)) { + count = count[0]; + } + state.badgeCount = count; + const scrollNumber = renderBadgeNumber(prefixCls, scrollNumberPrefixCls); + const statusText = renderStatusText(prefixCls); + const statusCls = classNames({ + [`${prefixCls}-status-dot`]: hasStatus.value, + [`${prefixCls}-status-${status}`]: !!status, + [`${prefixCls}-status-${color}`]: isPresetColor(color), + }); + const statusStyle: CSSProperties = {}; + if (color && !isPresetColor(color)) { + statusStyle.background = color; + } + // + if (!children.length && hasStatus.value) { + const styleWithOffset = getStyleWithOffset(); + const statusTextColor = styleWithOffset && styleWithOffset.color; + return ( + + + + {text} + + ); + } + + const transitionProps = getTransitionProps(children.length ? `${prefixCls}-zoom` : ''); + + return ( + + {children} + {scrollNumber} + {statusText} ); - } - - const transitionProps = getTransitionProps(children.length ? `${prefixCls}-zoom` : ''); - - return ( - - {children} - {scrollNumber} - {statusText} - - ); + }; }, }); diff --git a/components/badge/ScrollNumber.tsx b/components/badge/ScrollNumber.tsx index 6d2b7629f..dfe22dd30 100644 --- a/components/badge/ScrollNumber.tsx +++ b/components/badge/ScrollNumber.tsx @@ -1,10 +1,20 @@ import classNames from '../_util/classNames'; import PropTypes from '../_util/vue-types'; -import BaseMixin from '../_util/BaseMixin'; -import omit from 'omit.js'; +import { omit } from 'lodash-es'; import { cloneElement } from '../_util/vnode'; import { defaultConfigProvider } from '../config-provider'; -import { CSSProperties, defineComponent, inject } from 'vue'; +import { + defineComponent, + inject, + nextTick, + onBeforeUnmount, + onUpdated, + reactive, + watch, + ExtractPropTypes, + CSSProperties, + DefineComponent, +} from 'vue'; function getNumberArray(num: string | number | undefined | null) { return num @@ -19,7 +29,7 @@ function getNumberArray(num: string | number | undefined | null) { : []; } -const ScrollNumberProps = { +export const scrollNumberProps = { prefixCls: PropTypes.string, count: PropTypes.any, component: PropTypes.string, @@ -28,68 +38,30 @@ const ScrollNumberProps = { onAnimated: PropTypes.func, }; +export type ScrollNumberProps = ExtractPropTypes; + export default defineComponent({ name: 'ScrollNumber', - mixins: [BaseMixin], inheritAttrs: false, - props: ScrollNumberProps, + props: scrollNumberProps, emits: ['animated'], - setup() { - return { - configProvider: inject('configProvider', defaultConfigProvider), - lastCount: undefined, - timeout: undefined, - }; - }, - data() { - return { + setup(props, { emit, attrs }) { + const configProvider = inject('configProvider', defaultConfigProvider); + const state = reactive({ animateStarted: true, - sCount: this.count, - }; - }, - watch: { - count() { - this.lastCount = this.sCount; - this.setState({ - animateStarted: true, - }); - }, - }, - updated() { - const { animateStarted, count } = this; - if (animateStarted) { - this.clearTimeout(); - // Let browser has time to reset the scroller before actually - // performing the transition. - this.timeout = setTimeout(() => { - this.setState( - { - animateStarted: false, - sCount: count, - }, - this.handleAnimated, - ); - }); - } - }, - beforeUnmount() { - this.clearTimeout(); - }, - methods: { - clearTimeout() { - if (this.timeout) { - clearTimeout(this.timeout); - this.timeout = undefined; - } - }, - getPositionByNum(num: number, i: number) { - const { sCount } = this; - const currentCount = Math.abs(Number(sCount)); - const lastCount = Math.abs(Number(this.lastCount)); - const currentDigit = Math.abs(getNumberArray(sCount)[i] as number); - const lastDigit = Math.abs(getNumberArray(this.lastCount)[i] as number); + lastCount: undefined, + sCount: props.count, - if (this.animateStarted) { + timeout: undefined, + }); + + const getPositionByNum = (num: number, i: number) => { + const currentCount = Math.abs(Number(state.sCount)); + const lastCount = Math.abs(Number(state.lastCount)); + const currentDigit = Math.abs(getNumberArray(state.sCount)[i] as number); + const lastDigit = Math.abs(getNumberArray(state.lastCount)[i] as number); + + if (state.animateStarted) { return 10 + num; } // 同方向则在同一侧切换数字 @@ -103,12 +75,19 @@ export default defineComponent({ return 10 + num; } return num; - }, - handleAnimated() { - this.$emit('animated'); - }, + }; + const handleAnimated = () => { + emit('animated'); + }; - renderNumberList(position: number, className: string) { + const _clearTimeout = () => { + if (state.timeout) { + clearTimeout(state.timeout); + state.timeout = undefined; + } + }; + + const renderNumberList = (position: number, className: string) => { const childrenToReturn = []; for (let i = 0; i < 30; i++) { childrenToReturn.push( @@ -122,14 +101,14 @@ export default defineComponent({

, ); } - return childrenToReturn; - }, - renderCurrentNumber(prefixCls: string, num: number | string, i: number) { + }; + + const renderCurrentNumber = (prefixCls: string, num: number | string, i: number) => { if (typeof num === 'number') { - const position = this.getPositionByNum(num, i); + const position = getPositionByNum(num, i); const removeTransition = - this.animateStarted || getNumberArray(this.lastCount)[i] === undefined; + state.animateStarted || getNumberArray(state.lastCount)[i] === undefined; const style = { transition: removeTransition ? 'none' : undefined, msTransform: `translateY(${-position * 100}%)`, @@ -138,7 +117,7 @@ export default defineComponent({ }; return ( - {this.renderNumberList(position, `${prefixCls}-only-unit`)} + {renderNumberList(position, `${prefixCls}-only-unit`)} ); } @@ -147,57 +126,92 @@ export default defineComponent({ {num}
); - }, + }; - renderNumberElement(prefixCls: string) { - const { sCount } = this; - if (sCount && Number(sCount) % 1 === 0) { - return getNumberArray(sCount) - .map((num, i) => this.renderCurrentNumber(prefixCls, num, i)) + const renderNumberElement = (prefixCls: string) => { + if (state.sCount && Number(state.sCount) % 1 === 0) { + return getNumberArray(state.sCount) + .map((num, i) => renderCurrentNumber(prefixCls, num, i)) .reverse(); } - return sCount; - }, - }, - - render() { - const { prefixCls: customizePrefixCls, title, component: Tag = 'sup', displayComponent } = this; - const getPrefixCls = this.configProvider.getPrefixCls; - const prefixCls = getPrefixCls('scroll-number', customizePrefixCls); - const { class: className, style = {} } = this.$attrs as { - class?: string; - style?: CSSProperties; + return state.sCount; }; - if (displayComponent) { - return cloneElement(displayComponent, { - class: classNames( - `${prefixCls}-custom-component`, - displayComponent.props && displayComponent.props.class, - ), - }); - } - // fix https://fb.me/react-unknown-prop - const restProps = omit({ ...this.$props, ...this.$attrs }, [ - 'count', - 'onAnimated', - 'component', - 'prefixCls', - 'displayComponent', - ]); - const tempStyle = { ...style }; - const newProps = { - ...restProps, - title, - style: tempStyle, - class: classNames(prefixCls, className), - }; - // allow specify the border - // mock border-color by box-shadow for compatible with old usage: - // - if (style && style.borderColor) { - newProps.style.boxShadow = `0 0 0 1px ${style.borderColor} inset`; - } - return {this.renderNumberElement(prefixCls)}; + watch( + () => props.count, + () => { + state.lastCount = state.sCount; + state.animateStarted = true; + }, + ); + + onUpdated(() => { + if (state.animateStarted) { + _clearTimeout(); + // Let browser has time to reset the scroller before actually + // performing the transition. + state.timeout = setTimeout(() => { + state.animateStarted = false; + state.sCount = props.count; + nextTick(() => { + handleAnimated(); + }); + }); + } + }); + + onBeforeUnmount(() => { + _clearTimeout(); + }); + + // configProvider: inject('configProvider', defaultConfigProvider), + // lastCount: undefined, + // timeout: undefined, + + return () => { + const { + prefixCls: customizePrefixCls, + title, + component: Tag = ('sup' as unknown) as DefineComponent, + displayComponent, + } = props; + const getPrefixCls = configProvider.getPrefixCls; + const prefixCls = getPrefixCls('scroll-number', customizePrefixCls); + const { class: className, style = {} } = attrs as { + class?: string; + style?: CSSProperties; + }; + if (displayComponent) { + return cloneElement(displayComponent, { + class: classNames( + `${prefixCls}-custom-component`, + displayComponent.props && displayComponent.props.class, + ), + }); + } + // fix https://fb.me/react-unknown-prop + const restProps = omit({ ...props, ...attrs }, [ + 'count', + 'onAnimated', + 'component', + 'prefixCls', + 'displayComponent', + ]); + const tempStyle = { ...style }; + const newProps = { + ...restProps, + title, + style: tempStyle, + class: classNames(prefixCls, className), + }; + // allow specify the border + // mock border-color by box-shadow for compatible with old usage: + // + if (style && style.borderColor) { + newProps.style.boxShadow = `0 0 0 1px ${style.borderColor} inset`; + } + + return {renderNumberElement(prefixCls)}; + }; }, }); diff --git a/components/badge/style/index.less b/components/badge/style/index.less index a10e8918c..1bed0c228 100644 --- a/components/badge/style/index.less +++ b/components/badge/style/index.less @@ -31,6 +31,15 @@ } } + &-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; } diff --git a/components/style/themes/default.less b/components/style/themes/default.less index 2bc651de7..45787fea5 100644 --- a/components/style/themes/default.less +++ b/components/style/themes/default.less @@ -581,11 +581,14 @@ // Badge // --- @badge-height: 20px; +@badge-height-sm: 14px; @badge-dot-size: 6px; @badge-font-size: @font-size-sm; +@badge-font-size-sm: @font-size-sm; @badge-font-weight: normal; @badge-status-size: 6px; @badge-text-color: @component-background; +@badge-color: @highlight-color; // Rate // ---