/* eslint-disable react/button-has-type */ import classNames from 'classnames'; import omit from 'rc-util/lib/omit'; import { composeRef } from 'rc-util/lib/ref'; import React, { Children, createRef, forwardRef, useContext, useEffect, useMemo, useState, } from 'react'; import warning from '../_util/warning'; import Wave from '../_util/wave'; import { ConfigContext } from '../config-provider'; import DisabledContext from '../config-provider/DisabledContext'; import type { SizeType } from '../config-provider/SizeContext'; import useSize from '../config-provider/hooks/useSize'; import { useCompactItemContext } from '../space/Compact'; import IconWrapper from './IconWrapper'; import LoadingIcon from './LoadingIcon'; import Group, { GroupSizeContext } from './button-group'; import type { ButtonHTMLType, ButtonShape, ButtonType } from './buttonHelpers'; import { isTwoCNChar, isUnBorderedButtonType, spaceChildren } from './buttonHelpers'; import useStyle from './style'; export type LegacyButtonType = ButtonType | 'danger'; export function convertLegacyProps(type?: LegacyButtonType): ButtonProps { if (type === 'danger') { return { danger: true }; } return { type }; } export interface BaseButtonProps { type?: ButtonType; icon?: React.ReactNode; shape?: ButtonShape; size?: SizeType; disabled?: boolean; loading?: boolean | { delay?: number }; prefixCls?: string; className?: string; rootClassName?: string; ghost?: boolean; danger?: boolean; block?: boolean; children?: React.ReactNode; [key: `data-${string}`]: string; classNames?: { icon: string }; styles?: { icon: React.CSSProperties }; } export type AnchorButtonProps = { href: string; target?: React.HTMLAttributeAnchorTarget; onClick?: React.MouseEventHandler; } & BaseButtonProps & Omit, 'type' | 'onClick'>; export type NativeButtonProps = { htmlType?: ButtonHTMLType; onClick?: React.MouseEventHandler; } & BaseButtonProps & Omit, 'type' | 'onClick'>; export type ButtonProps = Partial; type CompoundedComponent = React.ForwardRefExoticComponent< ButtonProps & React.RefAttributes > & { Group: typeof Group; /** @internal */ __ANT_BUTTON: boolean; }; type Loading = number | boolean; type LoadingConfigType = { loading: boolean; delay: number; }; function getLoadingConfig(loading: BaseButtonProps['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 InternalButton: React.ForwardRefRenderFunction< HTMLButtonElement | HTMLAnchorElement, ButtonProps > = (props, ref) => { const { loading = false, prefixCls: customizePrefixCls, type = 'default', danger, shape = 'default', size: customizeSize, styles, disabled: customDisabled, className, rootClassName, children, icon, ghost = false, block = false, // React does not recognize the `htmlType` prop on a DOM element. Here we pick it out of `rest`. htmlType = 'button', classNames: customClassNames, style: customStyle = {}, ...rest } = props; const { getPrefixCls, autoInsertSpaceInButton, direction, button } = useContext(ConfigContext); const prefixCls = getPrefixCls('btn', customizePrefixCls); const [wrapSSR, hashId] = useStyle(prefixCls); const disabled = useContext(DisabledContext); const mergedDisabled = customDisabled ?? disabled; const groupSize = useContext(GroupSizeContext); const loadingOrDelay = useMemo(() => getLoadingConfig(loading), [loading]); const [innerLoading, setLoading] = useState(loadingOrDelay.loading); const [hasTwoCNChar, setHasTwoCNChar] = useState(false); const internalRef = createRef(); const buttonRef = composeRef(ref, internalRef); const needInserted = Children.count(children) === 1 && !icon && !isUnBorderedButtonType(type); useEffect(() => { let delayTimer: NodeJS.Timer | null = null; if (loadingOrDelay.delay > 0) { delayTimer = setTimeout(() => { delayTimer = null; setLoading(true); }, loadingOrDelay.delay); } else { setLoading(loadingOrDelay.loading); } function cleanupTimer() { if (delayTimer) { clearTimeout(delayTimer); delayTimer = null; } } return cleanupTimer; }, [loadingOrDelay]); useEffect(() => { // FIXME: for HOC usage like if (!buttonRef || !(buttonRef as any).current || autoInsertSpaceInButton === false) { return; } const buttonText = (buttonRef as any).current.textContent; if (needInserted && isTwoCNChar(buttonText)) { if (!hasTwoCNChar) { setHasTwoCNChar(true); } } else if (hasTwoCNChar) { setHasTwoCNChar(false); } }, [buttonRef]); const handleClick = (e: React.MouseEvent) => { const { onClick } = props; // FIXME: https://github.com/ant-design/ant-design/issues/30207 if (innerLoading || mergedDisabled) { e.preventDefault(); return; } (onClick as React.MouseEventHandler)?.(e); }; 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.", ); const autoInsertSpace = autoInsertSpaceInButton !== false; const { compactSize, compactItemClassnames } = useCompactItemContext(prefixCls, direction); const sizeClassNameMap = { large: 'lg', small: 'sm', middle: undefined }; const sizeFullName = useSize((ctxSize) => customizeSize ?? compactSize ?? groupSize ?? ctxSize); const sizeCls = sizeFullName ? sizeClassNameMap[sizeFullName] || '' : ''; const iconType = innerLoading ? 'loading' : icon; const linkButtonRestProps = omit(rest as ButtonProps & { navigate: any }, ['navigate']); const hrefAndDisabled = linkButtonRestProps.href !== undefined && mergedDisabled; const classes = classNames( prefixCls, hashId, { [`${prefixCls}-${shape}`]: shape !== 'default' && shape, [`${prefixCls}-${type}`]: type, [`${prefixCls}-${sizeCls}`]: sizeCls, [`${prefixCls}-icon-only`]: !children && children !== 0 && !!iconType, [`${prefixCls}-background-ghost`]: ghost && !isUnBorderedButtonType(type), [`${prefixCls}-loading`]: innerLoading, [`${prefixCls}-two-chinese-chars`]: hasTwoCNChar && autoInsertSpace && !innerLoading, [`${prefixCls}-block`]: block, [`${prefixCls}-dangerous`]: !!danger, [`${prefixCls}-rtl`]: direction === 'rtl', [`${prefixCls}-disabled`]: hrefAndDisabled, }, compactItemClassnames, className, rootClassName, button?.className, ); const fullStyle = { ...button?.style, ...customStyle }; const iconClasses = classNames(customClassNames?.icon, button?.classNames?.icon); const iconStyle = { ...(styles?.icon || {}), ...(button?.styles?.icon || {}) }; const iconNode = icon && !innerLoading ? ( {icon} ) : ( ); const kids = children || children === 0 ? spaceChildren(children, needInserted && autoInsertSpace) : null; if (linkButtonRestProps.href !== undefined) { return wrapSSR( } > {iconNode} {kids} , ); } let buttonNode = ( ); if (!isUnBorderedButtonType(type)) { buttonNode = {buttonNode}; } return wrapSSR(buttonNode); }; const Button = forwardRef( InternalButton, ) as CompoundedComponent; if (process.env.NODE_ENV !== 'production') { Button.displayName = 'Button'; } Button.Group = Group; Button.__ANT_BUTTON = true; export default Button;