Skip to content

Instantly share code, notes, and snippets.

@myndzi
Last active July 10, 2017 00:05
Show Gist options
  • Save myndzi/5b08aae738eff840235e9d0a8979abe4 to your computer and use it in GitHub Desktop.
Save myndzi/5b08aae738eff840235e9d0a8979abe4 to your computer and use it in GitHub Desktop.
import { cloneElement, Children, Component } from 'react';
// Adds sticky scrolling to some react component or element; usage:
// <ScrollLocked><whatever></ScrollLocked>
class ScrollLocked extends Component {
constructor(props) {
super(props);
this.el = null;
this.locked = false;
this.immediate = null;
this.debounce = null;
this.resized = false;
this.scrolled = false;
this.rebind = this.rebind.bind(this);
this.updateLock = this.updateLock.bind(this);
this.maybeScroll = this.maybeScroll.bind(this);
this.debouncedMaybeScroll = this.debouncedMaybeScroll.bind(this);
this.onResize = this.onResize.bind(this);
this.onScroll = this.onScroll.bind(this);
this.maybeUpdateLock = this.maybeUpdateLock.bind(this);
}
componentWillMount() {
window.addEventListener('resize', this.onResize);
}
componentDidMount() {
this.updateLock();
}
componentWillUnmount() {
const el = this.el;
window.removeEventListener('resize', this.onResize);
if (el) {
el.removeEventListener('DOMSubtreeModified', this.maybeScroll);
el.removeEventListener('scroll', this.onScroll);
}
}
rebind(el) {
if (this.el === el) { return; }
if (this.el) {
this.el.removeEventListener('DOMSubtreeModified', this.maybeScroll);
this.el.removeEventListener('scroll', this.onScroll);
this.el = null;
}
if (el) {
this.el = el;
this.el.addEventListener('DOMSubtreeModified', this.maybeScroll);
this.el.addEventListener('scroll', this.onScroll);
}
}
onResize() {
this.resized = true;
this.maybeUpdateLock();
}
onScroll() {
this.scrolled = true;
this.maybeUpdateLock();
}
maybeUpdateLock() {
if (this.immediate) {
clearImmediate(this.immediate);
}
// zooming/resizing can trigger the scroll event too,
// but in that case we don't want to update the lock
// status. this hack ensures we only perform updateLock
// if there was not also a resize event in the same tick
this.immediate = setImmediate(() => {
if (this.scrolled && !this.resized) {
this.updateLock();
} else {
this.debouncedMaybeScroll();
}
this.scrolled = false;
this.resized = false;
this.immediate = null;
});
}
updateLock() {
const el = this.el;
this.locked = (
el !== null &&
Math.ceil(el.scrollTop) === Math.ceil(el.scrollHeight - el.offsetHeight)
);
}
debouncedMaybeScroll() {
if (this.debounce) {
clearTimeout(this.debounce);
}
this.debounce = setTimeout(() => {
this.maybeScroll();
this.debounce = null;
}, 50);
}
maybeScroll() {
const el = this.el,
newTop = Math.ceil(el.scrollHeight - el.offsetHeight);
if (this.locked && el.scrollTop !== newTop) {
el.scrollTop = newTop;
}
}
render() {
return cloneElement(
Children.only(this.props.children),
{ ref: el => this.rebind(el) }
);
}
};
export default ScrollLocked;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment