react-scroll-percentage
Advanced tools
Comparing version 0.4.0 to 0.5.0
213
es/index.js
@@ -1,38 +0,11 @@ | ||
'use strict'; | ||
Object.defineProperty(exports, "__esModule", { | ||
value: true | ||
}); | ||
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; | ||
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); | ||
var _react = require('react'); | ||
var _react2 = _interopRequireDefault(_react); | ||
var _propTypes = require('prop-types'); | ||
var _propTypes2 = _interopRequireDefault(_propTypes); | ||
var _reactIntersectionObserver = require('react-intersection-observer'); | ||
var _reactIntersectionObserver2 = _interopRequireDefault(_reactIntersectionObserver); | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
function _objectWithoutProperties(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; } | ||
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } | ||
import React, { PureComponent } from 'react'; // eslint-disable-line no-unused-vars | ||
import PropTypes from 'prop-types'; | ||
import Observer from 'react-intersection-observer'; | ||
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } | ||
const isFunction = func => typeof func === 'function'; | ||
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } // eslint-disable-line no-unused-vars | ||
var isFunction = function isFunction(func) { | ||
return typeof func === 'function'; | ||
}; | ||
/** | ||
@@ -47,133 +20,95 @@ * Monitors scroll, and triggers the children function with updated props | ||
*/ | ||
class ScrollPercentage extends PureComponent { | ||
constructor(...args) { | ||
var _temp; | ||
var ScrollPercentage = function (_PureComponent) { | ||
_inherits(ScrollPercentage, _PureComponent); | ||
function ScrollPercentage() { | ||
var _ref; | ||
var _temp, _this, _ret; | ||
_classCallCheck(this, ScrollPercentage); | ||
for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { | ||
args[_key] = arguments[_key]; | ||
} | ||
return _ret = (_temp = (_this = _possibleConstructorReturn(this, (_ref = ScrollPercentage.__proto__ || Object.getPrototypeOf(ScrollPercentage)).call.apply(_ref, [this].concat(args))), _this), _this.state = { | ||
return _temp = super(...args), this.state = { | ||
percentage: 0, | ||
inView: false | ||
}, _this.observer = null, _this.handleChange = function (inView) { | ||
_this.setState({ inView: inView }); | ||
}, _this.handleNode = function (node) { | ||
return _this.observer = node; | ||
}, _this.handleScroll = function () { | ||
return requestAnimationFrame(function () { | ||
return _this.updatePercentage(); | ||
}); | ||
}, _temp), _possibleConstructorReturn(_this, _ret); | ||
}, this.observer = null, this.handleChange = inView => { | ||
this.setState({ inView }); | ||
}, this.handleNode = node => this.observer = node, this.handleScroll = () => requestAnimationFrame(() => this.updatePercentage()), _temp; | ||
} | ||
_createClass(ScrollPercentage, [{ | ||
key: 'componentWillUpdate', | ||
value: function componentWillUpdate(nextProps, nextState) { | ||
if (!nextProps.onChange) return; | ||
if (nextState.percentage !== this.state.percentage || nextState.inView !== this.state.inView) { | ||
nextProps.onChange(_extends({}, this.state)); | ||
} | ||
} | ||
}, { | ||
key: 'componentDidUpdate', | ||
value: function componentDidUpdate(prevProps, prevState) { | ||
if (prevState.inView !== this.state.inView) { | ||
this.monitorScroll(this.state.inView); | ||
} | ||
} | ||
}, { | ||
key: 'componentWillUnmount', | ||
value: function componentWillUnmount() { | ||
this.monitorScroll(false); | ||
this.observer = null; | ||
} | ||
}, { | ||
key: 'monitorScroll', | ||
value: function monitorScroll(enable) { | ||
if (enable) { | ||
window.addEventListener('scroll', this.handleScroll); | ||
this.handleScroll(); | ||
} else { | ||
window.removeEventListener('scroll', this.handleScroll); | ||
} | ||
} | ||
}, { | ||
key: 'updatePercentage', | ||
value: function updatePercentage() { | ||
var threshold = this.props.threshold; | ||
/** | ||
* Get the correct viewport height. If rendered inside an iframe, grab it from the parent | ||
*/ | ||
static viewportHeight() { | ||
return global.parent ? global.parent.innerHeight : global.innerHeight; | ||
} | ||
var _observer$node$getBou = this.observer.node.getBoundingClientRect(), | ||
bottom = _observer$node$getBou.bottom, | ||
height = _observer$node$getBou.height; | ||
static calculatePercentage(height, bottom, threshold = 0) { | ||
const vh = ScrollPercentage.viewportHeight(); | ||
const offsetTop = threshold * vh * 0.25; | ||
const offsetBottom = threshold * vh * 0.25; | ||
var percentage = ScrollPercentage.calculatePercentage(height, bottom, threshold); | ||
return 1 - Math.max(0, Math.min(1, (bottom - offsetTop) / (vh + height - offsetBottom - offsetTop))); | ||
} | ||
if (percentage !== this.state.percentage) { | ||
this.setState({ | ||
percentage: percentage | ||
}); | ||
} | ||
componentWillUpdate(nextProps, nextState) { | ||
if (!nextProps.onChange) return; | ||
if (nextState.percentage !== this.state.percentage || nextState.inView !== this.state.inView) { | ||
nextProps.onChange(_extends({}, this.state)); | ||
} | ||
}, { | ||
key: 'render', | ||
value: function render() { | ||
var _props = this.props, | ||
children = _props.children, | ||
threshold = _props.threshold, | ||
props = _objectWithoutProperties(_props, ['children', 'threshold']); | ||
} | ||
return _react2.default.createElement(_reactIntersectionObserver2.default, _extends({}, props, { | ||
onChange: this.handleChange, | ||
ref: this.handleNode | ||
}), | ||
// If children is a function, render it with the current percentage and inView status. | ||
// Otherwise always render children. Assume onChange is being used outside, to control the the state of children. | ||
isFunction(children) ? children({ | ||
percentage: this.state.percentage, | ||
inView: this.state.inView | ||
}) : children); | ||
componentDidUpdate(prevProps, prevState) { | ||
if (prevState.inView !== this.state.inView) { | ||
this.monitorScroll(this.state.inView); | ||
} | ||
}], [{ | ||
key: 'viewportHeight', | ||
} | ||
componentWillUnmount() { | ||
this.monitorScroll(false); | ||
this.observer = null; | ||
} | ||
/** | ||
* Get the correct viewport height. If rendered inside an iframe, grab it from the parent | ||
*/ | ||
value: function viewportHeight() { | ||
return global.parent ? global.parent.innerHeight : global.innerHeight; | ||
monitorScroll(enable) { | ||
if (enable) { | ||
window.addEventListener('scroll', this.handleScroll); | ||
this.handleScroll(); | ||
} else { | ||
window.removeEventListener('scroll', this.handleScroll); | ||
} | ||
}, { | ||
key: 'calculatePercentage', | ||
value: function calculatePercentage(height, bottom) { | ||
var threshold = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0; | ||
} | ||
var vh = ScrollPercentage.viewportHeight(); | ||
var offsetTop = threshold * vh * 0.25; | ||
var offsetBottom = threshold * vh * 0.25; | ||
updatePercentage() { | ||
const { threshold } = this.props; | ||
const { bottom, height } = this.observer.node.getBoundingClientRect(); | ||
const percentage = ScrollPercentage.calculatePercentage(height, bottom, threshold); | ||
return 1 - Math.max(0, Math.min(1, (bottom - offsetTop) / (vh + height - offsetBottom - offsetTop))); | ||
if (percentage !== this.state.percentage) { | ||
this.setState({ | ||
percentage | ||
}); | ||
} | ||
}]); | ||
} | ||
return ScrollPercentage; | ||
}(_react.PureComponent); | ||
render() { | ||
const _props = this.props, | ||
{ children, threshold } = _props, | ||
props = _objectWithoutProperties(_props, ['children', 'threshold']); | ||
return React.createElement(Observer, _extends({}, props, { | ||
onChange: this.handleChange, | ||
ref: this.handleNode | ||
}), | ||
// If children is a function, render it with the current percentage and inView status. | ||
// Otherwise always render children. Assume onChange is being used outside, to control the the state of children. | ||
isFunction(children) ? children({ | ||
percentage: this.state.percentage, | ||
inView: this.state.inView | ||
}) : children); | ||
} | ||
} | ||
ScrollPercentage.propTypes = { | ||
/** Element tag to use for the wrapping */ | ||
tag: _propTypes2.default.node, | ||
tag: PropTypes.node, | ||
/** Children should be either a function or a node */ | ||
children: _propTypes2.default.oneOfType([_propTypes2.default.func, _propTypes2.default.node]), | ||
children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]), | ||
/** Call this function whenever the percentage changes */ | ||
onChange: _propTypes2.default.func, | ||
onChange: PropTypes.func, | ||
/** Number between 0 and 1 indicating the the percentage that should be visible before triggering */ | ||
threshold: _propTypes2.default.number | ||
threshold: PropTypes.number | ||
}; | ||
@@ -183,2 +118,2 @@ ScrollPercentage.defaultProps = { | ||
threshold: 0 }; | ||
exports.default = ScrollPercentage; | ||
export default ScrollPercentage; |
{ | ||
"name": "react-scroll-percentage", | ||
"version": "0.4.0", | ||
"version": "0.5.0", | ||
"description": "Monitor the scroll percentage of a component inside the viewport, using the IntersectionObserver API.", | ||
@@ -42,38 +42,23 @@ "main": "lib/index.js", | ||
}, | ||
"babel": { | ||
"presets": [ | ||
"env", | ||
"react", | ||
"stage-2" | ||
], | ||
"env": { | ||
"es": { | ||
"presets": [ | ||
[ | ||
"env", | ||
{ | ||
"modules": false | ||
} | ||
], | ||
"react", | ||
"stage-2" | ||
] | ||
} | ||
} | ||
}, | ||
"eslintConfig": { | ||
"extends": [ | ||
"insilico", | ||
"prettier", | ||
"prettier/react" | ||
"insilico" | ||
] | ||
}, | ||
"babel": { | ||
"presets": [ | ||
"./.babelrc.js" | ||
] | ||
}, | ||
"jest": { | ||
"testEnvironment": "node", | ||
"snapshotSerializers": [ | ||
"<rootDir>/node_modules/enzyme-to-json/serializer" | ||
"enzyme-to-json/serializer" | ||
], | ||
"setupFiles": [ | ||
"<rootDir>/jest-setup.js" | ||
] | ||
}, | ||
"dependencies": { | ||
"react-intersection-observer": "^1.0.0" | ||
"react-intersection-observer": "^1.1.0" | ||
}, | ||
@@ -85,9 +70,9 @@ "peerDependencies": { | ||
"devDependencies": { | ||
"@storybook/addon-actions": "^3.2.0", | ||
"@storybook/addon-options": "^3.2.3", | ||
"@storybook/react": "^3.2.3", | ||
"babel-cli": "^6.24.1", | ||
"babel-core": "^6.25.0", | ||
"babel-eslint": "^7.2.3", | ||
"babel-jest": "^20.0.3", | ||
"@storybook/addon-actions": "^3.2.6", | ||
"@storybook/addon-options": "^3.2.6", | ||
"@storybook/react": "^3.2.8", | ||
"babel-cli": "^6.26.0", | ||
"babel-core": "^6.26.0", | ||
"babel-eslint": "^8.0.1", | ||
"babel-jest": "^21.0.0", | ||
"babel-preset-env": "^1.6.0", | ||
@@ -97,17 +82,17 @@ "babel-preset-react": "^6.24.1", | ||
"concurrently": "^3.5.0", | ||
"enzyme": "^2.9.1", | ||
"enzyme-to-json": "^1.5.1", | ||
"eslint": "^4.3.0", | ||
"eslint-config-insilico": "^4.1.1", | ||
"eslint-config-prettier": "^2.3.0", | ||
"enzyme": "^3.0.0", | ||
"enzyme-adapter-react-16": "^1.0.0", | ||
"enzyme-to-json": "^3.0.1", | ||
"eslint": "^4.6.1", | ||
"eslint-config-insilico": "^5.0.0", | ||
"husky": "^0.14.3", | ||
"intersection-observer": "^0.4.0", | ||
"jest": "^20.0.4", | ||
"lint-staged": "^4.0.2", | ||
"prettier": "^1.5.3", | ||
"prop-types": "^15.5.10", | ||
"react": "^15.6.1", | ||
"react-dom": "^15.6.1", | ||
"react-test-renderer": "^15.6.1" | ||
"intersection-observer": "^0.4.2", | ||
"jest": "^21.0.1", | ||
"lint-staged": "^4.1.0", | ||
"prettier": "^1.6.1", | ||
"prop-types": "^15.6.0", | ||
"react": "^16.0.0", | ||
"react-dom": "^16.0.0", | ||
"react-test-renderer": "^16.0.0" | ||
} | ||
} |
@@ -37,3 +37,3 @@ # react-scroll-percentage | ||
### intersection-observer | ||
The component requires the [intersection-observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) to be available on the global namespace. At the moment you include the polyfill to support all browsers | ||
The component requires the [intersection-observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) to be available on the global namespace. At the moment you should include a polyfill to ensure support in all browsers. | ||
@@ -46,3 +46,3 @@ You can import the [polyfill](https://yarnpkg.com/en/package/intersection-observer) directly or use a service like [polyfill.io](https://polyfill.io/v2/docs/) that can add it when needed. | ||
Then import it in your app | ||
Then import it in your app: | ||
@@ -53,2 +53,32 @@ ```js | ||
If you are using Webpack (or similar) you could use [dynamic imports](https://webpack.js.org/api/module-methods/#import-), to load the Polyfill only if needed. | ||
A basic implementation could look something like this: | ||
```js | ||
loadPolyfills() | ||
.then(() => /* Render React application now that your Polyfills are ready */) | ||
/** | ||
* Do feature detection, to figure out which polyfills needs to be imported. | ||
**/ | ||
function loadPolyfills() { | ||
const polyfills = [] | ||
if (!supportsIntersectionObserver()) { | ||
polyfills.push(import('intersection-observer')) | ||
} | ||
return Promise.all(polyfills) | ||
} | ||
function supportsIntersectionObserver() { | ||
return ( | ||
'IntersectionObserver' in global && | ||
'IntersectionObserverEntry' in global && | ||
'intersectionRatio' in IntersectionObserverEntry.prototype | ||
) | ||
} | ||
``` | ||
### requestAnimationFrame | ||
@@ -58,3 +88,3 @@ To optimize scroll updates, [requestAnimationFrame](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame) is used. Make sure your target browsers support it, or include the required polyfill. | ||
## Props | ||
The **`<Observer />`** accepts the following props: | ||
The **`<ScrollPercentage />`** accepts the following props: | ||
@@ -61,0 +91,0 @@ | Name | Type | Default | Required | Description | |
285700
13
123
386