/* eslint-disable react/button-has-type */ import * as React from 'react'; import classNames from 'classnames'; import omit from 'rc-util/lib/omit'; import Group from './button-group'; import { ConfigContext } from '../config-provider'; import Wave from '../_util/wave'; import { tuple } from '../_util/type'; import devWarning from '../_util/devWarning'; import SizeContext, { SizeType } from '../config-provider/SizeContext'; import LoadingIcon from './LoadingIcon'; import { cloneElement } from '../_util/reactNode'; const rxTwoCNChar = /^[\u4e00-\u9fa5]{2}$/; const isTwoCNChar = rxTwoCNChar.test.bind(rxTwoCNChar); function isString(str: any) { return typeof str === 'string'; } function isUnborderedButtonType(type: ButtonType | undefined) { return type === 'text' || type === 'link'; } function isReactFragment(node: React.ReactNode) { return React.isValidElement(node) && node.type === React.Fragment; } // Insert one space between two chinese characters automatically. function insertSpace(child: React.ReactChild, needInserted: boolean) { // Check the child if is undefined or null. if (child == null) { return; } const SPACE = needInserted ? ' ' : ''; // strictNullChecks oops. if ( typeof child !== 'string' && typeof child !== 'number' && isString(child.type) && isTwoCNChar(child.props.children) ) { return cloneElement(child, { children: child.props.children.split('').join(SPACE), }); } if (typeof child === 'string') { return isTwoCNChar(child) ? {child.split('').join(SPACE)} : {child}; } if (isReactFragment(child)) { return {child}; } return child; } function spaceChildren(children: React.ReactNode, needInserted: boolean) { let isPrevChildPure: boolean = false; const childList: React.ReactNode[] = []; React.Children.forEach(children, child => { const type = typeof child; const isCurrentChildPure = type === 'string' || type === 'number'; if (isPrevChildPure && isCurrentChildPure) { const lastIndex = childList.length - 1; const lastChild = childList[lastIndex]; childList[lastIndex] = `${lastChild}${child}`; } else { childList.push(child); } isPrevChildPure = isCurrentChildPure; }); // Pass to React.Children.map to auto fill key return React.Children.map(childList, child => insertSpace(child as React.ReactChild, needInserted), ); } const ButtonTypes = tuple('default', 'primary', 'ghost', 'dashed', 'link', 'text'); export type ButtonType = typeof ButtonTypes[number]; const ButtonShapes = tuple('default', 'circle', 'round'); export type ButtonShape = typeof ButtonShapes[number]; const ButtonHTMLTypes = tuple('submit', 'button', 'reset'); export type ButtonHTMLType = typeof ButtonHTMLTypes[number]; 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; /** @default default */ shape?: ButtonShape; size?: SizeType; loading?: boolean | { delay?: number }; prefixCls?: string; className?: string; ghost?: boolean; danger?: boolean; block?: boolean; children?: React.ReactNode; } // Typescript will make optional not optional if use Pick with union. // Should change to `AnchorButtonProps | NativeButtonProps` and `any` to `HTMLAnchorElement | HTMLButtonElement` if it fixed. // ref: https://github.com/ant-design/ant-design/issues/15930 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; interface CompoundedComponent extends React.ForwardRefExoticComponent> { Group: typeof Group; __ANT_BUTTON: boolean; } type Loading = number | boolean; const InternalButton: React.ForwardRefRenderFunction = (props, ref) => { const { loading = false, prefixCls: customizePrefixCls, type = 'default', danger, shape = 'default', size: customizeSize, className, children, icon, ghost = false, block = false, /** If we extract items here, we don't need use omit.js */ // React does not recognize the `htmlType` prop on a DOM element. Here we pick it out of `rest`. htmlType = 'button' as ButtonProps['htmlType'], ...rest } = props; const size = React.useContext(SizeContext); const [innerLoading, setLoading] = React.useState(!!loading); const [hasTwoCNChar, setHasTwoCNChar] = React.useState(false); const { getPrefixCls, autoInsertSpaceInButton, direction } = React.useContext(ConfigContext); const buttonRef = (ref as any) || React.createRef(); const isNeedInserted = () => React.Children.count(children) === 1 && !icon && !isUnborderedButtonType(type); const fixTwoCNChar = () => { // Fix 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); } }; // =============== Update Loading =============== const loadingOrDelay: Loading = typeof loading === 'object' && loading.delay ? loading.delay || true : !!loading; React.useEffect(() => { let delayTimer: number | null = null; if (typeof loadingOrDelay === 'number') { delayTimer = window.setTimeout(() => { delayTimer = null; setLoading(loadingOrDelay); }, loadingOrDelay); } else { setLoading(loadingOrDelay); } return () => { if (delayTimer) { // in order to not perform a React state update on an unmounted component // and clear timer after 'loadingOrDelay' updated. window.clearTimeout(delayTimer); delayTimer = null; } }; }, [loadingOrDelay]); React.useEffect(fixTwoCNChar, [buttonRef]); const handleClick = (e: React.MouseEvent) => { const { onClick, disabled } = props; // https://github.com/ant-design/ant-design/issues/30207 if (innerLoading || disabled) { e.preventDefault(); return; } (onClick as React.MouseEventHandler)?.(e); }; devWarning( !(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`, ); devWarning( !(ghost && isUnborderedButtonType(type)), 'Button', "`link` or `text` button can't be a `ghost` button.", ); const prefixCls = getPrefixCls('btn', customizePrefixCls); const autoInsertSpace = autoInsertSpaceInButton !== false; const sizeClassNameMap = { large: 'lg', small: 'sm', middle: undefined }; const sizeFullname = customizeSize || size; const sizeCls = sizeFullname ? sizeClassNameMap[sizeFullname] || '' : ''; const iconType = innerLoading ? 'loading' : icon; const classes = classNames( prefixCls, { [`${prefixCls}-${shape}`]: shape !== 'default' && shape, // Note: Shape also has `default` [`${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, [`${prefixCls}-block`]: block, [`${prefixCls}-dangerous`]: !!danger, [`${prefixCls}-rtl`]: direction === 'rtl', }, className, ); const iconNode = icon && !innerLoading ? ( icon ) : ( ); const kids = children || children === 0 ? spaceChildren(children, isNeedInserted() && autoInsertSpace) : null; const linkButtonRestProps = omit(rest as AnchorButtonProps & { navigate: any }, ['navigate']); if (linkButtonRestProps.href !== undefined) { return ( {iconNode} {kids} ); } const buttonNode = ( ); if (isUnborderedButtonType(type)) { return buttonNode; } return {buttonNode}; }; const Button = React.forwardRef(InternalButton) as CompoundedComponent; Button.displayName = 'Button'; Button.Group = Group; Button.__ANT_BUTTON = true; export default Button;