├── .gitignore ├── .storybook └── config.js ├── LICENSE ├── README.md ├── _config.yml ├── demo ├── demo.dist.js ├── demo.js └── index.html ├── dist └── reactAwesomeScroll.js ├── index.js ├── package.json ├── src └── Scroll │ ├── Scroll.jsx │ └── styles.js ├── stories └── index.js ├── webpack.config.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .DS_Store 3 | 4 | node_modules/ 5 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { configure } from '@storybook/react'; 2 | 3 | function loadStories() { 4 | require('../stories/index.js'); 5 | // You can require as many stories as you need. 6 | } 7 | 8 | configure(loadStories, module); -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 BananaBobby 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 | # react-awesome-scroll 2 | 3 | [![npm](https://img.shields.io/badge/npm-react--awesome--scroll-brightgreen.svg?style=flat-square)](https://bananabobby.github.io/react-awesome-scroll/) 4 | [![npm version](https://img.shields.io/npm/v/react-awesome-scroll.svg?style=flat-square)](https://www.npmjs.com/package/react-awesome-scroll) 5 | 6 | - Custom styled scrollbar with exact native behavior. 7 | - Easily customisable 8 | - No external dependencies 9 | - Has 2 style presets out of the box: You can use it out of box or just with the styles needed for component to scroll content properly. (Or disable all of the styles and add them manually in your project stylesheet system). 10 | 11 | [**Demo**](https://bananabobby.github.io/react-awesome-scroll/demo/) 12 | 13 | ## Installation 14 | 15 | ### npm 16 | ```bash 17 | npm install react-awesome-scroll --save 18 | ``` 19 | 20 | ### yarn 21 | ```bash 22 | yarn add react-awesome-scroll 23 | ``` 24 | 25 | ## Usage 26 | 27 | ### Basic 28 | 29 | In order to use the component with it out-of-the-box design, you'll need to just call the component in your React app. 30 | You will also need to limit the height of its wrapper, so that the component can get its size limits. 31 | 32 | ```javascript 33 | import Scroll from 'react-awesome-scroll'; 34 | 35 | class CustomScroll extends Component { 36 | // Contains demo wrapper 37 | render() { 38 | return ( 39 |
40 | 41 | /* Any content here */ 42 | 43 |
44 | ); 45 | } 46 | } 47 | 48 | ``` 49 | 50 | ### Customised 51 | 52 | 53 | ```javascript 54 | import Scroll from 'react-awesome-scroll'; 55 | 56 | class CustomScroll extends Component { 57 | // Contains demo wrapper 58 | render() { 59 | return ( 60 |
61 | 71 | /* Any content here */ 72 | 73 |
74 | ); 75 | } 76 | } 77 | ``` 78 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /demo/demo.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | import Scroll from '../dist/reactAwesomeScroll'; 6 | 7 | class App extends React.PureComponent { 8 | render() { 9 | return ( 10 |
19 |
20 | 21 |
22 | But I must explain to you how all this mistaken idea of denouncing pleasure and praising pain was born and I will give you a complete account of the system, and expound the actual teachings of the great explorer of the truth, the master-builder of human happiness. No one rejects, dislikes, or avoids pleasure itself, because it is pleasure, but because those who do not know how to pursue pleasure rationally encounter consequences that are extremely painful. Nor again is there anyone who loves or pursues or desires to obtain pain of itself, because it is pain, but because occasionally circumstances occur in which toil and pain can procure him some great pleasure. To take a trivial example, which of us ever undertakes laborious physical exercise, except to obtain some advantage from it? But who has any right to find fault with a man who chooses to enjoy a pleasure that has no annoying consequences, or one who avoids a pain that produces no resultant pleasure? On the other hand, we denounce with righteous indignation and dislike men who are so beguiled and demoralized by the charms of pleasure of the moment, so blinded by desire, that they cannot foresee the pain and trouble that are bound to ensue; and equal blame belongs to those who fail in their duty through weakness of will, which is the same as saying through shrinking from toil and pain. These cases are perfectly simple and easy to distinguish. In a free hour, when our power of choice is untrammelled and when nothing prevents our being able to do what we like best, every pleasure is to be welcomed and every pain avoided. But in certain circumstances and owing to the claims of duty or the obligations of business it will frequently occur that 23 |
24 |
25 |
26 |
27 | ); 28 | } 29 | } 30 | 31 | ReactDOM.render(, document.getElementById('body')); -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | React Awesome Scroll 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /dist/reactAwesomeScroll.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("react")):"function"==typeof define&&define.amd?define(["react"],t):"object"==typeof exports?exports.ReactAwesomeScroll=t(require("react")):e.ReactAwesomeScroll=t(e.React)}(this,function(e){return function(e){function t(r){if(o[r])return o[r].exports;var n=o[r]={i:r,l:!1,exports:{}};return e[r].call(n.exports,n,n.exports,t),n.l=!0,n.exports}var o={};return t.m=e,t.c=o,t.d=function(e,o,r){t.o(e,o)||Object.defineProperty(e,o,{configurable:!1,enumerable:!0,get:r})},t.n=function(e){var o=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(o,"a",o),o},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="",t(t.s=0)}([function(e,t,o){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function n(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function i(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function a(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(t,"__esModule",{value:!0});var l=Object.assign||function(e){for(var t=1;t { 23 | const elScroll = this.elScroll; 24 | const elInner = this.elInner; 25 | const elRoot = this.elRoot; 26 | const ratio = (elRoot.offsetHeight / elInner.offsetHeight) ; 27 | 28 | this.setState({ 29 | hasScroll: ratio < 1, 30 | scrollHeight: ratio * elScroll.offsetHeight, 31 | heightRatio: elScroll.offsetHeight / elInner.offsetHeight, 32 | }); 33 | }; 34 | 35 | limitTrackPosition(position) { 36 | const elScroll = this.elScroll; 37 | const elBar = this.elBar; 38 | 39 | return Math.max(0, Math.min(position, elScroll.offsetHeight - elBar.offsetHeight)); 40 | } 41 | 42 | scrollToPosition = (position) => { 43 | const { heightRatio } = this.state; 44 | const limitedPosition = this.limitTrackPosition(position); 45 | 46 | this.elContainer.scrollTop = position / heightRatio; 47 | this.setState({ 48 | scrollPosition: limitedPosition, 49 | }); 50 | }; 51 | 52 | handleScroll = () => { 53 | const { heightRatio } = this.state; 54 | 55 | this.setState({ 56 | scrollPosition: heightRatio * this.elContainer.scrollTop 57 | }); 58 | }; 59 | 60 | handleScrollClick = (event) => { 61 | const { isDragging } = this.state; 62 | const elBar = this.elBar; 63 | const top = this.elScroll.getBoundingClientRect().top; 64 | 65 | if (!isDragging && event.target !== elBar) { 66 | this.scrollToPosition(event.pageY - top); 67 | } 68 | }; 69 | 70 | handleDragStart = (event) => { 71 | const { scrollPosition } = this.state; 72 | 73 | this.startDragMousePosition = event.pageY; 74 | this.startDragPosition = scrollPosition; 75 | this.setState({ isDragging: true }); 76 | 77 | document.addEventListener('mousemove', this.handleDrag); 78 | document.addEventListener('mouseup', this.handleDragEnd); 79 | }; 80 | 81 | handleDrag = (event) => { 82 | const { isDragging } = this.state; 83 | 84 | if (isDragging) { 85 | this.scrollToPosition(this.startDragPosition + event.pageY - this.startDragMousePosition); 86 | } 87 | }; 88 | 89 | handleDragEnd = () => { 90 | this.setState({ 91 | isDragging: false 92 | }); 93 | }; 94 | 95 | componentDidMount() { 96 | this.toggleScroll(); 97 | window.addEventListener('resize', this.toggleScroll); 98 | } 99 | 100 | render() { 101 | const { 102 | className, containerClassName, innerClassName, scrollClassName, barClassName, barActiveClassName, 103 | children, 104 | disableUIStyles, disableStyles, 105 | } = this.props; 106 | const { hasScroll, scrollHeight, scrollPosition, isDragging } = this.state; 107 | const rootStyles = !disableStyles && styles.root; 108 | const containerStyles = Object.assign( 109 | {}, 110 | !disableStyles && styles.container, 111 | !disableUIStyles && !disableStyles && styles.containerUI, 112 | ); 113 | const innerStyles = !disableStyles && styles.inner; 114 | const scrollStyles = Object.assign( 115 | {}, 116 | !disableStyles && styles.scroll, 117 | !disableUIStyles && !disableStyles && styles.scrollUI, 118 | ); 119 | const barStyles = Object.assign( 120 | {}, 121 | !disableStyles && styles.bar, 122 | !disableUIStyles && !disableStyles && styles.barUI, 123 | ); 124 | const barClassNames = [barClassName]; 125 | 126 | if (isDragging && barActiveClassName) { 127 | barClassNames.push(barActiveClassName); 128 | } 129 | 130 | return ( 131 |
this.elRoot = c} 135 | > 136 |
this.elContainer = c} 140 | onScroll={this.handleScroll} 141 | > 142 |
this.elInner = c} 146 | > 147 | { children } 148 |
149 |
150 | 151 | { 152 | hasScroll && ( 153 |
this.elScroll = c} 155 | style={scrollStyles} 156 | className={scrollClassName} 157 | onClick={this.handleScrollClick} 158 | > 159 |
this.elBar = c} 166 | className={barClassNames.join(' ')} 167 | onMouseDown={this.handleDragStart} 168 | /> 169 |
170 | ) 171 | } 172 |
173 | ) 174 | } 175 | } 176 | 177 | export default Scroll; 178 | -------------------------------------------------------------------------------- /src/Scroll/styles.js: -------------------------------------------------------------------------------- 1 | export default { 2 | root: { 3 | position: 'relative', 4 | height: '100%', 5 | overflow: 'hidden', 6 | }, 7 | container: { 8 | paddingRight: 100, 9 | marginRight: -100, 10 | overflow: 'auto', 11 | maxHeight: '100%', 12 | position: 'relative', 13 | }, 14 | containerUI: { 15 | paddingRight: 110, 16 | }, 17 | inner: { 18 | position: 'relative', 19 | }, 20 | scroll: { 21 | position: 'absolute', 22 | }, 23 | scrollUI: { 24 | right: 5, 25 | width: 5, 26 | top: 10, 27 | bottom: 10, 28 | background: 'rgba(0, 0, 0, .1)', 29 | borderRadius: 3, 30 | cursor: 'pointer', 31 | }, 32 | bar: { 33 | position: 'relative', 34 | }, 35 | barUI: { 36 | background: '#333', 37 | opacity: 0.3, 38 | minHeight: 25, 39 | height: 0, 40 | borderRadius: 3, 41 | userSelect: 'none', 42 | }, 43 | }; 44 | -------------------------------------------------------------------------------- /stories/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | import Scroll from '../src/Scroll'; 4 | 5 | storiesOf('Scroll', module) 6 | .add('Default', () => ( 7 |
8 |
9 | 10 |
11 |
12 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. 13 |
14 |
15 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. 16 |
17 |
18 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. 19 |
20 |
21 |
22 |
23 |
24 | )); -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const path = require('path'); 3 | const webpack = require('webpack'); 4 | const UglifyJSPlugin = require('uglifyjs-webpack-plugin'); 5 | 6 | module.exports = [{ 7 | entry: './src/Scroll/Scroll.jsx', 8 | output: { 9 | path: path.join(__dirname, 'dist'), 10 | filename: 'reactAwesomeScroll.js', 11 | library: 'ReactAwesomeScroll', 12 | libraryTarget: 'umd', 13 | }, 14 | resolve: { 15 | extensions: ['.js', '.jsx'], 16 | }, 17 | module: { 18 | rules: [{ 19 | test: /\.(js|jsx)$/, 20 | use: { 21 | loader: 'babel-loader', 22 | options: { 23 | presets: ['es2015', 'stage-2', 'react'], 24 | }, 25 | } 26 | }], 27 | }, 28 | externals: { 29 | react: { 30 | root: 'React', 31 | commonjs2: 'react', 32 | commonjs: 'react', 33 | amd: 'react', 34 | } 35 | }, 36 | plugins: [ 37 | new UglifyJSPlugin(), 38 | ], 39 | // devtool: 'source-map', 40 | }, { 41 | entry: './demo/demo.js', 42 | output: { 43 | path: path.join(__dirname, 'demo'), 44 | filename: 'demo.dist.js', 45 | }, 46 | resolve: { 47 | extensions: ['.js', '.jsx'], 48 | }, 49 | module: { 50 | rules: [{ 51 | test: /\.(js|jsx)$/, 52 | use: { 53 | loader: 'babel-loader', 54 | options: { 55 | presets: ['stage-2', 'react'], 56 | }, 57 | } 58 | }], 59 | }, 60 | }]; --------------------------------------------------------------------------------