├── .babelrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── examples ├── index.html ├── js │ ├── app.jsx │ ├── changing-children.jsx │ ├── main.js │ └── simple.jsx └── less │ └── site.less ├── gulpfile.js ├── karma.config.js ├── package-lock.json ├── package.json ├── src ├── js │ ├── ScrollArea.jsx │ ├── ScrollAreaWithCss.js │ ├── ScrollAreaWithoutCss.js │ ├── Scrollbar.jsx │ └── utils.js └── less │ └── scrollArea.less ├── test ├── scrollArea.spec.js ├── scrollBar.spec.js ├── tests.bundle.js └── utils.spec.js ├── webpack.base.config.js ├── webpack.dev.config.js ├── webpack.production.config.js └── webpackExamples.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react", "stage-2"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /dist/ 2 | /node_modules/ 3 | /bower_components/ 4 | /.vscode/ 5 | /npm-debug.log 6 | /examples/main.js 7 | /examples/site.css 8 | jsconfig.json 9 | /.idea 10 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /bower_components/ 3 | /npm-debug.log 4 | /examples/basic/main.js 5 | /examples/changingChildren/main.js 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4" 4 | - "5" 5 | before_install: 6 | - "npm install -g gulp" 7 | script: 8 | - gulp 9 | - npm test 10 | branches: 11 | only: 12 | - master 13 | - dev 14 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Jakub Kłobus 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 | 2 | [![build status](https://img.shields.io/travis/souhe/reactScrollbar/master.svg?style=flat-square)](https://travis-ci.org/souhe/reactScrollbar) 3 | [![npm package](https://img.shields.io/npm/v/react-scrollbar.svg?style=flat-square)](https://www.npmjs.org/package/react-scrollbar) 4 | [![npm downloads](https://img.shields.io/npm/dm/react-scrollbar.svg?style=flat-square)](https://www.npmjs.org/package/react-scrollbar) 5 | # react-scrollbar 6 | 7 | Simple ScrollArea component built for [React](http://facebook.github.io/react/). 8 | 9 | [Demo](http://souhe.github.io/reactScrollbar) 10 | 11 | ```bash 12 | npm install react-scrollbar --save 13 | ``` 14 | 15 | React Scrollbar requires **React 0.13 or later** 16 | 17 | Features: 18 | - built with and for `React` 19 | - horizontal and vertical scrollbars 20 | - touch support 21 | - scrollbar dragging and clicking 22 | - smooth scrolling 23 | - universal app support 24 | - customizable styles 25 | - and more... 26 | 27 | ## Usage examples 28 | 29 | #### React 0.14 30 | ```js 31 | var React = require('react'); 32 | var ReactDOM = require('react-dom'); 33 | var ScrollArea = require('react-scrollbar'); 34 | 35 | var App = React.createClass({ 36 | render() { 37 | return ( 38 | 44 |
Some long content.
45 |
46 | ); 47 | } 48 | }); 49 | 50 | ReactDOM.render(, document.body); 51 | ``` 52 | 53 | #### React 0.13 54 | For **React 0.13** you need to wrap `` child into a function. 55 | ```js 56 | 57 | { () =>
Some long content.
} 58 |
59 | ``` 60 | 61 | #### Version without boundled css styles #### 62 | If you prefer including scrollbar without css styles boundled inline to js file it's possible to import package without them. It's useful when you want to make custom css changes in scrollbars without using `!important` in each line. 63 | 64 | ```js 65 | var ScrollArea = require('react-scrollbar/no-css'); 66 | ``` 67 | Then **include scrollArea.css** file into your project. 68 | 69 | 70 | ### Run the example app 71 | 72 | ```bash 73 | git clone https://github.com/souhe/reactScrollbar.git 74 | cd reactScrollbar 75 | npm install 76 | gulp build-examples 77 | gulp less-examples 78 | gulp watch 79 | ``` 80 | 81 | then open [http://localhost:8003](http://localhost:8003). 82 | 83 | ### Using in universal app 84 | `ScrollArea` component has now full universal app support. It's only one requirement: you have to use `react-scrollbar` in no-css version and then include css file into your project manually (see [this](#version-without-boundled-css-styles)). It's because of issue in webpack style-loader which is used to bundle css styles into main js file. 85 | 86 | ## API 87 | 88 | ### Props 89 | 90 | ```js 91 | {}} 104 | contentWindow={Object} 105 | ownerDocument={Object} 106 | smoothScrolling={Boolean} 107 | minScrollSize={Number} 108 | swapWheelAxes={Boolean} 109 | stopScrollPropagation={Boolean} 110 | focusableTabIndex={Number} 111 | > 112 | ``` 113 | 114 | #### speed 115 | Scroll speed applied to mouse wheel event. 116 | **Default: 1** 117 | 118 | #### onScroll 119 | `onScroll(value: Object)` event which can notify the parent component when the container scrolls. 120 | - `value: Object` - informations about current position 121 | - `value.leftPosition: Number` - content left position (distance in pixels from the left side of container) 122 | - `value.topPosition: Number` - content top position (distance in pixels from the top of container) 123 | - `value.containerHeight: Number` - container height 124 | - `value.containerWidth: Number` - container width 125 | - `value.realHeight: Number` - real content height 126 | - `value.realWidth: Number` - real content width 127 | 128 | #### className 129 | CSS class names added to main scroll area component. 130 | 131 | #### style 132 | Inline styles applied to the main scroll area component. 133 | 134 | #### contentClassName 135 | CSS class names added to element with scroll area content. 136 | 137 | #### contentStyle 138 | Inline styles applied to element with scroll area content. 139 | 140 | #### horizontal 141 | When set to false, horizontal scrollbar will not be available. 142 | **Default: true** 143 | 144 | #### horizontalContainerStyle 145 | Inline styles applied to horizontal scrollbar's container. 146 | 147 | #### horizontalScrollbarStyle 148 | Inline styles applied to horizontal scrollbar. 149 | 150 | #### vertical 151 | When set to false, vertical scrollbar will not be available, regardless of the content height. 152 | **Default: true** 153 | 154 | #### verticalContainerStyle 155 | Inline styles applied to vertical scrollbar's container. 156 | 157 | #### verticalScrollbarStyle 158 | Inline styles applied to vertical scrollbar. 159 | 160 | #### contentWindow 161 | You can override window to make scrollarea works inside iframe. 162 | **Default: window** 163 | 164 | #### ownerDocument 165 | You can override document to make scrollarea works inside iframe. 166 | **Default: document** 167 | 168 | #### smoothScrolling 169 | When set to true, smooth scrolling for both scrollbars is enabled. 170 | **Default: false** 171 | 172 | #### minScrollSize 173 | Using this prop it's possible to set minimal size in px for both scrollbars. 174 | 175 | #### swapWheelAxes 176 | After set to true, mouse wheel event has swapped directions. So normal scrolling moves horizontal scrollbar and scrolling with SHIFT key moves vertical scrollbar. It could be useful for applications with horizontal layout. 177 | **Default: false** 178 | 179 | #### stopScrollPropagation 180 | After set to true, mouse wheel event will not propagate. This option is specifically useful in preventing nested scroll areas from propagating scroll actions to parent scroll areas. 181 | **Default: false** 182 | 183 | #### focusableTabIndex 184 | After set to a number, scrollarea-content is rendered with a tabindex value set to the passed in. This option is specifically useful in allowing the scroll area to be focusable. 185 | **Default: undefined** 186 | 187 | 188 | ### Context 189 | In context of each `` child could be injected an object `scrollArea` contains method: 190 | 191 | #### `refresh()` 192 | That method allows manual refreshing of the scrollbar. 193 | 194 | React 0.14 example using ES6 syntax: 195 | ```js 196 | class App extends React.Component { 197 | render(){ 198 | return ( 199 | 200 | 201 | 202 | ); 203 | } 204 | } 205 | 206 | class Content extends React.Component { 207 | render(){ 208 | return ( 209 |
Some long content
210 | ); 211 | } 212 | 213 | handleSomeAction(){ 214 | this.context.scrollArea.refresh(); 215 | } 216 | } 217 | 218 | Content.contextTypes = { 219 | scrollArea: React.PropTypes.object 220 | }; 221 | ``` 222 | 223 | #### `scrollTop()` 224 | It allows to scroll to the top of `ScrollArea` component. 225 | 226 | #### `scrollBottom()` 227 | It allows to scroll to the bottom of `ScrollArea` component. 228 | 229 | #### `scrollYTo(topPosition)` 230 | It moves vertical scrollbar. `topPosition` is a distance between the top of `scrollArea` container and the top of `scrollArea` content. 231 | 232 | #### `scrollLeft()` 233 | It allows to scroll to the left of `ScrollArea` component. 234 | 235 | #### `scrollRight()` 236 | It allows to scroll to the right of `ScrollArea` component. 237 | 238 | #### `scrollXTo(leftPosition)` 239 | It moves horizontal scrollbar. `leftPosition` is a distance between left edge of `scrollArea` container and left edge of `scrollArea` content. 240 | 241 | # Change log 242 | Every release is documented on the Github [Releases](https://github.com/souhe/reactScrollbar/releases) page. 243 | 244 | # License 245 | MIT 246 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /examples/js/app.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import SimpleExample from './simple'; 3 | import ChangingChildren from './changing-children'; 4 | 5 | class App extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | 9 | this.state = { 10 | selected: 'basic' 11 | }; 12 | } 13 | 14 | switchExample(event) { 15 | this.setState({selected: event.target.value}); 16 | } 17 | 18 | render() { 19 | return ( 20 |
21 |
22 |
Example:
23 | 27 |
28 | {(() => { 29 | if (this.state.selected === 'changing-children') { 30 | return ; 31 | } else { 32 | return ; 33 | } 34 | })()} 35 |
36 | ); 37 | } 38 | } 39 | 40 | export default App; 41 | -------------------------------------------------------------------------------- /examples/js/changing-children.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types'; 3 | import ScrollArea from 'react-scrollbar'; 4 | 5 | class ChangingChildren extends React.Component{ 6 | constructor(props){ 7 | super(props); 8 | 9 | this.state = { 10 | itemsCount : 24 11 | }; 12 | } 13 | 14 | render() { 15 | return ( 16 |
17 | 18 | 19 | 20 | 21 | 22 |
23 | ); 24 | } 25 | } 26 | 27 | class Content extends React.Component { 28 | constructor(props){ 29 | super(props); 30 | 31 | this.state = { 32 | itemsCount : props.itemsCount 33 | }; 34 | } 35 | 36 | render(){ 37 | var itemElements = []; 38 | 39 | for( var i = 0; i< this.state.itemsCount; i++){ 40 | itemElements.push(
Item {i}
); 41 | } 42 | 43 | return ( 44 |
45 |
46 | 47 | 48 | 49 | 50 | 51 |
52 | {itemElements} 53 | 54 | 55 | 56 | 57 |
58 | ); 59 | } 60 | 61 | handleScrollTopButtonClick() { 62 | this.context.scrollArea.scrollTop(); 63 | } 64 | 65 | handleScrollBottomButtonClick() { 66 | this.context.scrollArea.scrollBottom(); 67 | } 68 | 69 | handleScroll100ButtonClick() { 70 | this.context.scrollArea.scrollXTo(100); 71 | } 72 | 73 | handleScrollLeftButtonClick() { 74 | this.context.scrollArea.scrollLeft(); 75 | } 76 | 77 | handleScrollRightButtonClick() { 78 | this.context.scrollArea.scrollRight(); 79 | } 80 | 81 | handleScrollY40ButtonClick() { 82 | this.context.scrollArea.scrollYTo(40); 83 | } 84 | 85 | handleAddButtonClick(){ 86 | this.setState({itemsCount: this.state.itemsCount + 10}, this.context.scrollArea.refresh); 87 | } 88 | 89 | handleRemoveButtonClick(){ 90 | this.setState({itemsCount: this.state.itemsCount - 10}, this.context.scrollArea.refresh); 91 | } 92 | } 93 | 94 | Content.contextTypes = { 95 | scrollArea: PropTypes.object, 96 | }; 97 | 98 | export default ChangingChildren; 99 | -------------------------------------------------------------------------------- /examples/js/main.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import App from './app'; 5 | 6 | ReactDOM.render(React.createElement(App, null), document.getElementById("main")); -------------------------------------------------------------------------------- /examples/js/simple.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ScrollArea from 'react-scrollbar'; 3 | 4 | class SimpleExample extends React.Component{ 5 | constructor(props){ 6 | super(props); 7 | 8 | this.state = { 9 | itemsCount : 40 10 | }; 11 | } 12 | 13 | handleScroll(scrollData){ 14 | console.log(scrollData); 15 | } 16 | 17 | render() { 18 | var itemElements = []; 19 | 20 | for( var i = 0; i< this.state.itemsCount; i++){ 21 | itemElements.push(
item {i}
); 22 | } 23 | 24 | let scrollbarStyles = {borderRadius: 5}; 25 | 26 | return ( 27 |
28 | 39 | 40 | {itemElements} 41 | 42 | 43 | 44 |
45 | ); 46 | } 47 | } 48 | 49 | export default SimpleExample; 50 | -------------------------------------------------------------------------------- /examples/less/site.less: -------------------------------------------------------------------------------- 1 | html, body, #main{ 2 | height: 100%; 3 | } 4 | body{ 5 | margin: 0; 6 | 7 | h1, h2, h3, h4, h5, h6 { 8 | font-weight: normal; 9 | } 10 | 11 | .container{ 12 | width: 100%; 13 | height: 100%; 14 | box-sizing: border-box; 15 | position: relative; 16 | background: white; 17 | 18 | padding: 20px; 19 | } 20 | 21 | .area{ 22 | margin: 0 auto; 23 | width: 300px; 24 | height: 300px; 25 | background: #e5e5e5; 26 | 27 | .content{ 28 | width: 400px; 29 | 30 | .item{ 31 | background: #82bb95; 32 | width: 180px; 33 | height: 70px; 34 | margin: 10px; 35 | float: left; 36 | padding: 8px; 37 | box-sizing: border-box; 38 | } 39 | } 40 | ul{ 41 | width: 700px; 42 | } 43 | } 44 | 45 | .example-selector { 46 | padding-bottom: 50px; 47 | text-align: center; 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var webpack = require('webpack-stream'); 3 | var concat = require('gulp-concat'); 4 | var less = require('gulp-less'); 5 | var babel = require('gulp-babel'); 6 | var connect = require('gulp-connect'); 7 | var merge = require('merge-stream'); 8 | var runSequence = require('run-sequence'); 9 | 10 | var webpackDevConf = require('./webpack.dev.config.js'); 11 | var webpackProductionConf = require('./webpack.production.config.js'); 12 | var webpackExamplesConf = require('./webpackExamples.config.js'); 13 | 14 | gulp.task('build', function() { 15 | return gulp.src('src/js/ScrollAreaWithCss.js') 16 | .pipe(webpack(webpackDevConf)) 17 | .pipe(concat('scrollArea.js')) 18 | .pipe(gulp.dest('dist')); 19 | }); 20 | 21 | gulp.task('build-nocss', function() { 22 | return gulp.src('src/js/ScrollAreaWithoutCss.js') 23 | .pipe(webpack(webpackDevConf)) 24 | .pipe(concat('no-css.js')) 25 | .pipe(gulp.dest('dist')); 26 | }); 27 | 28 | gulp.task('build-production', function() { 29 | return gulp.src('src/js/ScrollAreaWithCss.js') 30 | .pipe(webpack(webpackProductionConf)) 31 | .pipe(concat('scrollArea.js')) 32 | .pipe(gulp.dest('dist')); 33 | }); 34 | 35 | gulp.task('build-production-nocss', function() { 36 | return gulp.src('src/js/ScrollAreaWithoutCss.js') 37 | .pipe(webpack(webpackProductionConf)) 38 | .pipe(concat('no-css.js')) 39 | .pipe(gulp.dest('dist')); 40 | }); 41 | 42 | gulp.task('less', function(){ 43 | return gulp.src('./src/less/scrollArea.less') 44 | .pipe(less()) 45 | .pipe(gulp.dest('./dist/css/')); 46 | }); 47 | 48 | gulp.task('build-examples', function(){ 49 | return gulp.src('examples/js/main.js') 50 | .pipe(webpack(webpackExamplesConf)) 51 | .pipe(concat('main.js')) 52 | .pipe(gulp.dest('examples')) 53 | .pipe(connect.reload()); 54 | }); 55 | 56 | gulp.task("connect", function(){ 57 | connect.server({ 58 | root: 'dist', 59 | livereload: true, 60 | port: 8003 61 | }); 62 | }); 63 | 64 | gulp.task('less-examples', function(){ 65 | return gulp.src('./examples/less/**/*.less') 66 | .pipe(less()) 67 | .pipe(gulp.dest('examples')) 68 | .pipe(connect.reload()); 69 | }); 70 | 71 | gulp.task('default', function(callback){ 72 | runSequence('build', 'build-nocss', 'less', ['build-examples', 'less-examples'], callback); 73 | }); 74 | 75 | gulp.task('production', function(callback){ 76 | runSequence('build-production', 'build-production-nocss', 'less', callback); 77 | }); 78 | 79 | gulp.task('watch', function() { 80 | connect.server({ 81 | root: 'examples', 82 | livereload: true, 83 | port: 8003 84 | }); 85 | 86 | gulp.watch(['src/**/*.js', 'src/**/*.jsx', 'src/**/*.less'], ['default']); 87 | gulp.watch(['examples/**/js/**/*.js', 'examples/**/*.jsx'], ['build-examples']); 88 | gulp.watch('examples/**/*.less', ['less-examples']); 89 | }); 90 | -------------------------------------------------------------------------------- /karma.config.js: -------------------------------------------------------------------------------- 1 | const webpackDevConfig = require('./webpack.dev.config.js'); 2 | 3 | module.exports = function (config) { 4 | config.set({ 5 | browsers: [ 'PhantomJS', 'Chrome' ], 6 | files: [ 7 | './test/tests.bundle.js' 8 | ], 9 | frameworks: [ 'mocha' ], 10 | preprocessors: { 11 | './test/tests.bundle.js': [ 'webpack', 'sourcemap' ] 12 | }, 13 | reporters: [ 'mocha' ], 14 | singleRun: true, 15 | webpack: { 16 | devtool: 'inline-source-map', 17 | resolve: { 18 | modulesDirectories: ['node_modules', 'bower_components'], 19 | extensions: ['', '.js', '.jsx'] 20 | }, 21 | module: { 22 | loaders: [ 23 | { test: /\.jsx?$/, exclude: /node_modules/, loader: 'babel' }, 24 | { test: /\.less$/, loader: 'style!css!less' } 25 | ] 26 | } 27 | }, 28 | webpackServer: { 29 | noInfo: true 30 | } 31 | }); 32 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-scrollbar", 3 | "version": "0.5.6", 4 | "description": "ScrollArea component for react", 5 | "main": "./dist/scrollArea.js", 6 | "scripts": { 7 | "test": "./node_modules/.bin/karma start karma.config.js --single-run --browsers PhantomJS" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/souhe/reactScrollbar.git" 12 | }, 13 | "keywords": [ 14 | "react", 15 | "react-component", 16 | "component", 17 | "scrollbar", 18 | "scroll", 19 | "scrollarea" 20 | ], 21 | "author": "souhe", 22 | "license": "MIT", 23 | "dependencies": { 24 | "config": "^1.24.0", 25 | "line-height": "^0.1.1", 26 | "react-motion": "^0.5.2" 27 | }, 28 | "devDependencies": { 29 | "babel": "^6.5.2", 30 | "babel-core": "^6.9.0", 31 | "babel-loader": "^6.2.4", 32 | "babel-preset-es2015": "^6.9.0", 33 | "babel-preset-react": "^6.5.0", 34 | "babel-preset-stage-2": "^6.5.0", 35 | "babel-runtime": "^6.9.0", 36 | "chai": "^3.5.0", 37 | "css-loader": "^0.23.1", 38 | "expect": "^1.13.4", 39 | "expect-jsx": "^2.5.1", 40 | "gulp": "^3.9.0", 41 | "gulp-babel": "^6.1.2", 42 | "gulp-concat": "^2.5.2", 43 | "gulp-connect": "^4.0.0", 44 | "gulp-less": "^3.0.2", 45 | "karma": "^1.3.0", 46 | "karma-chrome-launcher": "^2.0.0", 47 | "karma-cli": "^1.0.0", 48 | "karma-mocha": "^1.0.1", 49 | "karma-mocha-reporter": "^2.0.3", 50 | "karma-phantomjs-launcher": "^1.0.0", 51 | "karma-sourcemap-loader": "^0.3.7", 52 | "karma-webpack": "^1.7.0", 53 | "less": "^2.5.1", 54 | "less-loader": "^2.2.1", 55 | "merge-stream": "^1.0.0", 56 | "mocha": "^3.2.0", 57 | "object-assign": "^4.0.1", 58 | "phantomjs-prebuilt": "^2.1.3", 59 | "prop-types": "^15.6.0", 60 | "react": "^15.4.1", 61 | "react-addons-test-utils": "^15.1.0", 62 | "react-dom": "^15.4.1", 63 | "run-sequence": "^1.1.4", 64 | "style-loader": "^0.13.0", 65 | "webpack": "^1.12.2", 66 | "webpack-stream": "^3.1.0" 67 | }, 68 | "peerDependencies": { 69 | "prop-types": "^15.6.0", 70 | "react": "^15.3.0 || ^16.0.0-rc || ^16.0" 71 | }, 72 | "bugs": { 73 | "url": "https://github.com/souhe/reactScrollbar/issues" 74 | }, 75 | "homepage": "https://github.com/souhe/reactScrollbar" 76 | } 77 | -------------------------------------------------------------------------------- /src/js/ScrollArea.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import lineHeight from 'line-height'; 4 | import { Motion, spring } from 'react-motion'; 5 | 6 | import { 7 | findDOMNode, warnAboutFunctionChild, warnAboutElementChild, positiveOrZero, modifyObjValues, 8 | } from './utils'; 9 | import ScrollBar from './Scrollbar'; 10 | 11 | const eventTypes = { 12 | wheel: 'wheel', 13 | api: 'api', 14 | touch: 'touch', 15 | touchEnd: 'touchEnd', 16 | mousemove: 'mousemove', 17 | keyPress: 'keypress' 18 | }; 19 | 20 | export default class ScrollArea extends React.Component { 21 | constructor(props) { 22 | super(props); 23 | this.state = { 24 | topPosition: 0, 25 | leftPosition: 0, 26 | realHeight: 0, 27 | containerHeight: 0, 28 | realWidth: 0, 29 | containerWidth: 0 30 | }; 31 | 32 | this.scrollArea = { 33 | refresh: () => { 34 | this.setSizesToState(); 35 | }, 36 | scrollTop: () => { 37 | this.scrollTop(); 38 | }, 39 | scrollBottom: () => { 40 | this.scrollBottom(); 41 | }, 42 | scrollYTo: (position) => { 43 | this.scrollYTo(position); 44 | }, 45 | scrollLeft: () => { 46 | this.scrollLeft(); 47 | }, 48 | scrollRight: () => { 49 | this.scrollRight(); 50 | }, 51 | scrollXTo: (position) => { 52 | this.scrollXTo(position); 53 | } 54 | }; 55 | 56 | this.evntsPreviousValues = { 57 | clientX: 0, 58 | clientY: 0, 59 | deltaX: 0, 60 | deltaY: 0 61 | }; 62 | 63 | this.bindedHandleWindowResize = this.handleWindowResize.bind(this); 64 | this.bindedHandleWheel = this.handleWheel.bind(this) 65 | } 66 | 67 | getChildContext() { 68 | return { 69 | scrollArea: this.scrollArea 70 | }; 71 | } 72 | 73 | componentDidMount() { 74 | if (this.props.contentWindow) { 75 | this.props.contentWindow.addEventListener("resize", this.bindedHandleWindowResize); 76 | } 77 | this.lineHeightPx = lineHeight(findDOMNode(this.content)); 78 | this.setSizesToState(); 79 | this.wrapper.addEventListener("wheel", this.bindedHandleWheel, {passive: false}) 80 | } 81 | 82 | componentWillUnmount() { 83 | if (this.props.contentWindow) { 84 | this.props.contentWindow.removeEventListener("resize", this.bindedHandleWindowResize); 85 | } 86 | this.wrapper.removeEventListener("wheel", this.bindedHandleWheel); 87 | } 88 | 89 | componentDidUpdate() { 90 | this.setSizesToState(); 91 | } 92 | 93 | render() { 94 | let {children, className, contentClassName, ownerDocument} = this.props; 95 | let withMotion = this.props.smoothScrolling && 96 | (this.state.eventType === eventTypes.wheel || this.state.eventType === eventTypes.api || this.state.eventType === eventTypes.touchEnd || 97 | this.state.eventType === eventTypes.keyPress); 98 | 99 | let scrollbarY = this.canScrollY() ? ( 100 | 113 | ) : null; 114 | 115 | let scrollbarX = this.canScrollX() ? ( 116 | 129 | ) : null; 130 | 131 | if (typeof children === 'function') { 132 | warnAboutFunctionChild(); 133 | children = children(); 134 | } else { 135 | warnAboutElementChild(); 136 | } 137 | 138 | let classes = 'scrollarea ' + (className || ''); 139 | let contentClasses = 'scrollarea-content ' + (contentClassName || ''); 140 | 141 | let contentStyle = { 142 | marginTop: -this.state.topPosition, 143 | marginLeft: -this.state.leftPosition 144 | }; 145 | let springifiedContentStyle = withMotion ? modifyObjValues(contentStyle, x => spring(x)) : contentStyle; 146 | 147 | return ( 148 | 149 | { style => 150 |
this.wrapper = x} 152 | className={classes} 153 | style={this.props.style}> 154 |
this.content = x} 156 | style={{ ...this.props.contentStyle, ...style }} 157 | className={contentClasses} 158 | onTouchStart={this.handleTouchStart.bind(this)} 159 | onTouchMove={this.handleTouchMove.bind(this)} 160 | onTouchEnd={this.handleTouchEnd.bind(this)} 161 | onKeyDown={this.handleKeyDown.bind(this)} 162 | tabIndex={this.props.focusableTabIndex} 163 | > 164 | {children} 165 |
166 | {scrollbarY} 167 | {scrollbarX} 168 |
169 | } 170 |
171 | ); 172 | } 173 | 174 | setStateFromEvent(newState, eventType) { 175 | if (this.props.onScroll) { 176 | this.props.onScroll(newState); 177 | } 178 | this.setState({...newState, eventType}); 179 | } 180 | 181 | handleTouchStart(e) { 182 | let {touches} = e; 183 | if (touches.length === 1) { 184 | let {clientX, clientY} = touches[0]; 185 | this.eventPreviousValues = { 186 | ...this.eventPreviousValues, 187 | clientY, 188 | clientX, 189 | timestamp: Date.now() 190 | }; 191 | } 192 | } 193 | 194 | handleTouchMove(e) { 195 | if (this.canScroll()) { 196 | e.preventDefault(); 197 | e.stopPropagation(); 198 | } 199 | 200 | let {touches} = e; 201 | if (touches.length === 1) { 202 | let {clientX, clientY} = touches[0]; 203 | 204 | let deltaY = this.eventPreviousValues.clientY - clientY; 205 | let deltaX = this.eventPreviousValues.clientX - clientX; 206 | 207 | this.eventPreviousValues = { 208 | ...this.eventPreviousValues, 209 | deltaY, 210 | deltaX, 211 | clientY, 212 | clientX, 213 | timestamp: Date.now() 214 | }; 215 | 216 | this.setStateFromEvent(this.composeNewState(-deltaX, -deltaY)); 217 | } 218 | } 219 | 220 | handleTouchEnd(e) { 221 | let {deltaX, deltaY, timestamp} = this.eventPreviousValues; 222 | if (typeof deltaX === 'undefined') deltaX = 0; 223 | if (typeof deltaY === 'undefined') deltaY = 0; 224 | if (Date.now() - timestamp < 200) { 225 | this.setStateFromEvent(this.composeNewState(-deltaX * 10, -deltaY * 10), eventTypes.touchEnd); 226 | } 227 | 228 | this.eventPreviousValues = { 229 | ...this.eventPreviousValues, 230 | deltaY: 0, 231 | deltaX: 0 232 | }; 233 | } 234 | 235 | handleScrollbarMove(deltaY, deltaX) { 236 | this.setStateFromEvent(this.composeNewState(deltaX, deltaY)); 237 | } 238 | 239 | handleScrollbarXPositionChange(position) { 240 | this.scrollXTo(position); 241 | } 242 | 243 | handleScrollbarYPositionChange(position) { 244 | this.scrollYTo(position); 245 | } 246 | 247 | handleWheel(e) { 248 | let deltaY = e.deltaY; 249 | let deltaX = e.deltaX; 250 | 251 | if (this.props.swapWheelAxes) { 252 | [deltaY, deltaX] = [deltaX, deltaY]; 253 | } 254 | 255 | /* 256 | * WheelEvent.deltaMode can differ between browsers and must be normalized 257 | * e.deltaMode === 0: The delta values are specified in pixels 258 | * e.deltaMode === 1: The delta values are specified in lines 259 | * https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent/deltaMode 260 | */ 261 | if (e.deltaMode === 1) { 262 | deltaY = deltaY * this.lineHeightPx; 263 | deltaX = deltaX * this.lineHeightPx; 264 | } 265 | 266 | deltaY = deltaY * this.props.speed; 267 | deltaX = deltaX * this.props.speed; 268 | 269 | let newState = this.composeNewState(-deltaX, -deltaY); 270 | 271 | if ((newState.topPosition && this.state.topPosition !== newState.topPosition) || 272 | (newState.leftPosition && this.state.leftPosition !== newState.leftPosition) || 273 | this.props.stopScrollPropagation) { 274 | e.preventDefault(); 275 | e.stopPropagation(); 276 | } 277 | 278 | this.setStateFromEvent(newState, eventTypes.wheel); 279 | this.focusContent(); 280 | } 281 | 282 | handleKeyDown(e) { 283 | // only handle if scroll area is in focus 284 | if (e.target.tagName.toLowerCase() !== 'input' && e.target.tagName.toLowerCase() !== 'textarea' && !e.target.isContentEditable) { 285 | let deltaY = 0; 286 | let deltaX = 0; 287 | let lineHeight = this.lineHeightPx ? this.lineHeightPx : 10; 288 | 289 | switch (e.keyCode) { 290 | case 33: // page up 291 | deltaY = this.state.containerHeight - lineHeight; 292 | break; 293 | case 34: // page down 294 | deltaY = -this.state.containerHeight + lineHeight; 295 | break; 296 | case 37: // left 297 | deltaX = lineHeight; 298 | break; 299 | case 38: // up 300 | deltaY = lineHeight; 301 | break; 302 | case 39: // right 303 | deltaX = -lineHeight; 304 | break; 305 | case 40: // down 306 | deltaY = -lineHeight; 307 | break; 308 | } 309 | 310 | // only compose new state if key code matches those above 311 | if (deltaY !== 0 || deltaX !== 0) { 312 | let newState = this.composeNewState(deltaX, deltaY); 313 | 314 | e.preventDefault(); 315 | e.stopPropagation(); 316 | 317 | this.setStateFromEvent(newState, eventTypes.keyPress); 318 | } 319 | } 320 | } 321 | 322 | handleWindowResize() { 323 | let newState = this.computeSizes(); 324 | newState = this.getModifiedPositionsIfNeeded(newState); 325 | this.setStateFromEvent(newState); 326 | } 327 | 328 | composeNewState(deltaX, deltaY) { 329 | let newState = this.computeSizes(); 330 | 331 | if (this.canScrollY(newState)) { 332 | newState.topPosition = this.computeTopPosition(deltaY, newState); 333 | } else { 334 | newState.topPosition = 0; 335 | } 336 | if (this.canScrollX(newState)) { 337 | newState.leftPosition = this.computeLeftPosition(deltaX, newState); 338 | } 339 | 340 | return newState; 341 | } 342 | 343 | computeTopPosition(deltaY, sizes) { 344 | let newTopPosition = this.state.topPosition - deltaY; 345 | return this.normalizeTopPosition(newTopPosition, sizes); 346 | } 347 | 348 | computeLeftPosition(deltaX, sizes) { 349 | let newLeftPosition = this.state.leftPosition - deltaX; 350 | return this.normalizeLeftPosition(newLeftPosition, sizes); 351 | } 352 | 353 | normalizeTopPosition(newTopPosition, sizes) { 354 | if (newTopPosition > sizes.realHeight - sizes.containerHeight) { 355 | newTopPosition = sizes.realHeight - sizes.containerHeight; 356 | } 357 | if (newTopPosition < 0) { 358 | newTopPosition = 0; 359 | } 360 | return newTopPosition; 361 | } 362 | 363 | normalizeLeftPosition(newLeftPosition, sizes) { 364 | if (newLeftPosition > sizes.realWidth - sizes.containerWidth) { 365 | newLeftPosition = sizes.realWidth - sizes.containerWidth; 366 | } else if (newLeftPosition < 0) { 367 | newLeftPosition = 0; 368 | } 369 | 370 | return newLeftPosition; 371 | } 372 | 373 | computeSizes() { 374 | let realHeight = this.content.offsetHeight; 375 | let containerHeight = this.wrapper.offsetHeight; 376 | let realWidth = this.content.offsetWidth; 377 | let containerWidth = this.wrapper.offsetWidth; 378 | 379 | return { 380 | realHeight: realHeight, 381 | containerHeight: containerHeight, 382 | realWidth: realWidth, 383 | containerWidth: containerWidth 384 | }; 385 | } 386 | 387 | setSizesToState() { 388 | let sizes = this.computeSizes(); 389 | if (sizes.realHeight !== this.state.realHeight || sizes.realWidth !== this.state.realWidth) { 390 | this.setStateFromEvent(this.getModifiedPositionsIfNeeded(sizes)); 391 | } 392 | } 393 | 394 | scrollTop() { 395 | this.scrollYTo(0); 396 | } 397 | 398 | scrollBottom() { 399 | this.scrollYTo((this.state.realHeight - this.state.containerHeight)); 400 | } 401 | 402 | scrollLeft() { 403 | this.scrollXTo(0); 404 | } 405 | 406 | scrollRight() { 407 | this.scrollXTo((this.state.realWidth - this.state.containerWidth)); 408 | } 409 | 410 | scrollYTo(topPosition) { 411 | if (this.canScrollY()) { 412 | let position = this.normalizeTopPosition(topPosition, this.computeSizes()); 413 | this.setStateFromEvent({topPosition: position}, eventTypes.api); 414 | } 415 | } 416 | 417 | scrollXTo(leftPosition) { 418 | if (this.canScrollX()) { 419 | let position = this.normalizeLeftPosition(leftPosition, this.computeSizes()); 420 | this.setStateFromEvent({leftPosition: position}, eventTypes.api); 421 | } 422 | } 423 | 424 | canScrollY(state = this.state) { 425 | let scrollableY = state.realHeight > state.containerHeight; 426 | return scrollableY && this.props.vertical; 427 | } 428 | 429 | canScrollX(state = this.state) { 430 | let scrollableX = state.realWidth > state.containerWidth; 431 | return scrollableX && this.props.horizontal; 432 | } 433 | 434 | canScroll(state = this.state) { 435 | return this.canScrollY(state) || this.canScrollX(state); 436 | } 437 | 438 | getModifiedPositionsIfNeeded(newState) { 439 | let bottomPosition = newState.realHeight - newState.containerHeight; 440 | if (this.state.topPosition >= bottomPosition) { 441 | newState.topPosition = this.canScrollY(newState) ? positiveOrZero(bottomPosition) : 0; 442 | } 443 | 444 | let rightPosition = newState.realWidth - newState.containerWidth; 445 | if (this.state.leftPosition >= rightPosition) { 446 | newState.leftPosition = this.canScrollX(newState) ? positiveOrZero(rightPosition) : 0; 447 | } 448 | 449 | return newState; 450 | } 451 | 452 | focusContent() { 453 | if(this.content) { 454 | findDOMNode(this.content).focus(); 455 | } 456 | } 457 | } 458 | 459 | ScrollArea.childContextTypes = { 460 | scrollArea: PropTypes.object, 461 | }; 462 | 463 | ScrollArea.propTypes = { 464 | className: PropTypes.string, 465 | style: PropTypes.object, 466 | speed: PropTypes.number, 467 | contentClassName: PropTypes.string, 468 | contentStyle: PropTypes.object, 469 | vertical: PropTypes.bool, 470 | verticalContainerStyle: PropTypes.object, 471 | verticalScrollbarStyle: PropTypes.object, 472 | horizontal: PropTypes.bool, 473 | horizontalContainerStyle: PropTypes.object, 474 | horizontalScrollbarStyle: PropTypes.object, 475 | onScroll: PropTypes.func, 476 | contentWindow: PropTypes.any, 477 | ownerDocument: PropTypes.any, 478 | smoothScrolling: PropTypes.bool, 479 | minScrollSize: PropTypes.number, 480 | swapWheelAxes: PropTypes.bool, 481 | stopScrollPropagation: PropTypes.bool, 482 | focusableTabIndex: PropTypes.number, 483 | }; 484 | 485 | ScrollArea.defaultProps = { 486 | speed: 1, 487 | vertical: true, 488 | horizontal: true, 489 | smoothScrolling: false, 490 | swapWheelAxes: false, 491 | contentWindow: (typeof window === "object") ? window : undefined, 492 | ownerDocument: (typeof document === "object") ? document : undefined, 493 | focusableTabIndex: 1, 494 | }; 495 | -------------------------------------------------------------------------------- /src/js/ScrollAreaWithCss.js: -------------------------------------------------------------------------------- 1 | import '../less/scrollArea.less'; 2 | import ScrollArea from './ScrollArea.jsx'; 3 | 4 | export default ScrollArea; -------------------------------------------------------------------------------- /src/js/ScrollAreaWithoutCss.js: -------------------------------------------------------------------------------- 1 | import ScrollArea from './ScrollArea.jsx'; 2 | 3 | export default ScrollArea; -------------------------------------------------------------------------------- /src/js/Scrollbar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Motion, spring } from 'react-motion'; 4 | import { modifyObjValues } from './utils'; 5 | 6 | class ScrollBar extends React.Component { 7 | constructor(props){ 8 | super(props); 9 | let newState = this.calculateState(props); 10 | this.state = { 11 | position: newState.position, 12 | scrollSize: newState.scrollSize, 13 | isDragging: false, 14 | lastClientPosition: 0 15 | }; 16 | 17 | if(props.type === 'vertical'){ 18 | this.bindedHandleMouseMove = this.handleMouseMoveForVertical.bind(this); 19 | } else { 20 | this.bindedHandleMouseMove = this.handleMouseMoveForHorizontal.bind(this); 21 | } 22 | 23 | this.bindedHandleMouseUp = this.handleMouseUp.bind(this); 24 | } 25 | 26 | componentDidMount(){ 27 | if (this.props.ownerDocument) { 28 | this.props.ownerDocument.addEventListener("mousemove", this.bindedHandleMouseMove); 29 | this.props.ownerDocument.addEventListener("mouseup", this.bindedHandleMouseUp); 30 | } 31 | } 32 | 33 | componentWillReceiveProps(nextProps){ 34 | this.setState(this.calculateState(nextProps)); 35 | } 36 | 37 | componentWillUnmount(){ 38 | if (this.props.ownerDocument) { 39 | this.props.ownerDocument.removeEventListener("mousemove", this.bindedHandleMouseMove); 40 | this.props.ownerDocument.removeEventListener("mouseup", this.bindedHandleMouseUp); 41 | } 42 | } 43 | 44 | calculateFractionalPosition(realContentSize, containerSize, contentPosition){ 45 | let relativeSize = realContentSize - containerSize; 46 | 47 | return 1 - ((relativeSize - contentPosition) / relativeSize); 48 | } 49 | 50 | calculateState(props){ 51 | let fractionalPosition = this.calculateFractionalPosition(props.realSize, props.containerSize, props.position); 52 | let proportionalToPageScrollSize = props.containerSize * props.containerSize / props.realSize; 53 | let scrollSize = proportionalToPageScrollSize < props.minScrollSize ? props.minScrollSize : proportionalToPageScrollSize; 54 | 55 | let scrollPosition = (props.containerSize - scrollSize) * fractionalPosition; 56 | return { 57 | scrollSize: scrollSize, 58 | position: Math.round(scrollPosition) 59 | }; 60 | } 61 | 62 | render(){ 63 | let {smoothScrolling, isDragging, type, scrollbarStyle, containerStyle} = this.props; 64 | let isVoriziontal = type === 'horizontal'; 65 | let isVertical = type === 'vertical'; 66 | let scrollStyles = this.createScrollStyles(); 67 | let springifiedScrollStyles = smoothScrolling ? modifyObjValues(scrollStyles, x => spring(x)) : scrollStyles; 68 | 69 | let scrollbarClasses = `scrollbar-container ${isDragging ? 'active' : ''} ${isVoriziontal ? 'horizontal' : ''} ${isVertical ? 'vertical' : ''}`; 70 | 71 | return ( 72 | 73 | { style => 74 |
this.scrollbarContainer = x } 79 | > 80 |
85 |
86 | } 87 | 88 | ); 89 | } 90 | 91 | handleScrollBarContainerClick(e) { 92 | e.preventDefault(); 93 | let multiplier = this.computeMultiplier(); 94 | let clientPosition = this.isVertical() ? e.clientY : e.clientX; 95 | let { top, left } = this.scrollbarContainer.getBoundingClientRect(); 96 | let clientScrollPosition = this.isVertical() ? top : left; 97 | 98 | let position = clientPosition - clientScrollPosition; 99 | let proportionalToPageScrollSize = this.props.containerSize * this.props.containerSize / this.props.realSize; 100 | 101 | this.setState({isDragging: true, lastClientPosition: clientPosition }); 102 | this.props.onPositionChange((position - proportionalToPageScrollSize / 2) / multiplier); 103 | } 104 | 105 | handleMouseMoveForHorizontal(e){ 106 | let multiplier = this.computeMultiplier(); 107 | 108 | if(this.state.isDragging){ 109 | e.preventDefault(); 110 | let deltaX = this.state.lastClientPosition - e.clientX; 111 | this.setState({ lastClientPosition: e.clientX }); 112 | this.props.onMove(0, deltaX / multiplier); 113 | } 114 | } 115 | 116 | handleMouseMoveForVertical(e){ 117 | let multiplier = this.computeMultiplier(); 118 | 119 | if(this.state.isDragging){ 120 | e.preventDefault(); 121 | let deltaY = this.state.lastClientPosition - e.clientY; 122 | this.setState({ lastClientPosition: e.clientY }); 123 | this.props.onMove(deltaY / multiplier, 0); 124 | } 125 | } 126 | 127 | handleMouseDown(e){ 128 | e.preventDefault(); 129 | e.stopPropagation(); 130 | let lastClientPosition = this.isVertical() ? e.clientY: e.clientX; 131 | this.setState({isDragging: true, lastClientPosition: lastClientPosition }); 132 | 133 | this.props.onFocus(); 134 | } 135 | 136 | handleMouseUp(e){ 137 | if (this.state.isDragging) { 138 | e.preventDefault(); 139 | this.setState({isDragging: false }); 140 | } 141 | } 142 | 143 | createScrollStyles(){ 144 | if(this.props.type === 'vertical'){ 145 | return { 146 | height: this.state.scrollSize, 147 | marginTop: this.state.position 148 | }; 149 | } else { 150 | return { 151 | width: this.state.scrollSize, 152 | marginLeft: this.state.position 153 | }; 154 | } 155 | } 156 | 157 | computeMultiplier(){ 158 | return (this.props.containerSize) / this.props.realSize; 159 | } 160 | 161 | isVertical(){ 162 | return this.props.type === 'vertical'; 163 | } 164 | } 165 | 166 | ScrollBar.propTypes = { 167 | onMove: PropTypes.func, 168 | onPositionChange: PropTypes.func, 169 | onFocus: PropTypes.func, 170 | realSize: PropTypes.number, 171 | containerSize: PropTypes.number, 172 | position: PropTypes.number, 173 | containerStyle: PropTypes.object, 174 | scrollbarStyle: PropTypes.object, 175 | type: PropTypes.oneOf(['vertical', 'horizontal']), 176 | ownerDocument: PropTypes.any, 177 | smoothScrolling: PropTypes.bool, 178 | minScrollSize: PropTypes.number 179 | }; 180 | 181 | ScrollBar.defaultProps = { 182 | type : 'vertical', 183 | smoothScrolling: false, 184 | }; 185 | 186 | export default ScrollBar; 187 | -------------------------------------------------------------------------------- /src/js/utils.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const react13 = isReact13(React); 4 | var didWarnAboutChild = false; 5 | 6 | export function findDOMNode(component){ 7 | if(!react13){ 8 | return component; 9 | }else{ 10 | return React.findDOMNode(component); 11 | } 12 | } 13 | 14 | export function warnAboutFunctionChild() { 15 | if (didWarnAboutChild || react13) { 16 | return; 17 | } 18 | 19 | didWarnAboutChild = true; 20 | console.error('With React 0.14 and later versions, you no longer need to wrap child into a function.'); 21 | } 22 | 23 | export function warnAboutElementChild() { 24 | if (didWarnAboutChild || !react13) { 25 | return; 26 | } 27 | 28 | didWarnAboutChild = true; 29 | console.error( 'With React 0.13, you need to wrap child into a function.' ); 30 | } 31 | 32 | export function positiveOrZero(number){ 33 | return number < 0 ? 0 : number; 34 | } 35 | 36 | export function modifyObjValues (obj, modifier = x => x){ 37 | let modifiedObj = {}; 38 | for(let key in obj){ 39 | if(obj.hasOwnProperty(key)) modifiedObj[key] = modifier(obj[key]); 40 | } 41 | 42 | return modifiedObj; 43 | } 44 | 45 | export function isReact13(React) { 46 | const { version } = React; 47 | if (typeof version !== 'string') { 48 | return true; 49 | } 50 | 51 | const parts = version.split('.'); 52 | const major = parseInt(parts[0], 10); 53 | const minor = parseInt(parts[1], 10); 54 | 55 | return major === 0 && minor === 13; 56 | } 57 | -------------------------------------------------------------------------------- /src/less/scrollArea.less: -------------------------------------------------------------------------------- 1 | .scrollarea-content{ 2 | margin: 0; 3 | padding: 0; 4 | overflow: hidden; 5 | position: relative; 6 | touch-action: none; 7 | 8 | &:focus { 9 | outline: 0; 10 | } 11 | } 12 | 13 | .scrollarea{ 14 | position: relative; 15 | overflow: hidden; 16 | 17 | .scrollbar-container{ 18 | 19 | &.horizontal{ 20 | width: 100%; 21 | height: 10px; 22 | left: 0; 23 | bottom: 0; 24 | 25 | .scrollbar{ 26 | width: 20px; 27 | height: 8px; 28 | background: black; 29 | margin-top: 1px; 30 | } 31 | } 32 | 33 | &.vertical{ 34 | width: 10px; 35 | height: 100%; 36 | right: 0; 37 | top: 0; 38 | 39 | .scrollbar{ 40 | width: 8px; 41 | height: 20px; 42 | background: black; 43 | margin-left: 1px; 44 | } 45 | } 46 | 47 | position: absolute; 48 | background: none; 49 | opacity: .1; 50 | z-index: 99; 51 | 52 | -webkit-transition: all .4s; 53 | -moz-transition: all .4s; 54 | -ms-transition: all .4s; 55 | -o-transition: all .4s; 56 | transition: all .4s; 57 | 58 | &:hover{ 59 | background: gray; 60 | opacity: .6 !important; 61 | } 62 | 63 | &.active{ 64 | background: gray; 65 | opacity: .6 !important; 66 | } 67 | } 68 | 69 | &:hover .scrollbar-container{ 70 | opacity: .3; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /test/scrollArea.spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import expectJSX from 'expect-jsx'; 3 | expect.extend(expectJSX); 4 | 5 | import React, { Component } from 'react'; 6 | import {render} from 'react-dom'; 7 | import TestUtils from 'react-addons-test-utils'; 8 | import ScrollArea from '../src/js/ScrollArea'; 9 | import Scrollbar from '../src/js/Scrollbar'; 10 | 11 | function setup(props, sizes){ 12 | let renderer = TestUtils.createRenderer(); 13 | renderer.render(

content

); 14 | let instance = getRendererComponentInstance(renderer); 15 | 16 | if(sizes){ 17 | instance.computeSizes = () => sizes; 18 | instance.setSizesToState(); 19 | } 20 | 21 | let output = renderer.getRenderOutput(); 22 | 23 | let wrapper = output.props.children(); 24 | 25 | let content = wrapper.props.children[0]; 26 | let scrollbars = wrapper.props.children.filter(element => element && element.type == Scrollbar); 27 | 28 | return { 29 | wrapper, 30 | content, 31 | scrollbars, 32 | renderer, 33 | output, 34 | instance 35 | }; 36 | } 37 | 38 | function setupComponentWithMockedSizes(props) { 39 | let component = setup(props, { 40 | realHeight: 300, 41 | containerHeight: 100, 42 | realWidth: 300, 43 | containerWidth: 100 44 | }); 45 | 46 | return component; 47 | } 48 | 49 | function getRendererComponentInstance(renderer){ 50 | return renderer._instance? renderer._instance._instance : null; 51 | } 52 | 53 | describe('ScrollArea component', () => { 54 | it('Should render children and both scrollbars', () => { 55 | let {scrollbars, content} = setupComponentWithMockedSizes(); 56 | 57 | expect(scrollbars.length).toBe(2); 58 | 59 | expect(content).toEqualJSX( 60 |
{}} 61 | style={{}} 62 | className="scrollarea-content " 63 | onTouchStart={() => {}} 64 | onTouchMove={() => {}} 65 | onTouchEnd={() => {}} 66 | onKeyDown={() => {}} 67 | tabIndex={1} 68 | > 69 |

content

70 |
); 71 | }); 72 | 73 | it('Should render with tabindex set', () => { 74 | let {scrollbars, content} = setupComponentWithMockedSizes({focusableTabIndex: 100}); 75 | 76 | expect(scrollbars.length).toBe(2); 77 | expect(content).toEqualJSX( 78 |
{}} 79 | style={{}} 80 | tabIndex={100} 81 | className="scrollarea-content " 82 | onTouchStart={() => {}} 83 | onTouchMove={() => {}} 84 | onTouchEnd={() => {}} 85 | onKeyDown={() => {}} 86 | > 87 |

content

88 |
); 89 | }); 90 | 91 | it('Could render only vertical scrollbar', () => { 92 | let {scrollbars} = setupComponentWithMockedSizes({vertical: true, horizontal: false}); 93 | let scrollbar = scrollbars[0]; 94 | 95 | expect(scrollbars.length).toBe(1); 96 | expect(scrollbar.props.type).toBe('vertical'); 97 | }); 98 | 99 | it('Could render only horizontal scrollbar', () => { 100 | let {scrollbars} = setupComponentWithMockedSizes({vertical: false, horizontal: true}); 101 | let scrollbar = scrollbars[0]; 102 | 103 | expect(scrollbars.length).toBe(1); 104 | expect(scrollbar.props.type).toBe('horizontal'); 105 | }); 106 | 107 | it('Should change content element class when contentClassName prop is used', () => { 108 | let {content} = setup({contentClassName: 'test-class'}); 109 | 110 | expect(content.props.className).toInclude('test-class'); 111 | }); 112 | 113 | it('Should have proper element style when contentStyle prop is used', () => { 114 | let {content, instance} = setupComponentWithMockedSizes({ 115 | contentStyle: {test: 'contentStyle'}, 116 | }); 117 | 118 | expect(content.props.style).toEqual({ test: 'contentStyle' }); 119 | }); 120 | 121 | it('Should have proper scrollbars styles', () => { 122 | let {content, scrollbars} = setupComponentWithMockedSizes({ 123 | vertical: true, 124 | verticalScrollbarStyle: {test: 'verticalScrollbarStyle'}, 125 | verticalContainerStyle: {test: 'verticalContainerStyle'}, 126 | horizontal: true, 127 | horizontalScrollbarStyle: {test: 'horizontalScrollbarStyle'}, 128 | horizontalContainerStyle: {test: 'horizontalContainerStyle'} 129 | }); 130 | 131 | let verticalScrollbar = scrollbars.filter(component => component.props.type === 'vertical')[0]; 132 | let horizontalScrollbar = scrollbars.filter(component => component.props.type === 'horizontal')[0]; 133 | 134 | expect(verticalScrollbar.props.containerStyle).toEqual({test: 'verticalContainerStyle'}); 135 | expect(verticalScrollbar.props.scrollbarStyle).toEqual({test: 'verticalScrollbarStyle'}); 136 | expect(horizontalScrollbar.props.containerStyle).toEqual({test: 'horizontalContainerStyle'}); 137 | expect(horizontalScrollbar.props.scrollbarStyle).toEqual({test: 'horizontalScrollbarStyle'}); 138 | }); 139 | 140 | it('normalizeTopPosition() should returns proper value', () => { 141 | let {instance} = setup(); 142 | let {normalizeTopPosition} = instance; 143 | let sizes = {realHeight: 30, containerHeight: 20}; 144 | 145 | expect(normalizeTopPosition(0, sizes)).toBe(0); 146 | expect(normalizeTopPosition(5, sizes)).toBe(5); 147 | expect(normalizeTopPosition(10, sizes)).toBe(10); 148 | expect(normalizeTopPosition(11, sizes)).toBe(10); 149 | expect(normalizeTopPosition(-1, sizes)).toBe(0); 150 | expect(normalizeTopPosition(-60, sizes)).toBe(0); 151 | expect(normalizeTopPosition(100, sizes)).toBe(10); 152 | }); 153 | 154 | it('normalizeLeftPosition() should returns proper value', () => { 155 | let {instance} = setup(); 156 | let {normalizeLeftPosition} = instance; 157 | let sizes = {realWidth: 30, containerWidth: 20}; 158 | 159 | expect(normalizeLeftPosition(0, sizes)).toBe(0); 160 | expect(normalizeLeftPosition(5, sizes)).toBe(5); 161 | expect(normalizeLeftPosition(10, sizes)).toBe(10); 162 | expect(normalizeLeftPosition(11, sizes)).toBe(10); 163 | expect(normalizeLeftPosition(-1, sizes)).toBe(0); 164 | expect(normalizeLeftPosition(-60, sizes)).toBe(0); 165 | expect(normalizeLeftPosition(100, sizes)).toBe(10); 166 | }); 167 | 168 | it('handleWheel method work properly when scrolling down', () => { 169 | let {instance} = setupComponentWithMockedSizes(); 170 | let e = {deltaY:20, deltaX: 0, preventDefault: () => {}, stopPropagation: () => {}}; 171 | instance.handleWheel(e); 172 | 173 | expect(instance.state.topPosition).toBe(20); 174 | }); 175 | 176 | it('handleWheel method work properly when scrolling up and actual topPosition is 0', () => { 177 | let {instance} = setupComponentWithMockedSizes(); 178 | let e = {deltaY:-10, deltaX: 0, preventDefault: () => {}, stopPropagation: () => {}}; 179 | instance.handleWheel(e); 180 | 181 | expect(instance.state.topPosition).toBe(0); 182 | }); 183 | 184 | it('handleWheel method work properly when scrolling down more then content height', () => { 185 | let {instance} = setupComponentWithMockedSizes(); 186 | 187 | for(let i = 0; i < 10; i++){ 188 | let e = {deltaY:50, deltaX: 0, preventDefault: () => {}, stopPropagation: () => {}}; 189 | instance.handleWheel(e); 190 | } 191 | 192 | expect(instance.state.topPosition).toBe(200); 193 | }); 194 | 195 | it('handleWheel method work properly when scrolling right', () => { 196 | let {instance} = setupComponentWithMockedSizes(); 197 | let e = {deltaY:0, deltaX: 20, preventDefault: () => {}, stopPropagation: () => {}}; 198 | instance.handleWheel(e); 199 | 200 | expect(instance.state.leftPosition).toBe(20); 201 | }); 202 | 203 | it('handleWheel method work properly when scrolling left and actual leftPosition is 0', () => { 204 | let {instance} = setupComponentWithMockedSizes(); 205 | let e = {deltaY:0, deltaX: -10, preventDefault: () => {}, stopPropagation: () => {}}; 206 | instance.handleWheel(e); 207 | 208 | expect(instance.state.leftPosition).toBe(0); 209 | }); 210 | 211 | it('handleWheel method work properly when scrolling right more then content height', () => { 212 | let {instance} = setupComponentWithMockedSizes(); 213 | 214 | for(let i = 0; i < 10; i++){ 215 | let e = {deltaY:0, deltaX: 50, preventDefault: () => {}, stopPropagation: () => {}}; 216 | instance.handleWheel(e); 217 | } 218 | 219 | expect(instance.state.leftPosition).toBe(200); 220 | }); 221 | 222 | it('handleWheel method should scroll down on scrollRight wheel event when revertWheelAxes prop is set to true', () => { 223 | let {instance} = setupComponentWithMockedSizes({ 224 | swapWheelAxes: true 225 | }); 226 | 227 | let e = {deltaY:0, deltaX: 20, preventDefault: () => {}, stopPropagation: () => {}}; 228 | instance.handleWheel(e); 229 | 230 | expect(instance.state.topPosition).toBe(20); 231 | }); 232 | 233 | it('handleWheel method should scroll down on scrollRight wheel event when revertWheelAxes prop is set to true', () => { 234 | let {instance} = setupComponentWithMockedSizes({ 235 | swapWheelAxes: true 236 | }); 237 | 238 | let e = {deltaY:20, deltaX: 0, preventDefault: () => {}, stopPropagation: () => {}}; 239 | instance.handleWheel(e); 240 | 241 | expect(instance.state.leftPosition).toBe(20); 242 | }); 243 | 244 | it('handleKeyDown method works properly when pressing key down', () => { 245 | let {instance} = setupComponentWithMockedSizes(); 246 | 247 | let e = {keyCode:40, target:{tagName:'div'}, preventDefault: () => {}, stopPropagation: () => {}}; 248 | instance.handleKeyDown(e); 249 | 250 | expect(instance.state.topPosition).toBe(10); 251 | }); 252 | 253 | it('handleKeyDown method works properly when pressing key right', () => { 254 | let {instance} = setupComponentWithMockedSizes(); 255 | 256 | let e = {keyCode:39, target:{tagName:'div'}, preventDefault: () => {}, stopPropagation: () => {}}; 257 | instance.handleKeyDown(e); 258 | 259 | expect(instance.state.leftPosition).toBe(10); 260 | }); 261 | 262 | it('handleKeyDown method works properly when pressing key page down', () => { 263 | let {instance} = setupComponentWithMockedSizes(); 264 | 265 | let e = {keyCode:34, target:{tagName:'div'}, preventDefault: () => {}, stopPropagation: () => {}}; 266 | instance.handleKeyDown(e); 267 | 268 | expect(instance.state.topPosition).toBe(instance.state.containerHeight - 10); 269 | }); 270 | 271 | it('handleKeyDown method should not scroll if input element is selected', () => { 272 | let {instance} = setupComponentWithMockedSizes(); 273 | 274 | let e = {keyCode:40, target:{tagName:'input'}, preventDefault: () => {}, stopPropagation: () => {}}; 275 | instance.handleKeyDown(e); 276 | 277 | expect(instance.state.topPosition).toBe(0); 278 | }); 279 | 280 | it('scrollBottom() method should work when content is smaller then container', () => { 281 | let {instance} = setup({}, { 282 | realHeight: 30, 283 | containerHeight: 100, 284 | realWidth: 30, 285 | containerWidth: 100 286 | }); 287 | 288 | instance.scrollBottom(); 289 | expect(instance.state.topPosition).toBe(0); 290 | }); 291 | 292 | 293 | it('scrollBottom() method should scroll to bottom', () => { 294 | let {instance} = setup({}, { 295 | realHeight: 200, 296 | containerHeight: 100, 297 | realWidth: 200, 298 | containerWidth: 100 299 | }); 300 | 301 | instance.scrollBottom(); 302 | expect(instance.state.topPosition).toBe(100); 303 | }); 304 | 305 | it('scrollBottom() should be impossible when there is disabled vertical scroll', () => { 306 | let {instance} = setup({vertical: false}, { 307 | realHeight: 200, 308 | containerHeight: 100, 309 | realWidth: 200, 310 | containerWidth: 100 311 | }); 312 | 313 | instance.scrollBottom(); 314 | expect(instance.state.topPosition).toBe(0); 315 | }); 316 | 317 | it('scrollRight() method should work when content is smaller then container', () => { 318 | let {instance} = setup({}, { 319 | realHeight: 30, 320 | containerHeight: 100, 321 | realWidth: 30, 322 | containerWidth: 100 323 | }); 324 | 325 | instance.scrollRight(); 326 | expect(instance.state.topPosition).toBe(0); 327 | }); 328 | 329 | 330 | it('scrollRight() method should scroll to right', () => { 331 | let {instance} = setup({}, { 332 | realHeight: 200, 333 | containerHeight: 100, 334 | realWidth: 200, 335 | containerWidth: 100 336 | }); 337 | 338 | instance.scrollRight(); 339 | expect(instance.state.leftPosition).toBe(100); 340 | }); 341 | 342 | it('scrollRight() should be impossible when there is disabled horizontal scroll', () => { 343 | let {instance} = setup({horizontal: false}, { 344 | realHeight: 200, 345 | containerHeight: 100, 346 | realWidth: 200, 347 | containerWidth: 100 348 | }); 349 | 350 | instance.scrollRight(); 351 | expect(instance.state.topPosition).toBe(0); 352 | }); 353 | 354 | it('scrollLeft() method should scroll to left', () => { 355 | let {instance} = setup({}, { 356 | realHeight: 200, 357 | containerHeight: 100, 358 | realWidth: 200, 359 | containerWidth: 100 360 | }); 361 | instance.scrollXTo(50); 362 | 363 | instance.scrollLeft(); 364 | expect(instance.state.leftPosition).toBe(0); 365 | }); 366 | 367 | it('scrollTop() method should scroll to top', () => { 368 | let {instance} = setup({}, { 369 | realHeight: 200, 370 | containerHeight: 100, 371 | realWidth: 200, 372 | containerWidth: 100 373 | }); 374 | instance.scrollYTo(50); 375 | 376 | instance.scrollTop(); 377 | expect(instance.state.topPosition).toBe(0); 378 | }); 379 | }); 380 | -------------------------------------------------------------------------------- /test/scrollBar.spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import expectJSX from 'expect-jsx'; 3 | expect.extend(expectJSX); 4 | 5 | import React, { Component } from 'react'; 6 | import {render} from 'react-dom'; 7 | import TestUtils from 'react-addons-test-utils'; 8 | import ScrollArea from '../src/js/ScrollArea'; 9 | import Scrollbar from '../src/js/Scrollbar'; 10 | 11 | function setupScrollbar(props){ 12 | let renderer = TestUtils.createRenderer(); 13 | renderer.render(); 14 | let instance = getRendererComponentInstance(renderer); 15 | let output = renderer.getRenderOutput(); 16 | let wrapper = output.props.children(); 17 | 18 | let content = wrapper.props.children; 19 | 20 | return { 21 | wrapper, 22 | content, 23 | renderer, 24 | output, 25 | instance 26 | }; 27 | } 28 | 29 | function getRendererComponentInstance(renderer){ 30 | return renderer._instance? renderer._instance._instance : null; 31 | } 32 | 33 | describe('ScrollBar component', () => { 34 | it('Vertical should have proper class', () => { 35 | let {wrapper} = setupScrollbar({type: 'vertical'}); 36 | 37 | expect(wrapper.props.className).toInclude('vertical'); 38 | }); 39 | 40 | it('Vertical should have proper container styles', () => { 41 | let {wrapper} = setupScrollbar({ 42 | type: 'vertical', 43 | containerStyle: {test: 'containerStyle'}, 44 | }); 45 | 46 | expect(wrapper.props.style).toEqual({test: 'containerStyle'}); 47 | }); 48 | 49 | it('Vertical should have proper scrollbar styles', () => { 50 | let {content} = setupScrollbar({ 51 | type: 'vertical', 52 | scrollbarStyle: {test: 'scrollbarStyle'}, 53 | }); 54 | 55 | expect(content.props.style).toEqual({test: 'scrollbarStyle'}); 56 | }); 57 | 58 | it('Horizontal should have proper class', () => { 59 | let {wrapper} = setupScrollbar({type: 'horizontal'}); 60 | 61 | expect(wrapper.props.className).toInclude('horizontal'); 62 | }); 63 | 64 | it('Horizontal should have proper container styles', () => { 65 | let {wrapper} = setupScrollbar({ 66 | type: 'horizontal', 67 | containerStyle: {test: 'containerStyle'}, 68 | }); 69 | 70 | expect(wrapper.props.style).toEqual({test: 'containerStyle'}); 71 | }); 72 | 73 | it('Horizontal should have proper scrollbar styles', () => { 74 | let {content} = setupScrollbar({ 75 | type: 'horizontal', 76 | scrollbarStyle: {test: 'scrollbarStyle'}, 77 | }); 78 | 79 | expect(content.props.style).toEqual({test: 'scrollbarStyle'}); 80 | }); 81 | 82 | it('ScrollBar should be in proper position', () => { 83 | let {instance} = setupScrollbar({ 84 | realSize: 400, 85 | containerSize: 100, 86 | position: 20 87 | }); 88 | 89 | expect(instance.state.position).toBe(5); 90 | }); 91 | 92 | it('ScrollBar should have proper size', () => { 93 | let { instance } = setupScrollbar({ 94 | realSize: 400, 95 | containerSize: 100 96 | }); 97 | 98 | expect(instance.state.scrollSize).toBe(25); 99 | }); 100 | 101 | it('Should propagate onMove event after move vertical scrollbar', () => { 102 | let handleMoveSpy = expect.createSpy(); 103 | let {instance} = setupScrollbar({ 104 | realSize: 200, 105 | containerSize: 100, 106 | onMove: handleMoveSpy, 107 | onFocus: () => {} 108 | }); 109 | let mouseDownEvent = {clientY: 0, preventDefault: () => {}, stopPropagation: () => {}}; 110 | let moveEvent = {clientY: 25, preventDefault: () => {}}; 111 | instance.handleMouseDown(mouseDownEvent); 112 | instance.handleMouseMoveForVertical(moveEvent); 113 | 114 | expect(handleMoveSpy.calls.length).toEqual(1); 115 | expect(handleMoveSpy.calls[0].arguments).toEqual([-50 , 0]); 116 | }); 117 | 118 | it('Should propagate onMove event after move horizontal scrollbar', () => { 119 | let handleMoveSpy = expect.createSpy(); 120 | let {instance} = setupScrollbar({ 121 | realSize: 200, 122 | containerSize: 100, 123 | onMove: handleMoveSpy, 124 | type: 'horizontal', 125 | onFocus: () => {} 126 | }); 127 | let mouseDownEvent = {clientX: 0, preventDefault: () => {}, stopPropagation: () => {}}; 128 | let moveEvent = {clientX: 25, preventDefault: () => {}}; 129 | instance.handleMouseDown(mouseDownEvent); 130 | instance.handleMouseMoveForHorizontal(moveEvent); 131 | 132 | expect(handleMoveSpy.calls.length).toEqual(1); 133 | expect(handleMoveSpy.calls[0].arguments).toEqual([0, -50]); 134 | }); 135 | 136 | it('Should propagate onMove event multiple times', () => { 137 | let handleMoveSpy = expect.createSpy(); 138 | let {instance} = setupScrollbar({ 139 | realSize: 400, 140 | containerSize: 100, 141 | onMove: handleMoveSpy, 142 | onFocus: () => {} 143 | }); 144 | let mouseDownEvent = {clientY: 0, preventDefault: () => {}, stopPropagation: () => {}}; 145 | let moveEvent = {clientY: 10, preventDefault: () => {}}; 146 | instance.handleMouseDown(mouseDownEvent); 147 | instance.handleMouseMoveForVertical(moveEvent); 148 | moveEvent.clientY = 20; 149 | instance.handleMouseMoveForVertical(moveEvent); 150 | moveEvent.clientY = 30; 151 | instance.handleMouseMoveForVertical(moveEvent); 152 | moveEvent.clientY = 40; 153 | instance.handleMouseMoveForVertical(moveEvent); 154 | 155 | expect(handleMoveSpy.calls.length).toEqual(4); 156 | expect(handleMoveSpy.calls[3].arguments).toEqual([-40 , 0]); 157 | }); 158 | 159 | it('Should be possible to set min scrollbar size', () => { 160 | let minScrollBarSize = 10; 161 | let {instance} = setupScrollbar({ 162 | realSize: 10000, 163 | containerSize: 100, 164 | type: 'vertical', 165 | minScrollSize: minScrollBarSize, 166 | onFocus: () => {} 167 | }); 168 | 169 | expect(instance.state.scrollSize).toBe(minScrollBarSize); 170 | }); 171 | 172 | it('Method calculateFractionalPosition should work properly for realSize: 300, containerSize: 100, position: 0', () => { 173 | let {instance} = setupScrollbar(); 174 | 175 | expect(instance.calculateFractionalPosition(300, 100, 0)).toEqual(0); 176 | }); 177 | 178 | it('Method calculateFractionalPosition should work properly for realSize: 300, containerSize: 100, position: 200', () => { 179 | let {instance} = setupScrollbar(); 180 | 181 | expect(instance.calculateFractionalPosition(300, 100, 200)).toEqual(1); 182 | }); 183 | 184 | it('Method calculateFractionalPosition should work properly for realSize: 300, containerSize: 100, position: 200', () => { 185 | let {instance} = setupScrollbar(); 186 | 187 | expect(instance.calculateFractionalPosition(300, 100, 100)).toEqual(0.5); 188 | }); 189 | 190 | it('Method calculateFractionalPosition should work properly for realSize: 160, containerSize: 80, position: 20', () => { 191 | let {instance} = setupScrollbar(); 192 | 193 | expect(instance.calculateFractionalPosition(160, 80, 20)).toEqual(0.25); 194 | }); 195 | 196 | it('Position of scrollbar should be proper when minScrollBarSize is set', () => { 197 | let {instance} = setupScrollbar({ 198 | position: 9900, 199 | realSize: 10000, 200 | containerSize: 100, 201 | type: 'vertical', 202 | minScrollSize: 10 203 | }); 204 | 205 | expect(instance.state.position).toBe(90); 206 | }); 207 | 208 | it('vertical scrollbar container click should move scrollbar', () => { 209 | let handlePositionChangeSpy = expect.createSpy(); 210 | let {instance} = setupScrollbar({ 211 | position: 0, 212 | realSize: 500, 213 | containerSize: 100, 214 | type: 'vertical', 215 | onPositionChange: handlePositionChangeSpy 216 | }); 217 | let mouseDownEvent = {clientY: 50, preventDefault: () => {}}; 218 | instance.scrollbarContainer = { 219 | getBoundingClientRect: () => ({ 220 | top: 0, left: 0 221 | }) 222 | }; 223 | instance.handleScrollBarContainerClick(mouseDownEvent); 224 | 225 | expect(handlePositionChangeSpy.calls.length).toEqual(1); 226 | expect(handlePositionChangeSpy.calls[0].arguments).toEqual([200]); 227 | }); 228 | 229 | it('vertical scrollbar container click should move scrollbar when minScrollbar size is set', () => { 230 | let handlePositionChangeSpy = expect.createSpy(); 231 | let {instance} = setupScrollbar({ 232 | position: 0, 233 | realSize: 1000, 234 | containerSize: 100, 235 | type: 'vertical', 236 | onPositionChange: handlePositionChangeSpy, 237 | minScrollBarSize: 30 238 | }); 239 | let mouseDownEvent = {clientY: 25, preventDefault: () => {}}; 240 | instance.scrollbarContainer = { 241 | getBoundingClientRect: () => ({ 242 | top: 0, left: 0 243 | }) 244 | }; 245 | instance.handleScrollBarContainerClick(mouseDownEvent); 246 | 247 | expect(handlePositionChangeSpy.calls.length).toEqual(1); 248 | expect(handlePositionChangeSpy.calls[0].arguments).toEqual([200]); 249 | }); 250 | 251 | it('horizontal scrollbar container click should move scrollbar', () => { 252 | let handlePositionChangeSpy = expect.createSpy(); 253 | let {instance} = setupScrollbar({ 254 | position: 0, 255 | realSize: 500, 256 | containerSize: 100, 257 | type: 'horizontal', 258 | onPositionChange: handlePositionChangeSpy 259 | }); 260 | let mouseDownEvent = {clientX: 50, preventDefault: () => {}}; 261 | instance.scrollbarContainer = { 262 | getBoundingClientRect: () => ({ 263 | top: 0, left: 0 264 | }) 265 | }; 266 | instance.handleScrollBarContainerClick(mouseDownEvent); 267 | 268 | expect(handlePositionChangeSpy.calls.length).toEqual(1); 269 | expect(handlePositionChangeSpy.calls[0].arguments).toEqual([200]); 270 | }); 271 | 272 | it('horizontal scrollbar container click should move scrollbar when minScrollbar size is set', () => { 273 | let handlePositionChangeSpy = expect.createSpy(); 274 | let {instance} = setupScrollbar({ 275 | position: 0, 276 | realSize: 1000, 277 | containerSize: 100, 278 | type: 'horizontal', 279 | onPositionChange: handlePositionChangeSpy, 280 | minScrollBarSize: 30 281 | }); 282 | let mouseDownEvent = {clientX: 25, preventDefault: () => {}}; 283 | instance.scrollbarContainer = { 284 | getBoundingClientRect: () => ({ 285 | top: 0, left: 0 286 | }) 287 | }; 288 | instance.handleScrollBarContainerClick(mouseDownEvent); 289 | 290 | expect(handlePositionChangeSpy.calls.length).toEqual(1); 291 | expect(handlePositionChangeSpy.calls[0].arguments).toEqual([200]); 292 | }); 293 | }); 294 | -------------------------------------------------------------------------------- /test/tests.bundle.js: -------------------------------------------------------------------------------- 1 | const context = require.context('.', true, /.+\.spec\.jsx?$/); 2 | context.keys().forEach(context); 3 | -------------------------------------------------------------------------------- /test/utils.spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import {positiveOrZero, modifyObjValues, isReact13} from '../src/js/utils'; 3 | 4 | describe('utils', () => { 5 | describe('positiveOrZero', () => { 6 | it('should return the same value for positive', () => { 7 | let result = positiveOrZero(12); 8 | expect(result).toBe(12); 9 | }); 10 | 11 | it('should return 0 for 0 as argument', () => { 12 | let result = positiveOrZero(0); 13 | expect(result).toBe(0); 14 | }); 15 | 16 | it('should return 0 for for negative argument value', () => { 17 | let result = positiveOrZero(-12); 18 | expect(result).toBe(0); 19 | }); 20 | }); 21 | 22 | describe('modifyObjValues', () => { 23 | it('should return object with modified all keys', () => { 24 | let result = modifyObjValues({a: 1, b: 2}, x => x + 1); 25 | expect(result).toEqual({a: 2, b: 3}); 26 | }); 27 | 28 | it('should return the same object when no second argument', () => { 29 | let result = modifyObjValues({a: 1, b: 2}); 30 | expect(result).toEqual({a: 1, b: 2}); 31 | }); 32 | }); 33 | 34 | describe('isReact13', () => { 35 | it('should return true for React 0.13', () => { 36 | let reactMockup = { 37 | version: '0.13' 38 | } 39 | let result = isReact13(reactMockup); 40 | expect(result).toBe(true); 41 | }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /webpack.base.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var webpack = require('webpack'); 4 | 5 | module.exports = { 6 | resolve: { 7 | modulesDirectories: ['node_modules', 'bower_components'], 8 | extensions: ['', '.js', '.jsx'] 9 | }, 10 | 11 | output: { 12 | library: 'ScrollArea', 13 | libraryTarget: 'umd' 14 | }, 15 | 16 | externals: [ 17 | { 18 | "react": { 19 | root: "React", 20 | commonjs2: "react", 21 | commonjs: "react", 22 | amd: "react" 23 | } 24 | } 25 | ], 26 | 27 | module: { 28 | loaders: [ 29 | { 30 | test: /\.jsx?$/, exclude: /node_modules/, loader: 'babel' 31 | }, 32 | { 33 | test: /\.less$/, 34 | loader: 'style!css!less' 35 | } 36 | ] 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /webpack.dev.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | var baseConfig = require('./webpack.base.config.js'); 3 | var objectAssign = require('object-assign'); 4 | 5 | var config = objectAssign({}, baseConfig); 6 | 7 | config.devtool = "inline-source-map"; 8 | 9 | module.exports = config; -------------------------------------------------------------------------------- /webpack.production.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var webpack = require('webpack'); 4 | var baseConfig = require('./webpack.base.config.js'); 5 | var objectAssign = require('object-assign'); 6 | 7 | var config = objectAssign({}, baseConfig); 8 | 9 | config.plugins = [ 10 | new webpack.optimize.OccurenceOrderPlugin(), 11 | new webpack.DefinePlugin({ 12 | 'process.env.NODE_ENV': JSON.stringify('production') 13 | }), 14 | new webpack.optimize.UglifyJsPlugin({ 15 | compressor: { 16 | screw_ie8: true, 17 | warnings: false 18 | }, 19 | sourceMap: false 20 | }) 21 | ]; 22 | 23 | module.exports = config; 24 | -------------------------------------------------------------------------------- /webpackExamples.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | resolve: { 3 | modulesDirectories: ['node_modules', 'bower_components'], 4 | extensions: ['', '.js', '.jsx'], 5 | alias: { 6 | 'react-scrollbar/no-css': '../../src/js/ScrollAreaWithoutCss.js', 7 | 'react-scrollbar': '../../src/js/ScrollAreaWithCss.js' 8 | } 9 | }, 10 | module: { 11 | loaders: [ 12 | { test: /\.jsx?$/, exclude: /node_modules/, loader: 'babel'}, 13 | { test: /\.less$/, loader: 'style!css!less' } 14 | ] 15 | }, 16 | devtool: "inline-source-map" 17 | }; 18 | --------------------------------------------------------------------------------