mirror of
https://github.com/ant-design/ant-design.git
synced 2024-11-28 05:05:48 +08:00
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:
parent
a91506cb6b
commit
aca2656721
11
components/_util/__tests__/getScroll.test.js
Normal file
11
components/_util/__tests__/getScroll.test.js
Normal 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);
|
||||
});
|
||||
});
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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>
|
||||
`;
|
||||
|
@ -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"
|
||||
/>
|
||||
`;
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
@ -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,
|
||||
);
|
||||
```
|
||||
|
@ -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;
|
||||
}
|
||||
```
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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>
|
||||
`;
|
||||
|
@ -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();
|
||||
|
Loading…
Reference in New Issue
Block a user