mirror of
https://github.com/antd-tiny-vue/antd-tiny-vue.git
synced 2024-11-10 09:39:15 +08:00
feat: add button
This commit is contained in:
parent
2b2ef3a3fb
commit
3ba0d997d5
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>
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user