mirror of
				https://github.com/antd-tiny-vue/antd-tiny-vue.git
				synced 2025-10-31 16:51:45 +08:00 
			
		
		
		
	feat: add button
This commit is contained in:
		| @@ -1,8 +1,33 @@ | ||||
| import { computed, defineComponent } from 'vue' | ||||
| import { computed, defineComponent, onMounted, shallowRef } from 'vue' | ||||
| import { tryOnBeforeUnmount } from '@vueuse/core' | ||||
| import { classNames, filterEmpty, getSlotsProps, runEvent, useState } from '@v-c/utils' | ||||
| import { useProviderConfigState } from '../config-provider/context' | ||||
| import warning from '../_util/warning' | ||||
| import Wave from '../_util/wave' | ||||
| import { useSize } from '../_util/hooks/size' | ||||
| import { useDisabled } from '../_util/hooks/disabled' | ||||
| import { useCompactItemContext } from '../space/compact' | ||||
| import useStyle from './style' | ||||
| import type { ButtonProps, LoadingConfigType } from './interface' | ||||
| import { buttonProps } from './interface' | ||||
| import { isTwoCNChar, isUnBorderedButtonType } from './button-helper' | ||||
| type Loading = number | boolean | ||||
|  | ||||
| function getLoadingConfig(loading: ButtonProps['loading']): LoadingConfigType { | ||||
|   if (typeof loading === 'object' && loading) { | ||||
|     const delay = loading?.delay | ||||
|     const isDelay = !Number.isNaN(delay) && typeof delay === 'number' | ||||
|     return { | ||||
|       loading: false, | ||||
|       delay: isDelay ? delay : 0 | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   return { | ||||
|     loading: !!loading, | ||||
|     delay: 0 | ||||
|   } | ||||
| } | ||||
|  | ||||
| const Button = defineComponent({ | ||||
|   name: 'AButton', | ||||
| @@ -12,29 +37,150 @@ const Button = defineComponent({ | ||||
|     ...buttonProps | ||||
|   }, | ||||
|   setup(props, { slots, attrs }) { | ||||
|     const { getPrefixCls } = useProviderConfigState() | ||||
|     const { getPrefixCls, autoInsertSpaceInButton, direction } = useProviderConfigState() | ||||
|     const prefixCls = computed(() => getPrefixCls('btn', props.prefixCls)) | ||||
|     const [wrapSSR, hashId] = useStyle(prefixCls) | ||||
|     const size = useSize(props) | ||||
|     const { compactSize, compactItemClassnames } = useCompactItemContext(prefixCls, direction) | ||||
|     const sizeCls = computed(() => { | ||||
|       const sizeClassNameMap = { large: 'lg', small: 'sm', middle: undefined } | ||||
|       const sizeFullname = compactSize?.value || size.value | ||||
|       return sizeClassNameMap[sizeFullname!] | ||||
|     }) | ||||
|     const disabled = useDisabled(props) | ||||
|     const buttonRef = shallowRef<HTMLButtonElement | null>(null) | ||||
|  | ||||
|     const cls = computed(() => { | ||||
|       return { | ||||
|         [prefixCls.value]: true, | ||||
|         [`${prefixCls.value}-${props.type}`]: !!props.type, | ||||
|         [hashId.value]: true | ||||
|       } | ||||
|     const loadingOrDelay = computed(() => { | ||||
|       return getLoadingConfig(props.loading) | ||||
|     }) | ||||
|  | ||||
|     const [innerLoading, setLoading] = useState<Loading>(loadingOrDelay.value.loading) | ||||
|     const [hasTwoCNChar, setHasTwoCNChar] = useState(false) | ||||
|  | ||||
|     let delayTimer: number | null = null | ||||
|  | ||||
|     onMounted(() => { | ||||
|       if (loadingOrDelay.value.delay > 0) { | ||||
|         delayTimer = window.setTimeout(() => { | ||||
|           delayTimer = null | ||||
|           setLoading(true) | ||||
|         }, loadingOrDelay.value.delay) | ||||
|       } else { | ||||
|         setLoading(loadingOrDelay.value.loading) | ||||
|       } | ||||
|       // fixTwoCNChar() | ||||
|     }) | ||||
|  | ||||
|     function cleanupTimer() { | ||||
|       if (delayTimer) { | ||||
|         window.clearTimeout(delayTimer) | ||||
|         delayTimer = null | ||||
|       } | ||||
|     } | ||||
|     tryOnBeforeUnmount(() => { | ||||
|       cleanupTimer() | ||||
|     }) | ||||
|  | ||||
|     const handleClick = (e: MouseEvent) => { | ||||
|       // FIXME: https://github.com/ant-design/ant-design/issues/30207 | ||||
|       if (innerLoading.value || disabled.value) { | ||||
|         e.preventDefault() | ||||
|         return | ||||
|       } | ||||
|       runEvent(props, 'onClick', e) | ||||
|     } | ||||
|  | ||||
|     const showError = () => { | ||||
|       const { ghost, type } = props | ||||
|  | ||||
|       const icon = getSlotsProps(slots, props, 'icon') | ||||
|  | ||||
|       warning(!(typeof icon === 'string' && icon.length > 2), 'Button', `\`icon\` is using ReactNode instead of string naming in v4. Please check \`${icon}\` at https://ant.design/components/icon`) | ||||
|  | ||||
|       warning(!(ghost && isUnBorderedButtonType(type)), 'Button', "`link` or `text` button can't be a `ghost` button.") | ||||
|     } | ||||
|  | ||||
|     return () => { | ||||
|       return wrapSSR( | ||||
|         <Wave> | ||||
|           <button | ||||
|             {...attrs} | ||||
|             class={[cls.value, attrs.class]} | ||||
|           > | ||||
|             {slots.default?.()} | ||||
|           </button> | ||||
|         </Wave> | ||||
|       const { shape, rootClassName, ghost, type, block, danger } = props | ||||
|       const icon = getSlotsProps(slots, props, 'icon') | ||||
|       const children = filterEmpty(slots.default?.()) | ||||
|       const isNeedInserted = () => { | ||||
|         return children.length === 1 && !slots.icon && isUnBorderedButtonType(props.type) | ||||
|       } | ||||
|  | ||||
|       const fixTwoCNChar = () => { | ||||
|         // FIXME: for HOC usage like <FormatMessage /> | ||||
|         if (!buttonRef.value || autoInsertSpaceInButton.value === false) { | ||||
|           return | ||||
|         } | ||||
|         const buttonText = buttonRef.value.textContent | ||||
|         if (isNeedInserted() && isTwoCNChar(buttonText as string)) { | ||||
|           if (!hasTwoCNChar) { | ||||
|             setHasTwoCNChar(true) | ||||
|           } | ||||
|         } else if (hasTwoCNChar) { | ||||
|           setHasTwoCNChar(false) | ||||
|         } | ||||
|       } | ||||
|       fixTwoCNChar() | ||||
|       showError() | ||||
|       const iconType = innerLoading.value ? 'loading' : icon | ||||
|  | ||||
|       const autoInsertSpace = autoInsertSpaceInButton.value !== false | ||||
|  | ||||
|       const hrefAndDisabled = attrs.href !== undefined && disabled.value | ||||
|  | ||||
|       const classes = classNames( | ||||
|         prefixCls.value, | ||||
|         hashId.value, | ||||
|         { | ||||
|           [`${prefixCls.value}-${shape}`]: shape !== 'default' && shape, | ||||
|           [`${prefixCls.value}-${type}`]: type, | ||||
|           [`${prefixCls.value}-${sizeCls.value}`]: sizeCls.value, | ||||
|           [`${prefixCls.value}-icon-only`]: !children && children !== 0 && !!iconType, | ||||
|           [`${prefixCls.value}-background-ghost`]: ghost && !isUnBorderedButtonType(type), | ||||
|           [`${prefixCls.value}-loading`]: innerLoading.value, | ||||
|           [`${prefixCls.value}-two-chinese-chars`]: hasTwoCNChar.value && autoInsertSpace && !innerLoading.value, | ||||
|           [`${prefixCls.value}-block`]: block, | ||||
|           [`${prefixCls.value}-dangerous`]: !!danger, | ||||
|           [`${prefixCls.value}-rtl`]: direction.value === 'rtl', | ||||
|           [`${prefixCls.value}-disabled`]: hrefAndDisabled | ||||
|         }, | ||||
|         attrs.class, | ||||
|         compactItemClassnames.value, | ||||
|         rootClassName | ||||
|       ) | ||||
|       const iconNode = icon && !innerLoading.value ? icon?.() : <>L</> | ||||
|  | ||||
|       if (attrs.href !== undefined) { | ||||
|         return wrapSSR( | ||||
|           <a | ||||
|             {...attrs} | ||||
|             {...props} | ||||
|             class={classes} | ||||
|             onClick={handleClick} | ||||
|             ref={buttonRef} | ||||
|           > | ||||
|             {iconNode} | ||||
|             {children} | ||||
|           </a> | ||||
|         ) | ||||
|       } | ||||
|       let buttonNode = ( | ||||
|         <button | ||||
|           {...attrs} | ||||
|           onClick={handleClick} | ||||
|           class={classes} | ||||
|         > | ||||
|           {children} | ||||
|         </button> | ||||
|       ) | ||||
|  | ||||
|       if (!isUnBorderedButtonType(type)) { | ||||
|         buttonNode = <Wave>{buttonNode}</Wave> | ||||
|       } | ||||
|  | ||||
|       return wrapSSR(buttonNode) | ||||
|     } | ||||
|   } | ||||
| }) | ||||
|   | ||||
| @@ -2,6 +2,10 @@ import { booleanType, someType, stringType, vNodeType } from '@v-c/utils' | ||||
| import type { ExtractPropTypes } from 'vue' | ||||
| import type { SizeType } from '../config-provider/context' | ||||
| import type { ButtonHTMLType, ButtonShape, ButtonType } from './button-helper' | ||||
| export interface LoadingConfigType { | ||||
|   loading: boolean | ||||
|   delay: number | ||||
| } | ||||
|  | ||||
| export const buttonProps = { | ||||
|   type: stringType<ButtonType>('default'), | ||||
| @@ -9,7 +13,7 @@ export const buttonProps = { | ||||
|   shape: stringType<ButtonShape>(), | ||||
|   size: someType<SizeType | 'default'>([String], 'default'), | ||||
|   disabled: booleanType(), | ||||
|   loading: someType<boolean | { delay?: number }>(), | ||||
|   loading: someType<boolean | LoadingConfigType>(), | ||||
|   prefixCls: stringType(), | ||||
|   rootClassName: stringType(), | ||||
|   ghost: booleanType(), | ||||
|   | ||||
		Reference in New Issue
	
	Block a user