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
// ---