mirror of
https://github.com/ant-design/ant-design.git
synced 2025-06-07 17:44:35 +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 Affix from '../affix';
|
||||||
import AnchorLink from './AnchorLink';
|
import AnchorLink from './AnchorLink';
|
||||||
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider';
|
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider';
|
||||||
|
import scrollTo from '../_util/scrollTo';
|
||||||
import getScroll from '../_util/getScroll';
|
import getScroll from '../_util/getScroll';
|
||||||
|
|
||||||
function getDefaultContainer() {
|
function getDefaultContainer() {
|
||||||
@ -35,54 +36,7 @@ function getOffsetTop(element: HTMLElement, container: AnchorContainer): number
|
|||||||
return rect.top;
|
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 = /#([^#]+)$/;
|
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 = {
|
type Section = {
|
||||||
link: string;
|
link: string;
|
||||||
@ -208,7 +162,35 @@ export default class Anchor extends React.Component<AnchorProps, AnchorState> {
|
|||||||
if (this.scrollEvent) {
|
if (this.scrollEvent) {
|
||||||
this.scrollEvent.remove();
|
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 {
|
getCurrentAnchor(offsetTop = 0, bounds = 5): string {
|
||||||
const { getCurrentAnchor } = this.props;
|
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 = () => {
|
updateInk = () => {
|
||||||
if (typeof document === 'undefined') {
|
if (typeof document === 'undefined') {
|
||||||
return;
|
return;
|
||||||
|
@ -1,16 +1,23 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { mount } from 'enzyme';
|
import { mount } from 'enzyme';
|
||||||
|
import { sleep } from '../../../tests/utils';
|
||||||
import BackTop from '..';
|
import BackTop from '..';
|
||||||
|
|
||||||
describe('BackTop', () => {
|
describe('BackTop', () => {
|
||||||
it('should scroll to top after click it', async () => {
|
it('should scroll to top after click it', async () => {
|
||||||
const wrapper = mount(<BackTop visibilityHeight={-1} />);
|
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
|
// trigger scroll manually
|
||||||
wrapper.instance().handleScroll();
|
wrapper.instance().handleScroll();
|
||||||
await new Promise(resolve => setTimeout(resolve, 0));
|
await sleep();
|
||||||
wrapper.find('.ant-back-top').simulate('click');
|
wrapper.find('.ant-back-top').simulate('click');
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
await sleep(1000);
|
||||||
expect(Math.abs(Math.round(document.documentElement.scrollTop))).toBe(0);
|
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 addEventListener from 'rc-util/lib/Dom/addEventListener';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import omit from 'omit.js';
|
import omit from 'omit.js';
|
||||||
import raf from 'raf';
|
|
||||||
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider';
|
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider';
|
||||||
import getScroll from '../_util/getScroll';
|
import getScroll from '../_util/getScroll';
|
||||||
|
import scrollTo from '../_util/scrollTo';
|
||||||
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() {}
|
|
||||||
|
|
||||||
function getDefaultTarget() {
|
function getDefaultTarget() {
|
||||||
return window;
|
return window;
|
||||||
@ -79,20 +68,13 @@ export default class BackTop extends React.Component<BackTopProps, any> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
scrollToTop = (e: React.MouseEvent<HTMLDivElement>) => {
|
scrollToTop = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
const scrollTop = this.getCurrentScrollTop();
|
const { target = getDefaultTarget } = this.props;
|
||||||
const startTime = Date.now();
|
scrollTo(window.pageXOffset, 0, {
|
||||||
const frameFunc = () => {
|
getContainer: target,
|
||||||
const timestamp = Date.now();
|
});
|
||||||
const time = timestamp - startTime;
|
if (typeof this.props.onClick === 'function') {
|
||||||
this.setScrollTop(easeInOutCubic(time, scrollTop, 0, 450));
|
this.props.onClick(e);
|
||||||
if (time < 450) {
|
}
|
||||||
raf(frameFunc);
|
|
||||||
} else {
|
|
||||||
this.setScrollTop(0);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
raf(frameFunc);
|
|
||||||
(this.props.onClick || noop)(e);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
handleScroll = () => {
|
handleScroll = () => {
|
||||||
|
@ -8,3 +8,5 @@ export function setMockDate(dateString = '2017-09-18T03:30:07.795') {
|
|||||||
export function resetMockDate() {
|
export function resetMockDate() {
|
||||||
MockDate.reset();
|
MockDate.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const sleep = (timeout = 0) => new Promise(resolve => setTimeout(resolve, timeout));
|
||||||
|
Loading…
Reference in New Issue
Block a user