fix: ⬆️ BackTop not working in iframe (#22788)

* docs: 🎬 improve BackTop demo

* fix Backtop not working in iframe

* fix lint

* fix ci

* fix ci

*  add more test case

*  add more test cases

* fix ci
This commit is contained in:
偏右 2020-04-01 17:38:21 +08:00 committed by GitHub
parent a91506cb6b
commit aca2656721
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 237 additions and 144 deletions

View File

@ -0,0 +1,11 @@
/**
* @jest-environment node
*/
import getScroll from '../getScroll';
describe('getScroll', () => {
it('getScroll return 0 in node envioronment', async () => {
expect(getScroll(null, true)).toBe(0);
expect(getScroll(null, false)).toBe(0);
});
});

View File

@ -1,17 +1,27 @@
export default function getScroll(target: HTMLElement | Window | null, top: boolean): number {
export function isWindow(obj: any) {
return obj !== null && obj !== undefined && obj === obj.window;
}
export default function getScroll(
target: HTMLElement | Window | Document | null,
top: boolean,
): number {
if (typeof window === 'undefined') {
return 0;
}
const prop = top ? 'pageYOffset' : 'pageXOffset';
const method = top ? 'scrollTop' : 'scrollLeft';
const isWindow = target === window;
let ret = isWindow ? (target as Window)[prop] : (target as HTMLElement)[method];
// ie6,7,8 standard mode
if (isWindow && typeof ret !== 'number') {
ret = (document.documentElement as HTMLElement)[method];
let result = 0;
if (isWindow(target)) {
result = (target as Window)[top ? 'pageYOffset' : 'pageXOffset'];
} else if (target instanceof Document) {
result = target.documentElement[method];
} else if (target) {
result = (target as HTMLElement)[method];
}
return ret;
if (target && !isWindow(target) && typeof result !== 'number') {
result = ((target as HTMLElement).ownerDocument || (target as Document)).documentElement[
method
];
}
return result;
}

View File

@ -1,10 +1,10 @@
import raf from 'raf';
import getScroll from './getScroll';
import getScroll, { isWindow } from './getScroll';
import { easeInOutCubic } from './easings';
interface ScrollToOptions {
/** Scroll container, default as window */
getContainer?: () => HTMLElement | Window;
getContainer?: () => HTMLElement | Window | Document;
/** Scroll end callback */
callback?: () => any;
/** Animation duration, default as 450 */
@ -22,8 +22,10 @@ export default function scrollTo(y: number, options: ScrollToOptions = {}) {
const timestamp = Date.now();
const time = timestamp - startTime;
const nextScrollTop = easeInOutCubic(time > duration ? duration : time, scrollTop, y, duration);
if (container === window) {
window.scrollTo(window.pageXOffset, nextScrollTop);
if (isWindow(container)) {
(container as Window).scrollTo(window.pageXOffset, nextScrollTop);
} else if (container instanceof Document) {
container.documentElement.scrollTop = nextScrollTop;
} else {
(container as HTMLElement).scrollTop = nextScrollTop;
}

View File

@ -1,25 +1,47 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders ./components/back-top/demo/basic.md correctly 1`] = `
<div>
Scroll down to see the bottom-right
Array [
<div
class="ant-back-top"
/>,
"Scroll down to see the bottom-right",
<strong
class="site-back-top-basic"
>
gray
</strong>
button.
</div>
</strong>,
"button.",
]
`;
exports[`renders ./components/back-top/demo/custom.md correctly 1`] = `
<div>
Scroll down to see the bottom-right
<strong
style="color:#1088e9"
>
blue
</strong>
button.
<div
style="height:600vh;padding:8px"
>
<div>
Scroll to bottom
</div>
<div>
Scroll to bottom
</div>
<div>
Scroll to bottom
</div>
<div>
Scroll to bottom
</div>
<div>
Scroll to bottom
</div>
<div>
Scroll to bottom
</div>
<div>
Scroll to bottom
</div>
<div
class="ant-back-top"
/>
</div>
`;

View File

@ -1,3 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`BackTop rtl render component should be rendered correctly in RTL direction 1`] = `null`;
exports[`BackTop rtl render component should be rendered correctly in RTL direction 1`] = `
<div
class="ant-back-top ant-back-top-rtl"
/>
`;

View File

@ -14,14 +14,16 @@ describe('BackTop', () => {
const scrollToSpy = jest.spyOn(window, 'scrollTo').mockImplementation((x, y) => {
window.scrollY = y;
window.pageYOffset = y;
document.documentElement.scrollTop = y;
});
window.scrollTo(0, 400);
expect(document.documentElement.scrollTop).toBe(400);
// trigger scroll manually
wrapper.instance().handleScroll();
await sleep();
wrapper.find('.ant-back-top').simulate('click');
await sleep(500);
expect(window.pageYOffset).toBe(0);
expect(document.documentElement.scrollTop).toBe(0);
scrollToSpy.mockRestore();
});
@ -32,6 +34,7 @@ describe('BackTop', () => {
window.scrollY = y;
window.pageYOffset = y;
});
document.dispatchEvent(new Event('scroll'));
window.scrollTo(0, 400);
// trigger scroll manually
wrapper.instance().handleScroll();
@ -40,4 +43,28 @@ describe('BackTop', () => {
expect(onClick).toHaveBeenCalled();
scrollToSpy.mockRestore();
});
it('should be able to update target', async () => {
const wrapper = mount(<BackTop />);
wrapper.instance().handleScroll = jest.fn();
const container = document.createElement('div');
wrapper.setProps({ target: () => container });
expect(wrapper.instance().handleScroll).toHaveBeenLastCalledWith({
target: container,
});
container.dispatchEvent(new Event('scroll'));
expect(wrapper.instance().handleScroll).toHaveBeenLastCalledWith(
expect.objectContaining({
currentTarget: container,
target: container,
}),
);
});
it('invalid target', async () => {
const onClick = jest.fn();
const wrapper = mount(<BackTop onClick={onClick} target={() => ({ documentElement: {} })} />);
wrapper.find('.ant-back-top').simulate('click');
expect(onClick).toHaveBeenCalled();
});
});

View File

@ -17,12 +17,12 @@ The most basic usage.
import { BackTop } from 'antd';
ReactDOM.render(
<div>
<>
<BackTop />
Scroll down to see the bottom-right
<strong className="site-back-top-basic"> gray </strong>
button.
</div>,
</>,
mountNode,
);
```

View File

@ -1,5 +1,6 @@
---
order: 1
iframe: 400
title:
zh-CN: 自定义样式
en-US: Custom style
@ -16,31 +17,30 @@ You can customize the style of the button, just note the size limit: no more tha
```jsx
import { BackTop } from 'antd';
const style = {
height: 40,
width: 40,
lineHeight: '40px',
borderRadius: 4,
backgroundColor: '#1088e9',
color: '#fff',
textAlign: 'center',
fontSize: 14,
};
ReactDOM.render(
<div>
<div style={{ height: '600vh', padding: 8 }}>
<div>Scroll to bottom</div>
<div>Scroll to bottom</div>
<div>Scroll to bottom</div>
<div>Scroll to bottom</div>
<div>Scroll to bottom</div>
<div>Scroll to bottom</div>
<div>Scroll to bottom</div>
<BackTop>
<div className="ant-back-top-inner">UP</div>
<div style={style}>UP</div>
</BackTop>
Scroll down to see the bottom-right
<strong style={{ color: '#1088e9' }}> blue </strong>
button.
</div>,
mountNode,
);
```
```css
#components-back-top-demo-custom .ant-back-top {
bottom: 100px;
}
#components-back-top-demo-custom .ant-back-top-inner {
height: 40px;
width: 40px;
line-height: 40px;
border-radius: 4px;
background-color: #1088e9;
color: #fff;
text-align: center;
font-size: 20px;
}
```

View File

@ -3,18 +3,15 @@ import Animate from 'rc-animate';
import addEventListener from 'rc-util/lib/Dom/addEventListener';
import classNames from 'classnames';
import omit from 'omit.js';
import { throttleByAnimationFrameDecorator } from '../_util/throttleByAnimationFrame';
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider';
import getScroll from '../_util/getScroll';
import scrollTo from '../_util/scrollTo';
function getDefaultTarget() {
return window;
}
export interface BackTopProps {
visibilityHeight?: number;
onClick?: React.MouseEventHandler<HTMLElement>;
target?: () => HTMLElement | Window;
target?: () => HTMLElement | Window | Document;
prefixCls?: string;
className?: string;
style?: React.CSSProperties;
@ -26,57 +23,101 @@ export default class BackTop extends React.Component<BackTopProps, any> {
visibilityHeight: 400,
};
state = {
visible: false,
};
scrollEvent: any;
constructor(props: BackTopProps) {
super(props);
this.state = {
visible: false,
};
}
node: HTMLDivElement;
componentDidMount() {
const getTarget = this.props.target || getDefaultTarget;
this.scrollEvent = addEventListener(getTarget(), 'scroll', this.handleScroll);
this.handleScroll();
this.bindScrollEvent();
}
componentDidUpdate(prevProps: BackTopProps) {
const { target } = this.props;
if (prevProps.target !== target) {
this.bindScrollEvent();
}
}
componentWillUnmount() {
if (this.scrollEvent) {
this.scrollEvent.remove();
}
(this.handleScroll as any).cancel();
}
bindScrollEvent() {
if (this.scrollEvent) {
this.scrollEvent.remove();
}
const { target } = this.props;
const getTarget = target || this.getDefaultTarget;
const container = getTarget();
this.scrollEvent = addEventListener(container, 'scroll', (e: React.UIEvent<HTMLElement>) => {
this.handleScroll(e);
});
this.handleScroll({
target: container,
});
}
getVisible() {
if ('visible' in this.props) {
return this.props.visible;
}
return this.state.visible;
}
getDefaultTarget = () => {
return this.node && this.node.ownerDocument ? this.node.ownerDocument : window;
};
saveDivRef = (node: HTMLDivElement) => {
this.node = node;
};
scrollToTop = (e: React.MouseEvent<HTMLDivElement>) => {
const { target = getDefaultTarget, onClick } = this.props;
const { onClick, target } = this.props;
scrollTo(0, {
getContainer: target,
getContainer: target || this.getDefaultTarget,
});
if (typeof onClick === 'function') {
onClick(e);
}
};
handleScroll = () => {
const { visibilityHeight, target = getDefaultTarget } = this.props;
const scrollTop = getScroll(target(), true);
@throttleByAnimationFrameDecorator()
handleScroll(e: React.UIEvent<HTMLElement> | { target: any }) {
const { visibilityHeight = 0 } = this.props;
const scrollTop = getScroll(e.target, true);
this.setState({
visible: scrollTop > (visibilityHeight as number),
});
};
renderBackTop = ({ getPrefixCls, direction }: ConfigConsumerProps) => {
const { prefixCls: customizePrefixCls, className = '', children } = this.props;
const prefixCls = getPrefixCls('back-top', customizePrefixCls);
const classString = classNames(prefixCls, className, {
[`${prefixCls}-rtl`]: direction === 'rtl',
visible: scrollTop > visibilityHeight,
});
}
renderChildren({ prefixCls }: { prefixCls: string }) {
const { children } = this.props;
const defaultElement = (
<div className={`${prefixCls}-content`}>
<div className={`${prefixCls}-icon`} />
</div>
);
return (
<Animate component="" transitionName="fade">
{this.getVisible() ? <div>{children || defaultElement}</div> : null}
</Animate>
);
}
renderBackTop = ({ getPrefixCls, direction }: ConfigConsumerProps) => {
const { prefixCls: customizePrefixCls, className = '' } = this.props;
const prefixCls = getPrefixCls('back-top', customizePrefixCls);
const classString = classNames(prefixCls, className, {
[`${prefixCls}-rtl`]: direction === 'rtl',
});
// fix https://fb.me/react-unknown-prop
const divProps = omit(this.props, [
@ -88,18 +129,10 @@ export default class BackTop extends React.Component<BackTopProps, any> {
'visible',
]);
const visible = 'visible' in this.props ? this.props.visible : this.state.visible;
const backTopBtn = visible ? (
<div {...divProps} className={classString} onClick={this.scrollToTop}>
{children || defaultElement}
</div>
) : null;
return (
<Animate component="" transitionName="fade">
{backTopBtn}
</Animate>
<div {...divProps} className={classString} onClick={this.scrollToTop} ref={this.saveDivRef}>
{this.renderChildren({ prefixCls })}
</div>
);
};

View File

@ -280,12 +280,14 @@ exports[`ConfigProvider components BackTop configProvider 1`] = `
<div
class="config-back-top"
>
<div
class="config-back-top-content"
>
<div>
<div
class="config-back-top-icon"
/>
class="config-back-top-content"
>
<div
class="config-back-top-icon"
/>
</div>
</div>
</div>
`;
@ -294,12 +296,14 @@ exports[`ConfigProvider components BackTop normal 1`] = `
<div
class="ant-back-top"
>
<div
class="ant-back-top-content"
>
<div>
<div
class="ant-back-top-icon"
/>
class="ant-back-top-content"
>
<div
class="ant-back-top-icon"
/>
</div>
</div>
</div>
`;
@ -308,12 +312,14 @@ exports[`ConfigProvider components BackTop prefixCls 1`] = `
<div
class="prefix-BackTop"
>
<div
class="prefix-BackTop-content"
>
<div>
<div
class="prefix-BackTop-icon"
/>
class="prefix-BackTop-content"
>
<div
class="prefix-BackTop-icon"
/>
</div>
</div>
</div>
`;

View File

@ -73,10 +73,16 @@ describe('Upload List', () => {
drawImageCallback = null;
});
let open;
beforeAll(() => {
open = jest.spyOn(window, 'open').mockImplementation(() => {});
});
afterAll(() => {
window.URL.createObjectURL = originCreateObjectURL;
imageSpy.mockRestore();
canvasSpy.mockRestore();
open.mockRestore();
});
// https://github.com/ant-design/ant-design/issues/4653
@ -118,11 +124,7 @@ describe('Upload List', () => {
</Upload>,
);
expect(wrapper.find('.ant-upload-list-item').length).toBe(2);
wrapper
.find('.ant-upload-list-item')
.at(0)
.find('.anticon-delete')
.simulate('click');
wrapper.find('.ant-upload-list-item').at(0).find('.anticon-delete').simulate('click');
await sleep(400);
wrapper.update();
expect(wrapper.find('.ant-upload-list-item').hostNodes().length).toBe(1);
@ -243,15 +245,9 @@ describe('Upload List', () => {
<button type="button">upload</button>
</Upload>,
);
wrapper
.find('.anticon-eye')
.at(0)
.simulate('click');
wrapper.find('.anticon-eye').at(0).simulate('click');
expect(handlePreview).toHaveBeenCalledWith(fileList[0]);
wrapper
.find('.anticon-eye')
.at(1)
.simulate('click');
wrapper.find('.anticon-eye').at(1).simulate('click');
expect(handlePreview).toHaveBeenCalledWith(fileList[1]);
});
@ -268,15 +264,9 @@ describe('Upload List', () => {
<button type="button">upload</button>
</Upload>,
);
wrapper
.find('.anticon-delete')
.at(0)
.simulate('click');
wrapper.find('.anticon-delete').at(0).simulate('click');
expect(handleRemove).toHaveBeenCalledWith(fileList[0]);
wrapper
.find('.anticon-delete')
.at(1)
.simulate('click');
wrapper.find('.anticon-delete').at(1).simulate('click');
expect(handleRemove).toHaveBeenCalledWith(fileList[1]);
await sleep();
expect(handleChange.mock.calls.length).toBe(2);
@ -303,10 +293,7 @@ describe('Upload List', () => {
<button type="button">upload</button>
</Upload>,
);
wrapper
.find('.anticon-download')
.at(0)
.simulate('click');
wrapper.find('.anticon-download').at(0).simulate('click');
});
it('should support no onDownload', async () => {
@ -328,10 +315,7 @@ describe('Upload List', () => {
<button type="button">upload</button>
</Upload>,
);
wrapper
.find('.anticon-download')
.at(0)
.simulate('click');
wrapper.find('.anticon-download').at(0).simulate('click');
});
describe('should generate thumbUrl from file', () => {
@ -504,16 +488,10 @@ describe('Upload List', () => {
<button type="button">upload</button>
</Upload>,
);
wrapper
.find('.custom-delete')
.at(0)
.simulate('click');
wrapper.find('.custom-delete').at(0).simulate('click');
expect(handleRemove).toHaveBeenCalledWith(fileList[0]);
expect(myClick).toHaveBeenCalled();
wrapper
.find('.custom-delete')
.at(1)
.simulate('click');
wrapper.find('.custom-delete').at(1).simulate('click');
expect(handleRemove).toHaveBeenCalledWith(fileList[1]);
expect(myClick).toHaveBeenCalled();
await sleep();