From 09e69c385eb23f944124e09549f3aa04227961b8 Mon Sep 17 00:00:00 2001 From: Tom Xu Date: Sat, 18 Apr 2020 13:06:04 +0800 Subject: [PATCH] refactor(button): rewrite with hook (#23367) --- components/button/__tests__/type.test.tsx | 3 +- components/button/button.tsx | 329 ++++++++++------------ components/button/index.tsx | 2 - 3 files changed, 146 insertions(+), 188 deletions(-) diff --git a/components/button/__tests__/type.test.tsx b/components/button/__tests__/type.test.tsx index 1dd1443be9..dcd4a8a52a 100644 --- a/components/button/__tests__/type.test.tsx +++ b/components/button/__tests__/type.test.tsx @@ -263,6 +263,7 @@ describe('Button', () => { throw new Error('Should not called!!!'); }, }); - wrapper.find('Button').instance().forceUpdate(); + + expect(wrapper.find('Button').instance()).toBe(null); }); }); diff --git a/components/button/button.tsx b/components/button/button.tsx index e0c3dbfc3b..84bc381f0f 100644 --- a/components/button/button.tsx +++ b/components/button/button.tsx @@ -4,7 +4,7 @@ import classNames from 'classnames'; import omit from 'omit.js'; import Group from './button-group'; -import { ConfigContext, ConfigConsumerProps } from '../config-provider'; +import { ConfigContext } from '../config-provider'; import Wave from '../_util/wave'; import { Omit, tuple } from '../_util/type'; import warning from '../_util/warning'; @@ -104,72 +104,57 @@ export type NativeButtonProps = { export type ButtonProps = Partial; -interface ButtonState { - loading?: boolean | { delay?: number }; - hasTwoCNChar: boolean; +interface ButtonTypeProps extends React.FC { + Group: typeof Group; + __ANT_BUTTON: boolean; } -class Button extends React.Component { - static Group: typeof Group; +const Button: ButtonTypeProps = ({ ...props }) => { + const [loading, setLoading] = React.useState(props.loading); + const [hasTwoCNChar, setHasTwoCNChar] = React.useState(false); + const { getPrefixCls, autoInsertSpaceInButton, direction } = React.useContext(ConfigContext); + const buttonRef = React.createRef(); + let delayTimeout: number; - static __ANT_BUTTON = true; - - static contextType = ConfigContext; - - static defaultProps = { - loading: false, - ghost: false, - block: false, - htmlType: 'button' as ButtonProps['htmlType'], + const isNeedInserted = () => { + const { icon, children, type } = props; + return React.Children.count(children) === 1 && !icon && type !== 'link'; }; - private delayTimeout: number; - - private buttonNode: HTMLElement | null; - - constructor(props: ButtonProps) { - super(props); - this.state = { - loading: props.loading, - hasTwoCNChar: false, - }; - } - - componentDidMount() { - this.fixTwoCNChar(); - } - - componentDidUpdate(prevProps: ButtonProps) { - this.fixTwoCNChar(); - - if (prevProps.loading && typeof prevProps.loading !== 'boolean') { - clearTimeout(this.delayTimeout); + const fixTwoCNChar = () => { + // Fix for HOC usage like + if (!buttonRef || !buttonRef.current || autoInsertSpaceInButton === false) { + return; } - - const { loading } = this.props; - if (loading && typeof loading !== 'boolean' && loading.delay) { - this.delayTimeout = window.setTimeout(() => { - this.setState({ loading }); - }, loading.delay); - } else if (prevProps.loading !== loading) { - // eslint-disable-next-line react/no-did-update-set-state - this.setState({ loading }); + const buttonText = buttonRef.current.textContent; + if (isNeedInserted() && isTwoCNChar(buttonText)) { + if (!hasTwoCNChar) { + setHasTwoCNChar(true); + } + } else if (hasTwoCNChar) { + setHasTwoCNChar(false); } - } - - componentWillUnmount() { - if (this.delayTimeout) { - clearTimeout(this.delayTimeout); - } - } - - saveButtonRef = (node: HTMLElement | null) => { - this.buttonNode = node; }; - handleClick: React.MouseEventHandler = e => { - const { loading } = this.state; - const { onClick } = this.props; + React.useEffect(() => { + if (props.loading && typeof props.loading !== 'boolean') { + clearTimeout(delayTimeout); + } + if (props.loading && typeof props.loading !== 'boolean' && props.loading.delay) { + delayTimeout = window.setTimeout(() => { + setLoading(props.loading); + }, props.loading.delay); + } else if (props.loading !== loading) { + setLoading(props.loading); + } + }, [props.loading]); + + React.useEffect(() => { + fixTwoCNChar(); + }, [buttonRef]); + + const handleClick = (e: React.MouseEvent) => { + const { onClick } = props; if (loading) { return; } @@ -178,143 +163,117 @@ class Button extends React.Component { } }; - fixTwoCNChar() { - const { autoInsertSpaceInButton }: ConfigConsumerProps = this.context; + return ( + + {size => { + const { + prefixCls: customizePrefixCls, + type, + danger, + shape, + size: customizeSize, + className, + children, + icon, + ghost, + block, + ...rest + } = props; - // Fix for HOC usage like - if (!this.buttonNode || autoInsertSpaceInButton === false) { - return; - } - const buttonText = this.buttonNode.textContent; - if (this.isNeedInserted() && isTwoCNChar(buttonText)) { - if (!this.state.hasTwoCNChar) { - this.setState({ - hasTwoCNChar: true, + 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`, + ); + + const prefixCls = getPrefixCls('btn', customizePrefixCls); + const autoInsertSpace = autoInsertSpaceInButton !== false; + + // large => lg + // small => sm + let sizeCls = ''; + switch (customizeSize || size) { + case 'large': + sizeCls = 'lg'; + break; + case 'small': + sizeCls = 'sm'; + break; + default: + break; + } + + const iconType = loading ? 'loading' : icon; + + const classes = classNames(prefixCls, className, { + [`${prefixCls}-${type}`]: type, + [`${prefixCls}-${shape}`]: shape, + [`${prefixCls}-${sizeCls}`]: sizeCls, + [`${prefixCls}-icon-only`]: !children && children !== 0 && iconType, + [`${prefixCls}-background-ghost`]: ghost, + [`${prefixCls}-loading`]: loading, + [`${prefixCls}-two-chinese-chars`]: hasTwoCNChar && autoInsertSpace, + [`${prefixCls}-block`]: block, + [`${prefixCls}-dangerous`]: !!danger, + [`${prefixCls}-rtl`]: direction === 'rtl', }); - } - } else if (this.state.hasTwoCNChar) { - this.setState({ - hasTwoCNChar: false, - }); - } - } - isNeedInserted() { - const { icon, children, type } = this.props; - return React.Children.count(children) === 1 && !icon && type !== 'link'; - } - - render() { - const { getPrefixCls, autoInsertSpaceInButton, direction }: ConfigConsumerProps = this.context; - - return ( - - {size => { - const { - prefixCls: customizePrefixCls, - type, - danger, - shape, - size: customizeSize, - className, - children, - icon, - ghost, - block, - ...rest - } = this.props; - const { loading, hasTwoCNChar } = this.state; - - 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`, + const iconNode = + icon && !loading ? ( + icon + ) : ( + ); - const prefixCls = getPrefixCls('btn', customizePrefixCls); - const autoInsertSpace = autoInsertSpaceInButton !== false; + const kids = + children || children === 0 + ? spaceChildren(children, isNeedInserted() && autoInsertSpace) + : null; - // large => lg - // small => sm - let sizeCls = ''; - switch (customizeSize || size) { - case 'large': - sizeCls = 'lg'; - break; - case 'small': - sizeCls = 'sm'; - break; - default: - break; - } - - const iconType = loading ? 'loading' : icon; - - const classes = classNames(prefixCls, className, { - [`${prefixCls}-${type}`]: type, - [`${prefixCls}-${shape}`]: shape, - [`${prefixCls}-${sizeCls}`]: sizeCls, - [`${prefixCls}-icon-only`]: !children && children !== 0 && iconType, - [`${prefixCls}-background-ghost`]: ghost, - [`${prefixCls}-loading`]: loading, - [`${prefixCls}-two-chinese-chars`]: hasTwoCNChar && autoInsertSpace, - [`${prefixCls}-block`]: block, - [`${prefixCls}-dangerous`]: !!danger, - [`${prefixCls}-rtl`]: direction === 'rtl', - }); - - const iconNode = - icon && !loading ? ( - icon - ) : ( - - ); - - const kids = - children || children === 0 - ? spaceChildren(children, this.isNeedInserted() && autoInsertSpace) - : null; - - const linkButtonRestProps = omit(rest as AnchorButtonProps, ['htmlType', 'loading']); - if (linkButtonRestProps.href !== undefined) { - return ( - - {iconNode} - {kids} - - ); - } - - // React does not recognize the `htmlType` prop on a DOM element. Here we pick it out of `rest`. - const { htmlType, ...otherProps } = rest as NativeButtonProps; - - const buttonNode = ( - + ); + } - if (type === 'link') { - return buttonNode; - } + // React does not recognize the `htmlType` prop on a DOM element. Here we pick it out of `rest`. + const { htmlType, ...otherProps } = rest as NativeButtonProps; - return {buttonNode}; - }} - - ); - } -} + const buttonNode = ( + + ); + + if (type === 'link') { + return buttonNode; + } + + return {buttonNode}; + }} + + ); +}; + +Button.defaultProps = { + loading: false, + ghost: false, + block: false, + htmlType: 'button' as ButtonProps['htmlType'], +}; + +Button.Group = Group; +Button.__ANT_BUTTON = true; export default Button; diff --git a/components/button/index.tsx b/components/button/index.tsx index 1f3b6d5996..3fd65352f3 100644 --- a/components/button/index.tsx +++ b/components/button/index.tsx @@ -1,9 +1,7 @@ import Button from './button'; -import ButtonGroup from './button-group'; export { ButtonProps, ButtonShape, ButtonType } from './button'; export { ButtonGroupProps } from './button-group'; export { SizeType as ButtonSize } from '../config-provider/SizeContext'; -Button.Group = ButtonGroup; export default Button;