feat: add button

This commit is contained in:
aibayanyu 2023-03-27 07:51:19 +08:00
parent 2b2ef3a3fb
commit 3ba0d997d5
8 changed files with 265 additions and 22 deletions

View 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>
}

View 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>
}

View 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

View File

@ -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)
}
}
})

View File

@ -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(),

View File

@ -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)
)
}

View 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
}
}

View File

@ -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>