fix[BackTop]: delete visible & use visibilityHeight=0 replace visible (#38763)

* type: delete visible & code optimization

* add PureBackTop

* fix: update snap

* fix: cov

* fix: add istanbul ignore

* fix

* feat: use visibilityHeight=0 replace visible=true

* snap

* cov

* test case

* test case

* fix

* fix cov

* fix test

* simplify code

* rename function
This commit is contained in:
lijianan 2022-11-24 14:59:17 +08:00 committed by GitHub
parent 618662a6d2
commit 0c3ba124b4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 90 additions and 125 deletions

View File

@ -5,47 +5,42 @@ import rtlTest from '../../../tests/shared/rtlTest';
import { fireEvent, render, waitFakeTimer } from '../../../tests/utils'; import { fireEvent, render, waitFakeTimer } from '../../../tests/utils';
describe('BackTop', () => { describe('BackTop', () => {
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
mountTest(BackTop); mountTest(BackTop);
rtlTest(BackTop); rtlTest(BackTop);
it('should scroll to top after click it', async () => { it('should scroll to top after click it', async () => {
jest.useFakeTimers(); const { container } = render(<BackTop />);
const { container } = render(<BackTop visibilityHeight={-1} />);
const scrollToSpy = jest.spyOn(window, 'scrollTo').mockImplementation((_, y) => { const scrollToSpy = jest.spyOn(window, 'scrollTo').mockImplementation((_, y) => {
window.scrollY = y; window.scrollY = y;
window.pageYOffset = y; window.pageYOffset = y;
document.documentElement.scrollTop = y; document.documentElement.scrollTop = y;
}); });
window.scrollTo(0, 400); window.scrollTo(0, 400);
await waitFakeTimer();
expect(document.documentElement.scrollTop).toBe(400); expect(document.documentElement.scrollTop).toBe(400);
fireEvent.click(container.querySelector('.ant-back-top')!); fireEvent.click(container.querySelector<HTMLDivElement>('.ant-back-top')!);
await waitFakeTimer(); await waitFakeTimer();
expect(document.documentElement.scrollTop).toBe(0); expect(document.documentElement.scrollTop).toBe(0);
scrollToSpy.mockRestore(); scrollToSpy.mockRestore();
jest.clearAllTimers();
jest.useRealTimers();
}); });
it('support onClick', async () => { it('support onClick', () => {
const onClick = jest.fn(); const onClick = jest.fn();
const { container } = render(<BackTop onClick={onClick} visibilityHeight={-1} />); const { container } = render(<BackTop onClick={onClick} visibilityHeight={0} />);
const scrollToSpy = jest.spyOn(window, 'scrollTo').mockImplementation((_, y) => { fireEvent.click(container.querySelector<HTMLDivElement>('.ant-back-top')!);
window.scrollY = y;
window.pageYOffset = y;
});
document.dispatchEvent(new Event('scroll'));
window.scrollTo(0, 400);
fireEvent.click(container.querySelector('.ant-back-top')!);
expect(onClick).toHaveBeenCalled(); expect(onClick).toHaveBeenCalled();
scrollToSpy.mockRestore();
}); });
it('invalid target', async () => { it('invalid target', () => {
const onClick = jest.fn(); const onClick = jest.fn();
const { container } = render(<BackTop onClick={onClick} visible target={undefined} />); const { container } = render(<BackTop onClick={onClick} target={undefined} />);
fireEvent.click(container.querySelector('.ant-back-top')!); fireEvent.click(container.querySelector<HTMLDivElement>('.ant-back-top')!);
expect(onClick).toHaveBeenCalled(); expect(onClick).toHaveBeenCalled();
}); });
it('should console Error', () => { it('should console Error', () => {

View File

@ -2,9 +2,9 @@ import VerticalAlignTopOutlined from '@ant-design/icons/VerticalAlignTopOutlined
import classNames from 'classnames'; import classNames from 'classnames';
import CSSMotion from 'rc-motion'; import CSSMotion from 'rc-motion';
import addEventListener from 'rc-util/lib/Dom/addEventListener'; import addEventListener from 'rc-util/lib/Dom/addEventListener';
import useMergedState from 'rc-util/lib/hooks/useMergedState';
import omit from 'rc-util/lib/omit'; import omit from 'rc-util/lib/omit';
import * as React from 'react'; import * as React from 'react';
import type { ConfigConsumerProps } from '../config-provider';
import { ConfigContext } from '../config-provider'; import { ConfigContext } from '../config-provider';
import getScroll from '../_util/getScroll'; import getScroll from '../_util/getScroll';
import { cloneElement } from '../_util/reactNode'; import { cloneElement } from '../_util/reactNode';
@ -22,62 +22,36 @@ export interface BackTopProps {
className?: string; className?: string;
style?: React.CSSProperties; style?: React.CSSProperties;
duration?: number; duration?: number;
visible?: boolean; // Only for test. Don't use it.
} }
interface ChildrenProps {
prefixCls: string;
rootPrefixCls: string;
children?: React.ReactNode;
visible?: boolean; // Only for test. Don't use it.
}
const BackTopContent: React.FC<ChildrenProps> = (props) => {
const { prefixCls, rootPrefixCls, children, visible } = props;
const defaultElement = (
<div className={`${prefixCls}-content`}>
<div className={`${prefixCls}-icon`}>
<VerticalAlignTopOutlined />
</div>
</div>
);
return (
<CSSMotion visible={visible} motionName={`${rootPrefixCls}-fade`}>
{({ className: motionClassName }) =>
cloneElement(children || defaultElement, ({ className }) => ({
className: classNames(motionClassName, className),
}))
}
</CSSMotion>
);
};
const BackTop: React.FC<BackTopProps> = (props) => { const BackTop: React.FC<BackTopProps> = (props) => {
const [visible, setVisible] = useMergedState(false, { const {
value: props.visible, prefixCls: customizePrefixCls,
}); className = '',
visibilityHeight = 400,
target,
onClick,
duration = 450,
} = props;
const [visible, setVisible] = React.useState<boolean>(visibilityHeight === 0);
const ref = React.createRef<HTMLDivElement>(); const ref = React.useRef<HTMLDivElement>(null);
const scrollEvent = React.useRef<ReturnType<typeof addEventListener>>(null); const scrollEvent = React.useRef<ReturnType<typeof addEventListener> | null>(null);
const getDefaultTarget = () => const getDefaultTarget = (): HTMLElement | Document | Window =>
ref.current && ref.current.ownerDocument ? ref.current.ownerDocument : window; ref.current && ref.current.ownerDocument ? ref.current.ownerDocument : window;
const handleScroll = throttleByAnimationFrame( const handleScroll = throttleByAnimationFrame(
(e: React.UIEvent<HTMLElement> | { target: any }) => { (e: React.UIEvent<HTMLElement, UIEvent> | { target: any }) => {
const { visibilityHeight = 400 } = props;
const scrollTop = getScroll(e.target, true); const scrollTop = getScroll(e.target, true);
setVisible(scrollTop > visibilityHeight); setVisible(scrollTop >= visibilityHeight);
}, },
); );
const bindScrollEvent = () => { const bindScrollEvent = () => {
const { target } = props;
const getTarget = target || getDefaultTarget; const getTarget = target || getDefaultTarget;
const container = getTarget(); const container = getTarget();
scrollEvent.current = addEventListener(container, 'scroll', (e: React.UIEvent<HTMLElement>) => { scrollEvent.current = addEventListener(container, 'scroll', handleScroll);
handleScroll(e);
});
handleScroll({ target: container }); handleScroll({ target: container });
}; };
@ -88,26 +62,18 @@ const BackTop: React.FC<BackTopProps> = (props) => {
React.useEffect(() => { React.useEffect(() => {
bindScrollEvent(); bindScrollEvent();
return () => { return () => {
if (scrollEvent.current) {
scrollEvent.current.remove();
}
handleScroll.cancel(); handleScroll.cancel();
scrollEvent.current?.remove();
}; };
}, [props.target]); }, [target]);
const scrollToTop = (e: React.MouseEvent<HTMLDivElement>) => { const scrollToTop = (e: React.MouseEvent<HTMLDivElement>) => {
const { onClick, target, duration = 450 } = props; scrollTo(0, { getContainer: target || getDefaultTarget, duration });
scrollTo(0, { onClick?.(e);
getContainer: target || getDefaultTarget,
duration,
});
if (typeof onClick === 'function') {
onClick(e);
}
}; };
const { getPrefixCls, direction } = React.useContext(ConfigContext); const { getPrefixCls, direction } = React.useContext<ConfigConsumerProps>(ConfigContext);
const { prefixCls: customizePrefixCls, className = '' } = props;
const prefixCls = getPrefixCls('back-top', customizePrefixCls); const prefixCls = getPrefixCls('back-top', customizePrefixCls);
const rootPrefixCls = getPrefixCls(); const rootPrefixCls = getPrefixCls();
const [wrapSSR, hashId] = useStyle(prefixCls); const [wrapSSR, hashId] = useStyle(prefixCls);
@ -128,14 +94,25 @@ const BackTop: React.FC<BackTopProps> = (props) => {
'children', 'children',
'visibilityHeight', 'visibilityHeight',
'target', 'target',
'visible',
]); ]);
const defaultElement = (
<div className={`${prefixCls}-content`}>
<div className={`${prefixCls}-icon`}>
<VerticalAlignTopOutlined />
</div>
</div>
);
return wrapSSR( return wrapSSR(
<div {...divProps} className={classString} onClick={scrollToTop} ref={ref}> <div {...divProps} className={classString} onClick={scrollToTop} ref={ref}>
<BackTopContent prefixCls={prefixCls} rootPrefixCls={rootPrefixCls} visible={visible}> <CSSMotion visible={visible} motionName={`${rootPrefixCls}-fade`}>
{props.children} {({ className: motionClassName }) =>
</BackTopContent> cloneElement(props.children || defaultElement, ({ className: cloneCls }) => ({
className: classNames(motionClassName, cloneCls),
}))
}
</CSSMotion>
</div>, </div>,
); );
}; };

View File

@ -144,7 +144,7 @@ describe('ConfigProvider', () => {
testPair('Avatar', (props) => <Avatar {...props} />); testPair('Avatar', (props) => <Avatar {...props} />);
// BackTop // BackTop
testPair('BackTop', (props) => <BackTop visible {...props} />); testPair('BackTop', (props) => <BackTop visibilityHeight={0} {...props} />);
// Badge // Badge
testPair('Badge', (props) => { testPair('Badge', (props) => {

View File

@ -2,8 +2,7 @@ import VerticalAlignTopOutlined from '@ant-design/icons/VerticalAlignTopOutlined
import classNames from 'classnames'; import classNames from 'classnames';
import CSSMotion from 'rc-motion'; import CSSMotion from 'rc-motion';
import addEventListener from 'rc-util/lib/Dom/addEventListener'; import addEventListener from 'rc-util/lib/Dom/addEventListener';
import useMergedState from 'rc-util/lib/hooks/useMergedState'; import React, { memo, useContext, useEffect, useRef, useState } from 'react';
import React, { memo, useContext, useEffect, useRef } from 'react';
import FloatButton, { floatButtonPrefixCls } from './FloatButton'; import FloatButton, { floatButtonPrefixCls } from './FloatButton';
import type { ConfigConsumerProps } from '../config-provider'; import type { ConfigConsumerProps } from '../config-provider';
import { ConfigContext } from '../config-provider'; import { ConfigContext } from '../config-provider';
@ -11,7 +10,7 @@ import getScroll from '../_util/getScroll';
import scrollTo from '../_util/scrollTo'; import scrollTo from '../_util/scrollTo';
import throttleByAnimationFrame from '../_util/throttleByAnimationFrame'; import throttleByAnimationFrame from '../_util/throttleByAnimationFrame';
import FloatButtonGroupContext from './context'; import FloatButtonGroupContext from './context';
import type { BackTopProps, FloatButtonShape } from './interface'; import type { BackTopProps, FloatButtonProps, FloatButtonShape } from './interface';
import useStyle from './style'; import useStyle from './style';
const BackTop: React.FC<BackTopProps> = (props) => { const BackTop: React.FC<BackTopProps> = (props) => {
@ -28,46 +27,39 @@ const BackTop: React.FC<BackTopProps> = (props) => {
...restProps ...restProps
} = props; } = props;
const [visible, setVisible] = useMergedState(false, { value: props.visible }); const [visible, setVisible] = useState<boolean>(visibilityHeight === 0);
const ref = useRef<HTMLAnchorElement | HTMLButtonElement>(null); const ref = useRef<HTMLAnchorElement | HTMLButtonElement>(null);
const scrollEvent = useRef<ReturnType<typeof addEventListener> | null>(null);
const scrollEvent = useRef<any>(null);
const getDefaultTarget = (): HTMLElement | Document | Window => const getDefaultTarget = (): HTMLElement | Document | Window =>
ref.current && ref.current.ownerDocument ? ref.current.ownerDocument : window; ref.current && ref.current.ownerDocument ? ref.current.ownerDocument : window;
const handleScroll = throttleByAnimationFrame( const handleScroll = throttleByAnimationFrame(
(e: React.UIEvent<HTMLElement> | { target: any }) => { (e: React.UIEvent<HTMLElement, UIEvent> | { target: any }) => {
const scrollTop = getScroll(e.target, true); const scrollTop = getScroll(e.target, true);
setVisible(scrollTop > visibilityHeight!); setVisible(scrollTop >= visibilityHeight);
}, },
); );
const bindScrollEvent = () => { const bindScrollEvent = () => {
const getTarget = target || getDefaultTarget; const getTarget = target || getDefaultTarget;
const container = getTarget(); const container = getTarget();
scrollEvent.current = addEventListener(container, 'scroll', (e: React.UIEvent<HTMLElement>) => { scrollEvent.current = addEventListener(container, 'scroll', handleScroll);
handleScroll(e);
});
handleScroll({ target: container }); handleScroll({ target: container });
}; };
useEffect(() => { useEffect(() => {
bindScrollEvent(); bindScrollEvent();
return () => { return () => {
if (scrollEvent.current) {
scrollEvent.current.remove();
}
handleScroll.cancel(); handleScroll.cancel();
scrollEvent.current?.remove();
}; };
}, [target]); }, [target]);
const scrollToTop: React.MouseEventHandler<HTMLDivElement> = (e) => { const scrollToTop: React.MouseEventHandler<HTMLDivElement> = (e) => {
scrollTo(0, { getContainer: target || getDefaultTarget, duration }); scrollTo(0, { getContainer: target || getDefaultTarget, duration });
if (typeof onClick === 'function') { onClick?.(e);
onClick(e);
}
}; };
const { getPrefixCls } = useContext<ConfigConsumerProps>(ConfigContext); const { getPrefixCls } = useContext<ConfigConsumerProps>(ConfigContext);
@ -80,7 +72,7 @@ const BackTop: React.FC<BackTopProps> = (props) => {
const mergeShape = groupShape || shape; const mergeShape = groupShape || shape;
const contentProps = { prefixCls, icon, type, shape: mergeShape, ...restProps }; const contentProps: FloatButtonProps = { prefixCls, icon, type, shape: mergeShape, ...restProps };
return wrapSSR( return wrapSSR(
<CSSMotion visible={visible} motionName={`${rootPrefixCls}-fade`}> <CSSMotion visible={visible} motionName={`${rootPrefixCls}-fade`}>

View File

@ -7,7 +7,7 @@ import BackTop from './BackTop';
import type { FloatButtonProps, FloatButtonGroupProps } from './interface'; import type { FloatButtonProps, FloatButtonGroupProps } from './interface';
import { ConfigContext } from '../config-provider'; import { ConfigContext } from '../config-provider';
export interface PureFloatButtonProps extends FloatButtonProps { export interface PureFloatButtonProps extends Omit<FloatButtonProps, 'target'> {
backTop?: boolean; backTop?: boolean;
} }
@ -18,11 +18,10 @@ export interface PurePanelProps
items?: PureFloatButtonProps[]; items?: PureFloatButtonProps[];
} }
function PureFloatButton({ backTop, ...props }: PureFloatButtonProps) { const PureFloatButton: React.FC<PureFloatButtonProps> = ({ backTop, ...props }) =>
return backTop ? <BackTop {...props} visible target={undefined} /> : <FloatButton {...props} />; backTop ? <BackTop {...props} visibilityHeight={0} /> : <FloatButton {...props} />;
}
export default function PurePanel({ className, items, ...props }: PurePanelProps) { function PurePanel({ className, items, ...props }: PurePanelProps) {
const { prefixCls: customizePrefixCls } = props; const { prefixCls: customizePrefixCls } = props;
const { getPrefixCls } = React.useContext(ConfigContext); const { getPrefixCls } = React.useContext(ConfigContext);
@ -41,3 +40,5 @@ export default function PurePanel({ className, items, ...props }: PurePanelProps
return <PureFloatButton className={classNames(className, pureCls)} {...props} />; return <PureFloatButton className={classNames(className, pureCls)} {...props} />;
} }
export default React.memo(PurePanel);

View File

@ -2,52 +2,53 @@ import React from 'react';
import FloatButton from '..'; import FloatButton from '..';
import mountTest from '../../../tests/shared/mountTest'; import mountTest from '../../../tests/shared/mountTest';
import rtlTest from '../../../tests/shared/rtlTest'; import rtlTest from '../../../tests/shared/rtlTest';
import { fireEvent, render, sleep } from '../../../tests/utils'; import { fireEvent, render, waitFakeTimer } from '../../../tests/utils';
const { BackTop } = FloatButton; const { BackTop } = FloatButton;
describe('BackTop', () => { describe('BackTop', () => {
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
mountTest(BackTop); mountTest(BackTop);
rtlTest(BackTop); rtlTest(BackTop);
it('should scroll to top after click it', async () => { it('should scroll to top after click it', async () => {
const { container } = render(<BackTop visible visibilityHeight={-1} />); const { container } = render(<BackTop />);
const scrollToSpy = jest.spyOn(window, 'scrollTo').mockImplementation((_, y) => { const scrollToSpy = jest.spyOn(window, 'scrollTo').mockImplementation((_, y) => {
window.scrollY = y; window.scrollY = y;
window.pageYOffset = y; window.pageYOffset = y;
document.documentElement.scrollTop = y; document.documentElement.scrollTop = y;
}); });
window.scrollTo(0, 400); window.scrollTo(0, 400);
await waitFakeTimer();
expect(document.documentElement.scrollTop).toBe(400); expect(document.documentElement.scrollTop).toBe(400);
fireEvent.click(container.querySelector('.ant-float-btn')!); fireEvent.click(container.querySelector<HTMLButtonElement>('.ant-float-btn')!);
await sleep(500); await waitFakeTimer();
expect(document.documentElement.scrollTop).toBe(0); expect(document.documentElement.scrollTop).toBe(0);
scrollToSpy.mockRestore(); scrollToSpy.mockRestore();
}); });
it('support onClick', () => { it('support onClick', () => {
const onClick = jest.fn(); const onClick = jest.fn();
const { container } = render(<BackTop visible visibilityHeight={-1} onClick={onClick} />); const { container } = render(<BackTop onClick={onClick} visibilityHeight={0} />);
const scrollToSpy = jest.spyOn(window, 'scrollTo').mockImplementation((_, y) => { fireEvent.click(container.querySelector<HTMLButtonElement>('.ant-float-btn')!);
window.scrollY = y;
window.pageYOffset = y;
});
document.dispatchEvent(new Event('scroll'));
window.scrollTo(0, 400);
fireEvent.click(container.querySelector('.ant-float-btn')!);
expect(onClick).toHaveBeenCalled(); expect(onClick).toHaveBeenCalled();
scrollToSpy.mockRestore();
}); });
it('invalid target', () => { it('support invalid target', () => {
const onClick = jest.fn(); const onClick = jest.fn();
const { container } = render(<BackTop onClick={onClick} visible target={undefined} />); const { container } = render(
fireEvent.click(container.querySelector('.ant-float-btn')!); <BackTop onClick={onClick} visibilityHeight={0} target={undefined} />,
);
fireEvent.click(container.querySelector<HTMLButtonElement>('.ant-float-btn')!);
expect(onClick).toHaveBeenCalled(); expect(onClick).toHaveBeenCalled();
}); });
it('pass style to float button', () => { it('pass style to float button', () => {
const { container } = render(<BackTop style={{ color: 'red' }} visible target={undefined} />); const { container } = render(<BackTop style={{ color: 'red' }} visibilityHeight={0} />);
const btn = container.querySelector('.ant-float-btn')!; expect(container.querySelector<HTMLButtonElement>('.ant-float-btn')?.style.color).toBe('red');
expect(btn).toHaveAttribute('style', 'color: red;');
}); });
}); });

View File

@ -3,8 +3,8 @@ import FloatButtonGroup from './FloatButtonGroup';
import BackTop from './BackTop'; import BackTop from './BackTop';
import PurePanel from './PurePanel'; import PurePanel from './PurePanel';
FloatButton.Group = FloatButtonGroup;
FloatButton.BackTop = BackTop; FloatButton.BackTop = BackTop;
FloatButton.Group = FloatButtonGroup;
FloatButton._InternalPanelDoNotUseOrYouWillBeFired = PurePanel; FloatButton._InternalPanelDoNotUseOrYouWillBeFired = PurePanel;
export default FloatButton; export default FloatButton;

View File

@ -53,7 +53,6 @@ export interface BackTopProps extends Omit<FloatButtonProps, 'target'> {
className?: string; className?: string;
style?: React.CSSProperties; style?: React.CSSProperties;
duration?: number; duration?: number;
visible?: boolean; // Only for test. Don't use it.
} }
export type CompoundedComponent = React.ForwardRefExoticComponent< export type CompoundedComponent = React.ForwardRefExoticComponent<