Skip to content

Commit 483e177

Browse files
authored
feat: add ribbon (#3681)
* feat: add ribbon * feat: ribbon support text slot * test: add ribbon test
1 parent 765b420 commit 483e177

File tree

7 files changed

+239
-4
lines changed

7 files changed

+239
-4
lines changed

components/badge/Badge.tsx

+16-2
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ import { cloneElement } from '../_util/vnode';
77
import { getTransitionProps, Transition } from '../_util/transition';
88
import isNumeric from '../_util/isNumeric';
99
import { defaultConfigProvider } from '../config-provider';
10-
import { inject, defineComponent, CSSProperties, VNode } from 'vue';
10+
import { inject, defineComponent, CSSProperties, VNode, App, Plugin } from 'vue';
1111
import { tuple } from '../_util/type';
12+
import Ribbon from './Ribbon';
1213

1314
const BadgeProps = {
1415
/** Number to show in badge */
@@ -30,8 +31,10 @@ const BadgeProps = {
3031
function isPresetColor(color?: string): boolean {
3132
return (PresetColorTypes as string[]).indexOf(color) !== -1;
3233
}
33-
export default defineComponent({
34+
35+
const Badge = defineComponent({
3436
name: 'ABadge',
37+
Ribbon: Ribbon,
3538
props: initDefaultProps(BadgeProps, {
3639
showZero: false,
3740
dot: false,
@@ -225,3 +228,14 @@ export default defineComponent({
225228
);
226229
},
227230
});
231+
232+
Badge.install = function(app: App) {
233+
app.component(Badge.name, Badge);
234+
app.component(Badge.Ribbon.displayName, Badge.Ribbon);
235+
return app;
236+
};
237+
238+
export default Badge as typeof Badge &
239+
Plugin & {
240+
readonly Ribbon: typeof Ribbon;
241+
};

components/badge/Ribbon.tsx

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { LiteralUnion, tuple } from '../_util/type';
2+
import { PresetColorType } from '../_util/colors';
3+
import { isPresetColor } from './utils';
4+
import { defaultConfigProvider } from '../config-provider';
5+
import { HTMLAttributes, FunctionalComponent, VNodeTypes, inject, CSSProperties } from 'vue';
6+
import PropTypes from '../_util/vue-types';
7+
8+
type RibbonPlacement = 'start' | 'end';
9+
10+
export interface RibbonProps extends HTMLAttributes {
11+
prefixCls?: string;
12+
text?: VNodeTypes;
13+
color?: LiteralUnion<PresetColorType, string>;
14+
placement?: RibbonPlacement;
15+
}
16+
17+
const Ribbon: FunctionalComponent<RibbonProps> = (props, { attrs, slots }) => {
18+
const { prefixCls: customizePrefixCls, color, text = slots.text?.(), placement = 'end' } = props;
19+
const { class: className, style } = attrs;
20+
const children = slots.default?.();
21+
const { getPrefixCls, direction } = inject('configProvider', defaultConfigProvider);
22+
23+
const prefixCls = getPrefixCls('ribbon', customizePrefixCls);
24+
const colorInPreset = isPresetColor(color);
25+
const ribbonCls = [
26+
prefixCls,
27+
`${prefixCls}-placement-${placement}`,
28+
{
29+
[`${prefixCls}-rtl`]: direction === 'rtl',
30+
[`${prefixCls}-color-${color}`]: colorInPreset,
31+
},
32+
className,
33+
];
34+
const colorStyle: CSSProperties = {};
35+
const cornerColorStyle: CSSProperties = {};
36+
if (color && !colorInPreset) {
37+
colorStyle.background = color;
38+
cornerColorStyle.color = color;
39+
}
40+
return (
41+
<div class={`${prefixCls}-wrapper`}>
42+
{children}
43+
<div class={ribbonCls} style={{ ...colorStyle, ...(style as CSSProperties) }}>
44+
<span class={`${prefixCls}-text`}>{text}</span>
45+
<div class={`${prefixCls}-corner`} style={cornerColorStyle} />
46+
</div>
47+
</div>
48+
);
49+
};
50+
51+
Ribbon.displayName = 'ABadgeRibbon';
52+
Ribbon.inheritAttrs = false;
53+
Ribbon.props = {
54+
prefix: PropTypes.string,
55+
color: PropTypes.string,
56+
text: PropTypes.any,
57+
placement: PropTypes.oneOf(tuple('start', 'end')),
58+
};
59+
60+
export default Ribbon;

components/badge/__tests__/index.test.js

+74
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { mount } from '@vue/test-utils';
22
import Badge from '../index';
3+
import mountTest from '../../../tests/shared/mountTest';
34

45
import { asyncExpect } from '@/tests/utils';
56
describe('Badge', () => {
@@ -147,3 +148,76 @@ describe('Badge', () => {
147148
expect(wrapper.html()).toMatchSnapshot();
148149
});
149150
});
151+
152+
describe('Ribbon', () => {
153+
mountTest(Badge.Ribbon);
154+
155+
describe('placement', () => {
156+
it('works with `start` & `end` placement', () => {
157+
const wrapperStart = mount({
158+
render() {
159+
return (
160+
<Badge.Ribbon placement="start">
161+
<div />
162+
</Badge.Ribbon>
163+
);
164+
},
165+
});
166+
167+
expect(wrapperStart.findAll('.ant-ribbon-placement-start').length).toEqual(1);
168+
169+
const wrapperEnd = mount({
170+
render() {
171+
return (
172+
<Badge.Ribbon placement="end">
173+
<div />
174+
</Badge.Ribbon>
175+
);
176+
},
177+
});
178+
expect(wrapperEnd.findAll('.ant-ribbon-placement-end').length).toEqual(1);
179+
});
180+
});
181+
182+
describe('color', () => {
183+
it('works with preset color', () => {
184+
const wrapper = mount({
185+
render() {
186+
return (
187+
<Badge.Ribbon color="green">
188+
<div />
189+
</Badge.Ribbon>
190+
);
191+
},
192+
});
193+
expect(wrapper.findAll('.ant-ribbon-color-green').length).toEqual(1);
194+
});
195+
});
196+
197+
describe('text', () => {
198+
it('works with string', () => {
199+
const wrapper = mount({
200+
render() {
201+
return (
202+
<Badge.Ribbon text="cool">
203+
<div />
204+
</Badge.Ribbon>
205+
);
206+
},
207+
});
208+
expect(wrapper.find('.ant-ribbon').text()).toEqual('cool');
209+
});
210+
it('works with element', () => {
211+
const wrapper = mount({
212+
render() {
213+
return (
214+
<Badge.Ribbon text={<span class="cool" />}>
215+
<div />
216+
</Badge.Ribbon>
217+
);
218+
},
219+
});
220+
expect(wrapper.findAll('.cool').length).toEqual(1);
221+
});
222+
});
223+
});

components/badge/index.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
import Badge from './Badge';
2-
import { withInstall } from '../_util/type';
32

4-
export default withInstall(Badge);
3+
export default Badge;

components/badge/style/index.less

+2
Original file line numberDiff line numberDiff line change
@@ -188,3 +188,5 @@
188188
opacity: 0;
189189
}
190190
}
191+
192+
@import './ribbon';

components/badge/style/ribbon.less

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
@import '../../style/themes/index';
2+
@import '../../style/mixins/index';
3+
4+
@ribbon-prefix-cls: ~'@{ant-prefix}-ribbon';
5+
@ribbon-wrapper-prefix-cls: ~'@{ant-prefix}-ribbon-wrapper';
6+
7+
.@{ribbon-wrapper-prefix-cls} {
8+
position: relative;
9+
}
10+
11+
.@{ribbon-prefix-cls} {
12+
.reset-component();
13+
14+
position: absolute;
15+
top: 8px;
16+
height: 22px;
17+
padding: 0 8px;
18+
color: @badge-text-color;
19+
line-height: 22px;
20+
white-space: nowrap;
21+
background-color: @primary-color;
22+
border-radius: @border-radius-sm;
23+
24+
&-text {
25+
color: @white;
26+
}
27+
28+
&-corner {
29+
position: absolute;
30+
top: 100%;
31+
width: 8px;
32+
height: 8px;
33+
color: currentColor;
34+
border: 4px solid;
35+
transform: scaleY(0.75);
36+
transform-origin: top;
37+
// If not support IE 11, use filter: brightness(75%) instead
38+
&::after {
39+
position: absolute;
40+
top: -4px;
41+
left: -4px;
42+
width: inherit;
43+
height: inherit;
44+
color: rgba(0, 0, 0, 0.25);
45+
border: inherit;
46+
content: '';
47+
}
48+
}
49+
50+
// colors
51+
// mixin to iterate over colors and create CSS class for each one
52+
.make-color-classes(@i: length(@preset-colors)) when (@i > 0) {
53+
.make-color-classes(@i - 1);
54+
@color: extract(@preset-colors, @i);
55+
@darkColor: '@{color}-6';
56+
&-color-@{color} {
57+
color: @@darkColor;
58+
background: @@darkColor;
59+
}
60+
}
61+
.make-color-classes();
62+
63+
// placement
64+
&.@{ribbon-prefix-cls}-placement-end {
65+
right: -8px;
66+
border-bottom-right-radius: 0;
67+
.@{ribbon-prefix-cls}-corner {
68+
right: 0;
69+
border-color: currentColor transparent transparent currentColor;
70+
}
71+
}
72+
73+
&.@{ribbon-prefix-cls}-placement-start {
74+
left: -8px;
75+
border-bottom-left-radius: 0;
76+
.@{ribbon-prefix-cls}-corner {
77+
left: 0;
78+
border-color: currentColor currentColor transparent transparent;
79+
}
80+
}
81+
}

components/badge/utils.ts

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { PresetColorTypes } from '../_util/colors';
2+
3+
export function isPresetColor(color?: string): boolean {
4+
return (PresetColorTypes as any[]).indexOf(color) !== -1;
5+
}

0 commit comments

Comments
 (0)