mirror of
https://github.com/ant-design/ant-design.git
synced 2025-06-10 11:03:19 +08:00
refactor(button): rewrite with hook (#23367)
This commit is contained in:
parent
f4c489553f
commit
09e69c385e
@ -263,6 +263,7 @@ describe('Button', () => {
|
|||||||
throw new Error('Should not called!!!');
|
throw new Error('Should not called!!!');
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
wrapper.find('Button').instance().forceUpdate();
|
|
||||||
|
expect(wrapper.find('Button').instance()).toBe(null);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -4,7 +4,7 @@ import classNames from 'classnames';
|
|||||||
import omit from 'omit.js';
|
import omit from 'omit.js';
|
||||||
|
|
||||||
import Group from './button-group';
|
import Group from './button-group';
|
||||||
import { ConfigContext, ConfigConsumerProps } from '../config-provider';
|
import { ConfigContext } from '../config-provider';
|
||||||
import Wave from '../_util/wave';
|
import Wave from '../_util/wave';
|
||||||
import { Omit, tuple } from '../_util/type';
|
import { Omit, tuple } from '../_util/type';
|
||||||
import warning from '../_util/warning';
|
import warning from '../_util/warning';
|
||||||
@ -104,72 +104,57 @@ export type NativeButtonProps = {
|
|||||||
|
|
||||||
export type ButtonProps = Partial<AnchorButtonProps & NativeButtonProps>;
|
export type ButtonProps = Partial<AnchorButtonProps & NativeButtonProps>;
|
||||||
|
|
||||||
interface ButtonState {
|
interface ButtonTypeProps extends React.FC<ButtonProps> {
|
||||||
loading?: boolean | { delay?: number };
|
Group: typeof Group;
|
||||||
hasTwoCNChar: boolean;
|
__ANT_BUTTON: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
class Button extends React.Component<ButtonProps, ButtonState> {
|
const Button: ButtonTypeProps = ({ ...props }) => {
|
||||||
static Group: typeof Group;
|
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 __ANT_BUTTON = true;
|
const isNeedInserted = () => {
|
||||||
|
const { icon, children, type } = props;
|
||||||
static contextType = ConfigContext;
|
return React.Children.count(children) === 1 && !icon && type !== 'link';
|
||||||
|
|
||||||
static defaultProps = {
|
|
||||||
loading: false,
|
|
||||||
ghost: false,
|
|
||||||
block: false,
|
|
||||||
htmlType: 'button' as ButtonProps['htmlType'],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private delayTimeout: number;
|
const fixTwoCNChar = () => {
|
||||||
|
// Fix for HOC usage like <FormatMessage />
|
||||||
private buttonNode: HTMLElement | null;
|
if (!buttonRef || !buttonRef.current || autoInsertSpaceInButton === false) {
|
||||||
|
return;
|
||||||
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 buttonText = buttonRef.current.textContent;
|
||||||
const { loading } = this.props;
|
if (isNeedInserted() && isTwoCNChar(buttonText)) {
|
||||||
if (loading && typeof loading !== 'boolean' && loading.delay) {
|
if (!hasTwoCNChar) {
|
||||||
this.delayTimeout = window.setTimeout(() => {
|
setHasTwoCNChar(true);
|
||||||
this.setState({ loading });
|
}
|
||||||
}, loading.delay);
|
} else if (hasTwoCNChar) {
|
||||||
} else if (prevProps.loading !== loading) {
|
setHasTwoCNChar(false);
|
||||||
// eslint-disable-next-line react/no-did-update-set-state
|
|
||||||
this.setState({ loading });
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
if (this.delayTimeout) {
|
|
||||||
clearTimeout(this.delayTimeout);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
saveButtonRef = (node: HTMLElement | null) => {
|
|
||||||
this.buttonNode = node;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
handleClick: React.MouseEventHandler<HTMLButtonElement | HTMLAnchorElement> = e => {
|
React.useEffect(() => {
|
||||||
const { loading } = this.state;
|
if (props.loading && typeof props.loading !== 'boolean') {
|
||||||
const { onClick } = this.props;
|
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<HTMLButtonElement | HTMLAnchorElement, MouseEvent>) => {
|
||||||
|
const { onClick } = props;
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -178,143 +163,117 @@ class Button extends React.Component<ButtonProps, ButtonState> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fixTwoCNChar() {
|
return (
|
||||||
const { autoInsertSpaceInButton }: ConfigConsumerProps = this.context;
|
<SizeContext.Consumer>
|
||||||
|
{size => {
|
||||||
|
const {
|
||||||
|
prefixCls: customizePrefixCls,
|
||||||
|
type,
|
||||||
|
danger,
|
||||||
|
shape,
|
||||||
|
size: customizeSize,
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
icon,
|
||||||
|
ghost,
|
||||||
|
block,
|
||||||
|
...rest
|
||||||
|
} = props;
|
||||||
|
|
||||||
// Fix for HOC usage like <FormatMessage />
|
warning(
|
||||||
if (!this.buttonNode || autoInsertSpaceInButton === false) {
|
!(typeof icon === 'string' && icon.length > 2),
|
||||||
return;
|
'Button',
|
||||||
}
|
`\`icon\` is using ReactNode instead of string naming in v4. Please check \`${icon}\` at https://ant.design/components/icon`,
|
||||||
const buttonText = this.buttonNode.textContent;
|
);
|
||||||
if (this.isNeedInserted() && isTwoCNChar(buttonText)) {
|
|
||||||
if (!this.state.hasTwoCNChar) {
|
const prefixCls = getPrefixCls('btn', customizePrefixCls);
|
||||||
this.setState({
|
const autoInsertSpace = autoInsertSpaceInButton !== false;
|
||||||
hasTwoCNChar: true,
|
|
||||||
|
// 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 iconNode =
|
||||||
const { icon, children, type } = this.props;
|
icon && !loading ? (
|
||||||
return React.Children.count(children) === 1 && !icon && type !== 'link';
|
icon
|
||||||
}
|
) : (
|
||||||
|
<LoadingIcon existIcon={!!icon} prefixCls={prefixCls} loading={loading} />
|
||||||
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 prefixCls = getPrefixCls('btn', customizePrefixCls);
|
const kids =
|
||||||
const autoInsertSpace = autoInsertSpaceInButton !== false;
|
children || children === 0
|
||||||
|
? spaceChildren(children, isNeedInserted() && autoInsertSpace)
|
||||||
|
: null;
|
||||||
|
|
||||||
// large => lg
|
const linkButtonRestProps = omit(rest as AnchorButtonProps, ['htmlType', 'loading']);
|
||||||
// small => sm
|
if (linkButtonRestProps.href !== undefined) {
|
||||||
let sizeCls = '';
|
return (
|
||||||
switch (customizeSize || size) {
|
<a {...linkButtonRestProps} className={classes} onClick={handleClick} ref={buttonRef}>
|
||||||
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}
|
|
||||||
>
|
|
||||||
{iconNode}
|
{iconNode}
|
||||||
{kids}
|
{kids}
|
||||||
</button>
|
</a>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (type === 'link') {
|
// React does not recognize the `htmlType` prop on a DOM element. Here we pick it out of `rest`.
|
||||||
return buttonNode;
|
const { htmlType, ...otherProps } = rest as NativeButtonProps;
|
||||||
}
|
|
||||||
|
|
||||||
return <Wave>{buttonNode}</Wave>;
|
const buttonNode = (
|
||||||
}}
|
<button
|
||||||
</SizeContext.Consumer>
|
{...(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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Button.defaultProps = {
|
||||||
|
loading: false,
|
||||||
|
ghost: false,
|
||||||
|
block: false,
|
||||||
|
htmlType: 'button' as ButtonProps['htmlType'],
|
||||||
|
};
|
||||||
|
|
||||||
|
Button.Group = Group;
|
||||||
|
Button.__ANT_BUTTON = true;
|
||||||
|
|
||||||
export default Button;
|
export default Button;
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
import Button from './button';
|
import Button from './button';
|
||||||
import ButtonGroup from './button-group';
|
|
||||||
|
|
||||||
export { ButtonProps, ButtonShape, ButtonType } from './button';
|
export { ButtonProps, ButtonShape, ButtonType } from './button';
|
||||||
export { ButtonGroupProps } from './button-group';
|
export { ButtonGroupProps } from './button-group';
|
||||||
export { SizeType as ButtonSize } from '../config-provider/SizeContext';
|
export { SizeType as ButtonSize } from '../config-provider/SizeContext';
|
||||||
|
|
||||||
Button.Group = ButtonGroup;
|
|
||||||
export default Button;
|
export default Button;
|
||||||
|
Loading…
Reference in New Issue
Block a user