Skip to content

Commit e442b0d

Browse files
committed
refactor: badge
1 parent 372ac5c commit e442b0d

File tree

10 files changed

+588
-441
lines changed

10 files changed

+588
-441
lines changed

components/badge/Badge.tsx

Lines changed: 152 additions & 178 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,16 @@ import classNames from '../_util/classNames';
44
import { getPropsSlot, flattenChildren } from '../_util/props-util';
55
import { cloneElement } from '../_util/vnode';
66
import { getTransitionProps, Transition } from '../_util/transition';
7-
import isNumeric from '../_util/isNumeric';
8-
import { defaultConfigProvider } from '../config-provider';
9-
import {
10-
inject,
11-
defineComponent,
12-
ExtractPropTypes,
13-
CSSProperties,
14-
VNode,
15-
App,
16-
Plugin,
17-
reactive,
18-
computed,
19-
} from 'vue';
7+
import { defineComponent, ExtractPropTypes, CSSProperties, computed, ref, watch } from 'vue';
208
import { tuple } from '../_util/type';
219
import Ribbon from './Ribbon';
2210
import { isPresetColor } from './utils';
11+
import useConfigInject from '../_util/hooks/useConfigInject';
12+
import isNumeric from '../_util/isNumeric';
2313

2414
export const badgeProps = {
2515
/** Number to show in badge */
26-
count: PropTypes.VNodeChild,
16+
count: PropTypes.any,
2717
showZero: PropTypes.looseBool,
2818
/** Max count to show */
2919
overflowCount: PropTypes.number.def(99),
@@ -43,205 +33,189 @@ export const badgeProps = {
4333

4434
export type BadgeProps = Partial<ExtractPropTypes<typeof badgeProps>>;
4535

46-
const Badge = defineComponent({
36+
export default defineComponent({
4737
name: 'ABadge',
4838
Ribbon,
4939
props: badgeProps,
50-
setup(props, { slots }) {
51-
const configProvider = inject('configProvider', defaultConfigProvider);
52-
const state = reactive({
53-
badgeCount: undefined,
40+
slots: ['text', 'count'],
41+
setup(props, { slots, attrs }) {
42+
const { prefixCls, direction } = useConfigInject('badge', props);
43+
44+
// ================================ Misc ================================
45+
const numberedDisplayCount = computed(() => {
46+
return ((props.count as number) > (props.overflowCount as number)
47+
? `${props.overflowCount}+`
48+
: props.count) as string | number | null;
5449
});
5550

56-
const getNumberedDispayCount = () => {
57-
const { overflowCount } = props;
58-
const count = state.badgeCount;
59-
const displayCount = count > overflowCount ? `${overflowCount}+` : count;
60-
return displayCount;
61-
};
62-
63-
const getDispayCount = computed(() => {
64-
// dot mode don't need count
65-
if (isDot.value) {
66-
return '';
67-
}
68-
return getNumberedDispayCount();
69-
});
70-
71-
const getScrollNumberTitle = () => {
72-
const { title } = props;
73-
const count = state.badgeCount;
74-
if (title) {
75-
return title;
76-
}
77-
return typeof count === 'string' || typeof count === 'number' ? count : undefined;
78-
};
79-
80-
const getStyleWithOffset = () => {
81-
const { offset, numberStyle } = props;
82-
return offset
83-
? {
84-
right: `${-parseInt(offset[0] as string, 10)}px`,
85-
marginTop: isNumeric(offset[1]) ? `${offset[1]}px` : offset[1],
86-
...numberStyle,
87-
}
88-
: { ...numberStyle };
89-
};
51+
const hasStatus = computed(
52+
() =>
53+
(props.status !== null && props.status !== undefined) ||
54+
(props.color !== null && props.color !== undefined),
55+
);
9056

91-
const hasStatus = computed(() => {
92-
const { status, color } = props;
93-
return !!status || !!color;
94-
});
57+
const isZero = computed(
58+
() => numberedDisplayCount.value === '0' || numberedDisplayCount.value === 0,
59+
);
9560

96-
const isZero = computed(() => {
97-
const numberedDispayCount = getNumberedDispayCount();
98-
return numberedDispayCount === '0' || numberedDispayCount === 0;
99-
});
61+
const showAsDot = computed(() => (props.dot && !isZero.value) || hasStatus.value);
10062

101-
const isDot = computed(() => {
102-
const { dot } = props;
103-
return (dot && !isZero.value) || hasStatus.value;
104-
});
63+
const mergedCount = computed(() => (showAsDot.value ? '' : numberedDisplayCount.value));
10564

10665
const isHidden = computed(() => {
107-
const { showZero } = props;
10866
const isEmpty =
109-
getDispayCount.value === null ||
110-
getDispayCount.value === undefined ||
111-
getDispayCount.value === '';
112-
return (isEmpty || (isZero.value && !showZero)) && !isDot.value;
67+
mergedCount.value === null || mergedCount.value === undefined || mergedCount.value === '';
68+
return (isEmpty || (isZero.value && !props.showZero)) && !showAsDot.value;
11369
});
11470

115-
const renderStatusText = (prefixCls: string) => {
116-
const text = getPropsSlot(slots, props, 'text');
117-
const hidden = isHidden.value;
118-
return hidden || !text ? null : <span class={`${prefixCls}-status-text`}>{text}</span>;
119-
};
71+
// Count should be cache in case hidden change it
72+
const livingCount = ref(props.count);
73+
74+
// We need cache count since remove motion should not change count display
75+
const displayCount = ref(mergedCount.value);
76+
77+
// We will cache the dot status to avoid shaking on leaved motion
78+
const isDotRef = ref(showAsDot.value);
79+
80+
watch(
81+
[() => props.count, mergedCount, showAsDot],
82+
() => {
83+
if (!isHidden.value) {
84+
livingCount.value = props.count;
85+
displayCount.value = mergedCount.value;
86+
isDotRef.value = showAsDot.value;
87+
}
88+
},
89+
{ immediate: true },
90+
);
91+
92+
// Shared styles
93+
const statusCls = computed(() => ({
94+
[`${prefixCls.value}-status-dot`]: hasStatus.value,
95+
[`${prefixCls.value}-status-${props.status}`]: !!props.status,
96+
[`${prefixCls.value}-status-${props.color}`]: isPresetColor(props.color),
97+
}));
98+
99+
const statusStyle = computed(() => {
100+
if (props.color && !isPresetColor(props.color)) {
101+
return { background: props.color };
102+
} else {
103+
return {};
104+
}
105+
});
120106

121-
const getBadgeClassName = (prefixCls: string, children: VNode[]) => {
122-
const status = hasStatus.value;
123-
return classNames(prefixCls, {
124-
[`${prefixCls}-status`]: status,
125-
[`${prefixCls}-dot-status`]: status && props.dot && !isZero.value,
126-
[`${prefixCls}-not-a-wrapper`]: !children.length,
127-
});
128-
};
107+
const scrollNumberCls = computed(() => ({
108+
[`${prefixCls.value}-dot`]: isDotRef.value,
109+
[`${prefixCls.value}-count`]: !isDotRef.value,
110+
[`${prefixCls.value}-count-sm`]: props.size === 'small',
111+
[`${prefixCls.value}-multiple-words`]:
112+
!isDotRef.value && displayCount.value && displayCount.value.toString().length > 1,
113+
[`${prefixCls.value}-status-${status}`]: !!status,
114+
[`${prefixCls.value}-status-${props.color}`]: isPresetColor(props.color),
115+
}));
129116

130-
const renderDispayComponent = () => {
131-
const count = state.badgeCount;
132-
const customNode = count;
133-
if (!customNode || typeof customNode !== 'object') {
134-
return undefined;
135-
}
136-
return cloneElement(
137-
customNode,
117+
return () => {
118+
const { offset, title, color } = props;
119+
const style = attrs.style as CSSProperties;
120+
const text = getPropsSlot(slots, props, 'text');
121+
const pre = prefixCls.value;
122+
const count = livingCount.value;
123+
let children = flattenChildren(slots.default?.());
124+
children = children.length ? children : null;
125+
126+
const visible = !!(!isHidden.value || slots.count);
127+
128+
// =============================== Styles ===============================
129+
const mergedStyle = (() => {
130+
if (!offset) {
131+
return { ...style };
132+
}
133+
134+
const offsetStyle: CSSProperties = {
135+
marginTop: isNumeric(offset[1]) ? `${offset[1]}px` : offset[1],
136+
};
137+
if (direction.value === 'rtl') {
138+
offsetStyle.left = `${parseInt(offset[0] as string, 10)}px`;
139+
} else {
140+
offsetStyle.right = `${-parseInt(offset[0] as string, 10)}px`;
141+
}
142+
143+
return {
144+
...offsetStyle,
145+
...style,
146+
};
147+
})();
148+
149+
// =============================== Render ===============================
150+
// >>> Title
151+
const titleNode =
152+
title ?? (typeof count === 'string' || typeof count === 'number' ? count : undefined);
153+
154+
// >>> Status Text
155+
const statusTextNode =
156+
visible || !text ? null : <span class={`${pre}-status-text`}>{text}</span>;
157+
158+
// >>> Display Component
159+
const displayNode = cloneElement(
160+
slots.count?.(),
138161
{
139-
style: getStyleWithOffset(),
162+
style: mergedStyle,
140163
},
141164
false,
142165
);
143-
};
144-
145-
const renderBadgeNumber = (prefixCls: string, scrollNumberPrefixCls: string) => {
146-
const { status, color, size } = props;
147-
const count = state.badgeCount;
148-
const displayCount = getDispayCount.value;
149-
150-
const scrollNumberCls = {
151-
[`${prefixCls}-dot`]: isDot.value,
152-
[`${prefixCls}-count`]: !isDot.value,
153-
[`${prefixCls}-count-sm`]: size === 'small',
154-
[`${prefixCls}-multiple-words`]:
155-
!isDot.value && count && count.toString && count.toString().length > 1,
156-
[`${prefixCls}-status-${status}`]: !!status,
157-
[`${prefixCls}-status-${color}`]: isPresetColor(color),
158-
};
159-
160-
let statusStyle = getStyleWithOffset();
161-
if (color && !isPresetColor(color)) {
162-
statusStyle = statusStyle || {};
163-
statusStyle.background = color;
164-
}
165166

166-
return isHidden.value ? null : (
167-
<ScrollNumber
168-
prefixCls={scrollNumberPrefixCls}
169-
data-show={!isHidden.value}
170-
v-show={!isHidden.value}
171-
class={scrollNumberCls}
172-
count={displayCount}
173-
displayComponent={renderDispayComponent()}
174-
title={getScrollNumberTitle()}
175-
style={statusStyle}
176-
key="scrollNumber"
177-
/>
167+
const badgeClassName = classNames(
168+
pre,
169+
{
170+
[`${pre}-status`]: hasStatus.value,
171+
[`${pre}-not-a-wrapper`]: !children,
172+
[`${pre}-rtl`]: direction.value === 'rtl',
173+
},
174+
attrs.class,
178175
);
179-
};
180176

181-
return () => {
182-
const {
183-
prefixCls: customizePrefixCls,
184-
scrollNumberPrefixCls: customizeScrollNumberPrefixCls,
185-
status,
186-
color,
187-
} = props;
188-
189-
const text = getPropsSlot(slots, props, 'text');
190-
const getPrefixCls = configProvider.getPrefixCls;
191-
const prefixCls = getPrefixCls('badge', customizePrefixCls);
192-
const scrollNumberPrefixCls = getPrefixCls('scroll-number', customizeScrollNumberPrefixCls);
193-
194-
const children = flattenChildren(slots.default?.());
195-
let count = getPropsSlot(slots, props, 'count');
196-
if (Array.isArray(count)) {
197-
count = count[0];
198-
}
199-
state.badgeCount = count;
200-
const scrollNumber = renderBadgeNumber(prefixCls, scrollNumberPrefixCls);
201-
const statusText = renderStatusText(prefixCls);
202-
const statusCls = classNames({
203-
[`${prefixCls}-status-dot`]: hasStatus.value,
204-
[`${prefixCls}-status-${status}`]: !!status,
205-
[`${prefixCls}-status-${color}`]: isPresetColor(color),
206-
});
207-
const statusStyle: CSSProperties = {};
208-
if (color && !isPresetColor(color)) {
209-
statusStyle.background = color;
210-
}
211177
// <Badge status="success" />
212-
if (!children.length && hasStatus.value) {
213-
const styleWithOffset = getStyleWithOffset();
214-
const statusTextColor = styleWithOffset && styleWithOffset.color;
178+
if (!children && hasStatus.value) {
179+
const statusTextColor = mergedStyle.color;
215180
return (
216-
<span class={getBadgeClassName(prefixCls, children)} style={styleWithOffset}>
217-
<span class={statusCls} style={statusStyle} />
218-
<span style={{ color: statusTextColor }} class={`${prefixCls}-status-text`}>
181+
<span {...attrs} class={badgeClassName} style={mergedStyle}>
182+
<span class={statusCls.value} style={statusStyle.value} />
183+
<span style={{ color: statusTextColor }} class={`${pre}-status-text`}>
219184
{text}
220185
</span>
221186
</span>
222187
);
223188
}
224189

225-
const transitionProps = getTransitionProps(children.length ? `${prefixCls}-zoom` : '');
190+
const transitionProps = getTransitionProps(children ? `${pre}-zoom` : '', {
191+
appear: false,
192+
});
193+
let scrollNumberStyle: CSSProperties = { ...mergedStyle, ...props.numberStyle };
194+
if (color && !isPresetColor(color)) {
195+
scrollNumberStyle = scrollNumberStyle || {};
196+
scrollNumberStyle.background = color;
197+
}
226198

227199
return (
228-
<span class={getBadgeClassName(prefixCls, children)}>
200+
<span {...attrs} class={badgeClassName}>
229201
{children}
230-
<Transition {...transitionProps}>{scrollNumber}</Transition>
231-
{statusText}
202+
<Transition {...transitionProps}>
203+
<ScrollNumber
204+
v-show={visible}
205+
prefixCls={props.scrollNumberPrefixCls}
206+
show={visible}
207+
class={scrollNumberCls.value}
208+
count={displayCount.value}
209+
title={titleNode}
210+
style={scrollNumberStyle}
211+
key="scrollNumber"
212+
>
213+
{displayNode}
214+
</ScrollNumber>
215+
</Transition>
216+
{statusTextNode}
232217
</span>
233218
);
234219
};
235220
},
236221
});
237-
238-
Badge.install = function(app: App) {
239-
app.component(Badge.name, Badge);
240-
app.component(Badge.Ribbon.displayName, Badge.Ribbon);
241-
return app;
242-
};
243-
244-
export default Badge as typeof Badge &
245-
Plugin & {
246-
readonly Ribbon: typeof Ribbon;
247-
};

0 commit comments

Comments
 (0)