├── .babelrc ├── .browserslistrc ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .npmignore ├── .npmingore ├── .size-snapshot.json ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── cypress.json ├── cypress ├── .eslintrc ├── fixtures │ └── example.json ├── integration │ └── scroll.spec.js ├── plugins │ └── index.js └── support │ ├── commands.js │ └── index.js ├── example ├── .gitignore ├── gatsby-config.js ├── package-lock.json ├── package.json └── src │ └── pages │ ├── CustomButton.css │ ├── index.js │ └── normalize.css ├── index.js ├── package-lock.json ├── package.json ├── rollup.config.js ├── src └── react-scroll-up-button.js └── tests ├── server ├── index.html ├── testing.js └── webpack.config.js └── specs ├── .eslintrc ├── custom prototype.js ├── named exports.js ├── pre-styled children.js └── unit-intergation.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["@babel/preset-env", { "loose": true }], "@babel/preset-react"] 3 | } -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | last 2 versions 2 | ie >= 11 3 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /dist/ 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "env": { 4 | "es6": true, 5 | "browser": true 6 | }, 7 | "rules": { 8 | "max-len": ["error", { "ignoreComments": true, "code": 120, "ignoreTemplateLiterals": true }], 9 | "linebreak-style": 0, 10 | "semi": 0, 11 | "no-shadow": 0, 12 | "no-use-before-define": ["error", { "variables": false }], 13 | "no-nested-ternary": 0, 14 | "comma-dangle": ["error", { 15 | "arrays": "always-multiline", 16 | "objects": "always-multiline", 17 | "imports": "always-multiline", 18 | "exports": "always-multiline", 19 | "functions": "ignore" 20 | }], 21 | "react/no-did-update-set-state": 0, 22 | "react/jsx-one-expression-per-line": 0, 23 | "react/jsx-uses-react": "error", 24 | "react/jsx-uses-vars": "error", 25 | "react/jsx-filename-extension": 0, 26 | "react/prop-types": ["error",{ "skipUndeclared" : true }], 27 | "react/prefer-stateless-function": 0, 28 | "import/no-named-as-default": 0, 29 | "import/no-named-as-default-member": 0, 30 | "jsx-a11y/label-has-associated-control": ["error", { "assert": "either" }], 31 | "jsx-a11y/label-has-for": [ 2, {"required": {"every": [ "id" ]}}] 32 | }, 33 | "plugins": [ 34 | "react", 35 | "import" 36 | ], 37 | "parser": "babel-eslint", 38 | "parserOptions": { 39 | "ecmaFeatures": { 40 | "jsx": true 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | coverage/ 4 | dist/ 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | *.log 3 | node_modules 4 | npm-debug.log 5 | UITesting 6 | coverage 7 | -------------------------------------------------------------------------------- /.npmingore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | coverage/ 4 | dist/ 5 | example/ 6 | -------------------------------------------------------------------------------- /.size-snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "dist/esm/react-scroll-up-button.js": { 3 | "bundled": 13634, 4 | "minified": 8157, 5 | "gzipped": 2377, 6 | "treeshaked": { 7 | "rollup": { 8 | "code": 5284, 9 | "import_statements": 285 10 | }, 11 | "webpack": { 12 | "code": 6558 13 | } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" 4 | script: 5 | - npm run eslint 6 | - npm run test 7 | - npm run test:cypress 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | #1.6.4 2 | * **Dev:** Removed Babel-Polyfill in favor spread/babel compile (I should have noticed this before hand). 3 | * **React:** Added custom propType validator to check StopPosition prop is less then ShowAtPosition prop. 4 | * **React:** Fixed mispelled word ShowAtPosition, was ShowAtPostion. 5 | * **Dev:** Deployed demo site https://react-scroll-up-button.com 6 | 7 | #1.6.3 8 | * **Dev:** Removed Babel-Polyfill in favor of Object-Assign. 9 | 10 | #1.6.2 11 | * **React:** Added Transform attribute to default button for better styling. 12 | * **Dev:** Added Babel-polyfill for object.assign polyfill, support for ie11. 13 | 14 | #1.6.1 15 | * **React:** Added catch if tween function tried to set scroll position past the stop position. 16 | * **Test:** Updated all tests, and added e2e testing. 17 | * **Dev:** Added cypress e2e testing and multiple package scripts for development and testing. 18 | * **NPM:** Moved destination directory from lib/ to dist/ 19 | 20 | #1.6.0 21 | * **React:** Moved themed buttons to exports 22 | 23 | #1.5.11 24 | * **React:** Added className overrides to the default button. 25 | 26 | #1.5.10 27 | * **NPM:** Replaced babel-preset-es2015 for babel-preset-env 28 | * **NPM:** Updated React libraries 29 | * **NPM:** Added enzyme-adapter-react 30 | * **NPM:** Replaced react-addons-test-utils for react-test-renderer 31 | * **NPM:** Added Node 8 for unit testing 32 | 33 | #1.5.9 34 | 35 | * **React:** Added Passive Events into listeners. 36 | * **React:** Replaced React.PropTypes with Proptypes See https://facebook.github.io/react/docs/typechecking-with-proptypes.html 37 | * **NPM:** Updated dependencies 38 | * **NPM:** Added detect-passive-events 39 | * **NPM:** Added prop-types 40 | 41 | #1.5.6 42 | 43 | * **NPM:** Updated README 44 | * **GITHUB:** Added build coverage 45 | 46 | #1.5.5 47 | 48 | * **NPM:** Update README, minor variable scoping. 49 | * **GITHUB:** Added build testing 50 | 51 | #1.5.4 52 | 53 | * **NPM:** Published 1.5.4 54 | * **GITHUB:** Updated README.md fixed arrow up example 55 | 56 | #1.5.2 57 | 58 | * **NPM:** Removed un-needed dependency 59 | 60 | #1.5.1 61 | 62 | * **Compatibility:** no longer requires a style-loader 63 | * **NPM:** Removed .css file 64 | * **GITHUB:** Added CHANGELOG.md 65 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![npm version](https://badge.fury.io/js/react-scroll-up-button.svg)](https://badge.fury.io/js/react-scroll-up-button) 2 | [![License](https://img.shields.io/npm/l/express.svg)]() 3 | [![Demo](https://img.shields.io/badge/Demo-Live-green.svg)](https://react-scroll-up-button.com) 4 | [![Build Status](https://travis-ci.org/dirtyredz/react-scroll-up-button.svg?branch=master)](https://travis-ci.org/dirtyredz/react-scroll-up-button) 5 | [![Coverage Status](https://coveralls.io/repos/github/dirtyredz/react-scroll-up-button/badge.svg?branch=master)](https://coveralls.io/github/dirtyredz/react-scroll-up-button?branch=master) 6 | 7 | # react-scroll-up-button 8 | React Component for a fixed scroll to top button. 9 | The button can use the default button, or can be customized to look like any button you want. 10 | The component contains all the code neccassary to scroll to the top of the page (Or desired position) Utilizing Easing effects. 11 | 12 | 13 | 14 | I am also adding many different style buttons that you can copy and paste into this component to quickly acheive a style you can appreciate and fits your websites look and feel. 15 | 16 | ## Demo 17 | 18 | https://react-scroll-up-button.com 19 | 20 | 21 | ## Install 22 | 23 | ```npm 24 | npm install react-scroll-up-button 25 | ``` 26 | 27 | ---- 28 | 29 | ## Usage 30 | 31 | ### Default Button: ![default_button](https://user-images.githubusercontent.com/7119499/46116181-5e9c8e00-c1c0-11e8-97cc-b20e905f0e51.PNG) 32 | 33 | ```jsx 34 | import React from "react"; 35 | import ScrollUpButton from "react-scroll-up-button"; //Add this line Here 36 | 37 | export default class Index extends React.Component { 38 | render() { 39 | return ( 40 |
41 | 42 | //This is all you need to get the default view working 43 |
44 | ); 45 | } 46 | } 47 | 48 | ``` 49 | 50 | Setting Custom Classes to the default button. 51 | This will apply the class names you specify to the scroll-up-button. 52 | ```jsx 53 | 54 | ``` 55 | 56 | Setting inline styles to the default button. 57 | This will apply the styles to the scroll-up-button. 58 | ```jsx 59 | 60 | ``` 61 | 62 | ---- 63 | 64 | ### Custom Button: 65 | ```jsx 66 | import React from "react"; 67 | import ScrollUpButton from "react-scroll-up-button"; 68 | 69 | export default class Index extends React.Component { 70 | render() { 71 | return ( 72 |
73 | 74 | // Here you can add any react component or jsx 75 | // ScrollButton will apply the classnames given to the container of whatever you put here. 76 | // Changing appearence this way will only work when importing the default ScrollUpButton, importing any of the specific buttons do not except children 77 | 78 |
79 | ); 80 | } 81 | } 82 | ``` 83 | ---- 84 | 85 | ### Configuration: 86 | ```javascript 87 | 97 | ``` 98 | StopPosition -- PageYOffset in which you want the page to stop at when scrolling up. 99 | 100 | ShowAtPosition -- PageYOffset position at which the button will show up. 101 | 102 | EasingType -- Easing option see : (https://www.npmjs.com/package/tween-functions) for available options. 103 | 104 | AnimationDuration -- Milisecond duration of scrolling up. 105 | 106 | ContainerClassName -- Class name applied to the container when NOT using the default view. 107 | 108 | TransitionClassName -- Class name applied to the container to show the button when NOT using the default view. 109 | 110 | style -- style the container directly with inline styleing, can be used with any imported button. 111 | 112 | ToggledStyle -- Style the toggled state of the container directly, can be used with any imported button. 113 | 114 | ---- 115 | 116 | ### Themed Buttons: 117 | Click on a button to see its code. 118 | 119 | Vertical Button | Circle Arrow Button | Tiny Up Button 120 | :---: | :---: | :---: 121 | [![vertical_button](https://cloud.githubusercontent.com/assets/7119499/21249476/ea4a02ce-c303-11e6-9448-6f2b078bc8d1.png)](#vertical-button) | [![circle_arrow_button](https://cloud.githubusercontent.com/assets/7119499/21251624/cf86fabc-c314-11e6-8f70-f6ec440ca187.png)](#circle-arrow-button) | [![tiny_up_button](https://user-images.githubusercontent.com/7119499/41563627-cb572f90-7315-11e8-8ae4-fae10b3642c3.PNG)](#tiny-up-button) 122 | 123 | 124 | ---- 125 | 126 | 127 | #### Vertical Button: 128 | ![vertical_button](https://cloud.githubusercontent.com/assets/7119499/21249476/ea4a02ce-c303-11e6-9448-6f2b078bc8d1.png) 129 | 130 | Vertical_Button | React: 131 | ``` 132 | import React from "react"; 133 | import {VerticleButton as ScrollUpButton} from "react-scroll-up-button"; //Add this line Here 134 | 135 | export default class Index extends React.Component { 136 | render() { 137 | return ( 138 |
139 | 140 |
141 | ); 142 | } 143 | } 144 | ``` 145 | 146 | ---- 147 | 148 | #### Circle Arrow Button: 149 | ![circle_arrow_button](https://cloud.githubusercontent.com/assets/7119499/21251624/cf86fabc-c314-11e6-8f70-f6ec440ca187.png) 150 | 151 | Circle_Arrow_Button | React: 152 | ``` 153 | import React from "react"; 154 | import {CircleArrow as ScrollUpButton} from "react-scroll-up-button"; //Add this line Here 155 | 156 | export default class Index extends React.Component { 157 | render() { 158 | return ( 159 |
160 | 161 |
162 | ); 163 | } 164 | } 165 | ``` 166 | 167 | #### Tiny Up Button: 168 | ![tiny_up_button](https://user-images.githubusercontent.com/7119499/41563627-cb572f90-7315-11e8-8ae4-fae10b3642c3.PNG) 169 | 170 | Tiny_Up_Button | React: 171 | ``` 172 | import React from "react"; 173 | import {TinyButton as ScrollUpButton} from "react-scroll-up-button"; //Add this line Here 174 | 175 | export default class Index extends React.Component { 176 | render() { 177 | return ( 178 |
179 | 180 |
181 | ); 182 | } 183 | } 184 | ``` 185 | 186 | ### More To Come!! 187 | 188 | ---- 189 | 190 | ### Credit 191 | I was inspired by: https://github.com/milosjanda/react-scroll-up 192 | With the base recource of the repo i was able to create my own in my style with additional functionality. 193 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "video": false 3 | } 4 | -------------------------------------------------------------------------------- /cypress/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "env": { 4 | "es6": true, 5 | "browser": true 6 | }, 7 | "rules": { 8 | "no-unused-vars": 0, 9 | "semi": 0, 10 | "linebreak-style": 0, 11 | "no-undef": 0, 12 | "no-console": 0 13 | }, 14 | "plugins": [ 15 | "import" 16 | ] 17 | } -------------------------------------------------------------------------------- /cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } -------------------------------------------------------------------------------- /cypress/integration/scroll.spec.js: -------------------------------------------------------------------------------- 1 | beforeEach(() => { 2 | cy.visit('http://localhost:8000') 3 | }) 4 | describe('E2E testing', () => { 5 | it('Applies Transition class!', () => { 6 | cy.get('[data-testid="react-scroll-up-button"]') 7 | .should('have.class', 'ScrollUpButton__Container') 8 | cy.get('[data-testid="react-scroll-up-button"]') 9 | .should('not.have.class', 'ScrollUpButton__Toggled') 10 | cy.scrollTo('bottom') 11 | cy.get('[data-testid="react-scroll-up-button"]') 12 | .should('have.class', 'ScrollUpButton__Toggled') 13 | }) 14 | 15 | it('can be clicked!', () => { 16 | cy.scrollTo('bottom') 17 | cy.get('[data-testid="react-scroll-up-button"]') 18 | .should('have.class', 'ScrollUpButton__Toggled') 19 | cy.get('[data-testid="react-scroll-up-button"]').click() 20 | cy.get('[data-testid="react-scroll-up-button"]') 21 | .should('not.have.class', 'ScrollUpButton__Toggled') 22 | cy.window().its('pageYOffset').should('eq', 0) 23 | }) 24 | 25 | it('Transitions to the top after clicking', () => { 26 | cy.window().its('pageYOffset').should('eq', 0) 27 | cy.scrollTo('bottom') 28 | cy.window().its('pageYOffset').should('not.eq', 0) 29 | cy.get('[data-testid="react-scroll-up-button"]').click() 30 | cy.window().its('pageYOffset').should('eq', 0) 31 | }) 32 | 33 | it('should be visible', () => { 34 | cy.get('[data-testid="react-scroll-up-button"]') 35 | .should('not.be.visible') 36 | cy.scrollTo('bottom') 37 | cy.get('[data-testid="react-scroll-up-button"]') 38 | .should('be.visible') 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example plugins/index.js can be used to load plugins 3 | // 4 | // You can change the location of this file or turn off loading 5 | // the plugins file with the 'pluginsFile' configuration option. 6 | // 7 | // You can read more here: 8 | // https://on.cypress.io/plugins-guide 9 | // *********************************************************** 10 | 11 | // This function is called when a project is opened or re-opened (e.g. due to 12 | // the project's config changing) 13 | 14 | module.exports = (on, config) => { 15 | // `on` is used to hook into various events Cypress emits 16 | // `config` is the resolved Cypress config 17 | } 18 | -------------------------------------------------------------------------------- /cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add("login", (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This is will overwrite an existing command -- 25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 26 | -------------------------------------------------------------------------------- /cypress/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Project dependencies 2 | .cache 3 | node_modules 4 | yarn-error.log 5 | 6 | # Build directory 7 | /public 8 | .DS_Store 9 | -------------------------------------------------------------------------------- /example/gatsby-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | siteMetadata: { 3 | title: 'react-scroll-up-button', 4 | }, 5 | plugins: [ 6 | 'gatsby-plugin-styled-components', 7 | ], 8 | } 9 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gatsby-starter-default", 3 | "description": "Gatsby default starter", 4 | "version": "1.0.0", 5 | "author": "Kyle Mathews ", 6 | "dependencies": { 7 | "babel-plugin-styled-components": "^1.8.0", 8 | "gatsby": "^2.0.0", 9 | "gatsby-plugin-styled-components": "^3.0.4", 10 | "react": "^16.5.1", 11 | "react-dom": "^16.5.1", 12 | "react-numeric-input": "^2.2.3", 13 | "react-select": "^2.1.2", 14 | "react-syntax-highlighter": "^10.1.1", 15 | "styled-components": "^4.1.2", 16 | "tween-functions": "^1.2.0" 17 | }, 18 | "keywords": [ 19 | "gatsby" 20 | ], 21 | "license": "MIT", 22 | "scripts": { 23 | "build": "gatsby build", 24 | "develop": "gatsby develop" 25 | }, 26 | "devDependencies": {}, 27 | "repository": { 28 | "type": "git", 29 | "url": "https://github.com/gatsbyjs/gatsby-starter-default" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /example/src/pages/CustomButton.css: -------------------------------------------------------------------------------- 1 | .AnyClassForContainer { 2 | position: fixed; 3 | right: -100px; 4 | bottom: 150px; 5 | transition: right 0.5s; 6 | cursor: pointer; 7 | background-color: white; 8 | font-size: 20px; 9 | padding: 10px; 10 | } 11 | 12 | .AnyClassForTransition { 13 | right: 20px; 14 | } 15 | -------------------------------------------------------------------------------- /example/src/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled, { createGlobalStyle } from 'styled-components' // eslint-disable-line 3 | import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; // eslint-disable-line 4 | import { darcula as codeBlockStyle } from 'react-syntax-highlighter/dist/esm/styles/prism'; // eslint-disable-line 5 | import TweenFunctions from 'tween-functions'; 6 | import Select from 'react-select'; // eslint-disable-line 7 | import NumericInput from 'react-numeric-input'; // eslint-disable-line 8 | import './normalize.css' 9 | import './CustomButton.css' 10 | 11 | let ScrollUpButtons = [] 12 | const TweenOptions = Object.keys(TweenFunctions).map(tween => ({ value: tween, label: tween })) 13 | 14 | if (process.env.NODE_ENV === 'development') { 15 | ScrollUpButtons = require('../../../src/react-scroll-up-button') // eslint-disable-line 16 | } else { 17 | ScrollUpButtons = require('../../../dist/cjs/react-scroll-up-button') // eslint-disable-line 18 | } 19 | 20 | const CustomScrollUp = props => ( 21 | 26 | Custom 27 | ) 28 | 29 | ScrollUpButtons.Custom = CustomScrollUp 30 | 31 | export default class Example extends React.Component { 32 | constructor() { 33 | super(); 34 | const { defaultProps } = ScrollUpButtons.default 35 | this.state = { 36 | currentBtn: 'default', 37 | EasingType: TweenOptions.filter(tween => tween.label === defaultProps.EasingType)[0], 38 | ShowAtPosition: defaultProps.ShowAtPosition, 39 | StopPosition: defaultProps.StopPosition, 40 | AnimationDuration: defaultProps.AnimationDuration, 41 | ShowAtPositionMaxValue: 3000, 42 | } 43 | this.EasingTypeOnChange = this.EasingTypeOnChange.bind(this) 44 | this.StopPositionOnChange = this.StopPositionOnChange.bind(this) 45 | this.ShowAtPositionOnChange = this.ShowAtPositionOnChange.bind(this) 46 | this.AnimationDurationOnChange = this.AnimationDurationOnChange.bind(this) 47 | } 48 | 49 | componentDidMount() { 50 | this.setState({ ShowAtPositionMaxValue: document.documentElement.offsetHeight - window.innerHeight - 1 }) 51 | } 52 | 53 | EasingTypeOnChange(EasingType) { 54 | this.setState({ EasingType }) 55 | } 56 | 57 | StopPositionOnChange(StopPosition) { 58 | this.setState({ StopPosition }) 59 | } 60 | 61 | ShowAtPositionOnChange(ShowAtPosition) { 62 | const { StopPosition } = this.state 63 | const max = document.documentElement.offsetHeight - window.innerHeight - 1 64 | const min = StopPosition + 1 65 | if (ShowAtPosition > max) { 66 | this.setState({ ShowAtPosition: max }) 67 | return 68 | } 69 | if (ShowAtPosition < min) { 70 | this.setState({ ShowAtPosition: min }) 71 | return 72 | } 73 | this.setState({ ShowAtPosition }) 74 | } 75 | 76 | AnimationDurationOnChange(AnimationDuration) { 77 | this.setState({ AnimationDuration }) 78 | } 79 | 80 | handleClick(btn) { 81 | this.setState({ currentBtn: btn }); 82 | } 83 | 84 | render() { 85 | const { 86 | currentBtn, 87 | EasingType, 88 | ShowAtPosition, 89 | StopPosition, 90 | AnimationDuration, 91 | ShowAtPositionMaxValue, 92 | } = this.state 93 | const { defaultProps } = ScrollUpButtons.default 94 | const EasingTypeProp = defaultProps.EasingType !== EasingType.value && `EasingType="${EasingType.value}"` 95 | const StopPositionProp = defaultProps.StopPosition !== StopPosition && `StopPosition={${StopPosition}}` 96 | const ShowAtPositionProp = defaultProps.ShowAtPosition !== ShowAtPosition && `ShowAtPosition={${ShowAtPosition}}` 97 | const AnimationDurationProp = defaultProps.AnimationDuration !== AnimationDuration && `AnimationDuration={${AnimationDuration}}` 98 | 99 | let ExampleProps = [ 100 | EasingTypeProp, 101 | StopPositionProp, 102 | ShowAtPositionProp, 103 | AnimationDurationProp, 104 | ] 105 | ExampleProps = ExampleProps.filter(prop => prop) 106 | 107 | return ( 108 | 109 | 110 | {Object.keys(ScrollUpButtons).map((btn) => { 111 | const CurBtn = ScrollUpButtons[btn] 112 | if (currentBtn === btn) { 113 | return ( 114 | 121 | ) 122 | } 123 | return null 124 | })} 125 |
126 |
127 | <a href="https://github.com/dirtyredz/react-scroll-up-button">react-scroll-up-button</a> 128 |
129 |
130 | 131 | An off-canvas button React component with pre styled buttons allowing "Scroll To Top" functionality. 132 | 133 |

134 | Inspired by 135 |  react-scroll-up 136 |  by milosjand, and 137 |  react-burger-menu 138 |  by negomi. 139 |

140 |
141 | {Object.keys(ScrollUpButtons).map(btn => ( 142 | 150 | ))} 151 |
152 |
153 | 154 | EasingType: 155 |
156 | 166 |
167 | StopPosition: 168 |
169 | 177 |
178 | 179 | ShowAtPosition: 180 |
181 | 189 |
190 | AnimationDuration: 191 |
192 | 199 |
200 |
201 |
202 | Scroll Down 203 |

Too see the component

204 | 205 | {Object.keys(ScrollUpButtons).map((btn) => { 206 | const str = `import React from "react"; 207 | import ${btn === 'default' || btn === 'Custom' 208 | ? 'ScrollUpButton' 209 | : `{${btn} as ScrollUpButton}`} from "react-scroll-up-button";${btn === 'Custom' 210 | ? ` 211 | import ./myCssFile.css;` 212 | : ''} 213 | 214 | export default class Index extends React.Component { 215 | render() { 216 | return ( 217 |
218 | ${btn === 'Custom' ? ` 0 221 | ? ` 222 | ${ExampleProps.map(prop => prop).join(` 223 | `)}}` 224 | : ''} 225 | > 226 | 227 | ` 228 | : ExampleProps.length > 0 ? ` prop).join(` 230 | `)} 231 | />` : '' 232 | 233 | } 234 |
235 | ); 236 | } 237 | }` 238 | 239 | const CustomCss = `.AnyClassForContainer { 240 | position: fixed; 241 | right: -100px; 242 | bottom: 150px; 243 | transition: right 0.5s; 244 | cursor: pointer; 245 | background-color: white; 246 | font-size: 20px; 247 | padding: 10px; 248 | } 249 | 250 | .AnyClassForTransition { 251 | right: 20px; 252 | }` 253 | if (currentBtn === btn) { 254 | if (btn === 'Custom') { 255 | return ( 256 |
257 | {str} 258 | {CustomCss} 259 |
260 | ) 261 | } 262 | return {str} 263 | } 264 | return null 265 | })} 266 |
267 |
268 | ); 269 | } 270 | } 271 | 272 | const Wrapper = styled.div` 273 | background: #b30000; 274 | width: 100%; 275 | height: 2000px; 276 | text-align: center; 277 | min-width: 300px; 278 | 279 | & a{ 280 | color: #f5f5f5; 281 | text-decoration: none; 282 | 283 | &:visited{ 284 | color: #f5f5f5; 285 | } 286 | &:hover{ 287 | color: #8a8a8a; 288 | } 289 | } 290 | ` 291 | const Button = styled.button` 292 | display: inline-block; 293 | margin: 0.75em; 294 | padding: 1.35em 1.1em; 295 | background: #252525; 296 | color: white; 297 | text-transform: uppercase; 298 | letter-spacing: 1px; 299 | font-weight: 800; 300 | border-top-left-radius: 20px 50px; 301 | border-top-right-radius: 20px 50px; 302 | border-bottom-right-radius: 20px 50px; 303 | border-bottom-left-radius: 20px 50px; 304 | cursor: pointer; 305 | border: none; 306 | 307 | &.selected{ 308 | opacity: 0.6; 309 | } 310 | &:not(.selected):hover, &:not(.selected):focus{ 311 | color: black; 312 | } 313 | ` 314 | 315 | const Title = styled.h1` 316 | margin: 0; 317 | font-size: 4em; 318 | ` 319 | 320 | const Description = styled.h2` 321 | max-width: 500px; 322 | margin: 1.2em auto 1em; 323 | ` 324 | 325 | const ScrollDown = styled.h2` 326 | color: #00c1b1; 327 | ` 328 | 329 | const CodeBlock = styled.div` 330 | max-width: 750px; 331 | margin: auto; 332 | ` 333 | const Form = styled.div` 334 | text-align: left; 335 | width: 225px; 336 | margin: auto; 337 | display: flex; 338 | justify-content: center; 339 | ` 340 | 341 | const FormChild = styled.div` 342 | padding: 20px; 343 | ` 344 | 345 | const Dropdown = styled(Select)` 346 | width: 229px; 347 | display: inline-block; 348 | ` 349 | const MyLabel = styled.label` 350 | display: inline-block; 351 | padding-top: 10px; 352 | padding-bottom: 5px; 353 | padding-left: 6px; 354 | ` 355 | 356 | const NumberInput = styled(NumericInput)` 357 | border-top-left-radius: 20px 50px; 358 | border-top-right-radius: 20px 50px; 359 | border-bottom-right-radius: 20px 50px; 360 | border-bottom-left-radius: 20px 50px; 361 | padding-right: 3ex; 362 | box-sizing: border-box; 363 | font-size: inherit; 364 | height: 38px; 365 | border: none; 366 | padding-left: 10px; 367 | display: block; 368 | -webkit-appearance: none; 369 | line-height: normal; 370 | background: #252525; 371 | color: white; 372 | ` 373 | 374 | const GlobalStyle = createGlobalStyle` 375 | .react-numeric-input { 376 | position: relative; 377 | display: inline-block; 378 | } 379 | .react-numeric-input b { 380 | position: absolute; 381 | right: 8px; 382 | width: 2.26ex; 383 | border-color: rgba(0, 0, 0, 0.1); 384 | border-style: solid; 385 | text-align: center; 386 | cursor: default; 387 | transition: all 0.1s ease 0s; 388 | background: rgba(247, 241, 241, 0.1); 389 | box-shadow: rgba(0,0,0,0.1) -1px -1px 3px inset, rgba(255,255,255,0.7) 1px 1px 3px inset; 390 | top: 2px; 391 | bottom: 50%; 392 | border-radius: 2px 2px 0px 0px; 393 | border-width: 1px 1px 0px; 394 | } 395 | .react-numeric-input b:last-child { 396 | top: 50%; 397 | bottom: 2px; 398 | border-radius: 0px 0px 2px 2px; 399 | border-width: 0px 1px 1px 400 | } 401 | .react-numeric-input i { 402 | position: absolute; 403 | top: 50%; 404 | left: 50%; 405 | width: 0px; 406 | height: 0px; 407 | border-width: 0px 0.6ex 0.6ex; 408 | border-color: transparent transparent rgba(241, 241, 241, 0.58); 409 | border-style: solid; 410 | margin: -0.3ex 0px 0px -0.56ex; 411 | } 412 | .react-numeric-input b:last-child > i { 413 | border-width: 0.6ex 0.6ex 0px; 414 | border-color: rgba(241, 241, 241, 0.58) transparent transparent; 415 | } 416 | 417 | .select > .select__control { 418 | background-color: #252525; 419 | border-color: #252525; 420 | border-top-left-radius: 20px 50px; 421 | border-top-right-radius: 20px 50px; 422 | border-bottom-right-radius: 20px 50px; 423 | border-bottom-left-radius: 20px 50px; 424 | } 425 | .select .select__single-value { 426 | color: white; 427 | } 428 | ` 429 | -------------------------------------------------------------------------------- /example/src/pages/normalize.css: -------------------------------------------------------------------------------- 1 | article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block;}audio,canvas,video{display:inline-block;}audio:not([controls]){display:none;height:0;}[hidden]{display:none;}html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;}body{margin:0;}a:focus{outline:thin dotted;}a:active,a:hover{outline:0;}h1{font-size:2em;margin:0.67em 0;}abbr[title]{border-bottom:1px dotted;}b,strong{font-weight:bold;}dfn{font-style:italic;}hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0;}mark{background:#ff0;color:#000;}code,kbd,pre,samp{font-family:monospace,serif;font-size:1em;}pre{white-space:pre-wrap;}q{quotes:"\201C" "\201D" "\2018" "\2019";}small{font-size:80%;}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline;}sup{top:-0.5em;}sub{bottom:-0.25em;}img{border:0;}svg:not(:root){overflow:hidden;}figure{margin:0;}fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:0.35em 0.625em 0.75em;}legend{border:0;padding:0;}button,input,select,textarea{font-family:inherit;font-size:100%;margin:0;}button,input{line-height:normal;}button,select{text-transform:none;}button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer;}button[disabled],html input[disabled]{cursor:default;}input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0;}input[type="search"]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box;}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none;}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0;}textarea{overflow:auto;vertical-align:top;}table{border-collapse:collapse;border-spacing:0;} -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | if (process.env.NODE_ENV === "production") { 4 | module.exports = require("./dist/cjs/react-scroll-up-button.min.js"); 5 | } else { 6 | module.exports = require("./dist/cjs/react-scroll-up-button.js"); 7 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-scroll-up-button", 3 | "version": "1.6.4", 4 | "main": "index.js", 5 | "module": "dist/esm/react-scroll-up-button.js", 6 | "sideEffects": false, 7 | "description": "React component Scroll up button, fixed button to scroll up to desired position", 8 | "files": [ 9 | "react-scroll-up-button.js", 10 | "cjs", 11 | "es", 12 | "esm", 13 | "index.js" 14 | ], 15 | "scripts": { 16 | "compile": "npx del-cli dist && rollup -c", 17 | "example:install": "cd example && npm install", 18 | "example:build": "npm run compile && npm run example:install && cd example && npm run build", 19 | "example:develop": "npm run example:install && cd example && npm run develop", 20 | "cypress:open": "cypress open", 21 | "cypress:run": "npx cypress run", 22 | "dev:server": "npm run example:develop", 23 | "dev:cypress": "start-server-and-test dev:server http://localhost:8000 cypress:open", 24 | "dev:test": "npx jest --no-cache --coverage --watch", 25 | "test": "npx jest --coverage --coverageReporters=text-lcov | coveralls", 26 | "test:server": "npm run compile && npx webpack-dev-server --config ./tests/server/webpack.config.js", 27 | "test:cypress": "start-server-and-test test:server http://localhost:8000 cypress:run", 28 | "prepublishOnly": "npm run compile", 29 | "eslint:example": "eslint example/src", 30 | "eslint:cypress": "eslint cypress", 31 | "eslint:tests": "eslint tests", 32 | "eslint:src": "eslint src", 33 | "eslint": "npm run compile && npm run eslint:src && npm run eslint:cypress && npm run eslint:tests && npm run eslint:example" 34 | }, 35 | "jest": { 36 | "verbose": true, 37 | "testRegex": "(/tests/specs/.*)", 38 | "globals": { 39 | "window": true 40 | }, 41 | "coverageThreshold": { 42 | "global": { 43 | "branches": 80, 44 | "functions": 80, 45 | "lines": 80, 46 | "statements": -10 47 | } 48 | }, 49 | "collectCoverageFrom": [ 50 | "src/*.{js,jsx}" 51 | ] 52 | }, 53 | "author": "Dirtyredz | David McCLain ", 54 | "license": "MIT", 55 | "repository": { 56 | "type": "git", 57 | "url": "https://github.com/dirtyredz/react-scroll-up-button.git" 58 | }, 59 | "keywords": [ 60 | "scroll", 61 | "scrollUp", 62 | "scrollToTop", 63 | "animation", 64 | "effects", 65 | "react", 66 | "react-component" 67 | ], 68 | "devDependencies": { 69 | "@babel/core": "7.1.6", 70 | "@babel/plugin-transform-runtime": "7.1.0", 71 | "@babel/preset-env": "7.1.6", 72 | "@babel/preset-react": "7.0.0", 73 | "babel-core": "7.0.0-bridge.0", 74 | "babel-eslint": "10.0.1", 75 | "babel-jest": "23.6.0", 76 | "babel-loader": "8.0.4", 77 | "coveralls": "^3.0.2", 78 | "del-cli": "1.1.0", 79 | "eslint": "5.9.0", 80 | "eslint-config-airbnb": "^17.1.0", 81 | "eslint-plugin-import": "^2.14.0", 82 | "eslint-plugin-jsx-a11y": "6.1.2", 83 | "eslint-plugin-prettier": "3.0.0", 84 | "eslint-plugin-react": "^7.11.1", 85 | "jest": "^23.6.0", 86 | "jest-dom": "2.1.1", 87 | "react": "16.6.3", 88 | "react-dom": "16.6.3", 89 | "react-testing-library": "5.3.0", 90 | "rollup": "0.67.3", 91 | "rollup-plugin-babel": "4.0.3", 92 | "rollup-plugin-commonjs": "9.2.0", 93 | "rollup-plugin-node-resolve": "3.4.0", 94 | "rollup-plugin-replace": "2.1.0", 95 | "rollup-plugin-size-snapshot": "0.7.0", 96 | "rollup-plugin-terser": "3.0.0", 97 | "start-server-and-test": "1.7.11", 98 | "webpack": "4.26.1", 99 | "webpack-cli": "3.1.2", 100 | "webpack-dev-server": ">=3.1.11" 101 | }, 102 | "peerDependencies": { 103 | "react": "^15.5.4 || ^16.0.0" 104 | }, 105 | "dependencies": { 106 | "@babel/runtime": "7.1.5", 107 | "cypress": "^3.1.3", 108 | "detect-passive-events": "^1.0.4", 109 | "prop-types": "^15.6.2", 110 | "tween-functions": "^1.2.0" 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel'; 2 | import replace from 'rollup-plugin-replace'; 3 | import { sizeSnapshot } from 'rollup-plugin-size-snapshot'; 4 | import { terser } from 'rollup-plugin-terser'; 5 | 6 | import pkg from './package.json'; 7 | 8 | const input = 'src/react-scroll-up-button.js'; 9 | 10 | const babelOptionsCJS = { 11 | exclude: /node_modules/, 12 | }; 13 | 14 | const babelOptionsESM = { 15 | exclude: /node_modules/, 16 | runtimeHelpers: true, 17 | plugins: [['@babel/transform-runtime', { useESModules: true }]], 18 | }; 19 | 20 | const external = id => !id.startsWith('.') && !id.startsWith('/'); 21 | 22 | export default [ 23 | { 24 | input, 25 | output: { file: `dist/cjs/${pkg.name}.js`, format: 'cjs' }, 26 | external, 27 | plugins: [ 28 | babel(babelOptionsCJS), 29 | replace({ 'process.env.NODE_ENV': JSON.stringify('development') }), 30 | ], 31 | }, 32 | 33 | { 34 | input, 35 | output: { file: `dist/cjs/${pkg.name}.min.js`, format: 'cjs' }, 36 | external, 37 | plugins: [ 38 | babel(babelOptionsCJS), 39 | replace({ 'process.env.NODE_ENV': JSON.stringify('production') }), 40 | terser(), 41 | ], 42 | }, 43 | 44 | { 45 | input, 46 | output: { file: `dist/esm/${pkg.name}.js`, format: 'esm' }, 47 | external, 48 | plugins: [babel(babelOptionsESM), sizeSnapshot()], 49 | }, 50 | ]; 51 | -------------------------------------------------------------------------------- /src/react-scroll-up-button.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TweenFunctions from 'tween-functions'; 3 | import PropTypes from 'prop-types'; 4 | import detectPassiveEvents from 'detect-passive-events'; 5 | 6 | class ScrollUpButton extends React.Component { 7 | constructor(props) { 8 | super(props) 9 | this.state = { ToggleScrollUp: '' }; 10 | this.Animation = { 11 | StartPosition: 0, 12 | CurrentAnimationTime: 0, 13 | StartTime: null, 14 | AnimationFrame: null, 15 | } 16 | this.HandleScroll = this.HandleScroll.bind(this) 17 | this.StopScrollingFrame = this.StopScrollingFrame.bind(this) 18 | this.ScrollingFrame = this.ScrollingFrame.bind(this) 19 | this.HandleClick = this.HandleClick.bind(this) 20 | } 21 | 22 | componentDidMount() { 23 | this.HandleScroll(); // run HandleScroll() at mount incase we are already scrolled down 24 | window.addEventListener('scroll', this.HandleScroll); 25 | window.addEventListener( 26 | 'wheel', 27 | this.StopScrollingFrame, 28 | detectPassiveEvents.hasSupport 29 | ? { passive: true } 30 | : false 31 | ); // Stop animation if user mouse wheels during animation. 32 | window.addEventListener( 33 | 'touchstart', 34 | this.StopScrollingFrame, 35 | detectPassiveEvents.hasSupport 36 | ? { passive: true } 37 | : false 38 | ); // Stop animation if user touches the screen during animation. 39 | } 40 | 41 | componentWillUnmount() { 42 | // Remove all events, since component is no longer mounted. 43 | window.removeEventListener('scroll', this.HandleScroll); 44 | window.removeEventListener('wheel', this.StopScrollingFrame, false); 45 | window.removeEventListener('touchstart', this.StopScrollingFrame, false); 46 | } 47 | 48 | HandleScroll() { 49 | const { ShowAtPosition, TransitionClassName } = this.props 50 | // window.pageYOffset = current scroll position 51 | // ShowAtPosition = position at which we want the button to show. 52 | if (window.pageYOffset > ShowAtPosition) { 53 | // styles.Toggled = the class name we want applied to transition the button in. 54 | this.setState({ ToggleScrollUp: TransitionClassName }); 55 | } else { 56 | // remove the class name 57 | this.setState({ ToggleScrollUp: '' }); 58 | } 59 | } 60 | 61 | HandleClick() { 62 | // Is this needed? 63 | // const { ShowAtPosition } = this.props 64 | // // For some reason the user was able to click the button. 65 | // if (window.pageYOffset < ShowAtPosition) { 66 | // event.preventDefault() 67 | // this.HandleScroll() 68 | // } 69 | // Scroll to StopPosition 70 | this.StopScrollingFrame();// Stoping all AnimationFrames 71 | this.Animation.StartPosition = window.pageYOffset;// current scroll position 72 | this.Animation.CurrentAnimationTime = 0; 73 | this.Animation.StartTime = null; 74 | // Start the scrolling animation. 75 | this.Animation.AnimationFrame = window.requestAnimationFrame(this.ScrollingFrame); 76 | } 77 | 78 | ScrollingFrame() { 79 | const { StopPosition, EasingType, AnimationDuration } = this.props 80 | const timestamp = Math.floor(Date.now()); 81 | // If StartTime has not been assigned a value, assign it the start timestamp. 82 | if (!this.Animation.StartTime) { 83 | this.Animation.StartTime = timestamp; 84 | } 85 | 86 | // set CurrentAnimationTime every iteration of ScrollingFrame() 87 | this.Animation.CurrentAnimationTime = timestamp - this.Animation.StartTime; 88 | // if we hit the StopPosition, StopScrollingFrame() 89 | if (window.pageYOffset <= StopPosition) { 90 | this.StopScrollingFrame(); 91 | } else { 92 | // Otherwise continue ScrollingFrame to the StopPosition. 93 | // Does not support horizontal ScrollingFrame. 94 | // Let TweenFunctions handle the math to give us a new position based on AnimationDuration and EasingType type 95 | let YPos = TweenFunctions[EasingType]( 96 | this.Animation.CurrentAnimationTime, 97 | this.Animation.StartPosition, 98 | StopPosition, 99 | AnimationDuration 100 | ); 101 | if (YPos <= StopPosition) { 102 | YPos = StopPosition 103 | } 104 | window.scrollTo(0, YPos); 105 | // Request another frame to be painted 106 | this.Animation.AnimationFrame = window.requestAnimationFrame(this.ScrollingFrame); 107 | } 108 | } 109 | 110 | StopScrollingFrame() { 111 | // Stop the Animation Frames. 112 | window.cancelAnimationFrame(this.Animation.AnimationFrame); 113 | } 114 | 115 | render() { 116 | const styles = { 117 | MainStyle: { 118 | backgroundColor: 'rgba(50, 50, 50, 0.5)', 119 | height: 50, 120 | position: 'fixed', 121 | bottom: 20, 122 | width: 50, 123 | WebkitTransition: 'all 0.5s ease-in-out', 124 | transition: 'all 0.5s ease-in-out', 125 | transitionProperty: 'opacity, right', 126 | cursor: 'pointer', 127 | opacity: 0, 128 | right: -50, 129 | zIndex: 1000, 130 | }, 131 | SvgStyle: { 132 | display: 'inline-block', 133 | width: '100%', 134 | height: '100%', 135 | strokeWidth: 0, 136 | stroke: 'white', 137 | fill: 'white', 138 | }, 139 | ToggledStyle: { 140 | opacity: 1, 141 | right: 20, 142 | }, 143 | } 144 | const { 145 | children, 146 | style, 147 | ToggledStyle, 148 | ContainerClassName, 149 | } = this.props 150 | const { ToggleScrollUp } = this.state 151 | if (children) { 152 | const childrenWithProps = React.Children.map(children, 153 | child => React.cloneElement(child, { 154 | className: this.className, 155 | })); 156 | return ( 157 | 173 | ); 174 | } 175 | return ( 176 | 208 | ); 209 | } 210 | } 211 | export default ScrollUpButton 212 | 213 | export const TinyButton = (props) => { 214 | const styles = { 215 | MainStyle: { 216 | backgroundColor: 'rgb(87, 86, 86)', 217 | height: 30, 218 | position: 'fixed', 219 | bottom: 20, 220 | width: 30, 221 | WebkitTransition: 'all 0.5s ease-in-out', 222 | transition: 'all 0.5s ease-in-out', 223 | transitionProperty: 'opacity, right', 224 | cursor: 'pointer', 225 | opacity: 0, 226 | right: -75, 227 | zIndex: 1000, 228 | fill: '#292929', 229 | paddingBottom: 1, 230 | paddingLeft: 1, 231 | paddingRight: 1, 232 | }, 233 | ToggledStyle: { 234 | opacity: 1, 235 | right: 30, 236 | }, 237 | } 238 | const { style, ToggledStyle } = props 239 | return ( 240 | 251 | 259 | 262 | 263 | 264 | ); 265 | } 266 | 267 | export const CircleArrow = (props) => { 268 | const styles = { 269 | MainStyle: { 270 | backgroundColor: 'rgb(255, 255, 255)', 271 | borderRadius: '50%', 272 | border: '5px solid black', 273 | height: 50, 274 | position: 'fixed', 275 | bottom: 20, 276 | width: 50, 277 | WebkitTransition: 'all 0.5s ease-in-out', 278 | transition: 'all 0.5s ease-in-out', 279 | transitionProperty: 'opacity, right', 280 | cursor: 'pointer', 281 | opacity: 0, 282 | right: -75, 283 | }, 284 | ToggledStyle: { 285 | opacity: 1, 286 | right: 20, 287 | }, 288 | } 289 | const { style, ToggledStyle } = props 290 | return ( 291 | 302 | 303 | 306 | 307 | 308 | ); 309 | } 310 | 311 | export const VerticleButton = (props) => { 312 | const styles = { 313 | MainStyle: { 314 | backgroundColor: 'rgb(58, 56, 56)', 315 | position: 'fixed', 316 | bottom: 40, 317 | padding: '5px 10px', 318 | WebkitTransition: 'all 0.5s ease-in-out', 319 | transition: 'all 0.5s ease-in-out', 320 | transitionProperty: 'opacity, right', 321 | cursor: 'pointer', 322 | opacity: 0, 323 | right: -75, 324 | transform: 'rotate(-90deg)', 325 | }, 326 | ToggledStyle: { 327 | opacity: 1, 328 | right: 10, 329 | }, 330 | } 331 | const { style, ToggledStyle } = props 332 | return ( 333 | 344 | UP → 345 | 346 | ); 347 | } 348 | 349 | ScrollUpButton.defaultProps = { 350 | ContainerClassName: 'ScrollUpButton__Container', 351 | StopPosition: 0, 352 | ShowAtPosition: 150, 353 | EasingType: 'easeOutCubic', 354 | AnimationDuration: 500, 355 | TransitionClassName: 'ScrollUpButton__Toggled', 356 | style: {}, 357 | ToggledStyle: {}, 358 | children: null, 359 | } 360 | 361 | function LessThanShowAtPosition(props, propName, componentName) { 362 | const { ShowAtPosition } = props; 363 | if (props[propName]) { // eslint-disable-line 364 | const value = props[propName]; 365 | if (typeof value === 'number') { 366 | if (value >= ShowAtPosition) { // Validate the incoming prop value againt the ShowAtPosition prop 367 | return new Error(`${propName} (${value}) in ${componentName} must be less then prop: ShowAtPosition (${ShowAtPosition})`); 368 | } 369 | return null 370 | } 371 | return new Error(`${propName} in ${componentName} must be a number.`); 372 | } 373 | return null; 374 | } 375 | 376 | ScrollUpButton.propTypes = { 377 | children: PropTypes.oneOfType([ 378 | PropTypes.arrayOf(PropTypes.node), 379 | PropTypes.node, 380 | ]), 381 | StopPosition: LessThanShowAtPosition, 382 | ShowAtPosition: PropTypes.number, // show button under this position, 383 | EasingType: PropTypes.oneOf(['linear', 'easeInQuad', 'easeOutQuad', 'easeInOutQuad', 'easeInCubic', 384 | 'easeOutCubic', 'easeInOutCubic', 'easeInQuart', 'easeOutQuart', 'easeInOutQuart', 'easeInQuint', 385 | 'easeOutQuint', 'easeInOutQuint', 'easeInSine', 'easeOutSine', 'easeInOutSine', 'easeInExpo', 'easeOutExpo', 386 | 'easeInOutExpo', 'easeInCirc', 'easeOutCirc', 'easeInOutCirc', 'easeInElastic', 'easeOutElastic', 387 | 'easeInOutElastic', 'easeInBack', 'easeOutBack', 'easeInOutBack', 'easeInBounce', 'easeOutBounce', 388 | 'easeInOutBounce']), 389 | AnimationDuration: PropTypes.number, // seconds 390 | style: PropTypes.object, // eslint-disable-line react/forbid-prop-types 391 | ToggledStyle: PropTypes.object, // eslint-disable-line react/forbid-prop-types 392 | ContainerClassName: PropTypes.string, 393 | TransitionClassName: PropTypes.string, 394 | } 395 | -------------------------------------------------------------------------------- /tests/server/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | React-Scroll-Up-Button 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /tests/server/testing.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import ScrollUpButton from '../../dist/esm/react-scroll-up-button'; 4 | 5 | const rootEl = document.getElementById('ReactRoot'); 6 | ReactDOM.render( 7 |
8 | 9 |
, 10 | rootEl 11 | ); 12 | -------------------------------------------------------------------------------- /tests/server/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | 3 | module.exports = { 4 | mode: 'production', 5 | module: { 6 | rules: [ 7 | { 8 | test: /\.js$/, 9 | exclude: /node_modules/, 10 | use: { 11 | loader: 'babel-loader', 12 | }, 13 | }, 14 | ], 15 | }, 16 | entry: { 17 | index: './tests/server/testing', 18 | }, 19 | output: { 20 | path: __dirname, 21 | filename: 'react.js', 22 | }, 23 | plugins: [ 24 | new webpack.HotModuleReplacementPlugin(), 25 | ], 26 | devServer: { 27 | hot: true, 28 | contentBase: __dirname, 29 | port: 8000, 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /tests/specs/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "env": { 4 | "es6": true, 5 | "browser": true 6 | }, 7 | "rules": { 8 | "no-unused-vars": 0, 9 | "semi": 0, 10 | "linebreak-style": 0, 11 | "no-undef": 0, 12 | "react/jsx-filename-extension": 0, 13 | "no-console": 0 14 | }, 15 | "plugins": [ 16 | "react", 17 | "import" 18 | ], 19 | "parserOptions": { 20 | "ecmaFeatures": { 21 | "jsx": true 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /tests/specs/custom prototype.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | render, 4 | cleanup, 5 | } from 'react-testing-library' 6 | import 'jest-dom/extend-expect' 7 | import detectPassiveEvents from 'detect-passive-events' 8 | import ScrollUpButton from '../../src/react-scroll-up-button' 9 | 10 | jest.mock('detect-passive-events') 11 | 12 | afterEach(() => { 13 | cleanup() 14 | }) 15 | 16 | describe('custom proptype validators', () => { 17 | console.error = (err) => { throw new Error(err); }; 18 | console.warn = (warning) => { throw new Error(warning); } 19 | 20 | test('LessThanShowAtPosition thows when StopPosition is not a number.', () => { 21 | expect(() => { 22 | render() 23 | }).toThrow() 24 | }) 25 | 26 | test('LessThanShowAtPosition throws when StopPosition is greater than ShowAtPosition.', () => { 27 | expect(() => { 28 | render() 29 | }).toThrow() 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /tests/specs/named exports.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | render, 4 | cleanup, 5 | } from 'react-testing-library' 6 | import 'jest-dom/extend-expect' 7 | import detectPassiveEvents from 'detect-passive-events'; 8 | import { CircleArrow, VerticleButton, TinyButton } from '../../src/react-scroll-up-button'; 9 | 10 | jest.mock('detect-passive-events') 11 | 12 | afterEach(() => { 13 | cleanup() 14 | }) 15 | beforeEach(() => { 16 | window.pageYOffset = 0 17 | global.scrollTo = jest.fn((x, y) => { 18 | window.pageXOffset = x 19 | window.pageYOffset = y 20 | window.dispatchEvent(new window.UIEvent('scroll')); 21 | }) 22 | }) 23 | 24 | describe('Named exports render', () => { 25 | test('CircleArrow is rendered', async () => { 26 | const { 27 | container, 28 | getByTestId, 29 | } = render() 30 | const Aside = getByTestId('react-scroll-up-button') 31 | expect(container).toContainElement(Aside) 32 | }); 33 | test('VerticleButton is rendered', async () => { 34 | const { 35 | container, 36 | getByTestId, 37 | } = render() 38 | const Aside = getByTestId('react-scroll-up-button') 39 | expect(container).toContainElement(Aside) 40 | }); 41 | test('TinyButton is rendered', async () => { 42 | const { 43 | container, 44 | getByTestId, 45 | } = render() 46 | const Aside = getByTestId('react-scroll-up-button') 47 | expect(container).toContainElement(Aside) 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /tests/specs/pre-styled children.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | render, 4 | cleanup, 5 | fireEvent, 6 | } from 'react-testing-library' 7 | import 'jest-dom/extend-expect' 8 | import detectPassiveEvents from 'detect-passive-events'; 9 | import { CircleArrow } from '../../src/react-scroll-up-button'; 10 | 11 | jest.mock('detect-passive-events') 12 | 13 | afterEach(() => { 14 | cleanup() 15 | }) 16 | beforeEach(() => { 17 | window.pageYOffset = 0 18 | global.scrollTo = jest.fn((x, y) => { 19 | window.pageXOffset = x 20 | window.pageYOffset = y 21 | window.dispatchEvent(new window.UIEvent('scroll')); 22 | }) 23 | }) 24 | 25 | describe('unit/intergration testing on children', () => { 26 | test('loses transition class after scrolling up', async (done) => { 27 | const { 28 | getByTestId, 29 | } = render() 30 | const Aside = getByTestId('react-scroll-up-button') 31 | expect(Aside).not.toHaveClass('ScrollUpButton__Toggled') 32 | 33 | window.pageYOffset = 160 34 | window.dispatchEvent(new window.UIEvent('scroll')); 35 | 36 | expect(Aside).toHaveClass('ScrollUpButton__Toggled') 37 | fireEvent.click(Aside) 38 | setTimeout(() => { 39 | expect(Aside).not.toHaveClass('ScrollUpButton__Toggled') 40 | done(); 41 | }, 1000) 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /tests/specs/unit-intergation.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | render, 4 | cleanup, 5 | fireEvent, 6 | } from 'react-testing-library' 7 | import 'jest-dom/extend-expect' 8 | import detectPassiveEvents from 'detect-passive-events'; 9 | import ScrollUpButton from '../../src/react-scroll-up-button'; 10 | 11 | jest.mock('detect-passive-events') 12 | 13 | afterEach(() => { 14 | cleanup() 15 | }) 16 | beforeEach(() => { 17 | window.pageYOffset = 0 18 | global.scrollTo = jest.fn((x, y) => { 19 | window.pageXOffset = x 20 | window.pageYOffset = y 21 | window.dispatchEvent(new window.UIEvent('scroll')); 22 | }) 23 | }) 24 | expect.extend({ 25 | toBeWithinRange(received, floor, ceiling) { 26 | const pass = received >= floor && received <= ceiling; 27 | if (pass) { 28 | return { 29 | message: () => `expected ${received} not to be within range ${floor} - ${ceiling}`, 30 | pass: true, 31 | }; 32 | } 33 | return { 34 | message: () => `expected ${received} to be within range ${floor} - ${ceiling}`, 35 | pass: false, 36 | }; 37 | }, 38 | }); 39 | 40 | describe('React unit/intergration testing', () => { 41 | test('Aside to be rendered', async () => { 42 | const { 43 | container, 44 | getByTestId, 45 | } = render() 46 | const Aside = getByTestId('react-scroll-up-button') 47 | expect(container).toContainElement(Aside) 48 | }); 49 | 50 | test('mock detect-passive-events, branch testing', async () => { 51 | const { 52 | rerender, 53 | } = render() 54 | detectPassiveEvents.hasSupport = true 55 | rerender() 56 | }); 57 | 58 | test('transition class applied on scroll', async () => { 59 | const { 60 | getByTestId, 61 | } = render() 62 | const Aside = getByTestId('react-scroll-up-button') 63 | 64 | expect(Aside).toHaveClass('ScrollUpButton__Container') 65 | window.pageYOffset = 160 66 | window.dispatchEvent(new window.UIEvent('scroll')); 67 | 68 | expect(Aside).toHaveClass('ScrollUpButton__Toggled') 69 | }); 70 | 71 | test('loses transition class after scrolling up', async (done) => { 72 | const { 73 | getByTestId, 74 | } = render() 75 | const Aside = getByTestId('react-scroll-up-button') 76 | expect(Aside).not.toHaveClass('ScrollUpButton__Toggled') 77 | 78 | window.pageYOffset = 160 79 | window.dispatchEvent(new window.UIEvent('scroll')); 80 | 81 | expect(Aside).toHaveClass('ScrollUpButton__Toggled') 82 | fireEvent.click(Aside) 83 | setTimeout(() => { 84 | expect(Aside).not.toHaveClass('ScrollUpButton__Toggled') 85 | done(); 86 | }, 1000) 87 | }); 88 | 89 | test('stopPosition props stops scrolling at desired position', async (done) => { 90 | const { 91 | getByTestId, 92 | } = render() 93 | const Aside = getByTestId('react-scroll-up-button') 94 | window.pageYOffset = 300 95 | window.dispatchEvent(new window.UIEvent('scroll')); 96 | 97 | expect(Aside).toHaveClass('ScrollUpButton__Toggled') 98 | fireEvent.click(Aside) 99 | setTimeout(() => { 100 | expect(Aside).not.toHaveClass('ScrollUpButton__Toggled') 101 | expect(global.scrollTo).toHaveBeenLastCalledWith(0, 30) 102 | done(); 103 | }, 1000) 104 | }); 105 | 106 | test('can accept props', async () => { 107 | render() 117 | }); 118 | }); 119 | --------------------------------------------------------------------------------