From 31d641e6232bcba99672374a3bd188f5c1896ca1 Mon Sep 17 00:00:00 2001 From: zkwolf Date: Thu, 4 Feb 2021 16:53:57 +0800 Subject: [PATCH 1/3] feat: add ribbon --- components/badge/Badge.tsx | 18 ++++++- components/badge/Ribbon.tsx | 52 +++++++++++++++++++ components/badge/index.ts | 3 +- components/badge/style/index.less | 2 + components/badge/style/ribbon.less | 81 ++++++++++++++++++++++++++++++ components/badge/utils.ts | 5 ++ 6 files changed, 157 insertions(+), 4 deletions(-) create mode 100644 components/badge/Ribbon.tsx create mode 100644 components/badge/style/ribbon.less create mode 100644 components/badge/utils.ts diff --git a/components/badge/Badge.tsx b/components/badge/Badge.tsx index ab004ef627..46040da43c 100644 --- a/components/badge/Badge.tsx +++ b/components/badge/Badge.tsx @@ -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; + }; diff --git a/components/badge/Ribbon.tsx b/components/badge/Ribbon.tsx new file mode 100644 index 0000000000..37e76ace84 --- /dev/null +++ b/components/badge/Ribbon.tsx @@ -0,0 +1,52 @@ +import { LiteralUnion } 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'; + +type RibbonPlacement = 'start' | 'end'; + +export interface RibbonProps extends HTMLAttributes { + prefixCls?: string; + text?: VNodeTypes; + color?: LiteralUnion; + placement?: RibbonPlacement; +} + +const Ribbon: FunctionalComponent = (props, { attrs, slots }) => { + const { prefixCls: customizePrefixCls, color, 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 ( +
+ {children} +
+ {text} +
+
+
+ ); +}; + +Ribbon.displayName = 'ABadgeRibbon'; + +export default Ribbon; diff --git a/components/badge/index.ts b/components/badge/index.ts index b90ac21b5d..0979058c57 100644 --- a/components/badge/index.ts +++ b/components/badge/index.ts @@ -1,4 +1,3 @@ import Badge from './Badge'; -import { withInstall } from '../_util/type'; -export default withInstall(Badge); +export default Badge; diff --git a/components/badge/style/index.less b/components/badge/style/index.less index 8a011e31b3..a10e8918c6 100644 --- a/components/badge/style/index.less +++ b/components/badge/style/index.less @@ -188,3 +188,5 @@ opacity: 0; } } + +@import './ribbon'; diff --git a/components/badge/style/ribbon.less b/components/badge/style/ribbon.less new file mode 100644 index 0000000000..b2ea714c04 --- /dev/null +++ b/components/badge/style/ribbon.less @@ -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; + } + } +} diff --git a/components/badge/utils.ts b/components/badge/utils.ts new file mode 100644 index 0000000000..21bebac2e4 --- /dev/null +++ b/components/badge/utils.ts @@ -0,0 +1,5 @@ +import { PresetColorTypes } from '../_util/colors'; + +export function isPresetColor(color?: string): boolean { + return (PresetColorTypes as any[]).indexOf(color) !== -1; +} From 65c466658e4da8b8218ffbdd913eb4236f91c37f Mon Sep 17 00:00:00 2001 From: zkwolf Date: Mon, 22 Feb 2021 01:43:41 +0800 Subject: [PATCH 2/3] feat: ribbon support text slot --- components/badge/Ribbon.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/components/badge/Ribbon.tsx b/components/badge/Ribbon.tsx index 37e76ace84..a9dbab83c1 100644 --- a/components/badge/Ribbon.tsx +++ b/components/badge/Ribbon.tsx @@ -1,8 +1,9 @@ -import { LiteralUnion } from '../_util/type'; +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'; @@ -14,7 +15,7 @@ export interface RibbonProps extends HTMLAttributes { } const Ribbon: FunctionalComponent = (props, { attrs, slots }) => { - const { prefixCls: customizePrefixCls, color, text, placement = 'end' } = props; + 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); @@ -48,5 +49,12 @@ const Ribbon: FunctionalComponent = (props, { attrs, slots }) => { }; 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; From 4f34757099c29371a67f9d795528054e7bbca609 Mon Sep 17 00:00:00 2001 From: zkwolf Date: Mon, 22 Feb 2021 01:45:55 +0800 Subject: [PATCH 3/3] test: add ribbon test --- components/badge/__tests__/index.test.js | 74 ++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/components/badge/__tests__/index.test.js b/components/badge/__tests__/index.test.js index aa27ec16bd..2b40008100 100644 --- a/components/badge/__tests__/index.test.js +++ b/components/badge/__tests__/index.test.js @@ -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 ( + +
+ + ); + }, + }); + + expect(wrapperStart.findAll('.ant-ribbon-placement-start').length).toEqual(1); + + const wrapperEnd = mount({ + render() { + return ( + +
+ + ); + }, + }); + expect(wrapperEnd.findAll('.ant-ribbon-placement-end').length).toEqual(1); + }); + }); + + describe('color', () => { + it('works with preset color', () => { + const wrapper = mount({ + render() { + return ( + +
+ + ); + }, + }); + expect(wrapper.findAll('.ant-ribbon-color-green').length).toEqual(1); + }); + }); + + describe('text', () => { + it('works with string', () => { + const wrapper = mount({ + render() { + return ( + +
+ + ); + }, + }); + expect(wrapper.find('.ant-ribbon').text()).toEqual('cool'); + }); + it('works with element', () => { + const wrapper = mount({ + render() { + return ( + }> +
+ + ); + }, + }); + expect(wrapper.findAll('.cool').length).toEqual(1); + }); + }); +});