react-stuck
Version:
React component which loosely implements `position: sticky`
117 lines (101 loc) • 3.27 kB
JavaScript
/* global window */
'use strict';
var React = require('react'),
ReactDOM = require('react-dom'),
PureRenderMixin = require('react-addons-pure-render-mixin'),
throttle = require('lodash/function/throttle'),
offset = require('dom-helpers/query/offset'),
on = require('dom-helpers/events/on'),
off = require('dom-helpers/events/off');
module.exports = React.createClass({
displayName: 'Stuck',
propTypes: {
children: React.PropTypes.any,
className: React.PropTypes.string,
height: React.PropTypes.number.isRequired,
style: React.PropTypes.object,
top: React.PropTypes.number
},
mixins: [PureRenderMixin],
getDefaultProps: function getDefaultProps() {
return {
top: 0,
style: {}
};
},
getInitialState: function getInitialState() {
this._offsets = {};
return {
className: 'stuck-top'
};
},
componentDidMount: function componentDidMount() {
this._throttledUpdateOffsets = this._getThrottledUpdateOffsets();
on(window, 'scroll', this._updateClass);
on(window, 'scroll', this._throttledUpdateOffsets);
this.componentWillReceiveProps(this.props);
},
componentWillReceiveProps: function componentWillReceiveProps(nextProps) {
this._updateOffsets(nextProps);
this._updateClass();
},
componentWillUnmount: function componentWillUnmount() {
off(window, 'scroll', this._updateClass);
off(window, 'scroll', this._throttledUpdateOffsets);
},
_getThrottledUpdateOffsets: function _getThrottledUpdateOffsets() {
function update() {
this._updateOffsets(this.props);
}
return throttle(update.bind(this), 500);
},
_updateOffsets: function _updateOffsets(nextProps) {
var containerNode = ReactDOM.findDOMNode(this),
stuckNode = ReactDOM.findDOMNode(this.refs.stuck),
top = offset(containerNode).top,
bottom = top + nextProps.height - stuckNode.offsetHeight;
this._offsets = {
top: top - nextProps.top,
bottom: bottom - nextProps.top
};
},
_updateClass: function _updateClass() {
var pageYOffset = window.pageYOffset,
c;
if (pageYOffset <= this._offsets.top) c = 'stuck-top';else if (pageYOffset >= this._offsets.bottom) c = 'stuck-bottom';else if (pageYOffset > this._offsets.top) c = 'stuck';
this.setState({ className: c });
},
_getStyleByClass: function _getStyleByClass(className) {
switch (className) {
case 'stuck':
return {
position: 'fixed',
top: this.props.top
};
case 'stuck-bottom':
return {
position: 'absolute',
top: this._offsets.bottom + this.props.top
};
default:
return {
// need 'inline-block' to accurately calculate
// the height (with margins) of the `props.children`
display: 'inline-block'
};
}
},
render: function render() {
return React.createElement(
'div',
{ className: this.props.className, style: this.props.style },
React.createElement(
'div',
{ ref: 'stuck',
style: this._getStyleByClass(this.state.className),
className: this.state.className },
this.props.children
)
);
}
});