Browse Source

refactor(button): rewrite with hook (#23367)

pull/23376/head
Tom Xu 5 years ago
committed by GitHub
parent
commit
09e69c385e
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      components/button/__tests__/type.test.tsx
  2. 329
      components/button/button.tsx
  3. 2
      components/button/index.tsx

3
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);
});
});

329
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<AnchorButtonProps & NativeButtonProps>;
interface ButtonState {
loading?: boolean | { delay?: number };
hasTwoCNChar: boolean;
interface ButtonTypeProps extends React.FC<ButtonProps> {
Group: typeof Group;
__ANT_BUTTON: boolean;
}
class Button extends React.Component<ButtonProps, ButtonState> {
static Group: typeof Group;
static __ANT_BUTTON = true;
static contextType = ConfigContext;
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<HTMLButtonElement>();
let delayTimeout: number;
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 <FormatMessage />
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);
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]);
saveButtonRef = (node: HTMLElement | null) => {
this.buttonNode = node;
};
React.useEffect(() => {
fixTwoCNChar();
}, [buttonRef]);
handleClick: React.MouseEventHandler<HTMLButtonElement | HTMLAnchorElement> = e => {
const { loading } = this.state;
const { onClick } = this.props;
const handleClick = (e: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement, MouseEvent>) => {
const { onClick } = props;
if (loading) {
return;
}
@ -178,143 +163,117 @@ class Button extends React.Component<ButtonProps, ButtonState> {
}
};
fixTwoCNChar() {
const { autoInsertSpaceInButton }: ConfigConsumerProps = this.context;
// Fix for HOC usage like <FormatMessage />
if (!this.buttonNode || autoInsertSpaceInButton === false) {
return;
}
const buttonText = this.buttonNode.textContent;
if (this.isNeedInserted() && isTwoCNChar(buttonText)) {
if (!this.state.hasTwoCNChar) {
this.setState({
hasTwoCNChar: true,
return (
<SizeContext.Consumer>
{size => {
const {
prefixCls: customizePrefixCls,
type,
danger,
shape,
size: customizeSize,
className,
children,
icon,
ghost,
block,
...rest
} = props;
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 (
<SizeContext.Consumer>
{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
) : (
<LoadingIcon existIcon={!!icon} prefixCls={prefixCls} loading={loading} />
);
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',
});
const iconNode =
icon && !loading ? (
icon
) : (
<LoadingIcon existIcon={!!icon} prefixCls={prefixCls} loading={loading} />
);
const kids =
children || children === 0
? spaceChildren(children, this.isNeedInserted() && autoInsertSpace)
: null;
const linkButtonRestProps = omit(rest as AnchorButtonProps, ['htmlType', 'loading']);
if (linkButtonRestProps.href !== undefined) {
return (
<a
{...linkButtonRestProps}
className={classes}
onClick={this.handleClick}
ref={this.saveButtonRef}
>
{iconNode}
{kids}
</a>
);
}
// 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 = (
<button
{...(omit(otherProps, ['loading']) as NativeButtonProps)}
type={htmlType}
className={classes}
onClick={this.handleClick}
ref={this.saveButtonRef}
>
const kids =
children || children === 0
? spaceChildren(children, isNeedInserted() && autoInsertSpace)
: null;
const linkButtonRestProps = omit(rest as AnchorButtonProps, ['htmlType', 'loading']);
if (linkButtonRestProps.href !== undefined) {
return (
<a {...linkButtonRestProps} className={classes} onClick={handleClick} ref={buttonRef}>
{iconNode}
{kids}
</button>
</a>
);
}
// 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 = (
<button
{...(omit(otherProps, ['loading']) as NativeButtonProps)}
type={htmlType}
className={classes}
onClick={handleClick}
ref={buttonRef}
>
{iconNode}
{kids}
</button>
);
if (type === 'link') {
return buttonNode;
}
return <Wave>{buttonNode}</Wave>;
}}
</SizeContext.Consumer>
);
};
if (type === 'link') {
return buttonNode;
}
Button.defaultProps = {
loading: false,
ghost: false,
block: false,
htmlType: 'button' as ButtonProps['htmlType'],
};
return <Wave>{buttonNode}</Wave>;
}}
</SizeContext.Consumer>
);
}
}
Button.Group = Group;
Button.__ANT_BUTTON = true;
export default Button;

2
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;

Loading…
Cancel
Save