feat: add ribbon (#3681)

* feat: add ribbon

* feat: ribbon support text slot

* test: add ribbon test
pull/3717/head
zkwolf 2021-02-25 13:39:39 +08:00 committed by GitHub
parent 765b420651
commit 483e177e13
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 239 additions and 4 deletions

View File

@ -7,8 +7,9 @@ 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 } from 'vue';
import { inject, defineComponent, CSSProperties, VNode, App, Plugin } from 'vue';
import { tuple } from '../_util/type';
import Ribbon from './Ribbon';
const BadgeProps = {
/** Number to show in badge */
@ -30,8 +31,10 @@ const BadgeProps = {
function isPresetColor(color?: string): boolean {
return (PresetColorTypes as string[]).indexOf(color) !== -1;
}
export default defineComponent({
const Badge = defineComponent({
name: 'ABadge',
Ribbon: Ribbon,
props: initDefaultProps(BadgeProps, {
showZero: false,
dot: false,
@ -225,3 +228,14 @@ export default defineComponent({
);
},
});
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;
};

View File

@ -0,0 +1,60 @@
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 PropTypes from '../_util/vue-types';
type RibbonPlacement = 'start' | 'end';
export interface RibbonProps extends HTMLAttributes {
prefixCls?: string;
text?: VNodeTypes;
color?: LiteralUnion<PresetColorType, string>;
placement?: RibbonPlacement;
}
const Ribbon: FunctionalComponent<RibbonProps> = (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 (
<div class={`${prefixCls}-wrapper`}>
{children}
<div class={ribbonCls} style={{ ...colorStyle, ...(style as CSSProperties) }}>
<span class={`${prefixCls}-text`}>{text}</span>
<div class={`${prefixCls}-corner`} style={cornerColorStyle} />
</div>
</div>
);
};
Ribbon.displayName = 'ABadgeRibbon';
Ribbon.inheritAttrs = false;
Ribbon.props = {
prefix: PropTypes.string,
color: PropTypes.string,
text: PropTypes.any,
placement: PropTypes.oneOf(tuple('start', 'end')),
};
export default Ribbon;

View File

@ -1,5 +1,6 @@
import { mount } from '@vue/test-utils';
import Badge from '../index';
import mountTest from '../../../tests/shared/mountTest';
import { asyncExpect } from '@/tests/utils';
describe('Badge', () => {
@ -147,3 +148,76 @@ describe('Badge', () => {
expect(wrapper.html()).toMatchSnapshot();
});
});
describe('Ribbon', () => {
mountTest(Badge.Ribbon);
describe('placement', () => {
it('works with `start` & `end` placement', () => {
const wrapperStart = mount({
render() {
return (
<Badge.Ribbon placement="start">
<div />
</Badge.Ribbon>
);
},
});
expect(wrapperStart.findAll('.ant-ribbon-placement-start').length).toEqual(1);
const wrapperEnd = mount({
render() {
return (
<Badge.Ribbon placement="end">
<div />
</Badge.Ribbon>
);
},
});
expect(wrapperEnd.findAll('.ant-ribbon-placement-end').length).toEqual(1);
});
});
describe('color', () => {
it('works with preset color', () => {
const wrapper = mount({
render() {
return (
<Badge.Ribbon color="green">
<div />
</Badge.Ribbon>
);
},
});
expect(wrapper.findAll('.ant-ribbon-color-green').length).toEqual(1);
});
});
describe('text', () => {
it('works with string', () => {
const wrapper = mount({
render() {
return (
<Badge.Ribbon text="cool">
<div />
</Badge.Ribbon>
);
},
});
expect(wrapper.find('.ant-ribbon').text()).toEqual('cool');
});
it('works with element', () => {
const wrapper = mount({
render() {
return (
<Badge.Ribbon text={<span class="cool" />}>
<div />
</Badge.Ribbon>
);
},
});
expect(wrapper.findAll('.cool').length).toEqual(1);
});
});
});

View File

@ -1,4 +1,3 @@
import Badge from './Badge';
import { withInstall } from '../_util/type';
export default withInstall(Badge);
export default Badge;

View File

@ -188,3 +188,5 @@
opacity: 0;
}
}
@import './ribbon';

View File

@ -0,0 +1,81 @@
@import '../../style/themes/index';
@import '../../style/mixins/index';
@ribbon-prefix-cls: ~'@{ant-prefix}-ribbon';
@ribbon-wrapper-prefix-cls: ~'@{ant-prefix}-ribbon-wrapper';
.@{ribbon-wrapper-prefix-cls} {
position: relative;
}
.@{ribbon-prefix-cls} {
.reset-component();
position: absolute;
top: 8px;
height: 22px;
padding: 0 8px;
color: @badge-text-color;
line-height: 22px;
white-space: nowrap;
background-color: @primary-color;
border-radius: @border-radius-sm;
&-text {
color: @white;
}
&-corner {
position: absolute;
top: 100%;
width: 8px;
height: 8px;
color: currentColor;
border: 4px solid;
transform: scaleY(0.75);
transform-origin: top;
// If not support IE 11, use filter: brightness(75%) instead
&::after {
position: absolute;
top: -4px;
left: -4px;
width: inherit;
height: inherit;
color: rgba(0, 0, 0, 0.25);
border: inherit;
content: '';
}
}
// colors
// mixin to iterate over colors and create CSS class for each one
.make-color-classes(@i: length(@preset-colors)) when (@i > 0) {
.make-color-classes(@i - 1);
@color: extract(@preset-colors, @i);
@darkColor: '@{color}-6';
&-color-@{color} {
color: @@darkColor;
background: @@darkColor;
}
}
.make-color-classes();
// placement
&.@{ribbon-prefix-cls}-placement-end {
right: -8px;
border-bottom-right-radius: 0;
.@{ribbon-prefix-cls}-corner {
right: 0;
border-color: currentColor transparent transparent currentColor;
}
}
&.@{ribbon-prefix-cls}-placement-start {
left: -8px;
border-bottom-left-radius: 0;
.@{ribbon-prefix-cls}-corner {
left: 0;
border-color: currentColor currentColor transparent transparent;
}
}
}

View File

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