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 { 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 () => {
|
||||||
|
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(
|
return wrapSSR(
|
||||||
<Wave>
|
<a
|
||||||
|
{...attrs}
|
||||||
|
{...props}
|
||||||
|
class={classes}
|
||||||
|
onClick={handleClick}
|
||||||
|
ref={buttonRef}
|
||||||
|
>
|
||||||
|
{iconNode}
|
||||||
|
{children}
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
let buttonNode = (
|
||||||
<button
|
<button
|
||||||
{...attrs}
|
{...attrs}
|
||||||
class={[cls.value, attrs.class]}
|
onClick={handleClick}
|
||||||
|
class={classes}
|
||||||
>
|
>
|
||||||
{slots.default?.()}
|
{children}
|
||||||
</button>
|
</button>
|
||||||
</Wave>
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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>
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user