/* eslint-disable react/button-has-type */ import classNames from 'classnames'; import omit from 'rc-util/lib/omit'; import * as React 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 SizeContext from '../config-provider/SizeContext'; import { useCompactItemContext } from '../space/Compact'; 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; } export type AnchorButtonProps = { href: string; target?: string; 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, 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', ...rest } = props; const { getPrefixCls, autoInsertSpaceInButton, direction } = React.useContext(ConfigContext); const prefixCls = getPrefixCls('btn', customizePrefixCls); const [wrapSSR, hashId] = useStyle(prefixCls); const size = React.useContext(SizeContext); const disabled = React.useContext(DisabledContext); const mergedDisabled = customDisabled ?? disabled; const groupSize = React.useContext(GroupSizeContext); const loadingOrDelay: LoadingConfigType = React.useMemo( () => getLoadingConfig(loading), [loading], ); const [innerLoading, setLoading] = React.useState(loadingOrDelay.loading); const [hasTwoCNChar, setHasTwoCNChar] = React.useState(false); const buttonRef = (ref as any) || React.createRef(); const isNeedInserted = () => React.Children.count(children) === 1 && !icon && !isUnBorderedButtonType(type); const fixTwoCNChar = () => { // FIXME: for HOC usage like if (!buttonRef || !buttonRef.current || autoInsertSpaceInButton === false) { return; } const buttonText = buttonRef.current.textContent; if (isNeedInserted() && isTwoCNChar(buttonText)) { if (!hasTwoCNChar) { setHasTwoCNChar(true); } } else if (hasTwoCNChar) { setHasTwoCNChar(false); } }; React.useEffect(() => { let delayTimer: number | null = null; if (loadingOrDelay.delay > 0) { delayTimer = window.setTimeout(() => { delayTimer = null; setLoading(true); }, loadingOrDelay.delay); } else { setLoading(loadingOrDelay.loading); } function cleanupTimer() { if (delayTimer) { window.clearTimeout(delayTimer); delayTimer = null; } } return cleanupTimer; }, [loadingOrDelay]); React.useEffect(fixTwoCNChar, [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 = compactSize || groupSize || customizeSize || size; 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, ); const iconNode = icon && !innerLoading ? ( icon ) : ( ); const kids = children || children === 0 ? spaceChildren(children, isNeedInserted() && autoInsertSpace) : null; if (linkButtonRestProps.href !== undefined) { return wrapSSR( {iconNode} {kids} , ); } let buttonNode = ( ); if (!isUnBorderedButtonType(type)) { buttonNode = {buttonNode}; } return wrapSSR(buttonNode); }; const Button = React.forwardRef( InternalButton, ) as CompoundedComponent; if (process.env.NODE_ENV !== 'production') { Button.displayName = 'Button'; } Button.Group = Group; Button.__ANT_BUTTON = true; export default Button;