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

View File

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

View File

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

View File

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

View File

@ -7,7 +7,7 @@ import BackTop from './BackTop';
import type { FloatButtonProps, FloatButtonGroupProps } from './interface';
import { ConfigContext } from '../config-provider';
export interface PureFloatButtonProps extends FloatButtonProps {
export interface PureFloatButtonProps extends Omit<FloatButtonProps, 'target'> {
backTop?: boolean;
}
@ -18,11 +18,10 @@ export interface PurePanelProps
items?: PureFloatButtonProps[];
}
function PureFloatButton({ backTop, ...props }: PureFloatButtonProps) {
return backTop ? <BackTop {...props} visible target={undefined} /> : <FloatButton {...props} />;
}
const PureFloatButton: React.FC<PureFloatButtonProps> = ({ backTop, ...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 { getPrefixCls } = React.useContext(ConfigContext);
@ -41,3 +40,5 @@ export default function PurePanel({ className, items, ...props }: PurePanelProps
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 mountTest from '../../../tests/shared/mountTest';
import rtlTest from '../../../tests/shared/rtlTest';
import { fireEvent, render, sleep } from '../../../tests/utils';
import { fireEvent, render, waitFakeTimer } from '../../../tests/utils';
const { BackTop } = FloatButton;
describe('BackTop', () => {
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
mountTest(BackTop);
rtlTest(BackTop);
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) => {
window.scrollY = y;
window.pageYOffset = y;
document.documentElement.scrollTop = y;
});
window.scrollTo(0, 400);
await waitFakeTimer();
expect(document.documentElement.scrollTop).toBe(400);
fireEvent.click(container.querySelector('.ant-float-btn')!);
await sleep(500);
fireEvent.click(container.querySelector<HTMLButtonElement>('.ant-float-btn')!);
await waitFakeTimer();
expect(document.documentElement.scrollTop).toBe(0);
scrollToSpy.mockRestore();
});
it('support onClick', () => {
const onClick = jest.fn();
const { container } = render(<BackTop visible visibilityHeight={-1} onClick={onClick} />);
const scrollToSpy = jest.spyOn(window, 'scrollTo').mockImplementation((_, y) => {
window.scrollY = y;
window.pageYOffset = y;
});
document.dispatchEvent(new Event('scroll'));
window.scrollTo(0, 400);
fireEvent.click(container.querySelector('.ant-float-btn')!);
const { container } = render(<BackTop onClick={onClick} visibilityHeight={0} />);
fireEvent.click(container.querySelector<HTMLButtonElement>('.ant-float-btn')!);
expect(onClick).toHaveBeenCalled();
scrollToSpy.mockRestore();
});
it('invalid target', () => {
it('support invalid target', () => {
const onClick = jest.fn();
const { container } = render(<BackTop onClick={onClick} visible target={undefined} />);
fireEvent.click(container.querySelector('.ant-float-btn')!);
const { container } = render(
<BackTop onClick={onClick} visibilityHeight={0} target={undefined} />,
);
fireEvent.click(container.querySelector<HTMLButtonElement>('.ant-float-btn')!);
expect(onClick).toHaveBeenCalled();
});
it('pass style to float button', () => {
const { container } = render(<BackTop style={{ color: 'red' }} visible target={undefined} />);
const btn = container.querySelector('.ant-float-btn')!;
expect(btn).toHaveAttribute('style', 'color: red;');
const { container } = render(<BackTop style={{ color: 'red' }} visibilityHeight={0} />);
expect(container.querySelector<HTMLButtonElement>('.ant-float-btn')?.style.color).toBe('red');
});
});

View File

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

View File

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