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 { getTransitionProps, Transition } from '../_util/transition';
|
||||||
import isNumeric from '../_util/isNumeric';
|
import isNumeric from '../_util/isNumeric';
|
||||||
import { defaultConfigProvider } from '../config-provider';
|
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 { tuple } from '../_util/type';
|
||||||
|
import Ribbon from './Ribbon';
|
||||||
|
|
||||||
const BadgeProps = {
|
const BadgeProps = {
|
||||||
/** Number to show in badge */
|
/** Number to show in badge */
|
||||||
|
@ -30,8 +31,10 @@ const BadgeProps = {
|
||||||
function isPresetColor(color?: string): boolean {
|
function isPresetColor(color?: string): boolean {
|
||||||
return (PresetColorTypes as string[]).indexOf(color) !== -1;
|
return (PresetColorTypes as string[]).indexOf(color) !== -1;
|
||||||
}
|
}
|
||||||
export default defineComponent({
|
|
||||||
|
const Badge = defineComponent({
|
||||||
name: 'ABadge',
|
name: 'ABadge',
|
||||||
|
Ribbon: Ribbon,
|
||||||
props: initDefaultProps(BadgeProps, {
|
props: initDefaultProps(BadgeProps, {
|
||||||
showZero: false,
|
showZero: false,
|
||||||
dot: 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 { mount } from '@vue/test-utils';
|
||||||
import Badge from '../index';
|
import Badge from '../index';
|
||||||
|
import mountTest from '../../../tests/shared/mountTest';
|
||||||
|
|
||||||
import { asyncExpect } from '@/tests/utils';
|
import { asyncExpect } from '@/tests/utils';
|
||||||
describe('Badge', () => {
|
describe('Badge', () => {
|
||||||
|
@ -147,3 +148,76 @@ describe('Badge', () => {
|
||||||
expect(wrapper.html()).toMatchSnapshot();
|
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 Badge from './Badge';
|
||||||
import { withInstall } from '../_util/type';
|
|
||||||
|
|
||||||
export default withInstall(Badge);
|
export default Badge;
|
||||||
|
|
|
@ -188,3 +188,5 @@
|
||||||
opacity: 0;
|
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