mirror of
https://gitee.com/ant-design/ant-design.git
synced 2024-11-30 02:59:04 +08:00
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:
parent
618662a6d2
commit
0c3ba124b4
@ -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', () => {
|
||||
|
@ -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>,
|
||||
);
|
||||
};
|
||||
|
@ -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) => {
|
||||
|
@ -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`}>
|
||||
|
@ -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);
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
@ -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;
|
||||
|
@ -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<
|
||||
|
Loading…
Reference in New Issue
Block a user