├── .npmignore ├── .travis.yml ├── .gitignore ├── LICENSE ├── CHANGELOG.md ├── package.json ├── README.md ├── test └── test.jsx ├── scrollUp.jsx └── index.js /.npmignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | npm-debug.log 4 | scrollUp.jsx 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "7" 4 | - "6" 5 | - "5" 6 | 7 | matrix: 8 | allow_failures: 9 | - node_js: "7" 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependency directory 2 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 3 | node_modules 4 | npm-debug.log 5 | 6 | # IntelliJ project files 7 | .idea 8 | 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Miloš Janda 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [1.2.1] 2 | 3 | * **Bugfix:** fix require of detect-passive-events 4 | 5 | # [1.2.0] 6 | 7 | * **Performance:** use of passive listeners to improve scrolling performance 8 | 9 | # [1.1.5] 10 | 11 | * **Compatibility:** update dependecies to react 15 12 | 13 | # [1.1.4] 14 | 15 | * **Compatibility:** update for react 15.0 16 | 17 | # [1.1.3] 18 | 19 | * **Compatibility:** Replace function window.scrollY with window.pageYOffset for better compatibility 20 | 21 | # [1.1.2] 22 | 23 | * **Bugfix:** Fix stop scrolling if top position reached and add touch events 24 | * **Bugfix:** Show element after browser refresh, if page is under top position 25 | 26 | # [1.1.1] 27 | 28 | * **Dependency:** update tween function 29 | * **Dependency:** update dev dependency 30 | * **NPM:** Update npmignore 31 | 32 | # [1.1.0] 33 | 34 | ### Other 35 | 36 | * **Compatibility:** update for react 0.14 37 | 38 | # [1.0.4] 39 | 40 | ### Bug 41 | 42 | * **Dependency:** fix wrong dependencies 43 | 44 | # [1.0.3] 45 | 46 | ### Bug 47 | 48 | * **Dependency:** fix wrong dependencies 49 | 50 | # [1.0.2] 51 | 52 | ### Bug 53 | 54 | * **Visibility:** set visibility hidden after hide button 55 | * **Dependency:** Update dependency information 56 | 57 | ### Other 58 | 59 | * **Default value:** change default value of easing 60 | 61 | 62 | # [1.0.1] 63 | 64 | ### Feature 65 | 66 | * **Performance:** animate over requestAnimationFrame 67 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-scroll-up", 3 | "version": "1.2.1", 4 | "description": "React component to render element for scroll to top of page", 5 | "author": "Milos Janda ", 6 | "scripts": { 7 | "test": "mocha --compilers js:babel-core/register test/test.jsx", 8 | "build": "babel scrollUp.jsx --out-file index.js", 9 | "prepublish": "npm run build" 10 | }, 11 | "main": "index.js", 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/milosjanda/react-scroll-up.git" 15 | }, 16 | "bug": { 17 | "url": "https://github.com/milosjanda/react-scroll-up/issues" 18 | }, 19 | "keywords": [ 20 | "scroll", 21 | "scrollUp", 22 | "scrollToTop", 23 | "animation", 24 | "effects", 25 | "react", 26 | "react-component" 27 | ], 28 | "babel": { 29 | "presets": [ 30 | "es2015", 31 | "react" 32 | ] 33 | }, 34 | "dependencies": { 35 | "detect-passive-events": "^1.0.0", 36 | "object-assign": "^4.0.1", 37 | "tween-functions": "^1.1.0" 38 | }, 39 | "peerDependencies": { 40 | "react": "0.13 - 15" 41 | }, 42 | "devDependencies": { 43 | "babel-cli": "6.16.0", 44 | "babel-core": "6.17.0", 45 | "babel-preset-es2015": "6.16.0", 46 | "babel-preset-react": "6.16.0", 47 | "chai": "3.5.0", 48 | "jsdom": "9.6.0", 49 | "mocha": "3.1.0", 50 | "raf": "3.3.0", 51 | "react": "15.3.2", 52 | "react-addons-test-utils": "15.3.2", 53 | "sinon": "1.17.6" 54 | }, 55 | "readmeFilename": "README.md", 56 | "license": "MIT" 57 | } 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-scroll-up 2 | [![npm version](https://badge.fury.io/js/react-scroll-up.svg)](https://badge.fury.io/js/react-scroll-up) 3 | [![License](https://img.shields.io/npm/l/react-scroll-up.svg)]() 4 | [![Dependency Status](https://img.shields.io/david/milosjanda/react-scroll-up.svg)]() 5 | [![peerDependency Status](https://img.shields.io/david/peer/milosjanda/react-scroll-up.svg)]() 6 | [![Build status](https://travis-ci.org/milosjanda/react-scroll-up.svg?branch=master)]() 7 | 8 | React component to add custom button (it can be something what you want) for scroll to top of page. 9 | 10 | Library uses [requestAnimationFrame](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame), 11 | if you want better browser compatibility (IE9 and older), you can use something like [https://gist.github.com/paulirish/1579671]. 12 | 13 | 14 | ## Install 15 | 16 | ```npm 17 | npm install react-scroll-up 18 | ``` 19 | 20 | ## How to use it 21 | 22 | [Live demo](http://milosjanda.github.io/react-scroll-up/) 23 | 24 | You have to define children element, for example `UP` 25 | 26 | ```jsx 27 | 28 | UP 29 | 30 | ``` 31 | 32 | ## Parameters 33 | 34 | ### showUnder:number in px (required) 35 | 36 | What position (and below) the button will be displayed. 37 | 38 | ### topPosition:number in px (optional) 39 | 40 | default: 0 41 | 42 | The position to which the scrollbar be moved after clicked. 43 | 44 | ### easing:string (optional) 45 | 46 | default: easeOutCubic 47 | 48 | Type of scrolling easing. You can specify some of this type of easing: https://github.com/chenglou/tween-functions 49 | 50 | In graphical representation: http://sole.github.io/tween.js/examples/03_graphs.html 51 | 52 | ### duration:number in miliseconds (optional) 53 | 54 | default: 250 55 | 56 | Time to reach the `topPosition` 57 | 58 | ### style:object (optional) 59 | 60 | default: 61 | 62 | ```javascript 63 | { 64 | position: 'fixed', 65 | bottom: 50, 66 | right: 30, 67 | cursor: 'pointer', 68 | transitionDuration: '0.2s', 69 | transitionTimingFunction: 'linear', 70 | transitionDelay: '0s' 71 | } 72 | ``` 73 | 74 | You can specify you own style and position of the button. 75 | 76 | Hide/show button is based on opacity, so this styles `opacity` and `transitionProperty` will be all time overwrite. 77 | 78 | If you can positioned button to left site, you have to reset css property `right: 'auto'`, and similar. 79 | 80 | -------------------------------------------------------------------------------- /test/test.jsx: -------------------------------------------------------------------------------- 1 | // JSDom is used to allow the tests to run right from the command line (no browsers needed) 2 | var jsdom = require('jsdom'); 3 | global.document = jsdom.jsdom(''); 4 | global.window = document.defaultView; 5 | global.navigator = window.navigator; 6 | // Also apply a requestAnimationFrame polyfill 7 | require('raf').polyfill() 8 | 9 | 10 | var React = require('react'); 11 | var sinon = require('sinon'); 12 | var expect = require('chai').expect; 13 | var TestUtils = require('react-addons-test-utils'); 14 | var ScrollUp = require('../index') 15 | 16 | // describe makes a test group 17 | describe(' states', function () { 18 | // This will be run before each test to reset the scroll position 19 | beforeEach(function () { 20 | window.pageYOffset = 0 21 | }) 22 | 23 | // and each `it` function describes an individual test 24 | it('is hidden when first rendered', function () { 25 | var renderedComponent = TestUtils.renderIntoDocument( 26 | 27 | UP 28 | 29 | ); 30 | 31 | expect(renderedComponent.state.show).to.be.false 32 | }); 33 | 34 | it('is shown if the page is scrolled past the `showUnder` point', function () { 35 | var renderedComponent = TestUtils.renderIntoDocument( 36 | 37 | UP 38 | 39 | ); 40 | 41 | // Set the scroll position to 200 and trigger the event manually 42 | window.pageYOffset = 200 43 | renderedComponent.handleScroll() 44 | 45 | expect(renderedComponent.state.show).to.be.true 46 | }); 47 | 48 | }); 49 | 50 | 51 | 52 | // describe makes a test group 53 | describe(' move 1', function () { 54 | 55 | var scrollToSpy 56 | var renderedComponent 57 | 58 | before(function () { 59 | window.pageYOffset = 0 60 | renderedComponent = TestUtils.renderIntoDocument( 61 | 62 | UP 63 | 64 | ); 65 | 66 | // "stub" the window.scrollTo function (because we want to see how it's called) 67 | scrollToSpy = sinon.stub(window, 'scrollTo', function (x, y) { 68 | window.pageXOffset = x 69 | window.pageYOffset = y 70 | renderedComponent.handleScroll() // And make sure to trigger the handleScroll for each call 71 | }) 72 | }) 73 | 74 | after(function () { 75 | scrollToSpy.restore() 76 | }) 77 | 78 | it('scrolls back up to the top when clicked', function (done) { 79 | // Ensure topPosition is set correctly 80 | expect(renderedComponent.props.topPosition).to.equal(0) 81 | 82 | // Set the scroll position to 200 and trigger the event manually 83 | window.pageYOffset = 200 84 | renderedComponent.handleScroll() 85 | 86 | // Now activate the click function 87 | renderedComponent.handleClick() 88 | 89 | // Give it a bit to scroll back up 90 | setTimeout(function () { 91 | expect(scrollToSpy.lastCall.args[1]).to.within(-0.1, 0.1) 92 | expect(renderedComponent.state.show).to.be.false 93 | done() 94 | }, 500) 95 | }); 96 | 97 | }); 98 | 99 | 100 | 101 | // describe makes a test group 102 | describe(' move 2', function () { 103 | 104 | var scrollToSpy 105 | var renderedComponent 106 | 107 | before(function () { 108 | window.pageYOffset = 0 109 | renderedComponent = TestUtils.renderIntoDocument( 110 | 111 | UP 112 | 113 | ); 114 | 115 | // "stub" the window.scrollTo function (because we want to see how it's called) 116 | scrollToSpy = sinon.stub(window, 'scrollTo', function (x, y) { 117 | window.pageXOffset = x 118 | window.pageYOffset = y 119 | renderedComponent.handleScroll() // And make sure to trigger the handleScroll for each call 120 | }) 121 | }) 122 | 123 | after(function () { 124 | scrollToSpy.restore() 125 | }) 126 | 127 | it('scrolls to `topPosition` when clicked', function (done) { 128 | // Ensure topPosition is set correctly 129 | expect(renderedComponent.props.topPosition).to.equal(100) 130 | 131 | // Set the scroll position to 200 and trigger the event manually 132 | window.pageYOffset = 200 133 | renderedComponent.handleScroll() 134 | 135 | // Now activate the click function 136 | renderedComponent.handleClick() 137 | 138 | // Give it a bit to scroll back up 139 | setTimeout(function () { 140 | expect(scrollToSpy.lastCall.args[1]).to.be.within(95, 105) 141 | expect(renderedComponent.state.show).to.be.false 142 | done() 143 | }, 500) 144 | }) 145 | 146 | }); 147 | -------------------------------------------------------------------------------- /scrollUp.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Milos Janda 3 | * @licence MIT 4 | */ 5 | 6 | 'use strict'; 7 | 8 | var React = require('react'); 9 | var TweenFunctions = require('tween-functions'); 10 | var objectAssign = require('object-assign'); 11 | var detectPassiveEvents = require('detect-passive-events').default; 12 | 13 | var ScrollUp = React.createClass({ 14 | 15 | data: { 16 | startValue: 0, 17 | currentTime: 0, // store current time of animation 18 | startTime: null, 19 | rafId: null 20 | }, 21 | 22 | propTypes: { 23 | topPosition: React.PropTypes.number, 24 | showUnder: React.PropTypes.number.isRequired, // show button under this position, 25 | easing: React.PropTypes.oneOf(['linear', 'easeInQuad', 'easeOutQuad', 'easeInOutQuad', 'easeInCubic', 26 | 'easeOutCubic', 'easeInOutCubic', 'easeInQuart', 'easeOutQuart', 'easeInOutQuart', 'easeInQuint', 27 | 'easeOutQuint', 'easeInOutQuint', 'easeInSine', 'easeOutSine', 'easeInOutSine', 'easeInExpo', 'easeOutExpo', 28 | 'easeInOutExpo', 'easeInCirc', 'easeOutCirc', 'easeInOutCirc', 'easeInElastic', 'easeOutElastic', 29 | 'easeInOutElastic', 'easeInBack', 'easeOutBack', 'easeInOutBack', 'easeInBounce', 'easeOutBounce', 30 | 'easeInOutBounce']), 31 | duration: React.PropTypes.number, // seconds 32 | style: React.PropTypes.object 33 | }, 34 | 35 | getDefaultProps: function () { 36 | return { 37 | duration: 250, 38 | easing: 'easeOutCubic', 39 | style: { 40 | position: 'fixed', 41 | bottom: 50, 42 | right: 30, 43 | cursor: 'pointer', 44 | transitionDuration: '0.2s', 45 | transitionTimingFunction: 'linear', 46 | transitionDelay: '0s' 47 | }, 48 | topPosition: 0 49 | } 50 | }, 51 | getInitialState: function () { 52 | return { 53 | show: false 54 | } 55 | }, 56 | shouldComponentUpdate: function (nextProps, nextState) { 57 | return nextState.show !== this.state.show; 58 | }, 59 | componentDidMount: function () { 60 | this.handleScroll(); // initialize state 61 | window.addEventListener('scroll', this.handleScroll); 62 | window.addEventListener("wheel", this.stopScrolling, detectPassiveEvents.hasSupport ? { passive : true } : false); 63 | window.addEventListener("touchstart", this.stopScrolling, detectPassiveEvents.hasSupport ? { passive : true } : false); 64 | }, 65 | 66 | componentWillUnmount: function () { 67 | window.removeEventListener('scroll', this.handleScroll); 68 | window.removeEventListener("wheel", this.stopScrolling, false); 69 | window.removeEventListener("touchstart", this.stopScrolling, false); 70 | }, 71 | 72 | handleScroll: function () { 73 | if (window.pageYOffset > this.props.showUnder) { 74 | this.setState({show: true}); 75 | } else { 76 | this.setState({show: false}); 77 | } 78 | }, 79 | handleClick: function () { 80 | this.stopScrolling(); 81 | this.data.startValue = window.pageYOffset; 82 | this.data.currentTime = 0; 83 | this.data.startTime = null; 84 | this.data.rafId = window.requestAnimationFrame(this.scrollStep); 85 | }, 86 | 87 | scrollStep: function (timestamp) { 88 | if (!this.data.startTime) { 89 | this.data.startTime = timestamp; 90 | } 91 | 92 | this.data.currentTime = timestamp - this.data.startTime; 93 | 94 | var position = TweenFunctions[this.props.easing]( 95 | this.data.currentTime, 96 | this.data.startValue, 97 | this.props.topPosition, 98 | this.props.duration 99 | ); 100 | 101 | if (window.pageYOffset <= this.props.topPosition) { 102 | this.stopScrolling(); 103 | } else { 104 | window.scrollTo(window.pageYOffset, position); 105 | this.data.rafId = window.requestAnimationFrame(this.scrollStep); 106 | } 107 | }, 108 | 109 | stopScrolling: function () { 110 | window.cancelAnimationFrame(this.data.rafId); 111 | }, 112 | 113 | render: function () { 114 | var propStyle = this.props.style; 115 | var element = 116 |
117 | {this.props.children} 118 |
; 119 | 120 | var style = objectAssign({}, propStyle); 121 | style.opacity = this.state.show ? 1 : 0; 122 | style.visibility = this.state.show ? 'visible' : 'hidden'; 123 | style.transitionProperty = 'opacity, visibility'; 124 | 125 | return React.cloneElement(element, {style: style}); 126 | } 127 | }); 128 | 129 | module.exports = ScrollUp; 130 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Milos Janda 3 | * @licence MIT 4 | */ 5 | 6 | 'use strict'; 7 | 8 | var React = require('react'); 9 | var TweenFunctions = require('tween-functions'); 10 | var objectAssign = require('object-assign'); 11 | var detectPassiveEvents = require('detect-passive-events').default; 12 | 13 | var ScrollUp = React.createClass({ 14 | displayName: 'ScrollUp', 15 | 16 | 17 | data: { 18 | startValue: 0, 19 | currentTime: 0, // store current time of animation 20 | startTime: null, 21 | rafId: null 22 | }, 23 | 24 | propTypes: { 25 | topPosition: React.PropTypes.number, 26 | showUnder: React.PropTypes.number.isRequired, // show button under this position, 27 | easing: React.PropTypes.oneOf(['linear', 'easeInQuad', 'easeOutQuad', 'easeInOutQuad', 'easeInCubic', 'easeOutCubic', 'easeInOutCubic', 'easeInQuart', 'easeOutQuart', 'easeInOutQuart', 'easeInQuint', 'easeOutQuint', 'easeInOutQuint', 'easeInSine', 'easeOutSine', 'easeInOutSine', 'easeInExpo', 'easeOutExpo', 'easeInOutExpo', 'easeInCirc', 'easeOutCirc', 'easeInOutCirc', 'easeInElastic', 'easeOutElastic', 'easeInOutElastic', 'easeInBack', 'easeOutBack', 'easeInOutBack', 'easeInBounce', 'easeOutBounce', 'easeInOutBounce']), 28 | duration: React.PropTypes.number, // seconds 29 | style: React.PropTypes.object 30 | }, 31 | 32 | getDefaultProps: function getDefaultProps() { 33 | return { 34 | duration: 250, 35 | easing: 'easeOutCubic', 36 | style: { 37 | position: 'fixed', 38 | bottom: 50, 39 | right: 30, 40 | cursor: 'pointer', 41 | transitionDuration: '0.2s', 42 | transitionTimingFunction: 'linear', 43 | transitionDelay: '0s' 44 | }, 45 | topPosition: 0 46 | }; 47 | }, 48 | getInitialState: function getInitialState() { 49 | return { 50 | show: false 51 | }; 52 | }, 53 | shouldComponentUpdate: function shouldComponentUpdate(nextProps, nextState) { 54 | return nextState.show !== this.state.show; 55 | }, 56 | componentDidMount: function componentDidMount() { 57 | this.handleScroll(); // initialize state 58 | window.addEventListener('scroll', this.handleScroll); 59 | window.addEventListener("wheel", this.stopScrolling, detectPassiveEvents.hasSupport ? { passive: true } : false); 60 | window.addEventListener("touchstart", this.stopScrolling, detectPassiveEvents.hasSupport ? { passive: true } : false); 61 | }, 62 | 63 | componentWillUnmount: function componentWillUnmount() { 64 | window.removeEventListener('scroll', this.handleScroll); 65 | window.removeEventListener("wheel", this.stopScrolling, false); 66 | window.removeEventListener("touchstart", this.stopScrolling, false); 67 | }, 68 | 69 | handleScroll: function handleScroll() { 70 | if (window.pageYOffset > this.props.showUnder) { 71 | this.setState({ show: true }); 72 | } else { 73 | this.setState({ show: false }); 74 | } 75 | }, 76 | handleClick: function handleClick() { 77 | this.stopScrolling(); 78 | this.data.startValue = window.pageYOffset; 79 | this.data.currentTime = 0; 80 | this.data.startTime = null; 81 | this.data.rafId = window.requestAnimationFrame(this.scrollStep); 82 | }, 83 | 84 | scrollStep: function scrollStep(timestamp) { 85 | if (!this.data.startTime) { 86 | this.data.startTime = timestamp; 87 | } 88 | 89 | this.data.currentTime = timestamp - this.data.startTime; 90 | 91 | var position = TweenFunctions[this.props.easing](this.data.currentTime, this.data.startValue, this.props.topPosition, this.props.duration); 92 | 93 | if (window.pageYOffset <= this.props.topPosition) { 94 | this.stopScrolling(); 95 | } else { 96 | window.scrollTo(window.pageYOffset, position); 97 | this.data.rafId = window.requestAnimationFrame(this.scrollStep); 98 | } 99 | }, 100 | 101 | stopScrolling: function stopScrolling() { 102 | window.cancelAnimationFrame(this.data.rafId); 103 | }, 104 | 105 | render: function render() { 106 | var propStyle = this.props.style; 107 | var element = React.createElement( 108 | 'div', 109 | { style: propStyle, onClick: this.handleClick }, 110 | this.props.children 111 | ); 112 | 113 | var style = objectAssign({}, propStyle); 114 | style.opacity = this.state.show ? 1 : 0; 115 | style.visibility = this.state.show ? 'visible' : 'hidden'; 116 | style.transitionProperty = 'opacity, visibility'; 117 | 118 | return React.cloneElement(element, { style: style }); 119 | } 120 | }); 121 | 122 | module.exports = ScrollUp; 123 | --------------------------------------------------------------------------------