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:
		
							
								
								
									
										8
									
								
								components/_util/hooks/disabled.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								components/_util/hooks/disabled.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| import type { ComputedRef } from 'vue' | ||||
| import { computed } from 'vue' | ||||
| import { useProviderConfigState } from '../../config-provider/context' | ||||
|  | ||||
| export const useDisabled = (props: Record<string, any>) => { | ||||
|   const { componentDisabled } = useProviderConfigState() | ||||
|   return computed(() => props.disabled || componentDisabled.value) as ComputedRef<boolean> | ||||
| } | ||||
							
								
								
									
										9
									
								
								components/_util/hooks/size.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								components/_util/hooks/size.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| import type { ComputedRef } from 'vue' | ||||
| import { computed } from 'vue' | ||||
| import type { SizeType } from '../../config-provider/context' | ||||
| import { useProviderConfigState } from '../../config-provider/context' | ||||
|  | ||||
| export const useSize = (props: Record<string, any>) => { | ||||
|   const { componentSize } = useProviderConfigState() | ||||
|   return computed(() => props.size || componentSize.value) as ComputedRef<SizeType> | ||||
| } | ||||
							
								
								
									
										21
									
								
								components/_util/warning.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								components/_util/warning.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| import { warning as rcWarning, resetWarned } from '@v-c/utils' | ||||
|  | ||||
| export { resetWarned } | ||||
| export function noop() {} | ||||
|  | ||||
| type Warning = (valid: boolean, component: string, message?: string) => void | ||||
|  | ||||
| // eslint-disable-next-line import/no-mutable-exports | ||||
| let warning: Warning = noop | ||||
| if (process.env.NODE_ENV !== 'production') { | ||||
|   warning = (valid, component, message) => { | ||||
|     rcWarning(valid, `[antd: ${component}] ${message}`) | ||||
|  | ||||
|     // StrictMode will inject console which will not throw warning in React 17. | ||||
|     if (process.env.NODE_ENV === 'test') { | ||||
|       resetWarned() | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| export default warning | ||||
| @@ -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(), | ||||
|   | ||||
| @@ -90,13 +90,17 @@ const configState = (props: ConfigProviderProps) => { | ||||
|   const csp = computed(() => props?.csp) | ||||
|   const componentSize = computed(() => props?.componentSize) | ||||
|   const componentDisabled = computed(() => props?.componentDisabled) | ||||
|   const autoInsertSpaceInButton = computed(() => props?.autoInsertSpaceInButton) | ||||
|   const direction = computed(() => props?.direction) | ||||
|   return { | ||||
|     getPrefixCls, | ||||
|     iconPrefixCls, | ||||
|     shouldWrapSSR, | ||||
|     csp, | ||||
|     componentSize, | ||||
|     componentDisabled | ||||
|     componentDisabled, | ||||
|     autoInsertSpaceInButton, | ||||
|     direction | ||||
|   } | ||||
| } | ||||
| const [useProviderConfigProvide, useProviderConfigInject] = createInjectionState(configState) | ||||
| @@ -107,7 +111,11 @@ export const useProviderConfigState = (): ReturnType<typeof configState> => { | ||||
|     useProviderConfigInject() ?? | ||||
|     ({ | ||||
|       getPrefixCls: defaultGetPrefixCls, | ||||
|       iconPrefixCls: computed(() => defaultIconPrefixCls) | ||||
|       iconPrefixCls: computed(() => defaultIconPrefixCls), | ||||
|       componentSize: computed(() => undefined), | ||||
|       componentDisabled: computed(() => false), | ||||
|       direction: computed(() => undefined), | ||||
|       autoInsertSpaceInButton: computed(() => undefined) | ||||
|     } as any) | ||||
|   ) | ||||
| } | ||||
|   | ||||
							
								
								
									
										41
									
								
								components/space/compact.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								components/space/compact.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | ||||
| import { classNames, createInjectionState } from '@v-c/utils' | ||||
| import type { Ref } from 'vue' | ||||
| import { computed } from 'vue' | ||||
| import type { DirectionType } from '../config-provider' | ||||
|  | ||||
| const spaceCompactItem = () => { | ||||
|   return { | ||||
|     compactDirection: computed(() => null), | ||||
|     isFirstItem: computed(() => null), | ||||
|     isLastItem: computed(() => null), | ||||
|     compactSize: computed(() => null) | ||||
|   } | ||||
| } | ||||
|  | ||||
| export const [useSpaceCompactProvider, useSpaceCompactInject] = createInjectionState(spaceCompactItem) | ||||
|  | ||||
| export const useSpaceCompactItemState = (): ReturnType<typeof spaceCompactItem> | undefined => useSpaceCompactInject() | ||||
|  | ||||
| export const useCompactItemContext = (prefixCls: Ref<string>, direction: Ref<DirectionType>) => { | ||||
|   const compactItemContext = useSpaceCompactItemState() | ||||
|  | ||||
|   const compactItemClassnames = computed(() => { | ||||
|     if (!compactItemContext) return '' | ||||
|  | ||||
|     const { compactDirection, isFirstItem, isLastItem } = compactItemContext | ||||
|     const separator = compactDirection.value === 'vertical' ? '-vertical-' : '-' | ||||
|  | ||||
|     return classNames({ | ||||
|       [`${prefixCls.value}-compact${separator}item`]: true, | ||||
|       [`${prefixCls.value}-compact${separator}first-item`]: isFirstItem.value, | ||||
|       [`${prefixCls.value}-compact${separator}last-item`]: isLastItem.value, | ||||
|       [`${prefixCls.value}-compact${separator}item-rtl`]: direction.value === 'rtl' | ||||
|     }) | ||||
|   }) | ||||
|  | ||||
|   return { | ||||
|     compactSize: compactItemContext?.compactSize, | ||||
|     compactDirection: compactItemContext?.compactDirection, | ||||
|     compactItemClassnames | ||||
|   } | ||||
| } | ||||
| @@ -10,9 +10,15 @@ title: 基础按钮 | ||||
| <script lang="ts" setup></script> | ||||
|  | ||||
| <template> | ||||
|   <div> | ||||
|   <div style="display: flex; gap: 10px; padding-bottom: 10px"> | ||||
|     <a-button>这是按钮</a-button> | ||||
|     <div style="height: 10px"></div> | ||||
|     <a-button type="primary">这是按钮</a-button> | ||||
|     <a-button | ||||
|       type="primary" | ||||
|       danger | ||||
|     > | ||||
|       这是按钮 | ||||
|     </a-button> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user