mirror of
				https://github.com/antd-tiny-vue/antd-tiny-vue.git
				synced 2025-11-04 10:31:46 +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