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 { useProviderConfigState } from '../config-provider/context' | ||||||
|  | import warning from '../_util/warning' | ||||||
| import Wave from '../_util/wave' | 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 useStyle from './style' | ||||||
|  | import type { ButtonProps, LoadingConfigType } from './interface' | ||||||
| import { buttonProps } 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({ | const Button = defineComponent({ | ||||||
|   name: 'AButton', |   name: 'AButton', | ||||||
| @@ -12,29 +37,150 @@ const Button = defineComponent({ | |||||||
|     ...buttonProps |     ...buttonProps | ||||||
|   }, |   }, | ||||||
|   setup(props, { slots, attrs }) { |   setup(props, { slots, attrs }) { | ||||||
|     const { getPrefixCls } = useProviderConfigState() |     const { getPrefixCls, autoInsertSpaceInButton, direction } = useProviderConfigState() | ||||||
|     const prefixCls = computed(() => getPrefixCls('btn', props.prefixCls)) |     const prefixCls = computed(() => getPrefixCls('btn', props.prefixCls)) | ||||||
|     const [wrapSSR, hashId] = useStyle(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(() => { |     const loadingOrDelay = computed(() => { | ||||||
|       return { |       return getLoadingConfig(props.loading) | ||||||
|         [prefixCls.value]: true, |  | ||||||
|         [`${prefixCls.value}-${props.type}`]: !!props.type, |  | ||||||
|         [hashId.value]: true |  | ||||||
|       } |  | ||||||
|     }) |     }) | ||||||
|  |  | ||||||
|  |     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 () => { | ||||||
|       return wrapSSR( |       const { shape, rootClassName, ghost, type, block, danger } = props | ||||||
|         <Wave> |       const icon = getSlotsProps(slots, props, 'icon') | ||||||
|           <button |       const children = filterEmpty(slots.default?.()) | ||||||
|             {...attrs} |       const isNeedInserted = () => { | ||||||
|             class={[cls.value, attrs.class]} |         return children.length === 1 && !slots.icon && isUnBorderedButtonType(props.type) | ||||||
|           > |       } | ||||||
|             {slots.default?.()} |  | ||||||
|           </button> |       const fixTwoCNChar = () => { | ||||||
|         </Wave> |         // 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 { ExtractPropTypes } from 'vue' | ||||||
| import type { SizeType } from '../config-provider/context' | import type { SizeType } from '../config-provider/context' | ||||||
| import type { ButtonHTMLType, ButtonShape, ButtonType } from './button-helper' | import type { ButtonHTMLType, ButtonShape, ButtonType } from './button-helper' | ||||||
|  | export interface LoadingConfigType { | ||||||
|  |   loading: boolean | ||||||
|  |   delay: number | ||||||
|  | } | ||||||
|  |  | ||||||
| export const buttonProps = { | export const buttonProps = { | ||||||
|   type: stringType<ButtonType>('default'), |   type: stringType<ButtonType>('default'), | ||||||
| @@ -9,7 +13,7 @@ export const buttonProps = { | |||||||
|   shape: stringType<ButtonShape>(), |   shape: stringType<ButtonShape>(), | ||||||
|   size: someType<SizeType | 'default'>([String], 'default'), |   size: someType<SizeType | 'default'>([String], 'default'), | ||||||
|   disabled: booleanType(), |   disabled: booleanType(), | ||||||
|   loading: someType<boolean | { delay?: number }>(), |   loading: someType<boolean | LoadingConfigType>(), | ||||||
|   prefixCls: stringType(), |   prefixCls: stringType(), | ||||||
|   rootClassName: stringType(), |   rootClassName: stringType(), | ||||||
|   ghost: booleanType(), |   ghost: booleanType(), | ||||||
|   | |||||||
| @@ -90,13 +90,17 @@ const configState = (props: ConfigProviderProps) => { | |||||||
|   const csp = computed(() => props?.csp) |   const csp = computed(() => props?.csp) | ||||||
|   const componentSize = computed(() => props?.componentSize) |   const componentSize = computed(() => props?.componentSize) | ||||||
|   const componentDisabled = computed(() => props?.componentDisabled) |   const componentDisabled = computed(() => props?.componentDisabled) | ||||||
|  |   const autoInsertSpaceInButton = computed(() => props?.autoInsertSpaceInButton) | ||||||
|  |   const direction = computed(() => props?.direction) | ||||||
|   return { |   return { | ||||||
|     getPrefixCls, |     getPrefixCls, | ||||||
|     iconPrefixCls, |     iconPrefixCls, | ||||||
|     shouldWrapSSR, |     shouldWrapSSR, | ||||||
|     csp, |     csp, | ||||||
|     componentSize, |     componentSize, | ||||||
|     componentDisabled |     componentDisabled, | ||||||
|  |     autoInsertSpaceInButton, | ||||||
|  |     direction | ||||||
|   } |   } | ||||||
| } | } | ||||||
| const [useProviderConfigProvide, useProviderConfigInject] = createInjectionState(configState) | const [useProviderConfigProvide, useProviderConfigInject] = createInjectionState(configState) | ||||||
| @@ -107,7 +111,11 @@ export const useProviderConfigState = (): ReturnType<typeof configState> => { | |||||||
|     useProviderConfigInject() ?? |     useProviderConfigInject() ?? | ||||||
|     ({ |     ({ | ||||||
|       getPrefixCls: defaultGetPrefixCls, |       getPrefixCls: defaultGetPrefixCls, | ||||||
|       iconPrefixCls: computed(() => defaultIconPrefixCls) |       iconPrefixCls: computed(() => defaultIconPrefixCls), | ||||||
|  |       componentSize: computed(() => undefined), | ||||||
|  |       componentDisabled: computed(() => false), | ||||||
|  |       direction: computed(() => undefined), | ||||||
|  |       autoInsertSpaceInButton: computed(() => undefined) | ||||||
|     } as any) |     } 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> | <script lang="ts" setup></script> | ||||||
|  |  | ||||||
| <template> | <template> | ||||||
|   <div> |   <div style="display: flex; gap: 10px; padding-bottom: 10px"> | ||||||
|     <a-button>这是按钮</a-button> |     <a-button>这是按钮</a-button> | ||||||
|     <div style="height: 10px"></div> |     <a-button type="primary">这是按钮</a-button> | ||||||
|  |     <a-button | ||||||
|  |       type="primary" | ||||||
|  |       danger | ||||||
|  |     > | ||||||
|  |       这是按钮 | ||||||
|  |     </a-button> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user