From e442b0d1ec23f4fa5e100262d7946085f9af386b Mon Sep 17 00:00:00 2001 From: tanjinzhou <415800467@qq.com> Date: Tue, 25 May 2021 17:30:22 +0800 Subject: [PATCH] refactor: badge --- components/badge/Badge.tsx | 328 ++++++++++++++---------------- components/badge/Ribbon.tsx | 97 +++++---- components/badge/ScrollNumber.tsx | 227 +++++---------------- components/badge/SingleNumber.tsx | 131 ++++++++++++ components/badge/index.ts | 13 +- components/badge/style/index.less | 74 ++++++- components/badge/style/rtl.less | 104 ++++++++++ components/badge/utils.ts | 2 +- examples/index.html | 51 +++-- v3-changelog.md | 0 10 files changed, 587 insertions(+), 440 deletions(-) create mode 100644 components/badge/SingleNumber.tsx create mode 100644 components/badge/style/rtl.less create mode 100644 v3-changelog.md diff --git a/components/badge/Badge.tsx b/components/badge/Badge.tsx index 49c957e3a..f1f774970 100644 --- a/components/badge/Badge.tsx +++ b/components/badge/Badge.tsx @@ -4,26 +4,16 @@ import classNames from '../_util/classNames'; 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, - ExtractPropTypes, - CSSProperties, - VNode, - App, - Plugin, - reactive, - computed, -} from 'vue'; +import { defineComponent, ExtractPropTypes, CSSProperties, computed, ref, watch } from 'vue'; import { tuple } from '../_util/type'; import Ribbon from './Ribbon'; import { isPresetColor } from './utils'; +import useConfigInject from '../_util/hooks/useConfigInject'; +import isNumeric from '../_util/isNumeric'; export const badgeProps = { /** Number to show in badge */ - count: PropTypes.VNodeChild, + count: PropTypes.any, showZero: PropTypes.looseBool, /** Max count to show */ overflowCount: PropTypes.number.def(99), @@ -43,205 +33,189 @@ export const badgeProps = { export type BadgeProps = Partial>; -const Badge = defineComponent({ +export default defineComponent({ name: 'ABadge', Ribbon, props: badgeProps, - setup(props, { slots }) { - const configProvider = inject('configProvider', defaultConfigProvider); - const state = reactive({ - badgeCount: undefined, + slots: ['text', 'count'], + setup(props, { slots, attrs }) { + const { prefixCls, direction } = useConfigInject('badge', props); + + // ================================ Misc ================================ + const numberedDisplayCount = computed(() => { + return ((props.count as number) > (props.overflowCount as number) + ? `${props.overflowCount}+` + : props.count) as string | number | null; }); - const getNumberedDispayCount = () => { - const { overflowCount } = props; - const count = state.badgeCount; - const displayCount = count > overflowCount ? `${overflowCount}+` : count; - return displayCount; - }; + const hasStatus = computed( + () => + (props.status !== null && props.status !== undefined) || + (props.color !== null && props.color !== undefined), + ); - const getDispayCount = computed(() => { - // dot mode don't need count - if (isDot.value) { - return ''; - } - return getNumberedDispayCount(); - }); + const isZero = computed( + () => numberedDisplayCount.value === '0' || numberedDisplayCount.value === 0, + ); - const getScrollNumberTitle = () => { - const { title } = props; - const count = state.badgeCount; - if (title) { - return title; - } - return typeof count === 'string' || typeof count === 'number' ? count : undefined; - }; + const showAsDot = computed(() => (props.dot && !isZero.value) || hasStatus.value); - const getStyleWithOffset = () => { - const { offset, numberStyle } = props; - return offset - ? { - right: `${-parseInt(offset[0] as string, 10)}px`, - marginTop: isNumeric(offset[1]) ? `${offset[1]}px` : offset[1], - ...numberStyle, - } - : { ...numberStyle }; - }; - - 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 mergedCount = computed(() => (showAsDot.value ? '' : numberedDisplayCount.value)); const isHidden = computed(() => { - const { showZero } = props; const isEmpty = - getDispayCount.value === null || - getDispayCount.value === undefined || - getDispayCount.value === ''; - return (isEmpty || (isZero.value && !showZero)) && !isDot.value; + mergedCount.value === null || mergedCount.value === undefined || mergedCount.value === ''; + return (isEmpty || (isZero.value && !props.showZero)) && !showAsDot.value; }); - const renderStatusText = (prefixCls: string) => { - const text = getPropsSlot(slots, props, 'text'); - const hidden = isHidden.value; - return hidden || !text ? null : {text}; - }; + // Count should be cache in case hidden change it + const livingCount = ref(props.count); - const getBadgeClassName = (prefixCls: string, children: VNode[]) => { - const status = hasStatus.value; - return classNames(prefixCls, { - [`${prefixCls}-status`]: status, - [`${prefixCls}-dot-status`]: status && props.dot && !isZero.value, - [`${prefixCls}-not-a-wrapper`]: !children.length, - }); - }; + // We need cache count since remove motion should not change count display + const displayCount = ref(mergedCount.value); - const renderDispayComponent = () => { - const count = state.badgeCount; - const customNode = count; - if (!customNode || typeof customNode !== 'object') { - return undefined; + // We will cache the dot status to avoid shaking on leaved motion + const isDotRef = ref(showAsDot.value); + + watch( + [() => props.count, mergedCount, showAsDot], + () => { + if (!isHidden.value) { + livingCount.value = props.count; + displayCount.value = mergedCount.value; + isDotRef.value = showAsDot.value; + } + }, + { immediate: true }, + ); + + // 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), + })); + + const statusStyle = computed(() => { + if (props.color && !isPresetColor(props.color)) { + return { background: props.color }; + } else { + return {}; } - return cloneElement( - customNode, + }); + + const scrollNumberCls = computed(() => ({ + [`${prefixCls.value}-dot`]: isDotRef.value, + [`${prefixCls.value}-count`]: !isDotRef.value, + [`${prefixCls.value}-count-sm`]: props.size === 'small', + [`${prefixCls.value}-multiple-words`]: + !isDotRef.value && displayCount.value && displayCount.value.toString().length > 1, + [`${prefixCls.value}-status-${status}`]: !!status, + [`${prefixCls.value}-status-${props.color}`]: isPresetColor(props.color), + })); + + return () => { + const { offset, title, color } = props; + const style = attrs.style as CSSProperties; + const text = getPropsSlot(slots, props, 'text'); + const pre = prefixCls.value; + const count = livingCount.value; + let children = flattenChildren(slots.default?.()); + children = children.length ? children : null; + + const visible = !!(!isHidden.value || slots.count); + + // =============================== Styles =============================== + const mergedStyle = (() => { + if (!offset) { + return { ...style }; + } + + const offsetStyle: CSSProperties = { + marginTop: isNumeric(offset[1]) ? `${offset[1]}px` : offset[1], + }; + if (direction.value === 'rtl') { + offsetStyle.left = `${parseInt(offset[0] as string, 10)}px`; + } else { + offsetStyle.right = `${-parseInt(offset[0] as string, 10)}px`; + } + + return { + ...offsetStyle, + ...style, + }; + })(); + + // =============================== Render =============================== + // >>> Title + const titleNode = + title ?? (typeof count === 'string' || typeof count === 'number' ? count : undefined); + + // >>> Status Text + const statusTextNode = + visible || !text ? null : {text}; + + // >>> Display Component + const displayNode = cloneElement( + slots.count?.(), { - style: getStyleWithOffset(), + style: mergedStyle, }, false, ); - }; - const renderBadgeNumber = (prefixCls: string, scrollNumberPrefixCls: string) => { - const { status, color, size } = props; - const count = state.badgeCount; - const displayCount = getDispayCount.value; - - const scrollNumberCls = { - [`${prefixCls}-dot`]: isDot.value, - [`${prefixCls}-count`]: !isDot.value, - [`${prefixCls}-count-sm`]: size === 'small', - [`${prefixCls}-multiple-words`]: - !isDot.value && count && count.toString && count.toString().length > 1, - [`${prefixCls}-status-${status}`]: !!status, - [`${prefixCls}-status-${color}`]: isPresetColor(color), - }; - - let statusStyle = getStyleWithOffset(); - if (color && !isPresetColor(color)) { - statusStyle = statusStyle || {}; - statusStyle.background = color; - } - - return isHidden.value ? null : ( - + const badgeClassName = classNames( + pre, + { + [`${pre}-status`]: hasStatus.value, + [`${pre}-not-a-wrapper`]: !children, + [`${pre}-rtl`]: direction.value === 'rtl', + }, + attrs.class, ); - }; - return () => { - const { - prefixCls: customizePrefixCls, - scrollNumberPrefixCls: customizeScrollNumberPrefixCls, - status, - color, - } = props; - - const text = getPropsSlot(slots, props, 'text'); - const getPrefixCls = configProvider.getPrefixCls; - const prefixCls = getPrefixCls('badge', customizePrefixCls); - const scrollNumberPrefixCls = getPrefixCls('scroll-number', customizeScrollNumberPrefixCls); - - 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; + if (!children && hasStatus.value) { + const statusTextColor = mergedStyle.color; return ( - - - + + + {text} ); } - const transitionProps = getTransitionProps(children.length ? `${prefixCls}-zoom` : ''); + const transitionProps = getTransitionProps(children ? `${pre}-zoom` : '', { + appear: false, + }); + let scrollNumberStyle: CSSProperties = { ...mergedStyle, ...props.numberStyle }; + if (color && !isPresetColor(color)) { + scrollNumberStyle = scrollNumberStyle || {}; + scrollNumberStyle.background = color; + } return ( - + {children} - {scrollNumber} - {statusText} + + + {displayNode} + + + {statusTextNode} ); }; }, }); - -Badge.install = function(app: App) { - app.component(Badge.name, Badge); - app.component(Badge.Ribbon.displayName, Badge.Ribbon); - return app; -}; - -export default Badge as typeof Badge & - Plugin & { - readonly Ribbon: typeof Ribbon; - }; diff --git a/components/badge/Ribbon.tsx b/components/badge/Ribbon.tsx index a9dbab83c..4eaaeaacd 100644 --- a/components/badge/Ribbon.tsx +++ b/components/badge/Ribbon.tsx @@ -1,60 +1,55 @@ import { LiteralUnion, tuple } from '../_util/type'; import { PresetColorType } from '../_util/colors'; import { isPresetColor } from './utils'; -import { defaultConfigProvider } from '../config-provider'; -import { HTMLAttributes, FunctionalComponent, VNodeTypes, inject, CSSProperties } from 'vue'; +import { CSSProperties, defineComponent, PropType, ExtractPropTypes, computed } from 'vue'; import PropTypes from '../_util/vue-types'; +import useConfigInject from '../_util/hooks/useConfigInject'; -type RibbonPlacement = 'start' | 'end'; - -export interface RibbonProps extends HTMLAttributes { - prefixCls?: string; - text?: VNodeTypes; - color?: LiteralUnion; - placement?: RibbonPlacement; -} - -const Ribbon: FunctionalComponent = (props, { attrs, slots }) => { - const { prefixCls: customizePrefixCls, color, text = slots.text?.(), placement = 'end' } = props; - const { class: className, style } = attrs; - const children = slots.default?.(); - const { getPrefixCls, direction } = inject('configProvider', defaultConfigProvider); - - const prefixCls = getPrefixCls('ribbon', customizePrefixCls); - const colorInPreset = isPresetColor(color); - const ribbonCls = [ - prefixCls, - `${prefixCls}-placement-${placement}`, - { - [`${prefixCls}-rtl`]: direction === 'rtl', - [`${prefixCls}-color-${color}`]: colorInPreset, - }, - className, - ]; - const colorStyle: CSSProperties = {}; - const cornerColorStyle: CSSProperties = {}; - if (color && !colorInPreset) { - colorStyle.background = color; - cornerColorStyle.color = color; - } - return ( -
- {children} -
- {text} -
-
-
- ); -}; - -Ribbon.displayName = 'ABadgeRibbon'; -Ribbon.inheritAttrs = false; -Ribbon.props = { +const ribbonProps = { prefix: PropTypes.string, - color: PropTypes.string, + color: { type: String as PropType> }, text: PropTypes.any, - placement: PropTypes.oneOf(tuple('start', 'end')), + placement: PropTypes.oneOf(tuple('start', 'end')).def('end'), }; -export default Ribbon; +export type RibbonProps = Partial>; + +export default defineComponent({ + name: 'ABadgeRibbon', + inheritAttrs: false, + props: ribbonProps, + slots: ['text'], + setup(props, { attrs, slots }) { + const { prefixCls, direction } = useConfigInject('ribbon', props); + const colorInPreset = computed(() => isPresetColor(props.color)); + const ribbonCls = computed(() => [ + prefixCls.value, + `${prefixCls.value}-placement-${props.placement}`, + { + [`${prefixCls.value}-rtl`]: direction.value === 'rtl', + [`${prefixCls.value}-color-${props.color}`]: colorInPreset.value, + }, + ]); + return () => { + const { class: className, style, ...restAttrs } = attrs; + const colorStyle: CSSProperties = {}; + const cornerColorStyle: CSSProperties = {}; + if (props.color && !colorInPreset.value) { + colorStyle.background = props.color; + cornerColorStyle.color = props.color; + } + return ( +
+ {slots.default?.()} +
+ {props.text || slots.text?.()} +
+
+
+ ); + }; + }, +}); diff --git a/components/badge/ScrollNumber.tsx b/components/badge/ScrollNumber.tsx index dfe22dd30..ffb7f7ee2 100644 --- a/components/badge/ScrollNumber.tsx +++ b/components/badge/ScrollNumber.tsx @@ -1,217 +1,90 @@ import classNames from '../_util/classNames'; import PropTypes from '../_util/vue-types'; -import { omit } from 'lodash-es'; import { cloneElement } from '../_util/vnode'; -import { defaultConfigProvider } from '../config-provider'; import { defineComponent, - inject, - nextTick, - onBeforeUnmount, - onUpdated, - reactive, - watch, ExtractPropTypes, CSSProperties, DefineComponent, + HTMLAttributes, } from 'vue'; - -function getNumberArray(num: string | number | undefined | null) { - return num - ? num - .toString() - .split('') - .reverse() - .map(i => { - const current = Number(i); - return isNaN(current) ? i : current; - }) - : []; -} +import useConfigInject from '../_util/hooks/useConfigInject'; +import SingleNumber from './SingleNumber'; +import { filterEmpty } from '../_util/props-util'; export const scrollNumberProps = { prefixCls: PropTypes.string, count: PropTypes.any, component: PropTypes.string, title: PropTypes.oneOfType([PropTypes.number, PropTypes.string, null]), - displayComponent: PropTypes.any, - onAnimated: PropTypes.func, + show: Boolean, }; -export type ScrollNumberProps = ExtractPropTypes; +export type ScrollNumberProps = Partial>; export default defineComponent({ name: 'ScrollNumber', inheritAttrs: false, props: scrollNumberProps, - emits: ['animated'], - setup(props, { emit, attrs }) { - const configProvider = inject('configProvider', defaultConfigProvider); - const state = reactive({ - animateStarted: true, - lastCount: undefined, - sCount: props.count, - - 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; - } - // 同方向则在同一侧切换数字 - if (currentCount > lastCount) { - if (currentDigit >= lastDigit) { - return 10 + num; - } - return 20 + num; - } - if (currentDigit <= lastDigit) { - return 10 + num; - } - return num; - }; - const handleAnimated = () => { - emit('animated'); - }; - - 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( -

- {i % 10} -

, - ); - } - return childrenToReturn; - }; - - const renderCurrentNumber = (prefixCls: string, num: number | string, i: number) => { - if (typeof num === 'number') { - const position = getPositionByNum(num, i); - const removeTransition = - state.animateStarted || getNumberArray(state.lastCount)[i] === undefined; - const style = { - transition: removeTransition ? 'none' : undefined, - msTransform: `translateY(${-position * 100}%)`, - WebkitTransform: `translateY(${-position * 100}%)`, - transform: `translateY(${-position * 100}%)`, - }; - return ( - - {renderNumberList(position, `${prefixCls}-only-unit`)} - - ); - } - return ( - - {num} - - ); - }; - - 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 state.sCount; - }; - - 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, + setup(props, { attrs, slots }) { + const { prefixCls } = useConfigInject('scroll-number', props); return () => { const { prefixCls: customizePrefixCls, + count, title, + show, 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 }; + class: className, + style, + ...restProps + } = { ...props, ...attrs } as ScrollNumberProps & HTMLAttributes & { style: CSSProperties }; + // ============================ Render ============================ const newProps = { ...restProps, - title, - style: tempStyle, - class: classNames(prefixCls, className), + style, + 'data-show': props.show, + class: classNames(prefixCls.value, className), + title: title as string, }; + + // Only integer need motion + let numberNodes: any = count; + if (count && Number(count) % 1 === 0) { + const numberList = String(count).split(''); + + numberNodes = numberList.map((num, i) => ( + + )); + } + // 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`; + newProps.style = { + ...(style as CSSProperties), + boxShadow: `0 0 0 1px ${style.borderColor} inset`, + }; + } + const children = filterEmpty(slots.default?.()); + if (children && children.length) { + return cloneElement( + children, + { + class: classNames(`${prefixCls.value}-custom-component`), + }, + false, + ); } - return {renderNumberElement(prefixCls)}; + return {numberNodes}; }; }, }); diff --git a/components/badge/SingleNumber.tsx b/components/badge/SingleNumber.tsx new file mode 100644 index 000000000..894801487 --- /dev/null +++ b/components/badge/SingleNumber.tsx @@ -0,0 +1,131 @@ +import { computed, CSSProperties, defineComponent, onUnmounted, reactive, ref, watch } from 'vue'; +import classNames from '../_util/classNames'; + +export interface UnitNumberProps { + prefixCls: string; + value: string | number; + offset?: number; + current?: boolean; +} + +function UnitNumber({ prefixCls, value, current, offset = 0 }: UnitNumberProps) { + let style: CSSProperties | undefined; + + if (offset) { + style = { + position: 'absolute', + top: `${offset}00%`, + left: 0, + }; + } + + return ( +

+ {value} +

+ ); +} + +function getOffset(start: number, end: number, unit: -1 | 1) { + let index = start; + let offset = 0; + + while ((index + 10) % 10 !== end) { + index += unit; + offset += unit; + } + + return offset; +} + +export default defineComponent({ + name: 'SingleNumber', + props: { + prefixCls: String, + value: String, + count: Number, + }, + setup(props) { + const originValue = computed(() => Number(props.value)); + const originCount = computed(() => Math.abs(props.count)); + const state = reactive({ + prevValue: originValue.value, + prevCount: originCount.value, + }); + + // ============================= Events ============================= + const onTransitionEnd = () => { + state.prevValue = originValue.value; + state.prevCount = originCount.value; + }; + const timeout = ref(); + // Fallback if transition event not support + watch( + originValue, + () => { + clearTimeout(timeout.value); + timeout.value = setTimeout(() => { + onTransitionEnd(); + }, 1000); + }, + { flush: 'post' }, + ); + onUnmounted(() => { + clearTimeout(timeout.value); + }); + + return () => { + let unitNodes: any[]; + let offsetStyle: CSSProperties = {}; + const value = originValue.value; + if (state.prevValue === value || Number.isNaN(value) || Number.isNaN(state.prevValue)) { + // Nothing to change + unitNodes = [UnitNumber({ ...props, current: true } as UnitNumberProps)]; + offsetStyle = { + transition: 'none', + }; + } else { + unitNodes = []; + + // Fill basic number units + const end = value + 10; + const unitNumberList: number[] = []; + for (let index = value; index <= end; index += 1) { + unitNumberList.push(index); + } + + // Fill with number unit nodes + const prevIndex = unitNumberList.findIndex(n => n % 10 === state.prevValue); + unitNodes = unitNumberList.map((n, index) => { + const singleUnit = n % 10; + return UnitNumber({ + ...props, + value: singleUnit, + offset: index - prevIndex, + current: index === prevIndex, + } as UnitNumberProps); + }); + + // Calculate container offset value + const unit = state.prevCount < originCount.value ? 1 : -1; + offsetStyle = { + transform: `translateY(${-getOffset(state.prevValue, value, unit)}00%)`, + }; + } + return ( + onTransitionEnd()} + > + {unitNodes} + + ); + }; + }, +}); diff --git a/components/badge/index.ts b/components/badge/index.ts index 0979058c5..e8de2f9ef 100644 --- a/components/badge/index.ts +++ b/components/badge/index.ts @@ -1,3 +1,14 @@ +import { App, Plugin } from 'vue'; import Badge from './Badge'; +import Ribbon from './Ribbon'; -export default Badge; +Badge.install = function(app: App) { + app.component(Badge.name, Badge); + app.component(Ribbon.name, Ribbon); + return app; +}; + +export default Badge as typeof Badge & + Plugin & { + readonly Ribbon: typeof Ribbon; + }; diff --git a/components/badge/style/index.less b/components/badge/style/index.less index 1bed0c228..435f08a66 100644 --- a/components/badge/style/index.less +++ b/components/badge/style/index.less @@ -9,10 +9,10 @@ position: relative; display: inline-block; - color: unset; line-height: 1; &-count { + z-index: @zindex-badge; min-width: @badge-height; height: @badge-height; padding: 0 6px; @@ -22,7 +22,7 @@ line-height: @badge-height; white-space: nowrap; text-align: center; - background: @highlight-color; + background: @badge-color; border-radius: (@badge-height / 2); box-shadow: 0 0 0 1px @shadow-color-inverse; a, @@ -45,7 +45,9 @@ } &-dot { + z-index: @zindex-badge; width: @badge-dot-size; + min-width: @badge-dot-size; height: @badge-dot-size; background: @highlight-color; border-radius: 100%; @@ -58,9 +60,12 @@ position: absolute; top: 0; right: 0; - z-index: @zindex-badge; transform: translate(50%, -50%); transform-origin: 100% 0%; + + &.@{iconfont-css-prefix}-spin { + animation: antBadgeLoadingCircle 1s infinite linear; + } } &-status { @@ -124,24 +129,39 @@ &-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; } + .@{number-prefix-cls}-custom-component { + transform: none; + } + + .@{number-prefix-cls}-custom-component, .@{ant-prefix}-scroll-number { position: relative; top: auto; display: block; + transform-origin: 50% 50%; } .@{badge-prefix-cls}-count { @@ -161,15 +181,25 @@ } } +// Safari will blink with transform when inner element has absolute style. +.safari-fix-motion() { + -webkit-transform-style: preserve-3d; + -webkit-backface-visibility: hidden; +} + .@{number-prefix-cls} { overflow: hidden; &-only { + position: relative; display: inline-block; height: @badge-height; - transition: all 0.3s @ease-in-out; + 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; } } @@ -198,4 +228,36 @@ } } +@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/rtl.less b/components/badge/style/rtl.less new file mode 100644 index 000000000..40c1b30f4 --- /dev/null +++ b/components/badge/style/rtl.less @@ -0,0 +1,104 @@ +.@{badge-prefix-cls} { + &-rtl { + direction: rtl; + } + + &-count, + &-dot, + .@{number-prefix-cls}-custom-component { + .@{badge-prefix-cls}-rtl & { + right: auto; + left: 0; + direction: ltr; + transform: translate(-50%, -50%); + transform-origin: 0% 0%; + } + } + + .@{badge-prefix-cls}-rtl& .@{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; + } + } + } + + &-zoom-appear, + &-zoom-enter { + .@{badge-prefix-cls}-rtl & { + animation-name: antZoomBadgeInRtl; + } + } + + &-zoom-leave { + .@{badge-prefix-cls}-rtl & { + animation-name: antZoomBadgeOutRtl; + } + } + + &-not-a-wrapper { + .@{badge-prefix-cls}-count { + transform: none; + } + } +} + +.@{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 index de602ff63..21bebac2e 100644 --- a/components/badge/utils.ts +++ b/components/badge/utils.ts @@ -1,5 +1,5 @@ import { PresetColorTypes } from '../_util/colors'; export function isPresetColor(color?: string): boolean { - return (PresetColorTypes as string[]).indexOf(color) !== -1; + return (PresetColorTypes as any[]).indexOf(color) !== -1; } diff --git a/examples/index.html b/examples/index.html index 4f11be55b..31ba9fc6c 100644 --- a/examples/index.html +++ b/examples/index.html @@ -1,30 +1,27 @@ - - - - - - - - - Ant Design Vue - - - - - -
- - + + + + + + + + + Ant Design Vue + + + + + + +
+ + + \ No newline at end of file diff --git a/v3-changelog.md b/v3-changelog.md new file mode 100644 index 000000000..e69de29bb