Skip to content

Commit f87115b

Browse files
committed
Animate scrollTop
1 parent 6427e7b commit f87115b

File tree

2 files changed

+129
-11
lines changed

2 files changed

+129
-11
lines changed

packages/component/src/ScrollTo.js

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import PropTypes from 'prop-types';
2+
import React from 'react';
3+
4+
function step(from, to, stepper, index) {
5+
let next = from;
6+
7+
for (let i = 0; i < index; i++) {
8+
next = stepper(next, to);
9+
}
10+
11+
return next;
12+
}
13+
14+
function squareStepper(current, to) {
15+
const sign = Math.sign(to - current);
16+
const step = Math.sqrt(Math.abs(to - current));
17+
const next = current + step * sign;
18+
19+
if (sign > 0) {
20+
return Math.min(to, next);
21+
} else {
22+
return Math.max(to, next);
23+
}
24+
}
25+
26+
export default class ScrollTo extends React.Component {
27+
constructor(props, context) {
28+
super(props, context);
29+
30+
this.handleCancelAnimation = this.handleCancelAnimation.bind(this);
31+
}
32+
33+
componentDidMount() {
34+
const { current } = this.props.target;
35+
36+
if (current) {
37+
this.addEventListeners(current);
38+
this.animate('scrollTop', current.scrollTop, this.props.scrollTop, 1);
39+
}
40+
}
41+
42+
componentDidUpdate(prevProps) {
43+
const { target: prevTarget } = prevProps;
44+
const { target } = this.props;
45+
const { current: prevCurrent } = prevTarget || {};
46+
const { current } = target || {};
47+
const scrollChanged = prevProps.scrollTop !== this.props.scrollTop;
48+
const targetChanged = prevCurrent !== current;
49+
50+
if (targetChanged) {
51+
this.removeEventListeners(prevCurrent);
52+
this.addEventListeners(current);
53+
}
54+
55+
if ((scrollChanged || targetChanged) && current) {
56+
this.animate('scrollTop', current.scrollTop, this.props.scrollTop, 1);
57+
}
58+
}
59+
60+
componentWillUnmount() {
61+
this.removeEventListeners(this.props.target && this.props.target.current);
62+
cancelAnimationFrame(this.animator);
63+
}
64+
65+
addEventListeners(current) {
66+
current && current.addEventListener('pointerdown', this.handleCancelAnimation, { passive: true });
67+
}
68+
69+
removeEventListeners(current) {
70+
current && current.removeEventListener('pointerdown', this.handleCancelAnimation);
71+
}
72+
73+
animate(name, from, to, index, start = Date.now()) {
74+
if (typeof to === 'number') {
75+
cancelAnimationFrame(this.animator);
76+
77+
this.animator = requestAnimationFrame(() => {
78+
const { current } = this.props.target || {};
79+
80+
if (current) {
81+
let nextValue = step(from, to, squareStepper, (Date.now() - start) / 5);
82+
83+
if (Math.abs(to - nextValue) < .5) {
84+
nextValue = to;
85+
}
86+
87+
current[name] = nextValue;
88+
89+
if (to === nextValue) {
90+
this.props.onEnd && this.props.onEnd(true);
91+
} else {
92+
this.animate(name, from, to, index + 1, start);
93+
}
94+
}
95+
});
96+
}
97+
}
98+
99+
handleCancelAnimation() {
100+
cancelAnimationFrame(this.animator);
101+
this.props.onEnd && this.props.onEnd(false);
102+
}
103+
104+
render() {
105+
return false;
106+
}
107+
}
108+
109+
ScrollTo.propTypes = {
110+
onEnd: PropTypes.func,
111+
scrollTop: PropTypes.number,
112+
target: PropTypes.any
113+
};

packages/component/src/ScrollToBottom/Composer.js

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import PropTypes from 'prop-types';
22
import React from 'react';
33

44
import Context from './Context';
5-
import DOMSetter from '../DOMSetter';
65
import EventSpy from '../EventSpy';
6+
import ScrollTo from '../ScrollTo';
77

88
function isBottom(current, threshold) {
99
const { offsetHeight, scrollHeight, scrollTop } = current;
@@ -16,19 +16,14 @@ export default class ScrollToBottomComposer extends React.Component {
1616
super(props);
1717

1818
this.handleScroll = this.handleScroll.bind(this);
19+
this.handleScrollEnd = this.handleScrollEnd.bind(this);
1920

2021
this.state = {
2122
bottom: true,
2223
handleUpdate: () => this.state.bottom && this.state.scrollToBottom(),
2324
scrollToBottom: () => this.setState(() => ({
2425
bottom: true,
25-
scrollSetter: () => {
26-
const { current } = this.state.target || {};
27-
28-
if (current) {
29-
current.scrollTop = current.scrollHeight;
30-
}
31-
}
26+
scrollTop: this.state.target && this.state.target.current && (this.state.target.current.scrollHeight - this.state.target.current.offsetHeight)
3227
})),
3328
setTarget: target => this.setState(() => ({ target })),
3429
target: null
@@ -41,6 +36,12 @@ export default class ScrollToBottomComposer extends React.Component {
4136
}));
4237
}
4338

39+
handleScrollEnd() {
40+
this.setState(() => ({
41+
scrollTop: null
42+
}));
43+
}
44+
4445
render() {
4546
return (
4647
<Context.Provider value={ this.state }>
@@ -50,9 +51,13 @@ export default class ScrollToBottomComposer extends React.Component {
5051
onEvent={ this.handleScroll }
5152
target={ this.state.target }
5253
/>
53-
<DOMSetter
54-
setter={ this.state.scrollSetter }
55-
/>
54+
{ typeof this.state.scrollTop === 'number' &&
55+
<ScrollTo
56+
onEnd={ this.handleScrollEnd }
57+
scrollTop={ this.state.scrollTop }
58+
target={ this.state.target }
59+
/>
60+
}
5661
</Context.Provider>
5762
);
5863
}

0 commit comments

Comments
 (0)