mirror of
https://gitee.com/ant-design/ant-design.git
synced 2024-11-30 02:59:04 +08:00
perf: optimization BackTop and Anchor components scrollTo func
This commit is contained in:
parent
90a65d1e57
commit
ccef30885a
43
components/_util/__tests__/scrollTo.test.js
Normal file
43
components/_util/__tests__/scrollTo.test.js
Normal file
@ -0,0 +1,43 @@
|
||||
import scrollTo from '../scrollTo';
|
||||
import { sleep } from '../../../tests/utils';
|
||||
|
||||
describe('Test ScrollTo function', () => {
|
||||
it('test scrollTo', async () => {
|
||||
const scrollToSpy = jest.spyOn(window, 'scrollTo').mockImplementation((x, y) => {
|
||||
const w = window;
|
||||
w.scrollY = y;
|
||||
w.pageYOffset = y;
|
||||
});
|
||||
scrollTo(0, 1000);
|
||||
await sleep(1000);
|
||||
expect(window.pageYOffset).toBe(1000);
|
||||
scrollToSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('test unknow easing funciton', () => {
|
||||
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
scrollTo(0, 0, {
|
||||
ease: 'ffff',
|
||||
});
|
||||
expect(warnSpy).toHaveBeenCalledWith('Unkonw easing funciton in scrollTo');
|
||||
warnSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('test callback - option', async () => {
|
||||
const cbMock = jest.fn();
|
||||
scrollTo(0, 1000, {
|
||||
callback: cbMock,
|
||||
});
|
||||
await sleep(1000);
|
||||
expect(cbMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('test getContainer - option', async () => {
|
||||
const div = document.createElement('div');
|
||||
scrollTo(0, 1000, {
|
||||
getContainer: () => div,
|
||||
});
|
||||
await sleep(1000);
|
||||
expect(div.scrollTop).toBe(1000);
|
||||
});
|
||||
});
|
17
components/_util/eases.ts
Normal file
17
components/_util/eases.ts
Normal file
@ -0,0 +1,17 @@
|
||||
const Eases = {
|
||||
easeInOutCubic: (t: number, b: number, c: number, d: number) => {
|
||||
const cc = c - b;
|
||||
t /= d / 2;
|
||||
if (t < 1) {
|
||||
return (cc / 2) * t * t * t + b;
|
||||
}
|
||||
return (cc / 2) * ((t -= 2) * t * t + 2) + b;
|
||||
},
|
||||
easeOutCubic: (t: number, b: number, c: number, d: number) => {
|
||||
const cc = c - b;
|
||||
t /= d - 1;
|
||||
return cc * (t * t * t + 1) + b;
|
||||
},
|
||||
};
|
||||
|
||||
export default Eases;
|
53
components/_util/scrollTo.ts
Normal file
53
components/_util/scrollTo.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import raf from 'raf';
|
||||
import getScroll from './getScroll';
|
||||
import Eases from './eases';
|
||||
|
||||
type easeType = keyof typeof Eases;
|
||||
|
||||
interface ScrollToOptions {
|
||||
/** Scroll container, deault as window */
|
||||
getContainer?: () => HTMLElement | Window;
|
||||
/** Scroll end callback */
|
||||
callback?: () => any;
|
||||
/** Animation duration, default as 450 */
|
||||
duration?: number;
|
||||
/** easing function name, default as easeInOutCubic */
|
||||
ease?: easeType;
|
||||
}
|
||||
|
||||
// TODO: support x
|
||||
export default function scrollTo(x: number, y: number, options: ScrollToOptions = {}) {
|
||||
const {
|
||||
getContainer = () => window,
|
||||
callback,
|
||||
duration = 450,
|
||||
ease = 'easeInOutCubic',
|
||||
} = options;
|
||||
|
||||
const container = getContainer();
|
||||
const scrollTop = getScroll(container, true);
|
||||
const startTime = Date.now();
|
||||
let easeFunc = Eases[ease];
|
||||
|
||||
if (!easeFunc) {
|
||||
console.warn('Unkonw easing funciton in scrollTo');
|
||||
easeFunc = Eases.easeInOutCubic;
|
||||
}
|
||||
|
||||
const frameFunc = () => {
|
||||
const timestamp = Date.now();
|
||||
const time = timestamp - startTime;
|
||||
const nextScrollTop = easeFunc(time > duration ? duration : time, scrollTop, y, duration);
|
||||
if (container === window) {
|
||||
window.scrollTo(x, nextScrollTop);
|
||||
} else {
|
||||
(container as HTMLElement).scrollTop = nextScrollTop;
|
||||
}
|
||||
if (time < duration) {
|
||||
raf(frameFunc);
|
||||
} else {
|
||||
if (typeof callback === 'function') callback();
|
||||
}
|
||||
};
|
||||
raf(frameFunc);
|
||||
}
|
@ -7,6 +7,7 @@ import raf from 'raf';
|
||||
import Affix from '../affix';
|
||||
import AnchorLink from './AnchorLink';
|
||||
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider';
|
||||
import scrollTo from '../_util/scrollTo';
|
||||
import getScroll from '../_util/getScroll';
|
||||
|
||||
function getDefaultContainer() {
|
||||
@ -35,54 +36,7 @@ function getOffsetTop(element: HTMLElement, container: AnchorContainer): number
|
||||
return rect.top;
|
||||
}
|
||||
|
||||
function easeInOutCubic(t: number, b: number, c: number, d: number) {
|
||||
const cc = c - b;
|
||||
t /= d / 2;
|
||||
if (t < 1) {
|
||||
return (cc / 2) * t * t * t + b;
|
||||
}
|
||||
const r = (cc / 2) * ((t -= 2) * t * t + 2) + b;
|
||||
// fix return value more than target value
|
||||
return cc > 0 ? (r > c ? c : r) : r < c ? c : r;
|
||||
}
|
||||
|
||||
const sharpMatcherRegx = /#([^#]+)$/;
|
||||
function scrollTo(
|
||||
href: string,
|
||||
targetOffset = 0,
|
||||
getContainer: () => AnchorContainer,
|
||||
callback = () => {},
|
||||
) {
|
||||
const container = getContainer();
|
||||
const scrollTop = getScroll(container, true);
|
||||
const sharpLinkMatch = sharpMatcherRegx.exec(href);
|
||||
if (!sharpLinkMatch) {
|
||||
return;
|
||||
}
|
||||
const targetElement = document.getElementById(sharpLinkMatch[1]);
|
||||
if (!targetElement) {
|
||||
return;
|
||||
}
|
||||
const eleOffsetTop = getOffsetTop(targetElement, container);
|
||||
const targetScrollTop = scrollTop + eleOffsetTop - targetOffset;
|
||||
const startTime = Date.now();
|
||||
const frameFunc = () => {
|
||||
const timestamp = Date.now();
|
||||
const time = timestamp - startTime;
|
||||
const nextScrollTop = easeInOutCubic(time, scrollTop, targetScrollTop, 450);
|
||||
if (container === window) {
|
||||
window.scrollTo(window.pageXOffset, nextScrollTop);
|
||||
} else {
|
||||
(container as HTMLElement).scrollTop = nextScrollTop;
|
||||
}
|
||||
if (time < 450) {
|
||||
raf(frameFunc);
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
raf(frameFunc);
|
||||
}
|
||||
|
||||
type Section = {
|
||||
link: string;
|
||||
@ -208,7 +162,35 @@ export default class Anchor extends React.Component<AnchorProps, AnchorState> {
|
||||
if (this.scrollEvent) {
|
||||
this.scrollEvent.remove();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handleScrollTo = (link: string) => {
|
||||
const { offsetTop, getContainer, targetOffset } = this.props as AnchorDefaultProps;
|
||||
|
||||
this.setState({ activeLink: link });
|
||||
const container = getContainer();
|
||||
const scrollTop = getScroll(container, true);
|
||||
const sharpLinkMatch = sharpMatcherRegx.exec(link);
|
||||
if (!sharpLinkMatch) {
|
||||
return;
|
||||
}
|
||||
const targetElement = document.getElementById(sharpLinkMatch[1]);
|
||||
if (!targetElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const eleOffsetTop = getOffsetTop(targetElement, container);
|
||||
let y = scrollTop + eleOffsetTop;
|
||||
y -= targetOffset !== undefined ? targetOffset : offsetTop || 0;
|
||||
this.animating = true;
|
||||
|
||||
scrollTo(window.pageXOffset, y, {
|
||||
callback: () => {
|
||||
this.animating = false;
|
||||
},
|
||||
getContainer,
|
||||
});
|
||||
};
|
||||
|
||||
getCurrentAnchor(offsetTop = 0, bounds = 5): string {
|
||||
const { getCurrentAnchor } = this.props;
|
||||
@ -267,15 +249,6 @@ export default class Anchor extends React.Component<AnchorProps, AnchorState> {
|
||||
}
|
||||
};
|
||||
|
||||
handleScrollTo = (link: string) => {
|
||||
const { offsetTop, getContainer, targetOffset } = this.props as AnchorDefaultProps;
|
||||
this.animating = true;
|
||||
this.setState({ activeLink: link });
|
||||
scrollTo(link, targetOffset !== undefined ? targetOffset : offsetTop, getContainer, () => {
|
||||
this.animating = false;
|
||||
});
|
||||
};
|
||||
|
||||
updateInk = () => {
|
||||
if (typeof document === 'undefined') {
|
||||
return;
|
||||
|
@ -1,16 +1,23 @@
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { sleep } from '../../../tests/utils';
|
||||
import BackTop from '..';
|
||||
|
||||
describe('BackTop', () => {
|
||||
it('should scroll to top after click it', async () => {
|
||||
const wrapper = mount(<BackTop visibilityHeight={-1} />);
|
||||
document.documentElement.scrollTop = 400;
|
||||
const scrollToSpy = jest.spyOn(window, 'scrollTo').mockImplementation((x, y) => {
|
||||
const w = window;
|
||||
w.scrollY = y;
|
||||
w.pageYOffset = y;
|
||||
});
|
||||
window.scrollTo(0, 400);
|
||||
// trigger scroll manually
|
||||
wrapper.instance().handleScroll();
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
await sleep();
|
||||
wrapper.find('.ant-back-top').simulate('click');
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
expect(Math.abs(Math.round(document.documentElement.scrollTop))).toBe(0);
|
||||
await sleep(1000);
|
||||
expect(window.pageYOffset).toBe(0);
|
||||
scrollToSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
@ -3,20 +3,9 @@ import Animate from 'rc-animate';
|
||||
import addEventListener from 'rc-util/lib/Dom/addEventListener';
|
||||
import classNames from 'classnames';
|
||||
import omit from 'omit.js';
|
||||
import raf from 'raf';
|
||||
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider';
|
||||
import getScroll from '../_util/getScroll';
|
||||
|
||||
const easeInOutCubic = (t: number, b: number, c: number, d: number) => {
|
||||
const cc = c - b;
|
||||
t /= d / 2;
|
||||
if (t < 1) {
|
||||
return (cc / 2) * t * t * t + b;
|
||||
}
|
||||
return (cc / 2) * ((t -= 2) * t * t + 2) + b;
|
||||
};
|
||||
|
||||
function noop() {}
|
||||
import scrollTo from '../_util/scrollTo';
|
||||
|
||||
function getDefaultTarget() {
|
||||
return window;
|
||||
@ -79,20 +68,13 @@ export default class BackTop extends React.Component<BackTopProps, any> {
|
||||
};
|
||||
|
||||
scrollToTop = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
const scrollTop = this.getCurrentScrollTop();
|
||||
const startTime = Date.now();
|
||||
const frameFunc = () => {
|
||||
const timestamp = Date.now();
|
||||
const time = timestamp - startTime;
|
||||
this.setScrollTop(easeInOutCubic(time, scrollTop, 0, 450));
|
||||
if (time < 450) {
|
||||
raf(frameFunc);
|
||||
} else {
|
||||
this.setScrollTop(0);
|
||||
}
|
||||
};
|
||||
raf(frameFunc);
|
||||
(this.props.onClick || noop)(e);
|
||||
const { target = getDefaultTarget } = this.props;
|
||||
scrollTo(window.pageXOffset, 0, {
|
||||
getContainer: target,
|
||||
});
|
||||
if (typeof this.props.onClick === 'function') {
|
||||
this.props.onClick(e);
|
||||
}
|
||||
};
|
||||
|
||||
handleScroll = () => {
|
||||
|
@ -8,3 +8,5 @@ export function setMockDate(dateString = '2017-09-18T03:30:07.795') {
|
||||
export function resetMockDate() {
|
||||
MockDate.reset();
|
||||
}
|
||||
|
||||
export const sleep = (timeout = 0) => new Promise(resolve => setTimeout(resolve, timeout));
|
||||
|
Loading…
Reference in New Issue
Block a user