From 389b233a401bb88642a1328fe92afcf7abb6304f Mon Sep 17 00:00:00 2001 From: John Date: Wed, 30 Jun 2021 10:03:17 +0800 Subject: [PATCH 1/2] refactor(3.x/button): use composition api (#4291) * refactor(button): use composition api * refactor: add rtl * refactor: sync antd * refactor: update css * refactor: add dev warning * test: add test case * refactor: export component type * refactor: optimize --- components/_util/hooks/useConfigInject.ts | 3 + .../__snapshots__/index.test.js.snap | 8 +- components/button/__tests__/index.test.js | 55 +++- components/button/__tests__/wave.test.js | 79 +++++ components/button/button-group.tsx | 82 +++-- components/button/button.tsx | 279 +++++++++--------- components/button/buttonTypes.ts | 36 ++- components/button/index.ts | 8 +- components/button/style/index.less | 96 ++++-- components/button/style/mixin.less | 185 +++++++++--- components/button/style/rtl.less | 108 +++++++ components/date-picker/style/Calendar.less | 2 +- components/modal/ActionButton.tsx | 11 +- components/modal/Modal.tsx | 21 +- components/popconfirm/index.tsx | 14 +- components/style/themes/default.less | 12 +- 16 files changed, 715 insertions(+), 284 deletions(-) create mode 100644 components/button/__tests__/wave.test.js create mode 100644 components/button/style/rtl.less diff --git a/components/_util/hooks/useConfigInject.ts b/components/_util/hooks/useConfigInject.ts index 78c1e3b4f..77be368ae 100644 --- a/components/_util/hooks/useConfigInject.ts +++ b/components/_util/hooks/useConfigInject.ts @@ -21,6 +21,7 @@ export default ( form?: ComputedRef<{ requiredMark?: RequiredMark; }>; + autoInsertSpaceInButton: ComputedRef; } => { const configProvider = inject>( 'configProvider', @@ -28,6 +29,7 @@ export default ( ); const prefixCls = computed(() => configProvider.getPrefixCls(name, props.prefixCls)); const direction = computed(() => configProvider.direction); + const autoInsertSpaceInButton = computed(() => configProvider.autoInsertSpaceInButton); const space = computed(() => configProvider.space); const pageHeader = computed(() => configProvider.pageHeader); const form = computed(() => configProvider.form); @@ -42,5 +44,6 @@ export default ( space, pageHeader, form, + autoInsertSpaceInButton, }; }; diff --git a/components/button/__tests__/__snapshots__/index.test.js.snap b/components/button/__tests__/__snapshots__/index.test.js.snap index 845b99e3a..59584b43b 100644 --- a/components/button/__tests__/__snapshots__/index.test.js.snap +++ b/components/button/__tests__/__snapshots__/index.test.js.snap @@ -26,7 +26,7 @@ exports[`Button renders Chinese characters correctly 1`] = ` exports[`Button renders Chinese characters correctly 2`] = ` `; @@ -36,9 +36,9 @@ exports[`Button renders Chinese characters correctly 3`] = ` `; -exports[`Button renders Chinese characters correctly 4`] = ``; +exports[`Button renders Chinese characters correctly 4`] = ``; -exports[`Button renders Chinese characters correctly 5`] = ``; +exports[`Button renders Chinese characters correctly 5`] = ``; exports[`Button renders Chinese characters correctly 6`] = ` ; @@ -247,4 +247,51 @@ describe('Button', () => { wrapper.unmount(); }).not.toThrow(); }); + + it('should warning when pass type=link and ghost=true', () => { + resetWarned(); + const warnSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + mount({ + render() { + return + ); + }, + }); + await asyncExpect(() => { + wrapper.trigger('click'); + }); + await asyncExpect(() => { + expect(onClick).not.toHaveBeenCalled(); + }); + }); }); diff --git a/components/button/__tests__/wave.test.js b/components/button/__tests__/wave.test.js new file mode 100644 index 000000000..d10b2374c --- /dev/null +++ b/components/button/__tests__/wave.test.js @@ -0,0 +1,79 @@ +import Button from '../index'; +import { mount } from '@vue/test-utils'; +import { asyncExpect, sleep } from '@/tests/utils'; + +describe('click wave effect', () => { + async function clickButton(wrapper) { + await asyncExpect(() => { + wrapper.find('.ant-btn').trigger('click'); + }); + wrapper.find('.ant-btn').element.dispatchEvent(new Event('transitionstart')); + await sleep(20); + wrapper.find('.ant-btn').element.dispatchEvent(new Event('animationend')); + await sleep(20); + } + + it('should have click wave effect for primary button', async () => { + const wrapper = mount({ + render() { + return ; + }, + }); + await clickButton(wrapper); + expect(wrapper.find('.ant-btn').attributes('ant-click-animating-without-extra-node')).toBe( + 'true', + ); + }); + + it('should have click wave effect for default button', async () => { + const wrapper = mount({ + render() { + return ; + }, + }); + await clickButton(wrapper); + expect(wrapper.find('.ant-btn').attributes('ant-click-animating-without-extra-node')).toBe( + 'true', + ); + }); + + it('should not have click wave effect for link type button', async () => { + const wrapper = mount({ + render() { + return ; + }, + }); + await clickButton(wrapper); + expect(wrapper.find('.ant-btn').attributes('ant-click-animating-without-extra-node')).toBe( + undefined, + ); + }); + + it('should not have click wave effect for text type button', async () => { + const wrapper = mount({ + render() { + return ; + }, + }); + await clickButton(wrapper); + expect(wrapper.find('.ant-btn').attributes('ant-click-animating-without-extra-node')).toBe( + undefined, + ); + }); + + it('should handle transitionstart', async () => { + const wrapper = mount({ + render() { + return ; + }, + }); + await clickButton(wrapper); + const buttonNode = wrapper.find('.ant-btn').element; + buttonNode.dispatchEvent(new Event('transitionstart')); + expect(wrapper.find('.ant-btn').attributes('ant-click-animating-without-extra-node')).toBe( + 'true', + ); + wrapper.unmount(); + buttonNode.dispatchEvent(new Event('transitionstart')); + }); +}); diff --git a/components/button/button-group.tsx b/components/button/button-group.tsx index 842be3c2d..f6a897d6d 100644 --- a/components/button/button-group.tsx +++ b/components/button/button-group.tsx @@ -1,53 +1,49 @@ -import { defineComponent, inject } from 'vue'; -import { filterEmpty, getSlot } from '../_util/props-util'; +import { defineComponent } from 'vue'; +import { flattenChildren } from '../_util/props-util'; import PropTypes from '../_util/vue-types'; -import { defaultConfigProvider } from '../config-provider'; -import { tuple } from '../_util/type'; +import useConfigInject from '../_util/hooks/useConfigInject'; -const ButtonGroupProps = { +import type { ExtractPropTypes, PropType } from 'vue'; +import type { SizeType } from '../config-provider'; + +const buttonGroupProps = { prefixCls: PropTypes.string, - size: PropTypes.oneOf(tuple('small', 'large', 'default')), + size: { + type: String as PropType, + }, }; -export { ButtonGroupProps }; +export { buttonGroupProps }; + +export type ButtonGroupProps = Partial>; + export default defineComponent({ name: 'AButtonGroup', - props: ButtonGroupProps, - setup() { - const configProvider = inject('configProvider', defaultConfigProvider); - return { - configProvider, - }; - }, - data() { - return { - sizeMap: { - large: 'lg', - small: 'sm', - }, - }; - }, - render() { - const { prefixCls: customizePrefixCls, size } = this; - const getPrefixCls = this.configProvider.getPrefixCls; - const prefixCls = getPrefixCls('btn-group', customizePrefixCls); + props: buttonGroupProps, + setup(props, { slots }) { + const { prefixCls, direction } = useConfigInject('btn-group', props); - // large => lg - // small => sm - let sizeCls = ''; - switch (size) { - case 'large': - sizeCls = 'lg'; - break; - case 'small': - sizeCls = 'sm'; - break; - default: - break; - } - const classes = { - [`${prefixCls}`]: true, - [`${prefixCls}-${sizeCls}`]: sizeCls, + return () => { + const { size } = props; + + // large => lg + // small => sm + let sizeCls = ''; + switch (size) { + case 'large': + sizeCls = 'lg'; + break; + case 'small': + sizeCls = 'sm'; + break; + default: + break; + } + const classes = { + [`${prefixCls.value}`]: true, + [`${prefixCls.value}-${sizeCls}`]: sizeCls, + [`${prefixCls.value}-rtl`]: direction.value === 'rtl', + }; + return
{flattenChildren(slots.default?.())}
; }; - return
{filterEmpty(getSlot(this))}
; }, }); diff --git a/components/button/button.tsx b/components/button/button.tsx index 545100b11..7c39538eb 100644 --- a/components/button/button.tsx +++ b/components/button/button.tsx @@ -1,80 +1,54 @@ -import { defineComponent, inject, Text, VNode } from 'vue'; +import { + computed, + defineComponent, + onBeforeUnmount, + onMounted, + onUpdated, + ref, + Text, + watch, + watchEffect, +} from 'vue'; import Wave from '../_util/wave'; -import LoadingOutlined from '@ant-design/icons-vue/LoadingOutlined'; import buttonTypes from './buttonTypes'; -import { getSlot, getComponent } from '../_util/props-util'; -import { defaultConfigProvider } from '../config-provider'; -// eslint-disable-next-line no-console +import LoadingOutlined from '@ant-design/icons-vue/LoadingOutlined'; +import { flattenChildren, getPropsSlot } from '../_util/props-util'; +import useConfigInject from '../_util/hooks/useConfigInject'; +import devWarning from '../vc-util/devWarning'; + +import type { ButtonType } from './buttonTypes'; +import type { VNode } from 'vue'; + const rxTwoCNChar = /^[\u4e00-\u9fa5]{2}$/; const isTwoCNChar = rxTwoCNChar.test.bind(rxTwoCNChar); const props = buttonTypes(); + +function isUnborderedButtonType(type: ButtonType | undefined) { + return type === 'text' || type === 'link'; +} + export default defineComponent({ name: 'AButton', inheritAttrs: false, __ANT_BUTTON: true, props, - setup() { - return { - configProvider: inject('configProvider', defaultConfigProvider), - children: [], - iconCom: undefined, - delayTimeout: undefined, - }; - }, - data() { - return { - sizeMap: { - large: 'lg', - small: 'sm', - }, - sLoading: false, - hasTwoCNChar: false, - }; - }, - watch: { - loading: { - handler(val, preVal) { - if (preVal && typeof preVal !== 'boolean') { - clearTimeout(this.delayTimeout); - } - if (val && typeof val !== 'boolean' && val.delay) { - this.delayTimeout = setTimeout(() => { - this.sLoading = !!val; - }, val.delay); - } else { - this.sLoading = !!val; - } - }, - immediate: true, - }, - }, - mounted() { - this.fixTwoCNChar(); - }, - updated() { - this.fixTwoCNChar(); - }, - beforeUnmount() { - if (this.delayTimeout) { - clearTimeout(this.delayTimeout); - } - }, - methods: { - getClasses() { - const { - prefixCls: customizePrefixCls, - type, - shape, - size, - hasTwoCNChar, - sLoading, - ghost, - block, - $attrs, - } = this; - const getPrefixCls = this.configProvider.getPrefixCls; - const prefixCls = getPrefixCls('btn', customizePrefixCls); - const autoInsertSpace = this.configProvider.autoInsertSpaceInButton !== false; + slots: ['icon'], + emits: ['click'], + setup(props, { slots, attrs, emit }) { + const { prefixCls, autoInsertSpaceInButton, direction } = useConfigInject('btn', props); + + const buttonNodeRef = ref(null); + const delayTimeout = ref(undefined); + const iconCom = ref(null); + const children = ref([]); + + const sLoading = ref(props.loading); + const hasTwoCNChar = ref(false); + + const autoInsertSpace = computed(() => autoInsertSpaceInButton.value !== false); + + const getClasses = () => { + const { type, shape, size, ghost, block, danger } = props; // large => lg // small => sm @@ -89,43 +63,49 @@ export default defineComponent({ default: break; } - const iconType = sLoading ? 'loading' : this.iconCom; + const iconType = sLoading.value ? 'loading' : iconCom.value; return { - [$attrs.class as string]: $attrs.class, - [`${prefixCls}`]: true, - [`${prefixCls}-${type}`]: type, - [`${prefixCls}-${shape}`]: shape, - [`${prefixCls}-${sizeCls}`]: sizeCls, - [`${prefixCls}-icon-only`]: this.children.length === 0 && iconType, - [`${prefixCls}-loading`]: sLoading, - [`${prefixCls}-background-ghost`]: ghost || type === 'ghost', - [`${prefixCls}-two-chinese-chars`]: hasTwoCNChar && autoInsertSpace, - [`${prefixCls}-block`]: block, + [attrs.class as string]: attrs.class, + [`${prefixCls.value}`]: true, + [`${prefixCls.value}-${type}`]: type, + [`${prefixCls.value}-${shape}`]: shape, + [`${prefixCls.value}-${sizeCls}`]: sizeCls, + [`${prefixCls.value}-icon-only`]: children.value.length === 0 && !!iconType, + [`${prefixCls.value}-loading`]: sLoading.value, + [`${prefixCls.value}-background-ghost`]: ghost && !isUnborderedButtonType(type), + [`${prefixCls.value}-two-chinese-chars`]: hasTwoCNChar.value && autoInsertSpace.value, + [`${prefixCls.value}-block`]: block, + [`${prefixCls.value}-dangerous`]: !!danger, + [`${prefixCls.value}-rtl`]: direction.value === 'rtl', }; - }, - fixTwoCNChar() { + }; + + const fixTwoCNChar = () => { // Fix for HOC usage like - const node = this.$refs.buttonNode as HTMLElement; - if (!node) { + const node = buttonNodeRef.value!; + if (!node || autoInsertSpaceInButton.value === false) { return; } const buttonText = node.textContent; - if (this.isNeedInserted() && isTwoCNChar(buttonText)) { - if (!this.hasTwoCNChar) { - this.hasTwoCNChar = true; + + if (isNeedInserted() && isTwoCNChar(buttonText)) { + if (!hasTwoCNChar.value) { + hasTwoCNChar.value = true; } - } else if (this.hasTwoCNChar) { - this.hasTwoCNChar = false; + } else if (hasTwoCNChar.value) { + hasTwoCNChar.value = false; } - }, - handleClick(event: Event) { - const { sLoading } = this.$data; - if (sLoading) { + }; + const handleClick = (event: Event) => { + // https://github.com/ant-design/ant-design/issues/30207 + if (sLoading.value || props.disabled) { + event.preventDefault(); return; } - this.$emit('click', event); - }, - insertSpace(child: VNode, needInserted: boolean) { + emit('click', event); + }; + + const insertSpace = (child: VNode, needInserted: boolean) => { const SPACE = needInserted ? ' ' : ''; if (child.type === Text) { let text = (child.children as string).trim(); @@ -135,53 +115,86 @@ export default defineComponent({ return {text}; } return child; - }, - isNeedInserted() { - const { iconCom, type } = this; - return this.children.length === 1 && !iconCom && type !== 'link'; - }, - }, - render() { - this.iconCom = getComponent(this, 'icon'); - const { type, htmlType, iconCom, disabled, handleClick, sLoading, href, title, $attrs } = this; - const children = getSlot(this); - this.children = children; - const classes = this.getClasses(); - - const buttonProps = { - ...$attrs, - title, - disabled, - class: classes, - onClick: handleClick, }; - const iconNode = sLoading ? : iconCom; - const autoInsertSpace = this.configProvider.autoInsertSpaceInButton !== false; - const kids = children.map(child => - this.insertSpace(child, this.isNeedInserted() && autoInsertSpace), + const isNeedInserted = () => + children.value.length === 1 && !iconCom.value && !isUnborderedButtonType(props.type); + + watchEffect(() => { + devWarning( + !(props.ghost && isUnborderedButtonType(props.type)), + 'Button', + "`link` or `text` button can't be a `ghost` button.", + ); + }); + + watch( + () => props.loading, + (val, preVal) => { + if (preVal && typeof preVal !== 'boolean') { + clearTimeout(delayTimeout.value); + } + if (val && typeof val !== 'boolean' && val.delay) { + delayTimeout.value = setTimeout(() => { + sLoading.value = !!val; + }, val.delay); + } else { + sLoading.value = !!val; + } + }, + { + immediate: true, + }, ); - if (href !== undefined) { - return ( - + onMounted(fixTwoCNChar); + onUpdated(fixTwoCNChar); + + onBeforeUnmount(() => { + delayTimeout.value && clearTimeout(delayTimeout.value); + }); + + return () => { + iconCom.value = getPropsSlot(slots, props, 'icon'); + children.value = flattenChildren(getPropsSlot(slots, props)); + + const { type, htmlType, disabled, href, title, target } = props; + const classes = getClasses(); + + const buttonProps = { + ...attrs, + title, + disabled, + class: classes, + onClick: handleClick, + }; + const iconNode = sLoading.value ? : iconCom.value; + + const kids = children.value.map((child) => + insertSpace(child, isNeedInserted() && autoInsertSpace.value), + ); + + if (href !== undefined) { + return ( + + {iconNode} + {kids} + + ); + } + + const buttonNode = ( + ); - } - const buttonNode = ( - - ); + if (isUnborderedButtonType(type)) { + return buttonNode; + } - if (type === 'link') { - return buttonNode; - } - - return {buttonNode}; + return {buttonNode}; + }; }, }); diff --git a/components/button/buttonTypes.ts b/components/button/buttonTypes.ts index 6715b5caa..e71a0b696 100644 --- a/components/button/buttonTypes.ts +++ b/components/button/buttonTypes.ts @@ -1,32 +1,48 @@ -import { ExtractPropTypes } from 'vue'; - import { tuple } from '../_util/type'; -import PropTypes, { withUndefined } from '../_util/vue-types'; +import PropTypes from '../_util/vue-types'; -const ButtonTypes = tuple('default', 'primary', 'ghost', 'dashed', 'danger', 'link'); +import type { ExtractPropTypes, PropType } from 'vue'; +import type { SizeType } from '../config-provider'; + +const ButtonTypes = tuple('default', 'primary', 'ghost', 'dashed', 'link', 'text'); export type ButtonType = typeof ButtonTypes[number]; const ButtonShapes = tuple('circle', 'circle-outline', 'round'); export type ButtonShape = typeof ButtonShapes[number]; -const ButtonSizes = tuple('large', 'default', 'small'); -export type ButtonSize = typeof ButtonSizes[number]; + const ButtonHTMLTypes = tuple('submit', 'button', 'reset'); export type ButtonHTMLType = typeof ButtonHTMLTypes[number]; +export type LegacyButtonType = ButtonType | 'danger'; +export function convertLegacyProps(type?: LegacyButtonType): ButtonProps { + if (type === 'danger') { + return { danger: true }; + } + return { type }; +} + const buttonProps = () => ({ prefixCls: PropTypes.string, type: PropTypes.oneOf(ButtonTypes), htmlType: PropTypes.oneOf(ButtonHTMLTypes).def('button'), - // icon: PropTypes.string, shape: PropTypes.oneOf(ButtonShapes), - size: PropTypes.oneOf(ButtonSizes).def('default'), - loading: withUndefined(PropTypes.oneOfType([PropTypes.looseBool, PropTypes.object])), + size: { + type: String as PropType, + }, + loading: { + type: [Boolean, Object], + default: (): boolean | { delay?: number } => false, + }, disabled: PropTypes.looseBool, ghost: PropTypes.looseBool, block: PropTypes.looseBool, + danger: PropTypes.looseBool, icon: PropTypes.VNodeChild, href: PropTypes.string, + target: PropTypes.string, title: PropTypes.string, - onClick: PropTypes.func, + onClick: { + type: Function as PropType<(event: MouseEvent) => void>, + }, }); export type ButtonProps = Partial>>; diff --git a/components/button/index.ts b/components/button/index.ts index 4ac09977f..4b9364911 100644 --- a/components/button/index.ts +++ b/components/button/index.ts @@ -2,10 +2,16 @@ import { App, Plugin } from 'vue'; import Button from './button'; import ButtonGroup from './button-group'; +import type { ButtonProps, ButtonShape, ButtonType } from './buttonTypes'; +import type { ButtonGroupProps } from './button-group'; +import type { SizeType as ButtonSize } from '../config-provider'; + +export type { ButtonProps, ButtonShape, ButtonType, ButtonGroupProps, ButtonSize }; + Button.Group = ButtonGroup; /* istanbul ignore next */ -Button.install = function(app: App) { +Button.install = function (app: App) { app.component(Button.name, Button); app.component(ButtonGroup.name, ButtonGroup); return app; diff --git a/components/button/style/index.less b/components/button/style/index.less index 32fccf33b..97fbeb2ed 100644 --- a/components/button/style/index.less +++ b/components/button/style/index.less @@ -15,22 +15,18 @@ // Fixing https://github.com/ant-design/ant-design/issues/12978 // Fixing https://github.com/ant-design/ant-design/issues/20058 // Fixing https://github.com/ant-design/ant-design/issues/19972 - // Fixing https://github.com/ant-design/ant-design/issues/12978 // Fixing https://github.com/ant-design/ant-design/issues/18107 // Fixing https://github.com/ant-design/ant-design/issues/13214 // It is a render problem of chrome, which is only happened in the codesandbox demo // 0.001px solution works and I don't why - line-height: @line-height-base; + line-height: @btn-line-height; .btn(); .btn-default(); - // Make sure that the target of Button's click event always be `button` - // Ref: https://github.com/ant-design/ant-design/issues/7034 - > i, + // Fix loading button animation + // https://github.com/ant-design/ant-design/issues/24323 > span { display: inline-block; - transition: margin-left 0.3s @ease-in-out; - pointer-events: none; } &-primary { @@ -48,6 +44,7 @@ .@{btn-prefix-cls}-group &:first-child { &:not(:last-child) { border-right-color: @btn-group-border; + &[disabled] { border-right-color: @btn-default-border; } @@ -57,6 +54,7 @@ .@{btn-prefix-cls}-group &:last-child:not(:first-child), .@{btn-prefix-cls}-group & + & { border-left-color: @btn-group-border; + &[disabled] { border-left-color: @btn-default-border; } @@ -71,6 +69,8 @@ .btn-dashed(); } + // type="danger" will deprecated + // use danger instead &-danger { .btn-danger(); } @@ -79,12 +79,29 @@ .btn-link(); } + &-text { + .btn-text(); + } + + &-dangerous { + .btn-danger-default(); + } + + &-dangerous&-primary { + .btn-danger(); + } + + &-dangerous&-link { + .btn-danger-link(); + } + + &-dangerous&-text { + .btn-danger-text(); + } + &-icon-only { .btn-square(@btn-prefix-cls); - - > i { - vertical-align: middle; - } + vertical-align: -1px; } &-round { @@ -94,17 +111,16 @@ } } - &-circle, - &-circle-outline { + &-circle { .btn-circle(@btn-prefix-cls); } &::before { position: absolute; - top: -1px; - right: -1px; - bottom: -1px; - left: -1px; + top: -@btn-border-width; + right: -@btn-border-width; + bottom: -@btn-border-width; + left: -@btn-border-width; z-index: 1; display: none; background: @component-background; @@ -133,10 +149,10 @@ &:not([disabled]) { pointer-events: none; } - } - &&-loading::before { - display: block; + &::before { + display: block; + } } &&-loading:not(&-circle):not(&-circle-outline):not(&-icon-only) { @@ -153,6 +169,25 @@ } } + // & > &-loading-icon { + // transition: all 0.3s @ease-in-out; + + // .@{iconfont-css-prefix} { + // padding-right: @padding-xs; + // animation: none; + // // for smooth button padding transition + // svg { + // animation: loadingCircle 1s infinite linear; + // } + // } + + // &:only-child { + // .@{iconfont-css-prefix} { + // padding-right: 0; + // } + // } + // } + &-group { .btn-group(@btn-prefix-cls); } @@ -166,7 +201,7 @@ // To ensure that a space will be placed between character and `Icon`. > .@{iconfont-css-prefix} + span, > span + .@{iconfont-css-prefix} { - margin-left: 8px; + margin-left: @margin-xs; } &-background-ghost { @@ -183,10 +218,12 @@ .button-variant-ghost(@btn-danger-border); } - &-background-ghost&-link { - .button-variant-ghost(@link-color; transparent); + &-background-ghost&-dangerous { + .button-variant-ghost(@btn-danger-border); + } - color: @component-background; + &-background-ghost&-dangerous&-link { + .button-variant-ghost(@btn-danger-border, transparent); } &-two-chinese-chars::first-letter { @@ -203,16 +240,21 @@ } // https://github.com/ant-design/ant-design/issues/12681 + // same method as Select &:empty { - vertical-align: top; + display: inline-block; + width: 0; + visibility: hidden; + content: '\a0'; } } a.@{btn-prefix-cls} { // Fixing https://github.com/ant-design/ant-design/issues/12978 + // https://github.com/ant-design/ant-design/issues/29978 // It is a render problem of chrome, which is only happened in the codesandbox demo // 0.1px for padding-top solution works and I don't why - padding-top: 0.1px; + padding-top: 0.01px !important; line-height: @btn-height-base - 2px; &-lg { @@ -222,3 +264,5 @@ a.@{btn-prefix-cls} { line-height: @btn-height-sm - 2px; } } + +@import './rtl'; diff --git a/components/button/style/mixin.less b/components/button/style/mixin.less index 90531b968..b02434fd3 100644 --- a/components/button/style/mixin.less +++ b/components/button/style/mixin.less @@ -1,21 +1,22 @@ // mixins for button // ------------------------ -.button-size(@height; @padding; @font-size; @border-radius) { +.button-size(@height; @padding-horizontal; @font-size; @border-radius) { + @padding-vertical: max( + (round(((@height - @font-size * @line-height-base) / 2) * 10) / 10) - @border-width-base, + 0 + ); height: @height; - padding: @padding; + padding: @padding-vertical @padding-horizontal; font-size: @font-size; border-radius: @border-radius; } .button-disabled(@color: @btn-disable-color; @background: @btn-disable-bg; @border: @btn-disable-border) { - &-disabled, - &.disabled, &[disabled] { &, &:hover, &:focus, - &:active, - &.active { + &:active { .button-color(@color; @background; @border); text-shadow: none; @@ -44,8 +45,7 @@ } } - &:active, - &.active { + &:active { & when (@theme = dark) { .button-color( @color; ~`colorPalette('@{background}', 5) `; ~`colorPalette('@{background}', 5) ` @@ -76,8 +76,7 @@ ); } } - &:active, - &.active { + &:active { & when (@theme = dark) { .button-color(@primary-7; @background; @primary-7); } @@ -103,7 +102,7 @@ .button-color(~`colorPalette('@{color}', 5) `; transparent; transparent); } } - & when not(@border = transparent) { + & when not (@border = transparent) { & when (@theme = dark) { .button-color( ~`colorPalette('@{color}', 7) `; transparent; ~`colorPalette('@{color}', 7) ` @@ -116,8 +115,7 @@ } } } - &:active, - &.active { + &:active { & when (@border = transparent) { & when (@theme = dark) { .button-color(~`colorPalette('@{color}', 5) `; transparent; transparent); @@ -143,9 +141,8 @@ } .button-color(@color; @background; @border) { color: @color; - background-color: @background; - border-color: @border; - // a inside Button which only work in Chrome + background: @background; + border-color: @border; // a inside Button which only work in Chrome // http://stackoverflow.com/a/17253457 > a:only-child { color: currentColor; @@ -168,37 +165,34 @@ position: relative; &:hover, &:focus, - &:active, - &.active { + &:active { z-index: 2; } - &:disabled { + &[disabled] { z-index: 0; } } - > .@{btnClassName}-icon-only { + .@{btnClassName}-icon-only { font-size: @font-size-base; } // size &-lg > .@{btnClassName}, &-lg > span > .@{btnClassName} { - .button-size(@btn-height-lg; @btn-padding-lg; @btn-font-size-lg; 0); - line-height: @btn-height-lg - 2px; + .button-size(@btn-height-lg; @btn-padding-horizontal-lg; @btn-font-size-lg; 0); } - &-lg > .@{btnClassName}.@{btnClassName}-icon-only { + &-lg .@{btnClassName}.@{btnClassName}-icon-only { .square(@btn-height-lg); padding-right: 0; padding-left: 0; } &-sm > .@{btnClassName}, &-sm > span > .@{btnClassName} { - .button-size(@btn-height-sm; @btn-padding-sm; @font-size-base; 0); - line-height: @btn-height-sm - 2px; + .button-size(@btn-height-sm; @btn-padding-horizontal-sm; @font-size-base; 0); > .@{iconfont-css-prefix} { font-size: @font-size-base; } } - &-sm > .@{btnClassName}.@{btnClassName}-icon-only { + &-sm .@{btnClassName}.@{btnClassName}-icon-only { .square(@btn-height-sm); padding-right: 0; padding-left: 0; @@ -219,7 +213,9 @@ transition: all 0.3s @ease-in-out; user-select: none; touch-action: manipulation; - .button-size(@btn-height-base; @btn-padding-base; @font-size-base; @btn-border-radius-base); + .button-size( + @btn-height-base; @btn-padding-horizontal-base; @font-size-base; @btn-border-radius-base + ); > .@{iconfont-css-prefix} { line-height: 1; } @@ -235,7 +231,6 @@ outline: 0; box-shadow: none; } - &.disabled, &[disabled] { cursor: not-allowed; > * { @@ -243,10 +238,14 @@ } } &-lg { - .button-size(@btn-height-lg; @btn-padding-lg; @btn-font-size-lg; @btn-border-radius-base); + .button-size( + @btn-height-lg; @btn-padding-horizontal-lg; @btn-font-size-lg; @btn-border-radius-base + ); } &-sm { - .button-size(@btn-height-sm; @btn-padding-sm; @btn-font-size-sm; @btn-border-radius-sm); + .button-size( + @btn-height-sm; @btn-padding-horizontal-sm; @btn-font-size-sm; @btn-border-radius-sm + ); } } // primary button style @@ -258,8 +257,7 @@ .button-variant-other(@btn-default-color; @btn-default-bg; @btn-default-border); &:hover, &:focus, - &:active, - &.active { + &:active { text-decoration: none; background: @btn-default-bg; } @@ -277,10 +275,70 @@ .btn-danger() { .button-variant-primary(@btn-danger-color, @btn-danger-bg); } +// danger default button style +.btn-danger-default() { + .button-color(@error-color, @btn-default-bg, @error-color); + &:hover, + &:focus { + & when (@theme = dark) { + .button-color( + ~`colorPalette('@{error-color}', 7) `; @btn-default-bg; ~`colorPalette('@{error-color}', 7) + ` + ); + } + & when not (@theme = dark) { + .button-color( + ~`colorPalette('@{error-color}', 5) `; @btn-default-bg; ~`colorPalette('@{error-color}', 5) + ` + ); + } + } + &:active { + & when (@theme = dark) { + .button-color( + ~`colorPalette('@{error-color}', 5) `; @btn-default-bg; ~`colorPalette('@{error-color}', 5) + ` + ); + } + & when not (@theme = dark) { + .button-color( + ~`colorPalette('@{error-color}', 7) `; @btn-default-bg; ~`colorPalette('@{error-color}', 7) + ` + ); + } + } + .button-disabled(); +} +// danger link button style +.btn-danger-link() { + .button-variant-other(@error-color, transparent, transparent); + box-shadow: none; + &:hover, + &:focus { + & when (@theme = dark) { + .button-color(~`colorPalette('@{error-color}', 7) `; transparent; transparent); + } + & when not (@theme = dark) { + .button-color(~`colorPalette('@{error-color}', 5) `; transparent; transparent); + } + } + &:active { + & when (@theme = dark) { + .button-color(~`colorPalette('@{error-color}', 5) `; transparent; transparent); + } + & when not (@theme = dark) { + .button-color(~`colorPalette('@{error-color}', 7) `; transparent; transparent); + } + } + .button-disabled(@disabled-color; transparent; transparent); +} // link button style .btn-link() { .button-variant-other(@link-color, transparent, transparent); box-shadow: none; + &:hover { + background: @btn-link-hover-bg; + } &:hover, &:focus, &:active { @@ -288,31 +346,82 @@ } .button-disabled(@disabled-color; transparent; transparent); } +// text button style +.btn-text() { + .button-variant-other(@text-color, transparent, transparent); + box-shadow: none; + &:hover, + &:focus { + color: @text-color; + background: @btn-text-hover-bg; + border-color: transparent; + } + + &:active { + color: @text-color; + background: fadein(@btn-text-hover-bg, 1%); + border-color: transparent; + } + + .button-disabled(@disabled-color; transparent; transparent); +} +.btn-danger-text() { + .button-variant-other(@error-color, transparent, transparent); + box-shadow: none; + &:hover, + &:focus { + & when (@theme = dark) { + .button-color(~`colorPalette('@{error-color}', 7) `; @btn-text-hover-bg; transparent); + } + & when not (@theme = dark) { + .button-color(~`colorPalette('@{error-color}', 5) `; @btn-text-hover-bg; transparent); + } + } + + &:active { + & when (@theme = dark) { + .button-color(~`colorPalette('@{error-color}', 5) `; fadein(@btn-text-hover-bg, 1%); transparent); + } + & when not (@theme = dark) { + .button-color(~`colorPalette('@{error-color}', 7) `; fadein(@btn-text-hover-bg, 1%); transparent); + } + } + .button-disabled(@disabled-color; transparent; transparent); +} // round button .btn-round(@btnClassName: btn) { - .button-size(@btn-circle-size; 0 (@btn-circle-size / 2) ; @font-size-base; @btn-circle-size); + .button-size(@btn-circle-size; (@btn-circle-size / 2); @font-size-base; @btn-circle-size); &.@{btnClassName}-lg { .button-size( - @btn-circle-size-lg; 0 (@btn-circle-size-lg / 2) ; @btn-font-size-lg; @btn-circle-size-lg + @btn-circle-size-lg; (@btn-circle-size-lg / 2); @btn-font-size-lg; @btn-circle-size-lg ); } &.@{btnClassName}-sm { .button-size( - @btn-circle-size-sm; 0 (@btn-circle-size-sm / 2) ; @font-size-base; @btn-circle-size-sm + @btn-circle-size-sm; (@btn-circle-size-sm / 2); @font-size-base; @btn-circle-size-sm ); } } // square button: the content only contains icon .btn-square(@btnClassName: btn) { .square(@btn-square-size); - .button-size(@btn-square-size; 0; @font-size-base + 2px; @btn-border-radius-base); + .button-size(@btn-square-size; 0; @btn-square-only-icon-size; @btn-border-radius-base); + & > * { + font-size: @btn-square-only-icon-size; + } &.@{btnClassName}-lg { .square(@btn-square-size-lg); - .button-size(@btn-square-size-lg; 0; @btn-font-size-lg + 2px; @btn-border-radius-base); + .button-size(@btn-square-size-lg; 0; @btn-square-only-icon-size-lg; @btn-border-radius-base); + & > * { + font-size: @btn-square-only-icon-size-lg; + } } &.@{btnClassName}-sm { .square(@btn-square-size-sm); - .button-size(@btn-square-size-sm; 0; @font-size-base; @btn-border-radius-base); + .button-size(@btn-square-size-sm; 0; @btn-square-only-icon-size-sm; @btn-border-radius-base); + & > * { + font-size: @btn-square-only-icon-size-sm; + } } } // circle button: the content only contains icon diff --git a/components/button/style/rtl.less b/components/button/style/rtl.less new file mode 100644 index 000000000..6cf8b6f96 --- /dev/null +++ b/components/button/style/rtl.less @@ -0,0 +1,108 @@ +.@{btn-prefix-cls} { + &-rtl { + direction: rtl; + } + + &-primary { + .@{btn-prefix-cls}-group &:last-child:not(:first-child), + .@{btn-prefix-cls}-group & + & { + .@{btn-prefix-cls}-group-rtl& { + border-right-color: @btn-group-border; + border-left-color: @btn-default-border; + } + &[disabled] { + .@{btn-prefix-cls}-group-rtl& { + border-right-color: @btn-default-border; + border-left-color: @btn-group-border; + } + } + } + } + + & > &-loading-icon { + .@{iconfont-css-prefix} { + .@{btn-prefix-cls}-rtl& { + padding-right: 0; + padding-left: @margin-xs; + } + } + + &:only-child { + .@{iconfont-css-prefix} { + padding-right: 0; + padding-left: 0; + } + } + } + + > .@{iconfont-css-prefix} + span, + > span + .@{iconfont-css-prefix} { + .@{btn-prefix-cls}-rtl& { + margin-right: 8px; + margin-left: 0; + } + } +} + +// mixin +.btn-group(@btnClassName: btn) { + .@{btnClassName} + .@{btnClassName}, + .@{btnClassName} + &, + span + .@{btnClassName}, + .@{btnClassName} + span, + > span + span, + & + .@{btnClassName}, + & + & { + .@{btnClassName}-rtl&, + .@{btnClassName}-group-rtl& { + margin-right: -1px; + margin-left: auto; + } + } + + &.@{btnClassName}-group-rtl { + direction: rtl; + } + + > .@{btnClassName}:first-child:not(:last-child), + > span:first-child:not(:last-child) > .@{btnClassName} { + .@{btnClassName}-group-rtl& { + border-top-left-radius: 0; + border-top-right-radius: @btn-border-radius-base; + border-bottom-right-radius: @btn-border-radius-base; + border-bottom-left-radius: 0; + } + } + + > .@{btnClassName}:last-child:not(:first-child), + > span:last-child:not(:first-child) > .@{btnClassName} { + .@{btnClassName}-group-rtl& { + border-top-left-radius: @btn-border-radius-base; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-bottom-left-radius: @btn-border-radius-base; + } + } + + &-sm { + > .@{btnClassName}:first-child:not(:last-child), + > span:first-child:not(:last-child) > .@{btnClassName} { + .@{btnClassName}-group-rtl& { + border-top-left-radius: 0; + border-top-right-radius: @btn-border-radius-sm; + border-bottom-right-radius: @btn-border-radius-sm; + border-bottom-left-radius: 0; + } + } + + > .@{btnClassName}:last-child:not(:first-child), + > span:last-child:not(:first-child) > .@{btnClassName} { + .@{btnClassName}-group-rtl& { + border-top-left-radius: @btn-border-radius-sm; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-bottom-left-radius: @btn-border-radius-sm; + } + } + } +} diff --git a/components/date-picker/style/Calendar.less b/components/date-picker/style/Calendar.less index fc287a89f..5dbac6f8c 100644 --- a/components/date-picker/style/Calendar.less +++ b/components/date-picker/style/Calendar.less @@ -396,7 +396,7 @@ .@{calendar-prefix-cls}-ok-btn { .btn(); .btn-primary(); - .button-size(@btn-height-sm; @btn-padding-sm; @font-size-base; @border-radius-base); + .button-size(@btn-height-sm; @btn-padding-horizontal-sm; @font-size-base; @border-radius-base); line-height: @btn-height-sm - 2px; diff --git a/components/modal/ActionButton.tsx b/components/modal/ActionButton.tsx index fd534805e..76a948264 100644 --- a/components/modal/ActionButton.tsx +++ b/components/modal/ActionButton.tsx @@ -1,13 +1,14 @@ -import { defineComponent, ExtractPropTypes } from 'vue'; +import { defineComponent, ExtractPropTypes, PropType } from 'vue'; import PropTypes from '../_util/vue-types'; import Button from '../button'; import BaseMixin from '../_util/BaseMixin'; -import buttonTypes from '../button/buttonTypes'; +import { convertLegacyProps, LegacyButtonType } from '../button/buttonTypes'; import { getSlot, findDOMNode } from '../_util/props-util'; -const ButtonType = buttonTypes().type; const ActionButtonProps = { - type: ButtonType, + type: { + type: String as PropType, + }, actionFn: PropTypes.func, closeModal: PropTypes.func, autofocus: PropTypes.looseBool, @@ -76,7 +77,7 @@ export default defineComponent({ render() { const { type, loading, buttonProps } = this; const props = { - type, + ...convertLegacyProps(type), onClick: this.onClick, loading, ...buttonProps, diff --git a/components/modal/Modal.tsx b/components/modal/Modal.tsx index 6cf4412a2..c0995dac0 100644 --- a/components/modal/Modal.tsx +++ b/components/modal/Modal.tsx @@ -13,15 +13,16 @@ import addEventListener from '../vc-util/Dom/addEventListener'; import { getConfirmLocale } from './locale'; import CloseOutlined from '@ant-design/icons-vue/CloseOutlined'; import Button from '../button'; -import buttonTypes, { ButtonType, ButtonProps as ButtonPropsType } from '../button/buttonTypes'; +import buttonTypes, { + ButtonProps as ButtonPropsType, + convertLegacyProps, + LegacyButtonType, +} from '../button/buttonTypes'; import LocaleReceiver from '../locale-provider/LocaleReceiver'; import { getComponent, getSlot } from '../_util/props-util'; import initDefaultProps from '../_util/props-util/initDefaultProps'; import { defaultConfigProvider } from '../config-provider'; -const ButtonProps = buttonTypes(); -const ButtonType = ButtonProps.type; - let mousePosition: { x: number; y: number } | null = null; // ref: https://github.com/ant-design/ant-design/issues/15795 const getClickPosition = (e: MouseEvent) => { @@ -71,7 +72,9 @@ const modalProps = { /** 确认按钮文字*/ okText: PropTypes.any, /** 确认按钮类型*/ - okType: ButtonType, + okType: { + type: String as PropType, + }, /** 取消按钮文字*/ cancelText: PropTypes.any, icon: PropTypes.any, @@ -112,7 +115,7 @@ export interface ModalFuncProps { centered?: boolean; width?: string | number; okText?: VNodeTypes; - okType?: ButtonType; + okType?: LegacyButtonType; cancelText?: VNodeTypes; icon?: VNodeTypes; /* Deprecated */ @@ -133,9 +136,7 @@ export interface ModalFuncProps { type getContainerFunc = () => HTMLElement; -export type ModalFunc = ( - props: ModalFuncProps, -) => { +export type ModalFunc = (props: ModalFuncProps) => { destroy: () => void; update: (newConfig: ModalFuncProps) => void; }; @@ -190,7 +191,7 @@ export default defineComponent({ const cancelBtnProps = { onClick: this.handleCancel, ...(this.cancelButtonProps || {}) }; const okBtnProps = { onClick: this.handleOk, - type: okType, + ...convertLegacyProps(okType), loading: confirmLoading, ...(this.okButtonProps || {}), }; diff --git a/components/popconfirm/index.tsx b/components/popconfirm/index.tsx index 35ea3951c..5521f9038 100644 --- a/components/popconfirm/index.tsx +++ b/components/popconfirm/index.tsx @@ -1,11 +1,11 @@ import omit from 'omit.js'; -import { defineComponent, inject } from 'vue'; +import { defineComponent, inject, PropType } from 'vue'; import Tooltip from '../tooltip'; import abstractTooltipProps from '../tooltip/abstractTooltipProps'; import PropTypes from '../_util/vue-types'; import { getOptionProps, hasProp, getComponent, mergeProps } from '../_util/props-util'; import BaseMixin from '../_util/BaseMixin'; -import buttonTypes from '../button/buttonTypes'; +import { LegacyButtonType, convertLegacyProps } from '../button/buttonTypes'; import ExclamationCircleFilled from '@ant-design/icons-vue/ExclamationCircleFilled'; import Button from '../button'; import LocaleReceiver from '../locale-provider/LocaleReceiver'; @@ -14,7 +14,6 @@ import { defaultConfigProvider } from '../config-provider'; import { withInstall } from '../_util/type'; const tooltipProps = abstractTooltipProps(); -const btnProps = buttonTypes(); const Popconfirm = defineComponent({ name: 'APopconfirm', @@ -26,7 +25,10 @@ const Popconfirm = defineComponent({ content: PropTypes.any, title: PropTypes.any, trigger: tooltipProps.trigger.def('click'), - okType: btnProps.type.def('primary'), + okType: { + type: String as PropType, + default: 'primary', + }, disabled: PropTypes.looseBool.def(false), okText: PropTypes.any, cancelText: PropTypes.any, @@ -97,7 +99,7 @@ const Popconfirm = defineComponent({ ...cancelButtonProps, }); const okBtnProps = mergeProps({ - type: okType, + ...convertLegacyProps(okType), size: 'small', onClick: this.onConfirmHandle, ...okButtonProps, @@ -137,7 +139,7 @@ const Popconfirm = defineComponent({ this.renderOverlay(prefixCls, popconfirmLocale)} + children={(popconfirmLocale) => this.renderOverlay(prefixCls, popconfirmLocale)} /> ); const tooltipProps = { diff --git a/components/style/themes/default.less b/components/style/themes/default.less index 76d4c9cf8..0510b4a8b 100644 --- a/components/style/themes/default.less +++ b/components/style/themes/default.less @@ -189,16 +189,18 @@ @btn-default-ghost-bg: transparent; @btn-default-ghost-border: @component-background; -@btn-padding-base: 0 @padding-md - 1px; @btn-font-size-lg: @font-size-lg; @btn-font-size-sm: @font-size-base; -@btn-padding-lg: @btn-padding-base; -@btn-padding-sm: 0 @padding-xs - 1px; +@btn-padding-horizontal-base: @padding-md - 1px; +@btn-padding-horizontal-lg: @btn-padding-horizontal-base; +@btn-padding-horizontal-sm: @padding-xs - 1px; @btn-height-base: 32px; @btn-height-lg: 40px; @btn-height-sm: 24px; +@btn-line-height: @line-height-base; + @btn-circle-size: @btn-height-base; @btn-circle-size-lg: @btn-height-lg; @btn-circle-size-sm: @btn-height-sm; @@ -206,9 +208,13 @@ @btn-square-size: @btn-height-base; @btn-square-size-lg: @btn-height-lg; @btn-square-size-sm: @btn-height-sm; +@btn-square-only-icon-size: @font-size-base + 2px; +@btn-square-only-icon-size-sm: @font-size-base; +@btn-square-only-icon-size-lg: @btn-font-size-lg + 2px; @btn-group-border: @primary-5; +@btn-link-hover-bg: transparent; @btn-text-hover-bg: rgba(0, 0, 0, 0.018); // Checkbox From 61c19a81c20052242d91fcac18f925e09008ab1f Mon Sep 17 00:00:00 2001 From: tangjinzhou <415800467@qq.com> Date: Wed, 30 Jun 2021 13:48:02 +0800 Subject: [PATCH 2/2] refactor: button --- components/button/button-group.tsx | 12 +-- components/button/button.tsx | 118 ++++++++++++++++------------- components/button/buttonTypes.ts | 2 +- components/button/index.ts | 2 + components/button/style/index.less | 42 ++++------ components/modal/Modal.tsx | 4 +- components/popconfirm/index.tsx | 2 +- v2-doc | 2 +- 8 files changed, 95 insertions(+), 89 deletions(-) diff --git a/components/button/button-group.tsx b/components/button/button-group.tsx index f6a897d6d..66494438d 100644 --- a/components/button/button-group.tsx +++ b/components/button/button-group.tsx @@ -1,4 +1,4 @@ -import { defineComponent } from 'vue'; +import { computed, defineComponent } from 'vue'; import { flattenChildren } from '../_util/props-util'; import PropTypes from '../_util/vue-types'; import useConfigInject from '../_util/hooks/useConfigInject'; @@ -21,10 +21,8 @@ export default defineComponent({ props: buttonGroupProps, setup(props, { slots }) { const { prefixCls, direction } = useConfigInject('btn-group', props); - - return () => { + const classes = computed(() => { const { size } = props; - // large => lg // small => sm let sizeCls = ''; @@ -38,12 +36,14 @@ export default defineComponent({ default: break; } - const classes = { + return { [`${prefixCls.value}`]: true, [`${prefixCls.value}-${sizeCls}`]: sizeCls, [`${prefixCls.value}-rtl`]: direction.value === 'rtl', }; - return
{flattenChildren(slots.default?.())}
; + }); + return () => { + return
{flattenChildren(slots.default?.())}
; }; }, }); diff --git a/components/button/button.tsx b/components/button/button.tsx index 7c39538eb..8929d7ace 100644 --- a/components/button/button.tsx +++ b/components/button/button.tsx @@ -4,6 +4,7 @@ import { onBeforeUnmount, onMounted, onUpdated, + Ref, ref, Text, watch, @@ -19,6 +20,8 @@ import devWarning from '../vc-util/devWarning'; import type { ButtonType } from './buttonTypes'; import type { VNode } from 'vue'; +type Loading = boolean | number; + const rxTwoCNChar = /^[\u4e00-\u9fa5]{2}$/; const isTwoCNChar = rxTwoCNChar.test.bind(rxTwoCNChar); const props = buttonTypes(); @@ -38,18 +41,41 @@ export default defineComponent({ const { prefixCls, autoInsertSpaceInButton, direction } = useConfigInject('btn', props); const buttonNodeRef = ref(null); - const delayTimeout = ref(undefined); - const iconCom = ref(null); - const children = ref([]); + const delayTimeoutRef = ref(undefined); + let isNeedInserted = false; - const sLoading = ref(props.loading); + const innerLoading: Ref = ref(false); const hasTwoCNChar = ref(false); const autoInsertSpace = computed(() => autoInsertSpaceInButton.value !== false); - const getClasses = () => { - const { type, shape, size, ghost, block, danger } = props; + // =============== Update Loading =============== + const loadingOrDelay = computed(() => + typeof props.loading === 'object' && props.loading.delay + ? props.loading.delay || true + : !!props.loading, + ); + watch( + loadingOrDelay, + (val) => { + clearTimeout(delayTimeoutRef.value); + if (typeof loadingOrDelay.value === 'number') { + delayTimeoutRef.value = window.setTimeout(() => { + innerLoading.value = val; + }, loadingOrDelay.value); + } else { + innerLoading.value = val; + } + }, + { + immediate: true, + }, + ); + + const classes = computed(() => { + const { type, shape, size, ghost, block, danger } = props; + const pre = prefixCls.value; // large => lg // small => sm let sizeCls = ''; @@ -63,22 +89,20 @@ export default defineComponent({ default: break; } - const iconType = sLoading.value ? 'loading' : iconCom.value; return { [attrs.class as string]: attrs.class, - [`${prefixCls.value}`]: true, - [`${prefixCls.value}-${type}`]: type, - [`${prefixCls.value}-${shape}`]: shape, - [`${prefixCls.value}-${sizeCls}`]: sizeCls, - [`${prefixCls.value}-icon-only`]: children.value.length === 0 && !!iconType, - [`${prefixCls.value}-loading`]: sLoading.value, - [`${prefixCls.value}-background-ghost`]: ghost && !isUnborderedButtonType(type), - [`${prefixCls.value}-two-chinese-chars`]: hasTwoCNChar.value && autoInsertSpace.value, - [`${prefixCls.value}-block`]: block, - [`${prefixCls.value}-dangerous`]: !!danger, - [`${prefixCls.value}-rtl`]: direction.value === 'rtl', + [`${pre}`]: true, + [`${pre}-${type}`]: type, + [`${pre}-${shape}`]: shape, + [`${pre}-${sizeCls}`]: sizeCls, + [`${pre}-loading`]: innerLoading.value, + [`${pre}-background-ghost`]: ghost && !isUnborderedButtonType(type), + [`${pre}-two-chinese-chars`]: hasTwoCNChar.value && autoInsertSpace.value, + [`${pre}-block`]: block, + [`${pre}-dangerous`]: !!danger, + [`${pre}-rtl`]: direction.value === 'rtl', }; - }; + }); const fixTwoCNChar = () => { // Fix for HOC usage like @@ -88,7 +112,7 @@ export default defineComponent({ } const buttonText = node.textContent; - if (isNeedInserted() && isTwoCNChar(buttonText)) { + if (isNeedInserted && isTwoCNChar(buttonText)) { if (!hasTwoCNChar.value) { hasTwoCNChar.value = true; } @@ -98,7 +122,7 @@ export default defineComponent({ }; const handleClick = (event: Event) => { // https://github.com/ant-design/ant-design/issues/30207 - if (sLoading.value || props.disabled) { + if (innerLoading.value || props.disabled) { event.preventDefault(); return; } @@ -117,9 +141,6 @@ export default defineComponent({ return child; }; - const isNeedInserted = () => - children.value.length === 1 && !iconCom.value && !isUnborderedButtonType(props.type); - watchEffect(() => { devWarning( !(props.ghost && isUnborderedButtonType(props.type)), @@ -128,50 +149,45 @@ export default defineComponent({ ); }); - watch( - () => props.loading, - (val, preVal) => { - if (preVal && typeof preVal !== 'boolean') { - clearTimeout(delayTimeout.value); - } - if (val && typeof val !== 'boolean' && val.delay) { - delayTimeout.value = setTimeout(() => { - sLoading.value = !!val; - }, val.delay); - } else { - sLoading.value = !!val; - } - }, - { - immediate: true, - }, - ); - onMounted(fixTwoCNChar); onUpdated(fixTwoCNChar); onBeforeUnmount(() => { - delayTimeout.value && clearTimeout(delayTimeout.value); + delayTimeoutRef.value && clearTimeout(delayTimeoutRef.value); }); return () => { - iconCom.value = getPropsSlot(slots, props, 'icon'); - children.value = flattenChildren(getPropsSlot(slots, props)); + const children = flattenChildren(getPropsSlot(slots, props)); + + const icon = getPropsSlot(slots, props, 'icon'); + + isNeedInserted = children.length === 1 && !icon && !isUnborderedButtonType(props.type); const { type, htmlType, disabled, href, title, target } = props; - const classes = getClasses(); + const iconType = innerLoading.value ? 'loading' : icon; const buttonProps = { ...attrs, title, disabled, - class: classes, + class: [ + classes.value, + attrs.class, + { [`${prefixCls.value}-icon-only`]: children.length === 0 && !!iconType }, + ], onClick: handleClick, }; - const iconNode = sLoading.value ? : iconCom.value; - const kids = children.value.map((child) => - insertSpace(child, isNeedInserted() && autoInsertSpace.value), + const iconNode = innerLoading.value ? ( + + + + ) : ( + icon + ); + + const kids = children.map((child) => + insertSpace(child, isNeedInserted && autoInsertSpace.value), ); if (href !== undefined) { diff --git a/components/button/buttonTypes.ts b/components/button/buttonTypes.ts index e71a0b696..87b1f498d 100644 --- a/components/button/buttonTypes.ts +++ b/components/button/buttonTypes.ts @@ -6,7 +6,7 @@ import type { SizeType } from '../config-provider'; const ButtonTypes = tuple('default', 'primary', 'ghost', 'dashed', 'link', 'text'); export type ButtonType = typeof ButtonTypes[number]; -const ButtonShapes = tuple('circle', 'circle-outline', 'round'); +const ButtonShapes = tuple('circle', 'round'); export type ButtonShape = typeof ButtonShapes[number]; const ButtonHTMLTypes = tuple('submit', 'button', 'reset'); diff --git a/components/button/index.ts b/components/button/index.ts index 4b9364911..b5136a9e2 100644 --- a/components/button/index.ts +++ b/components/button/index.ts @@ -17,6 +17,8 @@ Button.install = function (app: App) { return app; }; +export { ButtonGroup }; + export default Button as typeof Button & Plugin & { readonly Group: typeof ButtonGroup; diff --git a/components/button/style/index.less b/components/button/style/index.less index 97fbeb2ed..2ea7e01a3 100644 --- a/components/button/style/index.less +++ b/components/button/style/index.less @@ -155,39 +155,25 @@ } } - &&-loading:not(&-circle):not(&-circle-outline):not(&-icon-only) { - padding-left: 29px; - .@{iconfont-css-prefix}:not(:last-child) { - margin-left: -14px; - } - } + & > &-loading-icon { + transition: all 0.3s @ease-in-out; - &-sm&-loading:not(&-circle):not(&-circle-outline):not(&-icon-only) { - padding-left: 24px; .@{iconfont-css-prefix} { - margin-left: -17px; + padding-right: @padding-xs; + animation: none; + // for smooth button padding transition + svg { + animation: loadingCircle 1s infinite linear; + } + } + + &:only-child { + .@{iconfont-css-prefix} { + padding-right: 0; + } } } - // & > &-loading-icon { - // transition: all 0.3s @ease-in-out; - - // .@{iconfont-css-prefix} { - // padding-right: @padding-xs; - // animation: none; - // // for smooth button padding transition - // svg { - // animation: loadingCircle 1s infinite linear; - // } - // } - - // &:only-child { - // .@{iconfont-css-prefix} { - // padding-right: 0; - // } - // } - // } - &-group { .btn-group(@btn-prefix-cls); } diff --git a/components/modal/Modal.tsx b/components/modal/Modal.tsx index c0995dac0..66d06a74a 100644 --- a/components/modal/Modal.tsx +++ b/components/modal/Modal.tsx @@ -136,7 +136,9 @@ export interface ModalFuncProps { type getContainerFunc = () => HTMLElement; -export type ModalFunc = (props: ModalFuncProps) => { +export type ModalFunc = ( + props: ModalFuncProps, +) => { destroy: () => void; update: (newConfig: ModalFuncProps) => void; }; diff --git a/components/popconfirm/index.tsx b/components/popconfirm/index.tsx index 5521f9038..f4d739f3c 100644 --- a/components/popconfirm/index.tsx +++ b/components/popconfirm/index.tsx @@ -139,7 +139,7 @@ const Popconfirm = defineComponent({ this.renderOverlay(prefixCls, popconfirmLocale)} + children={popconfirmLocale => this.renderOverlay(prefixCls, popconfirmLocale)} /> ); const tooltipProps = { diff --git a/v2-doc b/v2-doc index 4c2982755..b6ab0fec2 160000 --- a/v2-doc +++ b/v2-doc @@ -1 +1 @@ -Subproject commit 4c298275518d5790a58d26f2ed9b83ee5ba1dba4 +Subproject commit b6ab0fec2cfa378bab8dfe6c8ef6b6a8664b970e