refactor(tooltip): rewrite with hook (#23699)

* refactor(tooltip): rewrite with hook

* fix lint

* fix

* fix test

* fix

* fix test

* refactor(tooltip): rewrite with hook

* fix lint

* fix test

* Update SliderTooltip.tsx

* Update tooltip.test.js

* Update index.test.js
This commit is contained in:
Tom Xu 2020-05-15 10:47:03 +08:00 committed by GitHub
parent 0cd0ebe7dc
commit f90702cd0c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 168 additions and 143 deletions

View File

@ -74,9 +74,10 @@ describe('Alert', () => {
});
it('could be used with Tooltip', async () => {
const ref = React.createRef<any>();
jest.useRealTimers();
const wrapper = mount(
<Tooltip title="xxx" mouseEnterDelay={0}>
<Tooltip title="xxx" mouseEnterDelay={0} ref={ref}>
<Alert
message="Warning Text Warning Text Warning TextW arning Text Warning Text Warning TextWarning Text"
type="warning"
@ -85,7 +86,7 @@ describe('Alert', () => {
);
wrapper.find('.ant-alert').simulate('mouseenter');
await sleep(0);
expect(wrapper.find<Tooltip>(Tooltip).instance().getPopupDomNode()).toBeTruthy();
expect(ref.current.getPopupDomNode()).toBeTruthy();
jest.useFakeTimers();
});

View File

@ -38,23 +38,21 @@ describe('Badge', () => {
it('should have an overriden title attribute', () => {
const badge = mount(<Badge count={10} title="Custom title" />);
expect(
badge
.find('.ant-scroll-number')
.getDOMNode()
.attributes.getNamedItem('title').value,
badge.find('.ant-scroll-number').getDOMNode().attributes.getNamedItem('title').value,
).toEqual('Custom title');
});
// https://github.com/ant-design/ant-design/issues/10626
it('should be composable with Tooltip', () => {
const ref = React.createRef();
const wrapper = mount(
<Tooltip title="Fix the error">
<Tooltip title="Fix the error" ref={ref}>
<Badge status="error" />
</Tooltip>,
);
wrapper.find('Badge').simulate('mouseenter');
jest.runAllTimers();
expect(wrapper.instance().tooltip.props.visible).toBe(true);
expect(ref.current.props.visible).toBe(true);
});
it('should render when count is changed', () => {

View File

@ -1,9 +1,30 @@
import * as React from 'react';
import Tooltip, { TooltipProps } from '../tooltip';
export default function SliderTooltip(props: TooltipProps) {
function useCombinedRefs(
...refs: Array<React.MutableRefObject<unknown> | ((instance: unknown) => void) | null>
) {
const targetRef = React.useRef();
React.useEffect(() => {
refs.forEach(ref => {
if (!ref) return;
if (typeof ref === 'function') {
ref(targetRef.current);
} else {
ref.current = targetRef.current;
}
});
}, [refs]);
return targetRef;
}
const SliderTooltip = React.forwardRef<unknown, TooltipProps>((props, ref) => {
const { visible } = props;
const tooltipRef = React.useRef<Tooltip>(null);
const innerRef = React.useRef<any>(null);
const tooltipRef = useCombinedRefs(ref, innerRef);
const rafRef = React.useRef<number | null>(null);
@ -18,9 +39,7 @@ export default function SliderTooltip(props: TooltipProps) {
}
rafRef.current = window.requestAnimationFrame(() => {
if (tooltipRef.current && (tooltipRef.current as any).tooltip) {
(tooltipRef.current as any).tooltip.forcePopupAlign();
}
(tooltipRef.current as any).forcePopupAlign();
rafRef.current = null;
keepAlign();
@ -38,4 +57,6 @@ export default function SliderTooltip(props: TooltipProps) {
}, [visible]);
return <Tooltip ref={tooltipRef} {...props} />;
}
});
export default SliderTooltip;

View File

@ -5,6 +5,7 @@ import ConfigProvider from '../../config-provider';
import mountTest from '../../../tests/shared/mountTest';
import rtlTest from '../../../tests/shared/rtlTest';
import focusTest from '../../../tests/shared/focusTest';
import SliderTooltip from '../SliderTooltip';
import { sleep } from '../../../tests/utils';
describe('Slider', () => {
@ -41,10 +42,19 @@ describe('Slider', () => {
});
it('should keepAlign by calling forcePopupAlign', async () => {
const wrapper = mount(<Slider defaultValue={30} tooltipVisible />);
wrapper.find('Tooltip').instance().tooltip.forcePopupAlign = jest.fn();
let ref;
mount(
<SliderTooltip
title="30"
visible
ref={node => {
ref = node;
}}
/>,
);
ref.forcePopupAlign = jest.fn();
await sleep(20);
expect(wrapper.find('Tooltip').instance().tooltip.forcePopupAlign).toHaveBeenCalled();
expect(ref.forcePopupAlign).toHaveBeenCalled();
});
it('tipFormatter should not crash with undefined value', () => {

View File

@ -103,6 +103,7 @@ const Slider = React.forwardRef<unknown, SliderProps>((props, ref) => {
</SliderTooltip>
);
};
const {
prefixCls: customizePrefixCls,
tooltipPrefixCls: customizeTooltipPrefixCls,

View File

@ -17,9 +17,16 @@ describe('Tooltip', () => {
it('check `onVisibleChange` arguments', () => {
const onVisibleChange = jest.fn();
const ref = React.createRef();
const wrapper = mount(
<Tooltip title="" mouseEnterDelay={0} mouseLeaveDelay={0} onVisibleChange={onVisibleChange}>
<Tooltip
title=""
mouseEnterDelay={0}
mouseLeaveDelay={0}
onVisibleChange={onVisibleChange}
ref={ref}
>
<div id="hello">Hello world!</div>
</Tooltip>,
);
@ -28,43 +35,46 @@ describe('Tooltip', () => {
const div = wrapper.find('#hello').at(0);
div.simulate('mouseenter');
expect(onVisibleChange).not.toHaveBeenCalled();
expect(wrapper.instance().tooltip.props.visible).toBe(false);
expect(ref.current.props.visible).toBe(false);
div.simulate('mouseleave');
expect(onVisibleChange).not.toHaveBeenCalled();
expect(wrapper.instance().tooltip.props.visible).toBe(false);
expect(ref.current.props.visible).toBe(false);
// update `title` value.
wrapper.setProps({ title: 'Have a nice day!' });
wrapper.find('#hello').simulate('mouseenter');
expect(onVisibleChange).toHaveBeenLastCalledWith(true);
expect(wrapper.instance().tooltip.props.visible).toBe(true);
expect(ref.current.props.visible).toBe(true);
wrapper.find('#hello').simulate('mouseleave');
expect(onVisibleChange).toHaveBeenLastCalledWith(false);
expect(wrapper.instance().tooltip.props.visible).toBe(false);
expect(ref.current.props.visible).toBe(false);
// add `visible` props.
wrapper.setProps({ visible: false });
wrapper.find('#hello').simulate('mouseenter');
expect(onVisibleChange).toHaveBeenLastCalledWith(true);
const lastCount = onVisibleChange.mock.calls.length;
expect(wrapper.instance().tooltip.props.visible).toBe(false);
expect(ref.current.props.visible).toBe(false);
// always trigger onVisibleChange
wrapper.simulate('mouseleave');
expect(onVisibleChange.mock.calls.length).toBe(lastCount); // no change with lastCount
expect(wrapper.instance().tooltip.props.visible).toBe(false);
expect(ref.current.props.visible).toBe(false);
});
it('should hide when mouse leave native disabled button', () => {
const onVisibleChange = jest.fn();
const ref = React.createRef();
const wrapper = mount(
<Tooltip
title="xxxxx"
mouseEnterDelay={0}
mouseLeaveDelay={0}
onVisibleChange={onVisibleChange}
ref={ref}
>
<button type="button" disabled>
Hello world!
@ -76,23 +86,25 @@ describe('Tooltip', () => {
const button = wrapper.find('span').at(0);
button.simulate('mouseenter');
expect(onVisibleChange).toHaveBeenCalledWith(true);
expect(wrapper.instance().tooltip.props.visible).toBe(true);
expect(ref.current.props.visible).toBe(true);
button.simulate('mouseleave');
expect(onVisibleChange).toHaveBeenCalledWith(false);
expect(wrapper.instance().tooltip.props.visible).toBe(false);
expect(ref.current.props.visible).toBe(false);
});
describe('should hide when mouse leave antd disabled component', () => {
function testComponent(name, Component) {
it(name, () => {
const onVisibleChange = jest.fn();
const ref = React.createRef();
const wrapper = mount(
<Tooltip
title="xxxxx"
mouseEnterDelay={0}
mouseLeaveDelay={0}
onVisibleChange={onVisibleChange}
ref={ref}
>
<Component disabled />
</Tooltip>,
@ -102,11 +114,11 @@ describe('Tooltip', () => {
const button = wrapper.find('span').at(0);
button.simulate('mouseenter');
expect(onVisibleChange).toHaveBeenCalledWith(true);
expect(wrapper.instance().tooltip.props.visible).toBe(true);
expect(ref.current.props.visible).toBe(true);
button.simulate('mouseleave');
expect(onVisibleChange).toHaveBeenCalledWith(false);
expect(wrapper.instance().tooltip.props.visible).toBe(false);
expect(ref.current.props.visible).toBe(false);
});
}
@ -183,9 +195,10 @@ describe('Tooltip', () => {
it('should works for date picker', async () => {
const onVisibleChange = jest.fn();
const ref = React.createRef();
const wrapper = mount(
<Tooltip title="date picker" onVisibleChange={onVisibleChange}>
<Tooltip title="date picker" onVisibleChange={onVisibleChange} ref={ref}>
<DatePicker />
</Tooltip>,
);
@ -195,19 +208,19 @@ describe('Tooltip', () => {
picker.simulate('mouseenter');
await sleep(100);
expect(onVisibleChange).toHaveBeenCalledWith(true);
expect(wrapper.instance().tooltip.props.visible).toBe(true);
expect(ref.current.props.visible).toBe(true);
picker.simulate('mouseleave');
await sleep(100);
expect(onVisibleChange).toHaveBeenCalledWith(false);
expect(wrapper.instance().tooltip.props.visible).toBe(false);
expect(ref.current.props.visible).toBe(false);
});
it('should works for input group', async () => {
const onVisibleChange = jest.fn();
const ref = React.createRef();
const wrapper = mount(
<Tooltip title="hello" onVisibleChange={onVisibleChange}>
<Tooltip title="hello" onVisibleChange={onVisibleChange} ref={ref}>
<Group>
<Input style={{ width: '50%' }} />
<Input style={{ width: '50%' }} />
@ -220,12 +233,12 @@ describe('Tooltip', () => {
picker.simulate('mouseenter');
await sleep(100);
expect(onVisibleChange).toHaveBeenCalledWith(true);
expect(wrapper.instance().tooltip.props.visible).toBe(true);
expect(ref.current.props.visible).toBe(true);
picker.simulate('mouseleave');
await sleep(100);
expect(onVisibleChange).toHaveBeenCalledWith(false);
expect(wrapper.instance().tooltip.props.visible).toBe(false);
expect(ref.current.props.visible).toBe(false);
});
// https://github.com/ant-design/ant-design/issues/20891
@ -279,6 +292,7 @@ describe('Tooltip', () => {
});
it('other placement when mouse enter', async () => {
const ref = React.createRef();
const wrapper = mount(
<Tooltip
title="xxxxx"
@ -286,6 +300,7 @@ describe('Tooltip', () => {
transitionName=""
popupTransitionName=""
mouseEnterDelay={0}
ref={ref}
>
<span>Hello world!</span>
</Tooltip>,
@ -295,10 +310,11 @@ describe('Tooltip', () => {
const button = wrapper.find('span').at(0);
button.simulate('mouseenter');
await sleep(500);
expect(wrapper.instance().getPopupDomNode().className).toContain('placement-topRight');
expect(ref.current.getPopupDomNode().className).toContain('placement-topRight');
});
it('should works for mismatch placement', async () => {
const ref = React.createRef();
const wrapper = mount(
<Tooltip
title="xxxxx"
@ -306,6 +322,7 @@ describe('Tooltip', () => {
points: ['bc', 'tl'],
}}
mouseEnterDelay={0}
ref={ref}
>
<span>Hello world!</span>
</Tooltip>,
@ -313,6 +330,6 @@ describe('Tooltip', () => {
const button = wrapper.find('span').at(0);
button.simulate('mouseenter');
await sleep(600);
expect(wrapper.instance().getPopupDomNode().className).toContain('ant-tooltip');
expect(ref.current.getPopupDomNode().className).toContain('ant-tooltip');
});
});

View File

@ -4,8 +4,8 @@ import { TooltipProps as RcTooltipProps } from 'rc-tooltip/lib/Tooltip';
import classNames from 'classnames';
import { BuildInPlacements } from 'rc-trigger/lib/interface';
import getPlacements, { AdjustOverflow, PlacementsConfig } from './placements';
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider';
import { cloneElement, isValidElement } from '../_util/reactNode';
import { ConfigContext } from '../config-provider';
export { AdjustOverflow, PlacementsConfig };
@ -122,49 +122,35 @@ function getDisabledCompatibleChildren(element: React.ReactElement<any>, prefixC
return element;
}
class Tooltip extends React.Component<TooltipProps, any> {
static defaultProps = {
placement: 'top' as TooltipPlacement,
transitionName: 'zoom-big-fast',
mouseEnterDelay: 0.1,
mouseLeaveDelay: 0.1,
arrowPointAtCenter: false,
autoAdjustOverflow: true,
const Tooltip = React.forwardRef<unknown, TooltipProps>((props, ref) => {
const { getPopupContainer: getContextPopupContainer, getPrefixCls, direction } = React.useContext(
ConfigContext,
);
const [visible, setVisible] = React.useState(!!props.visible || !!props.defaultVisible);
React.useEffect(() => {
if ('visible' in props) {
setVisible(props.visible!);
}
}, [props.visible]);
const isNoTitle = () => {
const { title, overlay } = props;
return !title && !overlay && title !== 0; // overlay for old version compatibility
};
static getDerivedStateFromProps(nextProps: TooltipProps) {
if ('visible' in nextProps) {
return { visible: nextProps.visible };
const onVisibleChange = (vis: boolean) => {
if (!('visible' in props)) {
setVisible(isNoTitle() ? false : vis);
}
return null;
}
private tooltip: typeof RcTooltip;
constructor(props: TooltipProps) {
super(props);
this.state = {
visible: !!props.visible || !!props.defaultVisible,
};
}
onVisibleChange = (visible: boolean) => {
const { onVisibleChange } = this.props;
if (!('visible' in this.props)) {
this.setState({ visible: this.isNoTitle() ? false : visible });
}
if (onVisibleChange && !this.isNoTitle()) {
onVisibleChange(visible);
if (props.onVisibleChange && !isNoTitle()) {
props.onVisibleChange(vis);
}
};
getPopupDomNode() {
return (this.tooltip as any).getPopupDomNode();
}
getPlacements() {
const { builtinPlacements, arrowPointAtCenter, autoAdjustOverflow } = this.props;
const getTooltipPlacements = () => {
const { builtinPlacements, arrowPointAtCenter, autoAdjustOverflow } = props;
return (
builtinPlacements ||
getPlacements({
@ -172,15 +158,11 @@ class Tooltip extends React.Component<TooltipProps, any> {
autoAdjustOverflow,
})
);
}
saveTooltip = (node: typeof RcTooltip) => {
this.tooltip = node;
};
// 动态设置动画点
onPopupAlign = (domNode: HTMLElement, align: any) => {
const placements: any = this.getPlacements();
const onPopupAlign = (domNode: HTMLElement, align: any) => {
const placements: any = getTooltipPlacements();
// 当前返回的位置
const placement = Object.keys(placements).filter(
key =>
@ -209,74 +191,69 @@ class Tooltip extends React.Component<TooltipProps, any> {
domNode.style.transformOrigin = `${transformOrigin.left} ${transformOrigin.top}`;
};
isNoTitle() {
const { title, overlay } = this.props;
return !title && !overlay && title !== 0; // overlay for old version compatibility
}
getOverlay() {
const { title, overlay } = this.props;
const getOverlay = () => {
const { title, overlay } = props;
if (title === 0) {
return title;
}
return overlay || title || '';
}
renderTooltip = ({
getPopupContainer: getContextPopupContainer,
getPrefixCls,
direction,
}: ConfigConsumerProps) => {
const { props, state } = this;
const {
prefixCls: customizePrefixCls,
openClassName,
getPopupContainer,
getTooltipContainer,
overlayClassName,
} = props;
const children = props.children as React.ReactElement<any>;
const prefixCls = getPrefixCls('tooltip', customizePrefixCls);
let { visible } = state;
// Hide tooltip when there is no title
if (!('visible' in props) && this.isNoTitle()) {
visible = false;
}
const child = getDisabledCompatibleChildren(
isValidElement(children) ? children : <span>{children}</span>,
prefixCls,
);
const childProps = child.props;
const childCls = classNames(childProps.className, {
[openClassName || `${prefixCls}-open`]: true,
});
const customOverlayClassName = classNames(overlayClassName, {
[`${prefixCls}-rtl`]: direction === 'rtl',
});
return (
<RcTooltip
{...this.props}
prefixCls={prefixCls}
overlayClassName={customOverlayClassName}
getTooltipContainer={getPopupContainer || getTooltipContainer || getContextPopupContainer}
ref={this.saveTooltip}
builtinPlacements={this.getPlacements()}
overlay={this.getOverlay()}
visible={visible}
onVisibleChange={this.onVisibleChange}
onPopupAlign={this.onPopupAlign}
>
{visible ? cloneElement(child, { className: childCls }) : child}
</RcTooltip>
);
};
render() {
return <ConfigConsumer>{this.renderTooltip}</ConfigConsumer>;
const {
prefixCls: customizePrefixCls,
openClassName,
getPopupContainer,
getTooltipContainer,
overlayClassName,
} = props;
const children = props.children as React.ReactElement<any>;
const prefixCls = getPrefixCls('tooltip', customizePrefixCls);
let tempVisible = visible;
// Hide tooltip when there is no title
if (!('visible' in props) && isNoTitle()) {
tempVisible = false;
}
}
const child = getDisabledCompatibleChildren(
isValidElement(children) ? children : <span>{children}</span>,
prefixCls,
);
const childProps = child.props;
const childCls = classNames(childProps.className, {
[openClassName || `${prefixCls}-open`]: true,
});
const customOverlayClassName = classNames(overlayClassName, {
[`${prefixCls}-rtl`]: direction === 'rtl',
});
return (
<RcTooltip
{...props}
prefixCls={prefixCls}
overlayClassName={customOverlayClassName}
getTooltipContainer={getPopupContainer || getTooltipContainer || getContextPopupContainer}
ref={ref}
builtinPlacements={getTooltipPlacements()}
overlay={getOverlay()}
visible={tempVisible}
onVisibleChange={onVisibleChange}
onPopupAlign={onPopupAlign}
>
{tempVisible ? cloneElement(child, { className: childCls }) : child}
</RcTooltip>
);
});
Tooltip.displayName = 'Tooltip';
Tooltip.defaultProps = {
placement: 'top' as TooltipPlacement,
transitionName: 'zoom-big-fast',
mouseEnterDelay: 0.1,
mouseLeaveDelay: 0.1,
arrowPointAtCenter: false,
autoAdjustOverflow: true,
};
export default Tooltip;