feat: add ribbon (#3681)
* feat: add ribbon * feat: ribbon support text slot * test: add ribbon testpull/3717/head
parent
765b420651
commit
483e177e13
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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;
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import Badge from './Badge';
|
||||
import { withInstall } from '../_util/type';
|
||||
|
||||
export default withInstall(Badge);
|
||||
export default Badge;
|
||||
|
|
|
@ -188,3 +188,5 @@
|
|||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@import './ribbon';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
import { PresetColorTypes } from '../_util/colors';
|
||||
|
||||
export function isPresetColor(color?: string): boolean {
|
||||
return (PresetColorTypes as any[]).indexOf(color) !== -1;
|
||||
}
|
Loading…
Reference in New Issue