import React, { Children, useContext, useEffect, useMemo, useRef, useState } from 'react'; import classNames from 'classnames'; import omit from 'rc-util/lib/omit'; import { useComposeRef } from 'rc-util/lib/ref'; import { devUseWarning } from '../_util/warning'; import Wave from '../_util/wave'; import { ConfigContext } from '../config-provider'; import DisabledContext from '../config-provider/DisabledContext'; import useSize from '../config-provider/hooks/useSize'; import type { SizeType } from '../config-provider/SizeContext'; import { useCompactItemContext } from '../space/Compact'; import Group, { GroupSizeContext } from './button-group'; import type { ButtonColorType, ButtonHTMLType, ButtonShape, ButtonType, ButtonVariantType, } from './buttonHelpers'; import { isTwoCNChar, isUnBorderedButtonVariant, spaceChildren } from './buttonHelpers'; import IconWrapper from './IconWrapper'; import LoadingIcon from './LoadingIcon'; import useStyle from './style'; import Compact from './style/compact'; export type LegacyButtonType = ButtonType | 'danger'; export interface BaseButtonProps { type?: ButtonType; color?: ButtonColorType; variant?: ButtonVariantType; icon?: React.ReactNode; iconPosition?: 'start' | 'end'; 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 }; } type MergedHTMLAttributes = Omit< React.HTMLAttributes & React.ButtonHTMLAttributes & React.AnchorHTMLAttributes, 'type' | 'color' >; export interface ButtonProps extends BaseButtonProps, MergedHTMLAttributes { href?: string; htmlType?: ButtonHTMLType; autoInsertSpace?: boolean; } type LoadingConfigType = { loading: boolean; delay: number; }; function getLoadingConfig(loading: BaseButtonProps['loading']): LoadingConfigType { if (typeof loading === 'object' && loading) { let delay = loading?.delay; delay = !Number.isNaN(delay) && typeof delay === 'number' ? delay : 0; return { loading: delay <= 0, delay, }; } return { loading: !!loading, delay: 0, }; } type ColorVariantPairType = [color?: ButtonColorType, variant?: ButtonVariantType]; const ButtonTypeMap: Partial> = { default: ['default', 'outlined'], primary: ['primary', 'solid'], dashed: ['default', 'dashed'], link: ['primary', 'link'], text: ['default', 'text'], }; const InternalCompoundedButton = React.forwardRef< HTMLButtonElement | HTMLAnchorElement, ButtonProps >((props, ref) => { const { loading = false, prefixCls: customizePrefixCls, color, variant, type, danger = false, shape = 'default', size: customizeSize, styles, disabled: customDisabled, className, rootClassName, children, icon, iconPosition = 'start', 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 = {}, autoInsertSpace, autoFocus, ...rest } = props; // https://github.com/ant-design/ant-design/issues/47605 // Compatible with original `type` behavior const mergedType = type || 'default'; const [mergedColor, mergedVariant] = useMemo(() => { if (color && variant) { return [color, variant]; } const colorVariantPair = ButtonTypeMap[mergedType] || []; if (danger) { return ['danger', colorVariantPair[1]]; } return colorVariantPair; }, [type, color, variant, danger]); const isDanger = mergedColor === 'danger'; const mergedColorText = isDanger ? 'dangerous' : mergedColor; const { getPrefixCls, direction, button } = useContext(ConfigContext); const mergedInsertSpace = autoInsertSpace ?? button?.autoInsertSpace ?? true; const prefixCls = getPrefixCls('btn', customizePrefixCls); const [wrapCSSVar, hashId, cssVarCls] = 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 buttonRef = useRef(); const mergedRef = useComposeRef(ref, buttonRef); const needInserted = Children.count(children) === 1 && !icon && !isUnBorderedButtonVariant(mergedVariant); // ========================= Effect ========================= // Loading useEffect(() => { let delayTimer: ReturnType | 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]); // Two chinese characters check useEffect(() => { // FIXME: for HOC usage like if (!buttonRef.current || !mergedInsertSpace) { return; } const buttonText = buttonRef.current.textContent || ''; if (needInserted && isTwoCNChar(buttonText)) { if (!hasTwoCNChar) { setHasTwoCNChar(true); } } else if (hasTwoCNChar) { setHasTwoCNChar(false); } }); // Auto focus useEffect(() => { if (autoFocus && buttonRef.current) { buttonRef.current.focus(); } }, []); // ========================= Events ========================= const handleClick = React.useCallback( (e: React.MouseEvent) => { // FIXME: https://github.com/ant-design/ant-design/issues/30207 if (innerLoading || mergedDisabled) { e.preventDefault(); return; } props.onClick?.(e); }, [props.onClick, innerLoading, mergedDisabled], ); // ========================== Warn ========================== if (process.env.NODE_ENV !== 'production') { const warning = devUseWarning('Button'); warning( !(typeof icon === 'string' && icon.length > 2), 'breaking', `\`icon\` is using ReactNode instead of string naming in v4. Please check \`${icon}\` at https://ant.design/components/icon`, ); warning( !(ghost && isUnBorderedButtonVariant(mergedVariant)), 'usage', "`link` or `text` button can't be a `ghost` button.", ); } // ========================== Size ========================== 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']); // ========================= Render ========================= const classes = classNames( prefixCls, hashId, cssVarCls, { [`${prefixCls}-${shape}`]: shape !== 'default' && shape, // line(253 - 254): Compatible with versions earlier than 5.21.0 [`${prefixCls}-${mergedType}`]: mergedType, [`${prefixCls}-dangerous`]: danger, [`${prefixCls}-color-${mergedColorText}`]: mergedColorText, [`${prefixCls}-variant-${mergedVariant}`]: mergedVariant, [`${prefixCls}-${sizeCls}`]: sizeCls, [`${prefixCls}-icon-only`]: !children && children !== 0 && !!iconType, [`${prefixCls}-background-ghost`]: ghost && !isUnBorderedButtonVariant(mergedVariant), [`${prefixCls}-loading`]: innerLoading, [`${prefixCls}-two-chinese-chars`]: hasTwoCNChar && mergedInsertSpace && !innerLoading, [`${prefixCls}-block`]: block, [`${prefixCls}-rtl`]: direction === 'rtl', [`${prefixCls}-icon-end`]: iconPosition === 'end', }, compactItemClassnames, className, rootClassName, button?.className, ); const fullStyle: React.CSSProperties = { ...button?.style, ...customStyle }; const iconClasses = classNames(customClassNames?.icon, button?.classNames?.icon); const iconStyle: React.CSSProperties = { ...(styles?.icon || {}), ...(button?.styles?.icon || {}), }; const iconNode = icon && !innerLoading ? ( {icon} ) : ( ); const kids = children || children === 0 ? spaceChildren(children, needInserted && mergedInsertSpace) : null; if (linkButtonRestProps.href !== undefined) { return wrapCSSVar( } tabIndex={mergedDisabled ? -1 : 0} > {iconNode} {kids} , ); } let buttonNode = ( ); if (!isUnBorderedButtonVariant(mergedVariant)) { buttonNode = ( {buttonNode} ); } return wrapCSSVar(buttonNode); }); type CompoundedComponent = typeof InternalCompoundedButton & { Group: typeof Group; /** @internal */ __ANT_BUTTON: boolean; }; const Button = InternalCompoundedButton as CompoundedComponent; Button.Group = Group; Button.__ANT_BUTTON = true; if (process.env.NODE_ENV !== 'production') { Button.displayName = 'Button'; } export default Button;