├── .babelrc ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .npmignore ├── .nvmrc ├── .travis.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE.md ├── README.md ├── docs ├── API.md ├── README.md ├── customization.md ├── upgrade-guide-v2-v3.md ├── usage.md └── v2-documentation.md ├── karma.conf.js ├── package.json ├── prepublish.js ├── src ├── Scrollbars │ ├── defaultRenderElements.js │ ├── index.js │ └── styles.js ├── index.js └── utils │ ├── getInnerHeight.js │ ├── getInnerWidth.js │ ├── getScrollbarWidth.js │ ├── isString.js │ └── returnFalse.js ├── test.js ├── test ├── .eslintrc ├── Scrollbars │ ├── autoHeight.js │ ├── autoHide.js │ ├── clickTrack.js │ ├── dragThumb.js │ ├── flexbox.js │ ├── gettersSetters.js │ ├── hideTracks.js │ ├── index.js │ ├── onUpdate.js │ ├── rendering.js │ ├── resizing.js │ ├── scrolling.js │ └── universal.js ├── browser.spec.js ├── mobile.spec.js └── utils.spec.js ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "sourceMaps": true, 3 | "presets": [ 4 | "latest", 5 | "stage-0" 6 | ], 7 | "plugins": [ 8 | ["transform-react-jsx", { "pragma": "h" }] 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | lib 2 | **/node_modules 3 | **/webpack.config.js 4 | **/prepublish.js 5 | examples/**/server.js 6 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": [ 4 | "standard", 5 | ], 6 | "settings": { 7 | "react": { 8 | "pragma": "h", 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | .DS_Store 4 | dist 5 | lib 6 | coverage 7 | examples/simple/static 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.log 3 | src 4 | test 5 | examples 6 | coverage 7 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 6 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "iojs" 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change log 2 | 3 | This project adheres to [Semantic Versioning](http://semver.org/). 4 | 5 | [Have a look at the releases](https://github.com/lucafalasco/preact-custom-scrollbars/releases) 6 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. 4 | 5 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, age, or religion. 6 | 7 | Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. 8 | 9 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team. 10 | 11 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. 12 | 13 | This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/) 14 | 15 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 preact-custom-scrollbars 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 | preact-custom-scrollbars 2 | ========================= 3 | 4 | This is a port of malte-wessel's [react-custom-scrollbars](https://github.com/malte-wessel/react-custom-scrollbars) for [Preact](https://github.com/developit/preact) 5 | 6 | 7 | [![npm](https://img.shields.io/badge/npm-preact--custom--scrollbars-brightgreen.svg?style=flat-square)](https://www.npmjs.com/package/preact-custom-scrollbars) 8 | [![npm version](https://img.shields.io/npm/v/preact-custom-scrollbars.svg?style=flat-square)](https://www.npmjs.com/package/preact-custom-scrollbars) 9 | [![npm downloads](https://img.shields.io/npm/dm/preact-custom-scrollbars.svg?style=flat-square)](https://www.npmjs.com/package/preact-custom-scrollbars) 10 | 11 | * frictionless native browser scrolling 12 | * native scrollbars for mobile devices 13 | * [fully customizable](https://github.com/lucafalasco/preact-custom-scrollbars/blob/master/docs/customization.md) 14 | * [auto hide](https://github.com/lucafalasco/preact-custom-scrollbars/blob/master/docs/usage.md#auto-hide) 15 | * [auto height](https://github.com/lucafalasco/preact-custom-scrollbars/blob/master/docs/usage.md#auto-height) 16 | * [universal](https://github.com/lucafalasco/preact-custom-scrollbars/blob/master/docs/usage.md#universal-rendering) (runs on client & server) 17 | * `requestAnimationFrame` for 60fps 18 | * no extra stylesheets 19 | * well tested, 100% code coverage 20 | 21 | **[Documentation](https://github.com/lucafalasco/preact-custom-scrollbars/tree/master/docs)** 22 | 23 | ## Installation 24 | ```bash 25 | npm install preact-custom-scrollbars --save 26 | ``` 27 | or 28 | ```bash 29 | yarn add preact-custom-scrollbars 30 | ``` 31 | 32 | This assumes that you’re using [npm](http://npmjs.com/) or [yarn](https://yarnpkg.com/lang/en/) package manager with a module bundler like [Webpack](http://webpack.github.io) or [Browserify](http://browserify.org/) to consume [CommonJS modules](http://webpack.github.io/docs/commonjs.html). 33 | 34 | If you don’t yet use [npm](http://npmjs.com/) or a modern module bundler, and would rather prefer a single-file [UMD](https://github.com/umdjs/umd) build that makes `PreactCustomScrollbars` available as a global object, you can grab a pre-built version from [unpkg](https://unpkg.com/preact-custom-scrollbars/dist/preact-custom-scrollbars.js). We *don’t* recommend this approach for any serious application, as most of the libraries complementary to `preact-custom-scrollbars` are only available on [npm](http://npmjs.com/). 35 | 36 | ## Usage 37 | 38 | This is the minimal configuration. [Check out the Documentation for advanced usage](https://github.com/lucafalasco/preact-custom-scrollbars/tree/master/docs). 39 | 40 | ```javascript 41 | import { Scrollbars } from 'preact-custom-scrollbars'; 42 | 43 | class App extends Component { 44 | render() { 45 | return ( 46 | 47 |

Some great content...

48 |
49 | ); 50 | } 51 | } 52 | ``` 53 | 54 | The `` component is completely customizable. Check out the following code: 55 | 56 | ```javascript 57 | import { Scrollbars } from 'preact-custom-scrollbars'; 58 | 59 | class CustomScrollbars extends Component { 60 | render() { 61 | return ( 62 | 82 | ); 83 | } 84 | } 85 | ``` 86 | 87 | All properties are documented in the [API docs](https://github.com/lucafalasco/preact-custom-scrollbars/blob/master/docs/API.md) 88 | 89 | ## Tests 90 | ```bash 91 | # Make sure that you've installed the dependencies 92 | npm install 93 | # Run tests 94 | npm test 95 | ``` 96 | 97 | ### Code Coverage 98 | ```bash 99 | # Run code coverage. Results can be found in `./coverage` 100 | npm run test:cov 101 | ``` 102 | 103 | 104 | ## License 105 | 106 | MIT 107 | -------------------------------------------------------------------------------- /docs/API.md: -------------------------------------------------------------------------------- 1 | # API 2 | 3 | ## `` 4 | 5 | ### Props 6 | 7 | * `onScroll`: (Function) Event handler 8 | * Signature: `onScroll(event)` 9 | * `onScrollFrame`: (Function) Runs inside the animation frame. 10 | * Signature: `onScroll(values)` 11 | * `values`: (Object) Values about the current position 12 | * `values.top`: (Number) scrollTop progess, from 0 to 1 13 | * `values.left`: (Number) scrollLeft progess, from 0 to 1 14 | * `values.clientWidth`: (Number) Width of the view 15 | * `values.clientHeight`: (Number) Height of the view 16 | * `values.scrollWidth`: (Number) Native scrollWidth 17 | * `values.scrollHeight`: (Number) Native scrollHeight 18 | * `values.scrollLeft`: (Number) Native scrollLeft 19 | * `values.scrollTop`: (Number) Native scrollTop 20 | * `onScrollStart` (Function) Called when scrolling starts 21 | * `onScrollStop` (Function) Called when scrolling stops 22 | * `onUpdate` (Function) Called when ever the component is updated. Runs inside the animation frame 23 | * Signature: `onUpdate(values)` 24 | * `renderView`: (Function) The element your content will be rendered in 25 | * `renderTrackHorizontal`: (Function) Horizontal track element 26 | * `renderTrackVertical`: (Function) Vertical track element 27 | * `renderThumbHorizontal`: (Function) Horizontal thumb element 28 | * `renderThumbVertical`: (Function) Vertical thumb element 29 | * `hideTracksWhenNotNeeded`: (Boolean) Hide tracks (`visibility: hidden`) when content does not overflow container. (default: false) 30 | * `thumbSize`: (Number) Set a fixed size for thumbs in px. 31 | * `thumbMinSize`: (Number) Minimal thumb size in px. (default: 30) 32 | * `autoHide`: (Boolean) Enable auto-hide mode (default: `false`) 33 | * When `true` tracks will hide automatically and are only visible while scrolling. 34 | * `autoHideTimeout`: (Number) Hide delay in ms. (default: 1000) 35 | * `autoHideDuration`: (Number) Duration for hide animation in ms. (default: 200) 36 | * `autoHeight`: (Boolean) Enable auto-height mode. (default: false) 37 | * When `true` container grows with content 38 | * `autoHeightMin`: (Number) Set a minimum height for auto-height mode (default: 0) 39 | * `autoHeightMax`: (Number) Set a maximum height for auto-height mode (default: 200) 40 | * `universal`: (Boolean) Enable universal rendering (default: `false`) 41 | * [Learn how to use universal rendering](#link) 42 | 43 | ### Methods 44 | 45 | * `scrollTop(top)`: scroll to the top value 46 | * `scrollLeft(left)`: scroll to the left value 47 | * `scrollToTop()`: scroll to top 48 | * `scrollToBottom()`: scroll to bottom 49 | * `scrollToLeft()`: scroll to left 50 | * `scrollToRight()`: scroll to right 51 | * `getScrollLeft()`: get scrollLeft value 52 | * `getScrollTop()`: get scrollTop value 53 | * `getScrollWidth()`: get scrollWidth value 54 | * `getScrollHeight()`: get scrollHeight value 55 | * `getClientWidth()`: get view client width 56 | * `getClientHeight()`: get view client height 57 | * `getValues()`: get an object with values about the current position. 58 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Table of Contents 2 | 3 | * [Usage](usage.md) 4 | * [Auto hide](usage.md#auto-hide) 5 | * [Auto height](usage.md#auto-height) 6 | * [Working with events](usage.md#events) 7 | * [Universal rendering](usage.md#universal-rendering) 8 | * [Customization](customization.md) 9 | * [API](API.md) 10 | 11 | ## Older versions 12 | * [Upgrade guide from v2.x to v3.x](upgrade-guide-v2-v3.md) 13 | * [v2.x documentation](v2-documentation.md) 14 | -------------------------------------------------------------------------------- /docs/customization.md: -------------------------------------------------------------------------------- 1 | # Customization 2 | 3 | The `` component consists of the following elements: 4 | 5 | * `view` The element your content is rendered in 6 | * `trackHorizontal` The horizontal scrollbars track 7 | * `trackVertical` The vertical scrollbars track 8 | * `thumbHorizontal` The horizontal thumb 9 | * `thumbVertical` The vertical thumb 10 | 11 | Each element can be **rendered individually** with a function that you pass to the component. Say, you want use your own `className` for each element: 12 | 13 | ```javascript 14 | import { Scrollbars } from 'preact-custom-scrollbars'; 15 | 16 | class CustomScrollbars extends Component { 17 | render() { 18 | return ( 19 |
} 21 | renderTrackVertical={props =>
} 22 | renderThumbHorizontal={props =>
} 23 | renderThumbVertical={props =>
} 24 | renderView={props =>
}> 25 | {this.props.children} 26 | 27 | ); 28 | } 29 | } 30 | 31 | class App extends Component { 32 | render() { 33 | return ( 34 | 35 |

Some great content...

36 |
37 | ); 38 | } 39 | } 40 | ``` 41 | 42 | **Important**: **You will always need to pass through the given props** for the respective element like in the example above: `
`. 43 | This is because we need to pass some default `styles` down to the element in order to make the component work. 44 | 45 | If you are working with **inline styles**, you could do something like this: 46 | 47 | ```javascript 48 | import { Scrollbars } from 'preact-custom-scrollbars'; 49 | 50 | class CustomScrollbars extends Component { 51 | render() { 52 | return ( 53 | 55 |
56 | }> 57 | {this.props.children} 58 | 59 | ); 60 | } 61 | } 62 | ``` 63 | 64 | ## Respond to scroll events 65 | 66 | If you want to change the appearance in respond to the scrolling position, you could do that like: 67 | 68 | ```javascript 69 | import { Scrollbars } from 'preact-custom-scrollbars'; 70 | class CustomScrollbars extends Component { 71 | constructor(props, context) { 72 | super(props, context) 73 | this.state = { top: 0 }; 74 | this.handleScrollFrame = this.handleScrollFrame.bind(this); 75 | this.renderView = this.renderView.bind(this); 76 | } 77 | 78 | handleScrollFrame(values) { 79 | const { top } = values; 80 | this.setState({ top }); 81 | } 82 | 83 | renderView({ style, ...props }) { 84 | const { top } = this.state; 85 | const color = top * 255; 86 | const customStyle = { 87 | backgroundColor: `rgb(${color}, ${color}, ${color})` 88 | }; 89 | return ( 90 |
91 | ); 92 | } 93 | 94 | render() { 95 | return ( 96 | 100 | ); 101 | } 102 | } 103 | ``` 104 | 105 | Check out these examples for some inspiration: 106 | * [ColoredScrollbars](https://github.com/malte-wessel/preact-custom-scrollbars/tree/master/examples/simple/components/ColoredScrollbars) 107 | * [ShadowScrollbars](https://github.com/malte-wessel/preact-custom-scrollbars/tree/master/examples/simple/components/ShadowScrollbars) 108 | -------------------------------------------------------------------------------- /docs/upgrade-guide-v2-v3.md: -------------------------------------------------------------------------------- 1 | # Upgrade guide from 2.x to 3.x 2 | 3 | ## Render functions 4 | 5 | ```javascript 6 | // v2.x 7 |
} 9 | renderScrollbarVertical={props =>
}> 10 | {/* */} 11 | 12 | 13 | // v3.x 14 |
} 16 | renderTrackVertical={props =>
}> 17 | {/* */} 18 | 19 | ``` 20 | 21 | ## onScroll handler 22 | 23 | ```javascript 24 | // v2.x 25 | { 27 | // do something with event 28 | // do something with values, animate 29 | }}> 30 | {/* */} 31 | 32 | 33 | // v3.x 34 | { 36 | // do something with event 37 | }} 38 | onScrollFrame={values => { 39 | // do something with values, animate 40 | // runs inside animation frame 41 | }}> 42 | {/* */} 43 | 44 | ``` 45 | -------------------------------------------------------------------------------- /docs/usage.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | ## Default Scrollbars 4 | 5 | The `` component works out of the box with some default styles. The only thing you need to care about is that the component has a `width` and `height`: 6 | 7 | ```javascript 8 | import { Scrollbars } from 'preact-custom-scrollbars'; 9 | 10 | class App extends Component { 11 | render() { 12 | return ( 13 | 14 |

Some great content...

15 |
16 | ); 17 | } 18 | } 19 | ``` 20 | 21 | Also don't forget to set the `viewport` meta tag, if you want to **support mobile devices** 22 | 23 | ```html 24 | 27 | ``` 28 | 29 | ## Events 30 | 31 | There are several events you can listen to: 32 | 33 | ```javascript 34 | import { Scrollbars } from 'preact-custom-scrollbars'; 35 | 36 | class App extends Component { 37 | render() { 38 | return ( 39 | 48 | // Called when ever the component is updated. Runs inside the animation frame 49 | onUpdate={this.handleUpdate} 50 |

Some great content...

51 |
52 | ); 53 | } 54 | } 55 | ``` 56 | 57 | 58 | ## Auto-hide 59 | 60 | You can activate auto-hide by setting the `autoHide` property. 61 | 62 | ```javascript 63 | import { Scrollbars } from 'preact-custom-scrollbars'; 64 | 65 | class App extends Component { 66 | render() { 67 | return ( 68 | 75 |

Some great content...

76 |
77 | ); 78 | } 79 | } 80 | ``` 81 | 82 | ## Auto-height 83 | 84 | You can active auto-height by setting the `autoHeight` property. 85 | ```javascript 86 | import { Scrollbars } from 'preact-custom-scrollbars'; 87 | 88 | class App extends Component { 89 | render() { 90 | return ( 91 | 96 |

Some great content...

97 |
98 | ); 99 | } 100 | } 101 | ``` 102 | 103 | ## Universal rendering 104 | 105 | If your app runs on both client and server, activate the `universal` mode. This will ensure that the initial markup on client and server are the same: 106 | 107 | ```javascript 108 | import { Scrollbars } from 'preact-custom-scrollbars'; 109 | 110 | class App extends Component { 111 | render() { 112 | return ( 113 | // This will activate universal mode 114 | 115 |

Some great content...

116 |
117 | ); 118 | } 119 | } 120 | ``` 121 | -------------------------------------------------------------------------------- /docs/v2-documentation.md: -------------------------------------------------------------------------------- 1 | # v2.x Documentation 2 | ## Table of Contents 3 | 4 | - [Customization](#customization) 5 | - [API](#api) 6 | 7 | ## Customization 8 | ```javascript 9 | import { Scrollbars } from 'preact-custom-scrollbars'; 10 | 11 | class CustomScrollbars extends Component { 12 | render() { 13 | return ( 14 |
} 17 | renderScrollbarVertical={props =>
} 18 | renderThumbHorizontal={props =>
} 19 | renderThumbVertical={props =>
} 20 | renderView={props =>
}> 21 | {this.props.children} 22 | 23 | ); 24 | } 25 | } 26 | 27 | class App extends Component { 28 | render() { 29 | return ( 30 | 31 |

Some great content...

32 |
33 | ); 34 | } 35 | } 36 | ``` 37 | 38 | **NOTE**: If you use `renderScrollbarHorizontal`, **make sure that you define a height value** with css or inline styles. If you use `renderScrollbarVertical`, **make sure that you define a width value with** css or inline styles. 39 | 40 | ## API 41 | 42 | ### `` 43 | 44 | #### Props 45 | 46 | * `renderScrollbarHorizontal`: (Function) Horizontal scrollbar element 47 | * `renderScrollbarVertical`: (Function) Vertical scrollbar element 48 | * `renderThumbHorizontal`: (Function) Horizontal thumb element 49 | * `renderThumbVertical`: (Function) Vertical thumb element 50 | * `renderView`: (Function) The element your content will be rendered in 51 | * `onScroll`: (Function) Event handler. Will be called with the native scroll event and some handy values about the current position. 52 | * **Signature**: `onScroll(event, values)` 53 | * `event`: (Event) Native onScroll event 54 | * `values`: (Object) Values about the current position 55 | * `values.top`: (Number) scrollTop progess, from 0 to 1 56 | * `values.left`: (Number) scrollLeft progess, from 0 to 1 57 | * `values.clientWidth`: (Number) width of the view 58 | * `values.clientHeight`: (Number) height of the view 59 | * `values.scrollWidth`: (Number) native scrollWidth 60 | * `values.scrollHeight`: (Number) native scrollHeight 61 | * `values.scrollLeft`: (Number) native scrollLeft 62 | * `values.scrollTop`: (Number) native scrollTop 63 | 64 | **Don't forget to pass the received props to your custom element. Example:** 65 | 66 | **NOTE**: If you use `renderScrollbarHorizontal`, **make sure that you define a height value** with css or inline styles. If you use `renderScrollbarVertical`, **make sure that you define a width value with** css or inline styles. 67 | 68 | ```javascript 69 | import { Scrollbars } from 'preact-custom-scrollbars'; 70 | 71 | class CustomScrollbars extends Component { 72 | render() { 73 | return ( 74 |
} 77 | // Customize inline styles 78 | renderScrollbarVertical={({ style, ...props}) => { 79 | return
; 80 | }}> 81 | {this.props.children} 82 | 83 | ); 84 | } 85 | } 86 | ``` 87 | 88 | #### Methods 89 | 90 | * `scrollTop(top)`: scroll to the top value 91 | * `scrollLeft(left)`: scroll to the left value 92 | * `scrollToTop()`: scroll to top 93 | * `scrollToBottom()`: scroll to bottom 94 | * `scrollToLeft()`: scroll to left 95 | * `scrollToRight()`: scroll to right 96 | * `getScrollLeft`: get scrollLeft value 97 | * `getScrollTop`: get scrollTop value 98 | * `getScrollWidth`: get scrollWidth value 99 | * `getScrollHeight`: get scrollHeight value 100 | * `getWidth`: get view client width 101 | * `getHeight`: get view client height 102 | * `getValues`: get an object with values about the current position. 103 | * `left`, `top`, `scrollLeft`, `scrollTop`, `scrollWidth`, `scrollHeight`, `clientWidth`, `clientHeight` 104 | 105 | ```javascript 106 | import { Scrollbars } from 'preact-custom-scrollbars'; 107 | 108 | class App extends Component { 109 | handleClick() { 110 | this.refs.scrollbars.scrollToTop() 111 | }, 112 | render() { 113 | return ( 114 |
115 | 118 | {/* your content */} 119 | 120 | 123 |
124 | ); 125 | } 126 | } 127 | ``` 128 | 129 | ### Receive values about the current position 130 | 131 | ```javascript 132 | class CustomScrollbars extends Component { 133 | handleScroll(event, values) { 134 | console.log(values); 135 | /* 136 | { 137 | left: 0, 138 | top: 0.21513353115727002 139 | clientWidth: 952 140 | clientHeight: 300 141 | scrollWidth: 952 142 | scrollHeight: 1648 143 | scrollLeft: 0 144 | scrollTop: 290 145 | } 146 | */ 147 | } 148 | render() { 149 | return ( 150 | 151 | {this.props.children} 152 | 153 | ); 154 | } 155 | } 156 | ``` 157 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | /* eslint no-var: 0, no-unused-vars: 0 */ 2 | var path = require('path') 3 | var webpack = require('webpack') 4 | var runCoverage = process.env.COVERAGE === 'true' 5 | 6 | var coverageLoaders = [] 7 | var coverageReporters = [] 8 | 9 | if (runCoverage) { 10 | coverageLoaders.push({ 11 | test: /\.js$/, 12 | include: path.resolve('src/'), 13 | loader: 'isparta', 14 | }) 15 | coverageReporters.push('coverage') 16 | } 17 | 18 | module.exports = function karmaConfig(config) { 19 | config.set({ 20 | browsers: ['Chrome'], 21 | singleRun: true, 22 | frameworks: ['mocha'], 23 | files: ['./test.js'], 24 | preprocessors: { 25 | './test.js': ['webpack', 'sourcemap'], 26 | }, 27 | reporters: ['mocha'].concat(coverageReporters), 28 | webpack: { 29 | devtool: 'inline-source-map', 30 | resolve: { 31 | alias: { 32 | 'preact-custom-scrollbars': path.resolve(__dirname, './src'), 33 | }, 34 | }, 35 | module: { 36 | loaders: [{ 37 | test: /\.js$/, 38 | loader: 'babel', 39 | exclude: /(node_modules)/, 40 | }].concat(coverageLoaders), 41 | }, 42 | }, 43 | coverageReporter: { 44 | dir: 'coverage/', 45 | reporters: [ 46 | { type: 'html', subdir: 'report-html' }, 47 | { type: 'text', subdir: '.', file: 'text.txt' }, 48 | { type: 'text-summary', subdir: '.', file: 'text-summary.txt' }, 49 | ], 50 | }, 51 | }) 52 | } 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "preact-custom-scrollbars", 3 | "version": "4.0.2", 4 | "description": "Preact scrollbars component", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "clean": "rimraf lib dist", 8 | "build": "babel src --out-dir lib", 9 | "build:umd": "NODE_ENV=development webpack src/index.js dist/preact-custom-scrollbars.js", 10 | "build:umd:min": "NODE_ENV=production webpack src/index.js dist/preact-custom-scrollbars.min.js", 11 | "lint": "eslint src test examples", 12 | "test": "NODE_ENV=test karma start", 13 | "test:watch": "NODE_ENV=test karma start --auto-watch --no-single-run", 14 | "test:cov": "NODE_ENV=test COVERAGE=true karma start --single-run", 15 | "prepublish": "npm run clean && npm run build && npm run build:umd && npm run build:umd:min && node ./prepublish" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/lucafalasco/preact-custom-scrollbars.git" 20 | }, 21 | "keywords": [ 22 | "scroll", 23 | "scroller", 24 | "scrollbars", 25 | "preact-component", 26 | "preact", 27 | "custom" 28 | ], 29 | "author": "Luca Falasco", 30 | "license": "MIT", 31 | "bugs": { 32 | "url": "https://github.com/lucafalasco/preact-custom-scrollbars/issues" 33 | }, 34 | "homepage": "https://github.com/lucafalasco/preact-custom-scrollbars", 35 | "devDependencies": { 36 | "babel-cli": "^6.2.0", 37 | "babel-core": "^6.2.1", 38 | "babel-eslint": "^6.1.2", 39 | "babel-loader": "^6.2.0", 40 | "babel-plugin-transform-react-jsx": "^6.23.0", 41 | "babel-preset-es2015": "^6.1.18", 42 | "babel-preset-latest": "^6.22.0", 43 | "babel-preset-stage-0": "^6.22.0", 44 | "babel-register": "^6.3.13", 45 | "babel-runtime": "^6.3.19", 46 | "es3ify": "^0.2.1", 47 | "eslint": "^2.9.0", 48 | "eslint-config-standard": "^5.3.1", 49 | "eslint-plugin-promise": "^1.3.2", 50 | "eslint-plugin-react": "^6.9.0", 51 | "eslint-plugin-standard": "^1.3.2", 52 | "expect": "^1.6.0", 53 | "glob": "^7.0.0", 54 | "isparta-loader": "^2.0.0", 55 | "karma": "^1.1.1", 56 | "karma-chrome-launcher": "^1.0.1", 57 | "karma-cli": "^1.0.1", 58 | "karma-coverage": "^1.1.0", 59 | "karma-mocha": "^0.2.0", 60 | "karma-mocha-reporter": "^2.0.4", 61 | "karma-sourcemap-loader": "^0.3.6", 62 | "karma-webpack": "^1.6.0", 63 | "mocha": "^2.2.5", 64 | "preact": "^7.1.0", 65 | "preact-dom": "^1.0.1", 66 | "rimraf": "^2.3.4", 67 | "simulant": "^0.2.2", 68 | "webpack": "^1.9.6", 69 | "webpack-dev-server": "^1.8.2" 70 | }, 71 | "peerDependencies": { 72 | "preact": "*" 73 | }, 74 | "dependencies": { 75 | "dom-css": "^2.0.0", 76 | "raf": "^3.1.0" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /prepublish.js: -------------------------------------------------------------------------------- 1 | var glob = require('glob'); 2 | var fs = require('fs'); 3 | var es3ify = require('es3ify'); 4 | 5 | glob('./@(lib|dist)/**/*.js', function (err, files) { 6 | if (err) throw err; 7 | 8 | files.forEach(function(file) { 9 | fs.readFile(file, 'utf8', function (err, data) { 10 | if (err) throw err; 11 | fs.writeFile(file, es3ify.transform(data), function (err) { 12 | if (err) throw err 13 | console.log('es3ified ' + file); 14 | }) 15 | }) 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /src/Scrollbars/defaultRenderElements.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact' 2 | 3 | export function renderViewDefault(props) { 4 | return
5 | } 6 | 7 | export function renderTrackHorizontalDefault({ style, ...props }) { 8 | const finalStyle = { 9 | ...style, 10 | right: 2, 11 | bottom: 2, 12 | left: 2, 13 | borderRadius: 3, 14 | } 15 | return
16 | } 17 | 18 | export function renderTrackVerticalDefault({ style, ...props }) { 19 | const finalStyle = { 20 | ...style, 21 | right: 2, 22 | bottom: 2, 23 | top: 2, 24 | borderRadius: 3, 25 | } 26 | return
27 | } 28 | 29 | export function renderThumbHorizontalDefault({ style, ...props }) { 30 | const finalStyle = { 31 | ...style, 32 | cursor: 'pointer', 33 | borderRadius: 'inherit', 34 | backgroundColor: 'rgba(0,0,0,.2)', 35 | } 36 | return
37 | } 38 | 39 | export function renderThumbVerticalDefault({ style, ...props }) { 40 | const finalStyle = { 41 | ...style, 42 | cursor: 'pointer', 43 | borderRadius: 'inherit', 44 | backgroundColor: 'rgba(0,0,0,.2)', 45 | } 46 | return
47 | } 48 | -------------------------------------------------------------------------------- /src/Scrollbars/index.js: -------------------------------------------------------------------------------- 1 | import raf, { cancel as caf } from 'raf' 2 | import css from 'dom-css' 3 | import { Component, h, cloneElement } from 'preact' 4 | import isString from '../utils/isString' 5 | import getScrollbarWidth from '../utils/getScrollbarWidth' 6 | import returnFalse from '../utils/returnFalse' 7 | import getInnerWidth from '../utils/getInnerWidth' 8 | import getInnerHeight from '../utils/getInnerHeight' 9 | 10 | import { 11 | containerStyleDefault, 12 | containerStyleAutoHeight, 13 | viewStyleDefault, 14 | viewStyleAutoHeight, 15 | viewStyleUniversalInitial, 16 | trackHorizontalStyleDefault, 17 | trackVerticalStyleDefault, 18 | thumbHorizontalStyleDefault, 19 | thumbVerticalStyleDefault, 20 | disableSelectStyle, 21 | disableSelectStyleReset 22 | } from './styles' 23 | 24 | import { 25 | renderViewDefault, 26 | renderTrackHorizontalDefault, 27 | renderTrackVerticalDefault, 28 | renderThumbHorizontalDefault, 29 | renderThumbVerticalDefault 30 | } from './defaultRenderElements' 31 | 32 | export default class Scrollbars extends Component { 33 | static defaultProps = { 34 | renderView: renderViewDefault, 35 | renderTrackHorizontal: renderTrackHorizontalDefault, 36 | renderTrackVertical: renderTrackVerticalDefault, 37 | renderThumbHorizontal: renderThumbHorizontalDefault, 38 | renderThumbVertical: renderThumbVerticalDefault, 39 | tagName: 'div', 40 | thumbMinSize: 30, 41 | hideTracksWhenNotNeeded: false, 42 | autoHide: false, 43 | autoHideTimeout: 1000, 44 | autoHideDuration: 200, 45 | autoHeight: false, 46 | autoHeightMin: 0, 47 | autoHeightMax: 200, 48 | universal: false 49 | } 50 | 51 | refs = {} 52 | 53 | state = { 54 | didMountUniversal: false 55 | } 56 | 57 | componentDidMount = () => { 58 | this.addListeners() 59 | this.update() 60 | this.componentDidMountUniversal() 61 | } 62 | 63 | componentDidMountUniversal () { // eslint-disable-line react/sort-comp 64 | const { universal } = this.props 65 | if (!universal) return 66 | this.setState({ didMountUniversal: true }) 67 | } 68 | 69 | componentDidUpdate = () => { 70 | this.update() 71 | } 72 | 73 | componentWillUnmount = () => { 74 | this.unsetDomStyles() 75 | this.removeListeners() 76 | caf(this.requestFrame) 77 | clearTimeout(this.hideTracksTimeout) 78 | clearInterval(this.detectScrollingInterval) 79 | } 80 | 81 | unsetDomStyles () { 82 | const { thumbHorizontal, thumbVertical, trackHorizontal, trackVertical } = this.refs 83 | const stylesReset = { 84 | width: '', 85 | height: '', 86 | transform: '', 87 | opacity: '', 88 | visibility: '' 89 | } 90 | css(thumbVertical, stylesReset) 91 | css(thumbHorizontal, stylesReset) 92 | css(trackVertical, stylesReset) 93 | css(trackHorizontal, stylesReset) 94 | } 95 | 96 | getScrollLeft = () => { 97 | const { view } = this.refs 98 | return view.scrollLeft 99 | } 100 | 101 | getScrollTop = () => { 102 | const { view } = this.refs 103 | return view.scrollTop 104 | } 105 | 106 | getScrollWidth = () => { 107 | const { view } = this.refs 108 | return view.scrollWidth 109 | } 110 | 111 | getScrollHeight = () => { 112 | const { view } = this.refs 113 | return view.scrollHeight 114 | } 115 | 116 | getClientWidth = () => { 117 | const { view } = this.refs 118 | return view.clientWidth 119 | } 120 | 121 | getClientHeight = () => { 122 | const { view } = this.refs 123 | return view.clientHeight 124 | } 125 | 126 | getValues = () => { 127 | const { view } = this.refs 128 | const { 129 | scrollLeft, 130 | scrollTop, 131 | scrollWidth, 132 | scrollHeight, 133 | clientWidth, 134 | clientHeight 135 | } = view 136 | 137 | return { 138 | left: (scrollLeft / (scrollWidth - clientWidth)) || 0, 139 | top: (scrollTop / (scrollHeight - clientHeight)) || 0, 140 | scrollLeft, 141 | scrollTop, 142 | scrollWidth, 143 | scrollHeight, 144 | clientWidth, 145 | clientHeight 146 | } 147 | } 148 | 149 | getThumbHorizontalWidth = () => { 150 | const { thumbSize, thumbMinSize } = this.props 151 | const { view, trackHorizontal } = this.refs 152 | const { scrollWidth, clientWidth } = view 153 | const trackWidth = getInnerWidth(trackHorizontal) 154 | const width = Math.ceil(clientWidth / scrollWidth * trackWidth) 155 | if (trackWidth === width) return 0 156 | if (thumbSize) return thumbSize 157 | return Math.max(width, thumbMinSize) 158 | } 159 | 160 | getThumbVerticalHeight = () => { 161 | const { thumbSize, thumbMinSize } = this.props 162 | const { view, trackVertical } = this.refs 163 | const { scrollHeight, clientHeight } = view 164 | const trackHeight = getInnerHeight(trackVertical) 165 | const height = Math.ceil(clientHeight / scrollHeight * trackHeight) 166 | if (trackHeight === height) return 0 167 | if (thumbSize) return thumbSize 168 | return Math.max(height, thumbMinSize) 169 | } 170 | 171 | getScrollLeftForOffset = (offset) => { 172 | const { view, trackHorizontal } = this.refs 173 | const { scrollWidth, clientWidth } = view 174 | const trackWidth = getInnerWidth(trackHorizontal) 175 | const thumbWidth = this.getThumbHorizontalWidth() 176 | return offset / (trackWidth - thumbWidth) * (scrollWidth - clientWidth) 177 | } 178 | 179 | getScrollTopForOffset = (offset) => { 180 | const { view, trackVertical } = this.refs 181 | const { scrollHeight, clientHeight } = view 182 | const trackHeight = getInnerHeight(trackVertical) 183 | const thumbHeight = this.getThumbVerticalHeight() 184 | return offset / (trackHeight - thumbHeight) * (scrollHeight - clientHeight) 185 | } 186 | 187 | scrollLeft = (left = 0) => { 188 | const { view } = this.refs 189 | view.scrollLeft = left 190 | } 191 | 192 | scrollTop = (top = 0) => { 193 | const { view } = this.refs 194 | view.scrollTop = top 195 | } 196 | 197 | scrollToLeft = () => { 198 | const { view } = this.refs 199 | view.scrollLeft = 0 200 | } 201 | 202 | scrollToTop = () => { 203 | const { view } = this.refs 204 | view.scrollTop = 0 205 | } 206 | 207 | scrollToRight = () => { 208 | const { view } = this.refs 209 | view.scrollLeft = view.scrollWidth 210 | } 211 | 212 | scrollToBottom = () => { 213 | const { view } = this.refs 214 | view.scrollTop = view.scrollHeight 215 | } 216 | 217 | addListeners = () => { 218 | /* istanbul ignore if */ 219 | if (typeof document === 'undefined') return 220 | const { view, trackHorizontal, trackVertical, thumbHorizontal, thumbVertical } = this.refs 221 | view.addEventListener('scroll', this.handleScroll) 222 | if (!getScrollbarWidth()) return 223 | trackHorizontal.addEventListener('mouseenter', this.handleTrackMouseEnter) 224 | trackHorizontal.addEventListener('mouseleave', this.handleTrackMouseLeave) 225 | trackHorizontal.addEventListener('mousedown', this.handleHorizontalTrackMouseDown) 226 | trackVertical.addEventListener('mouseenter', this.handleTrackMouseEnter) 227 | trackVertical.addEventListener('mouseleave', this.handleTrackMouseLeave) 228 | trackVertical.addEventListener('mousedown', this.handleVerticalTrackMouseDown) 229 | thumbHorizontal.addEventListener('mousedown', this.handleHorizontalThumbMouseDown) 230 | thumbVertical.addEventListener('mousedown', this.handleVerticalThumbMouseDown) 231 | window.addEventListener('resize', this.handleWindowResize) 232 | } 233 | 234 | removeListeners = () => { 235 | /* istanbul ignore if */ 236 | if (typeof document === 'undefined') return 237 | const { view, trackHorizontal, trackVertical, thumbHorizontal, thumbVertical } = this.refs 238 | view.removeEventListener('scroll', this.handleScroll) 239 | if (!getScrollbarWidth()) return 240 | trackHorizontal.removeEventListener('mouseenter', this.handleTrackMouseEnter) 241 | trackHorizontal.removeEventListener('mouseleave', this.handleTrackMouseLeave) 242 | trackHorizontal.removeEventListener('mousedown', this.handleHorizontalTrackMouseDown) 243 | trackVertical.removeEventListener('mouseenter', this.handleTrackMouseEnter) 244 | trackVertical.removeEventListener('mouseleave', this.handleTrackMouseLeave) 245 | trackVertical.removeEventListener('mousedown', this.handleVerticalTrackMouseDown) 246 | thumbHorizontal.removeEventListener('mousedown', this.handleHorizontalThumbMouseDown) 247 | thumbVertical.removeEventListener('mousedown', this.handleVerticalThumbMouseDown) 248 | window.removeEventListener('resize', this.handleWindowResize) 249 | // Possibly setup by `handleDragStart` 250 | this.teardownDragging() 251 | } 252 | 253 | handleScroll = (event) => { 254 | const { onScroll, onScrollFrame } = this.props 255 | if (onScroll) onScroll(event) 256 | this.update(values => { 257 | const { scrollLeft, scrollTop } = values 258 | this.viewScrollLeft = scrollLeft 259 | this.viewScrollTop = scrollTop 260 | if (onScrollFrame) onScrollFrame(values) 261 | }) 262 | this.detectScrolling() 263 | } 264 | 265 | handleScrollStart = () => { 266 | const { onScrollStart } = this.props 267 | if (onScrollStart) onScrollStart() 268 | this.handleScrollStartAutoHide() 269 | } 270 | 271 | handleScrollStartAutoHide = () => { 272 | const { autoHide } = this.props 273 | if (!autoHide) return 274 | this.showTracks() 275 | } 276 | 277 | handleScrollStop = () => { 278 | const { onScrollStop } = this.props 279 | if (onScrollStop) onScrollStop() 280 | this.handleScrollStopAutoHide() 281 | } 282 | 283 | handleScrollStopAutoHide = () => { 284 | const { autoHide } = this.props 285 | if (!autoHide) return 286 | this.hideTracks() 287 | } 288 | 289 | handleWindowResize = () => { 290 | this.update() 291 | } 292 | 293 | handleHorizontalTrackMouseDown = (event) => { 294 | event.preventDefault() 295 | const { view } = this.refs 296 | const { target, clientX } = event 297 | const { left: targetLeft } = target.getBoundingClientRect() 298 | const thumbWidth = this.getThumbHorizontalWidth() 299 | const offset = Math.abs(targetLeft - clientX) - thumbWidth / 2 300 | view.scrollLeft = this.getScrollLeftForOffset(offset) 301 | } 302 | 303 | handleVerticalTrackMouseDown = (event) => { 304 | event.preventDefault() 305 | const { view } = this.refs 306 | const { target, clientY } = event 307 | const { top: targetTop } = target.getBoundingClientRect() 308 | const thumbHeight = this.getThumbVerticalHeight() 309 | const offset = Math.abs(targetTop - clientY) - thumbHeight / 2 310 | view.scrollTop = this.getScrollTopForOffset(offset) 311 | } 312 | 313 | handleHorizontalThumbMouseDown = (event) => { 314 | event.preventDefault() 315 | this.handleDragStart(event) 316 | const { target, clientX } = event 317 | const { offsetWidth } = target 318 | const { left } = target.getBoundingClientRect() 319 | this.prevPageX = offsetWidth - (clientX - left) 320 | } 321 | 322 | handleVerticalThumbMouseDown = (event) => { 323 | event.preventDefault() 324 | this.handleDragStart(event) 325 | const { target, clientY } = event 326 | const { offsetHeight } = target 327 | const { top } = target.getBoundingClientRect() 328 | this.prevPageY = offsetHeight - (clientY - top) 329 | } 330 | 331 | setupDragging = () => { 332 | css(document.body, disableSelectStyle) 333 | document.addEventListener('mousemove', this.handleDrag) 334 | document.addEventListener('mouseup', this.handleDragEnd) 335 | document.onselectstart = returnFalse 336 | } 337 | 338 | teardownDragging = () => { 339 | css(document.body, disableSelectStyleReset) 340 | document.removeEventListener('mousemove', this.handleDrag) 341 | document.removeEventListener('mouseup', this.handleDragEnd) 342 | document.onselectstart = undefined 343 | } 344 | 345 | handleDragStart = (event) => { 346 | this.dragging = true 347 | event.stopImmediatePropagation() 348 | this.setupDragging() 349 | } 350 | 351 | handleDrag = (event) => { 352 | if (this.prevPageX) { 353 | const { clientX } = event 354 | const { view, trackHorizontal } = this.refs 355 | const { left: trackLeft } = trackHorizontal.getBoundingClientRect() 356 | const thumbWidth = this.getThumbHorizontalWidth() 357 | const clickPosition = thumbWidth - this.prevPageX 358 | const offset = -trackLeft + clientX - clickPosition 359 | view.scrollLeft = this.getScrollLeftForOffset(offset) 360 | } 361 | if (this.prevPageY) { 362 | const { clientY } = event 363 | const { view, trackVertical } = this.refs 364 | const { top: trackTop } = trackVertical.getBoundingClientRect() 365 | const thumbHeight = this.getThumbVerticalHeight() 366 | const clickPosition = thumbHeight - this.prevPageY 367 | const offset = -trackTop + clientY - clickPosition 368 | view.scrollTop = this.getScrollTopForOffset(offset) 369 | } 370 | return false 371 | } 372 | 373 | handleDragEnd = () => { 374 | this.dragging = false 375 | this.prevPageX = this.prevPageY = 0 376 | this.teardownDragging() 377 | this.handleDragEndAutoHide() 378 | } 379 | 380 | handleDragEndAutoHide = () => { 381 | const { autoHide } = this.props 382 | if (!autoHide) return 383 | this.hideTracks() 384 | } 385 | 386 | handleTrackMouseEnter = () => { 387 | this.trackMouseOver = true 388 | this.handleTrackMouseEnterAutoHide() 389 | } 390 | 391 | handleTrackMouseEnterAutoHide = () => { 392 | const { autoHide } = this.props 393 | if (!autoHide) return 394 | this.showTracks() 395 | } 396 | 397 | handleTrackMouseLeave = () => { 398 | this.trackMouseOver = false 399 | this.handleTrackMouseLeaveAutoHide() 400 | } 401 | 402 | handleTrackMouseLeaveAutoHide = () => { 403 | const { autoHide } = this.props 404 | if (!autoHide) return 405 | this.hideTracks() 406 | } 407 | 408 | showTracks = () => { 409 | const { trackHorizontal, trackVertical } = this.refs 410 | clearTimeout(this.hideTracksTimeout) 411 | css(trackHorizontal, { opacity: 1 }) 412 | css(trackVertical, { opacity: 1 }) 413 | } 414 | 415 | hideTracks = () => { 416 | if (this.dragging) return 417 | if (this.scrolling) return 418 | if (this.trackMouseOver) return 419 | const { autoHideTimeout } = this.props 420 | const { trackHorizontal, trackVertical } = this.refs 421 | clearTimeout(this.hideTracksTimeout) 422 | this.hideTracksTimeout = setTimeout(() => { 423 | css(trackHorizontal, { opacity: 0 }) 424 | css(trackVertical, { opacity: 0 }) 425 | }, autoHideTimeout) 426 | } 427 | 428 | detectScrolling = () => { 429 | if (this.scrolling) return 430 | this.scrolling = true 431 | this.handleScrollStart() 432 | this.detectScrollingInterval = setInterval(() => { 433 | if (this.lastViewScrollLeft === this.viewScrollLeft && 434 | this.lastViewScrollTop === this.viewScrollTop) { 435 | clearInterval(this.detectScrollingInterval) 436 | this.scrolling = false 437 | this.handleScrollStop() 438 | } 439 | this.lastViewScrollLeft = this.viewScrollLeft 440 | this.lastViewScrollTop = this.viewScrollTop 441 | }, 100) 442 | } 443 | 444 | raf = (callback) => { 445 | if (this.requestFrame) raf.cancel(this.requestFrame) 446 | this.requestFrame = raf(() => { 447 | this.requestFrame = undefined 448 | callback() 449 | }) 450 | } 451 | 452 | update = (callback) => { 453 | this.raf(() => this._update(callback)) 454 | } 455 | 456 | _update = (callback) => { 457 | const { onUpdate, hideTracksWhenNotNeeded } = this.props 458 | const values = this.getValues() 459 | if (getScrollbarWidth()) { 460 | const { thumbHorizontal, thumbVertical, trackHorizontal, trackVertical } = this.refs 461 | const { scrollLeft, clientWidth, scrollWidth } = values 462 | const trackHorizontalWidth = getInnerWidth(trackHorizontal) 463 | const thumbHorizontalWidth = this.getThumbHorizontalWidth() 464 | const thumbHorizontalX = scrollLeft / (scrollWidth - clientWidth) * (trackHorizontalWidth - thumbHorizontalWidth) 465 | const thumbHorizontalStyle = { 466 | width: thumbHorizontalWidth, 467 | transform: `translateX(${thumbHorizontalX}px)` 468 | } 469 | const { scrollTop, clientHeight, scrollHeight } = values 470 | const trackVerticalHeight = getInnerHeight(trackVertical) 471 | const thumbVerticalHeight = this.getThumbVerticalHeight() 472 | const thumbVerticalY = scrollTop / (scrollHeight - clientHeight) * (trackVerticalHeight - thumbVerticalHeight) 473 | const thumbVerticalStyle = { 474 | height: thumbVerticalHeight, 475 | transform: `translateY(${thumbVerticalY}px)` 476 | } 477 | if (hideTracksWhenNotNeeded) { 478 | const trackHorizontalStyle = { 479 | visibility: scrollWidth > clientWidth ? 'visible' : 'hidden' 480 | } 481 | const trackVerticalStyle = { 482 | visibility: scrollHeight > clientHeight ? 'visible' : 'hidden' 483 | } 484 | css(trackHorizontal, trackHorizontalStyle) 485 | css(trackVertical, trackVerticalStyle) 486 | } 487 | css(thumbHorizontal, thumbHorizontalStyle) 488 | css(thumbVertical, thumbVerticalStyle) 489 | } 490 | if (onUpdate) onUpdate(values) 491 | if (typeof callback !== 'function') return 492 | callback(values) 493 | } 494 | 495 | render = () => { 496 | const scrollbarWidth = getScrollbarWidth() 497 | /* eslint-disable no-unused-vars */ 498 | const { 499 | onScroll, 500 | onScrollFrame, 501 | onScrollStart, 502 | onScrollStop, 503 | onUpdate, 504 | renderView, 505 | renderTrackHorizontal, 506 | renderTrackVertical, 507 | renderThumbHorizontal, 508 | renderThumbVertical, 509 | tagName, 510 | hideTracksWhenNotNeeded, 511 | autoHide, 512 | autoHideTimeout, 513 | autoHideDuration, 514 | thumbSize, 515 | thumbMinSize, 516 | universal, 517 | autoHeight, 518 | autoHeightMin, 519 | autoHeightMax, 520 | style, 521 | children, 522 | ...props 523 | } = this.props 524 | /* eslint-enable no-unused-vars */ 525 | 526 | const { didMountUniversal } = this.state 527 | 528 | const containerStyle = { 529 | ...containerStyleDefault, 530 | ...(autoHeight && { 531 | ...containerStyleAutoHeight, 532 | minHeight: autoHeightMin, 533 | maxHeight: autoHeightMax 534 | }), 535 | ...style 536 | } 537 | 538 | const viewStyle = { 539 | ...viewStyleDefault, 540 | // Hide scrollbars by setting a negative margin 541 | marginRight: scrollbarWidth ? -scrollbarWidth : 0, 542 | marginBottom: scrollbarWidth ? -scrollbarWidth : 0, 543 | ...(autoHeight && { 544 | ...viewStyleAutoHeight, 545 | // Add scrollbarWidth to autoHeight in order to compensate negative margins 546 | minHeight: isString(autoHeightMin) 547 | ? `calc(${autoHeightMin} + ${scrollbarWidth}px)` 548 | : autoHeightMin + scrollbarWidth, 549 | maxHeight: isString(autoHeightMax) 550 | ? `calc(${autoHeightMax} + ${scrollbarWidth}px)` 551 | : autoHeightMax + scrollbarWidth 552 | }), 553 | // Override min/max height for initial universal rendering 554 | ...((autoHeight && universal && !didMountUniversal) && { 555 | minHeight: autoHeightMin, 556 | maxHeight: autoHeightMax 557 | }), 558 | // Override 559 | ...((universal && !didMountUniversal) && viewStyleUniversalInitial) 560 | } 561 | 562 | const trackAutoHeightStyle = { 563 | transition: `opacity ${autoHideDuration}ms`, 564 | opacity: 0 565 | } 566 | 567 | const trackHorizontalStyle = { 568 | ...trackHorizontalStyleDefault, 569 | ...(autoHide && trackAutoHeightStyle), 570 | ...((!scrollbarWidth || (universal && !didMountUniversal)) && { 571 | display: 'none' 572 | }) 573 | } 574 | 575 | const trackVerticalStyle = { 576 | ...trackVerticalStyleDefault, 577 | ...(autoHide && trackAutoHeightStyle), 578 | ...((!scrollbarWidth || (universal && !didMountUniversal)) && { 579 | display: 'none' 580 | }) 581 | } 582 | 583 | return h(tagName, { ...props, style: containerStyle, ref: (r) => { this.refs.container = r } }, [ 584 | cloneElement( 585 | renderView({ style: viewStyle }), 586 | { key: 'view', ref: (r) => { this.refs.view = r } }, 587 | children 588 | ), 589 | cloneElement( 590 | renderTrackHorizontal({ style: trackHorizontalStyle }), 591 | { key: 'trackHorizontal', ref: (r) => { this.refs.trackHorizontal = r } }, 592 | cloneElement(renderThumbHorizontal({ style: thumbHorizontalStyleDefault }), { ref: (r) => { this.refs.thumbHorizontal = r } }) 593 | ), 594 | cloneElement( 595 | renderTrackVertical({ style: trackVerticalStyle }), 596 | { key: 'trackVertical', ref: (r) => { this.refs.trackVertical = r } }, 597 | cloneElement(renderThumbVertical({ style: thumbVerticalStyleDefault }), { ref: (r) => { this.refs.thumbVertical = r } }) 598 | ) 599 | ]) 600 | } 601 | } 602 | -------------------------------------------------------------------------------- /src/Scrollbars/styles.js: -------------------------------------------------------------------------------- 1 | export const containerStyleDefault = { 2 | position: 'relative', 3 | overflow: 'hidden', 4 | width: '100%', 5 | height: '100%', 6 | } 7 | 8 | // Overrides containerStyleDefault properties 9 | export const containerStyleAutoHeight = { 10 | height: 'auto', 11 | } 12 | 13 | export const viewStyleDefault = { 14 | position: 'absolute', 15 | top: 0, 16 | left: 0, 17 | right: 0, 18 | bottom: 0, 19 | overflow: 'scroll', 20 | WebkitOverflowScrolling: 'touch', 21 | } 22 | 23 | // Overrides viewStyleDefault properties 24 | export const viewStyleAutoHeight = { 25 | position: 'relative', 26 | top: undefined, 27 | left: undefined, 28 | right: undefined, 29 | bottom: undefined, 30 | } 31 | 32 | export const viewStyleUniversalInitial = { 33 | overflow: 'hidden', 34 | marginRight: 0, 35 | marginBottom: 0, 36 | } 37 | 38 | export const trackHorizontalStyleDefault = { 39 | position: 'absolute', 40 | height: 6, 41 | } 42 | 43 | export const trackVerticalStyleDefault = { 44 | position: 'absolute', 45 | width: 6, 46 | } 47 | 48 | export const thumbHorizontalStyleDefault = { 49 | position: 'relative', 50 | display: 'block', 51 | height: '100%', 52 | } 53 | 54 | export const thumbVerticalStyleDefault = { 55 | position: 'relative', 56 | display: 'block', 57 | width: '100%', 58 | } 59 | 60 | export const disableSelectStyle = { 61 | userSelect: 'none', 62 | } 63 | 64 | export const disableSelectStyleReset = { 65 | userSelect: '', 66 | } 67 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Scrollbars from './Scrollbars' 2 | export default Scrollbars 3 | export { Scrollbars } 4 | -------------------------------------------------------------------------------- /src/utils/getInnerHeight.js: -------------------------------------------------------------------------------- 1 | export default function getInnerHeight(el) { 2 | const { clientHeight } = el 3 | const { paddingTop, paddingBottom } = window.getComputedStyle(el) 4 | return clientHeight - parseFloat(paddingTop) - parseFloat(paddingBottom) 5 | } 6 | -------------------------------------------------------------------------------- /src/utils/getInnerWidth.js: -------------------------------------------------------------------------------- 1 | export default function getInnerWidth(el) { 2 | const { clientWidth } = el 3 | const { paddingLeft, paddingRight } = window.getComputedStyle(el) 4 | return clientWidth - parseFloat(paddingLeft) - parseFloat(paddingRight) 5 | } 6 | -------------------------------------------------------------------------------- /src/utils/getScrollbarWidth.js: -------------------------------------------------------------------------------- 1 | import css from 'dom-css' 2 | let scrollbarWidth = false 3 | 4 | export default function getScrollbarWidth() { 5 | if (scrollbarWidth !== false) return scrollbarWidth 6 | /* istanbul ignore else */ 7 | if (typeof document !== 'undefined') { 8 | const div = document.createElement('div') 9 | css(div, { 10 | width: 100, 11 | height: 100, 12 | position: 'absolute', 13 | top: -9999, 14 | overflow: 'scroll', 15 | MsOverflowStyle: 'scrollbar', 16 | }) 17 | document.body.appendChild(div) 18 | scrollbarWidth = (div.offsetWidth - div.clientWidth) 19 | document.body.removeChild(div) 20 | } else { 21 | scrollbarWidth = 0 22 | } 23 | return scrollbarWidth || 0 24 | } 25 | -------------------------------------------------------------------------------- /src/utils/isString.js: -------------------------------------------------------------------------------- 1 | export default function isString(maybe) { 2 | return typeof maybe === 'string' 3 | } 4 | -------------------------------------------------------------------------------- /src/utils/returnFalse.js: -------------------------------------------------------------------------------- 1 | export default function returnFalse() { 2 | return false 3 | } 4 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect' 2 | window.expect = expect 3 | window.createSpy = expect.createSpy 4 | window.spyOn = expect.spyOn 5 | window.isSpy = expect.isSpy 6 | 7 | const context = require.context('./test', true, /\.spec\.js$/) 8 | context.keys().forEach(context) 9 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "describe": true, 4 | "it": true, 5 | "expect": true, 6 | "before": true, 7 | "beforeEach": true, 8 | "after": true, 9 | "afterEach": true, 10 | "createSpy": true, 11 | "spyOn": true, 12 | "isSpy": true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/Scrollbars/autoHeight.js: -------------------------------------------------------------------------------- 1 | import { Scrollbars } from 'preact-custom-scrollbars' 2 | import { render, unmountComponentAtNode, findDOMNode } from 'preact-dom' 3 | import Preact, { createClass } from 'preact' 4 | 5 | export default function createTests(scrollbarWidth, envScrollbarWidth) { 6 | describe('autoHeight', () => { 7 | let node 8 | beforeEach(() => { 9 | node = document.createElement('div') 10 | document.body.appendChild(node) 11 | }) 12 | afterEach(() => { 13 | unmountComponentAtNode(node) 14 | document.body.removeChild(node) 15 | }) 16 | 17 | describe('when rendered', () => { 18 | it('should have min-height and max-height', done => { 19 | render(( 20 | 24 |
25 | 26 | ), node, function callback() { 27 | const scrollbars = findDOMNode(this) 28 | const view = this.refs.view 29 | expect(scrollbars.style.position).toEqual('relative') 30 | expect(scrollbars.style.minHeight).toEqual('0px') 31 | expect(scrollbars.style.maxHeight).toEqual('100px') 32 | expect(view.style.position).toEqual('relative') 33 | expect(view.style.minHeight).toEqual(`${scrollbarWidth}px`) 34 | expect(view.style.maxHeight).toEqual(`${100 + scrollbarWidth}px`) 35 | done() 36 | }) 37 | }) 38 | }) 39 | 40 | describe('when native scrollbars have a width', () => { 41 | if (!scrollbarWidth) return 42 | it('hides native scrollbars', done => { 43 | render(( 44 | 47 |
48 | 49 | ), node, function callback() { 50 | const width = `-${scrollbarWidth}px` 51 | expect(this.refs.view.style.marginRight).toEqual(width) 52 | expect(this.refs.view.style.marginBottom).toEqual(width) 53 | done() 54 | }) 55 | }) 56 | }) 57 | 58 | describe('when native scrollbars have no width', () => { 59 | if (scrollbarWidth) return 60 | it('hides bars', done => { 61 | render(( 62 | 65 |
66 | 67 | ), node, function callback() { 68 | setTimeout(() => { 69 | expect(this.refs.trackVertical.style.display).toEqual('none') 70 | expect(this.refs.trackHorizontal.style.display).toEqual('none') 71 | done() 72 | }, 100) 73 | }) 74 | }) 75 | }) 76 | 77 | describe('when content is smaller than maxHeight', () => { 78 | it('should have the content\'s height', done => { 79 | render(( 80 | 83 |
84 | 85 | ), node, function callback() { 86 | setTimeout(() => { 87 | const scrollbars = findDOMNode(this) 88 | const view = this.refs.view 89 | const thumbVertical = this.refs.thumbVertical 90 | expect(scrollbars.clientHeight).toEqual(50 + (envScrollbarWidth - scrollbarWidth)) 91 | expect(view.clientHeight).toEqual(50) 92 | expect(view.scrollHeight).toEqual(50) 93 | expect(thumbVertical.clientHeight).toEqual(0) 94 | done() 95 | }, 100) 96 | }) 97 | }) 98 | }) 99 | 100 | describe('when content is larger than maxHeight', () => { 101 | it('should show scrollbars', done => { 102 | render(( 103 | 106 |
107 | 108 | ), node, function callback() { 109 | setTimeout(() => { 110 | const scrollbars = findDOMNode(this) 111 | const view = this.refs.view 112 | const thumbVertical = this.refs.thumbVertical 113 | expect(scrollbars.clientHeight).toEqual(100) 114 | expect(view.clientHeight).toEqual(100 - (envScrollbarWidth - scrollbarWidth)) 115 | expect(view.scrollHeight).toEqual(200) 116 | if (scrollbarWidth) { 117 | // 100 / 200 * 96 = 48 118 | expect(thumbVertical.clientHeight).toEqual(48) 119 | } 120 | done() 121 | }, 100) 122 | }) 123 | }) 124 | }) 125 | 126 | describe('when minHeight is greater than 0', () => { 127 | it('should have height greater than 0', done => { 128 | render(( 129 | 133 |
134 | 135 | ), node, function callback() { 136 | setTimeout(() => { 137 | const scrollbars = findDOMNode(this) 138 | const view = this.refs.view 139 | const thumbVertical = this.refs.thumbVertical 140 | expect(scrollbars.clientHeight).toEqual(100) 141 | expect(view.clientHeight).toEqual(100 - (envScrollbarWidth - scrollbarWidth)) 142 | expect(thumbVertical.clientHeight).toEqual(0) 143 | done() 144 | }, 100) 145 | }) 146 | }) 147 | }) 148 | 149 | describe('when using perecentages', () => { 150 | it('should use calc', done => { 151 | const Root = createClass({ 152 | render() { 153 | return ( 154 |
155 | 160 |
161 | 162 |
163 | ) 164 | }, 165 | }) 166 | render(, node, function callback() { 167 | setTimeout(() => { 168 | const { scrollbars } = this.refs 169 | const $scrollbars = findDOMNode(scrollbars) 170 | const view = scrollbars.refs.view 171 | expect($scrollbars.clientWidth).toEqual(500) 172 | expect($scrollbars.clientHeight).toEqual(250) 173 | expect($scrollbars.style.position).toEqual('relative') 174 | expect($scrollbars.style.minHeight).toEqual('50%') 175 | expect($scrollbars.style.maxHeight).toEqual('100%') 176 | expect(view.style.position).toEqual('relative') 177 | expect(view.style.minHeight).toEqual(`calc(50% + ${scrollbarWidth}px)`) 178 | expect(view.style.maxHeight).toEqual(`calc(100% + ${scrollbarWidth}px)`) 179 | done() 180 | }, 100) 181 | }) 182 | }) 183 | }) 184 | 185 | describe('when using other units', () => { 186 | it('should use calc', done => { 187 | render(( 188 | 192 |
193 | 194 | ), node, function callback() { 195 | const scrollbars = findDOMNode(this) 196 | const view = this.refs.view 197 | expect(scrollbars.style.position).toEqual('relative') 198 | expect(scrollbars.style.minHeight).toEqual('10em') 199 | expect(scrollbars.style.maxHeight).toEqual('100em') 200 | expect(view.style.position).toEqual('relative') 201 | expect(view.style.minHeight).toEqual(`calc(10em + ${scrollbarWidth}px)`) 202 | expect(view.style.maxHeight).toEqual(`calc(100em + ${scrollbarWidth}px)`) 203 | done() 204 | }) 205 | }) 206 | }) 207 | }) 208 | } 209 | -------------------------------------------------------------------------------- /test/Scrollbars/autoHide.js: -------------------------------------------------------------------------------- 1 | import { Scrollbars } from 'preact-custom-scrollbars' 2 | import { render, unmountComponentAtNode } from 'preact-dom' 3 | import Preact from 'preact' 4 | import simulant from 'simulant' 5 | 6 | export default function createTests(scrollbarWidth) { 7 | // Not for mobile environment 8 | if (!scrollbarWidth) return 9 | 10 | let node 11 | beforeEach(() => { 12 | node = document.createElement('div') 13 | document.body.appendChild(node) 14 | }) 15 | afterEach(() => { 16 | unmountComponentAtNode(node) 17 | document.body.removeChild(node) 18 | }) 19 | 20 | describe('autoHide', () => { 21 | describe('when Scrollbars are rendered', () => { 22 | it('should hide tracks', done => { 23 | render(( 24 | 25 |
26 | 27 | ), node, function callback() { 28 | const { trackHorizontal, trackVertical } = this.refs 29 | expect(trackHorizontal.style.opacity).toEqual('0') 30 | expect(trackVertical.style.opacity).toEqual('0') 31 | done() 32 | }) 33 | }) 34 | }) 35 | describe('enter/leave track', () => { 36 | describe('when entering horizontal track', () => { 37 | it('should show tracks', done => { 38 | render(( 39 | 40 |
41 | 42 | ), node, function callback() { 43 | const { trackHorizontal: track } = this.refs 44 | simulant.fire(track, 'mouseenter') 45 | expect(track.style.opacity).toEqual('1') 46 | done() 47 | }) 48 | }) 49 | it('should not hide tracks', done => { 50 | render(( 51 | 55 |
56 | 57 | ), node, function callback() { 58 | const { trackHorizontal: track } = this.refs 59 | simulant.fire(track, 'mouseenter') 60 | setTimeout(() => this.hideTracks(), 10) 61 | setTimeout(() => { 62 | expect(track.style.opacity).toEqual('1') 63 | }, 100) 64 | done() 65 | }) 66 | }) 67 | }) 68 | describe('when leaving horizontal track', () => { 69 | it('should hide tracks', done => { 70 | render(( 71 | 76 |
77 | 78 | ), node, function callback() { 79 | const { trackHorizontal: track } = this.refs 80 | simulant.fire(track, 'mouseenter') 81 | simulant.fire(track, 'mouseleave') 82 | setTimeout(() => { 83 | expect(track.style.opacity).toEqual('0') 84 | done() 85 | }, 100) 86 | }) 87 | }) 88 | }) 89 | describe('when entering vertical track', () => { 90 | it('should show tracks', done => { 91 | render(( 92 | 93 |
94 | 95 | ), node, function callback() { 96 | const { trackVertical: track } = this.refs 97 | simulant.fire(track, 'mouseenter') 98 | expect(track.style.opacity).toEqual('1') 99 | done() 100 | }) 101 | }) 102 | it('should not hide tracks', done => { 103 | render(( 104 | 108 |
109 | 110 | ), node, function callback() { 111 | const { trackVertical: track } = this.refs 112 | simulant.fire(track, 'mouseenter') 113 | setTimeout(() => this.hideTracks(), 10) 114 | setTimeout(() => { 115 | expect(track.style.opacity).toEqual('1') 116 | }, 100) 117 | done() 118 | }) 119 | }) 120 | }) 121 | describe('when leaving vertical track', () => { 122 | it('should hide tracks', done => { 123 | render(( 124 | 129 |
130 | 131 | ), node, function callback() { 132 | const { trackVertical: track } = this.refs 133 | simulant.fire(track, 'mouseenter') 134 | simulant.fire(track, 'mouseleave') 135 | setTimeout(() => { 136 | expect(track.style.opacity).toEqual('0') 137 | done() 138 | }, 100) 139 | }) 140 | }) 141 | }) 142 | }) 143 | describe('when scrolling', () => { 144 | it('should show tracks', done => { 145 | render(( 146 | 147 |
148 | 149 | ), node, function callback() { 150 | this.scrollTop(50) 151 | setTimeout(() => { 152 | const { trackHorizontal, trackVertical } = this.refs 153 | expect(trackHorizontal.style.opacity).toEqual('1') 154 | expect(trackVertical.style.opacity).toEqual('1') 155 | done() 156 | }, 100) 157 | }) 158 | }) 159 | it('should hide tracks after scrolling', done => { 160 | render(( 161 | 166 |
167 | 168 | ), node, function callback() { 169 | this.scrollTop(50) 170 | setTimeout(() => { 171 | const { trackHorizontal, trackVertical } = this.refs 172 | expect(trackHorizontal.style.opacity).toEqual('0') 173 | expect(trackVertical.style.opacity).toEqual('0') 174 | done() 175 | }, 300) 176 | }) 177 | }) 178 | it('should not hide tracks', done => { 179 | render(( 180 | 184 |
185 | 186 | ), node, function callback() { 187 | this.scrollTop(50) 188 | setTimeout(() => this.hideTracks()) 189 | setTimeout(() => { 190 | const { trackHorizontal, trackVertical } = this.refs 191 | expect(trackHorizontal.style.opacity).toEqual('1') 192 | expect(trackVertical.style.opacity).toEqual('1') 193 | done() 194 | }, 50) 195 | }) 196 | }) 197 | }) 198 | describe('when dragging x-axis', () => { 199 | it('should show tracks', done => { 200 | render(( 201 | 206 |
207 | 208 | ), node, function callback() { 209 | const { thumbHorizontal: thumb, trackHorizontal: track } = this.refs 210 | const { left } = thumb.getBoundingClientRect() 211 | simulant.fire(thumb, 'mousedown', { 212 | target: thumb, 213 | clientX: left + 1, 214 | }) 215 | simulant.fire(document, 'mousemove', { 216 | clientX: left + 100, 217 | }) 218 | setTimeout(() => { 219 | expect(track.style.opacity).toEqual('1') 220 | done() 221 | }, 100) 222 | }) 223 | }) 224 | 225 | it('should hide tracks on end', done => { 226 | render(( 227 | 232 |
233 | 234 | ), node, function callback() { 235 | const { thumbHorizontal: thumb, trackHorizontal: track } = this.refs 236 | const { left } = thumb.getBoundingClientRect() 237 | simulant.fire(thumb, 'mousedown', { 238 | target: thumb, 239 | clientX: left + 1, 240 | }) 241 | simulant.fire(document, 'mouseup') 242 | setTimeout(() => { 243 | expect(track.style.opacity).toEqual('0') 244 | done() 245 | }, 100) 246 | }) 247 | }) 248 | 249 | describe('and leaving track', () => { 250 | it('should not hide tracks', done => { 251 | render(( 252 | 257 |
258 | 259 | ), node, function callback() { 260 | setTimeout(() => { 261 | const { thumbHorizontal: thumb, trackHorizontal: track } = this.refs 262 | const { left } = thumb.getBoundingClientRect() 263 | simulant.fire(thumb, 'mousedown', { 264 | target: thumb, 265 | clientX: left + 1, 266 | }) 267 | simulant.fire(document, 'mousemove', { 268 | clientX: left + 100, 269 | }) 270 | simulant.fire(track, 'mouseleave') 271 | setTimeout(() => { 272 | expect(track.style.opacity).toEqual('1') 273 | done() 274 | }, 200) 275 | }, 100) 276 | }) 277 | }) 278 | }) 279 | }) 280 | describe('when dragging y-axis', () => { 281 | it('should show tracks', done => { 282 | render(( 283 | 288 |
289 | 290 | ), node, function callback() { 291 | const { thumbVertical: thumb, trackVertical: track } = this.refs 292 | const { top } = thumb.getBoundingClientRect() 293 | simulant.fire(thumb, 'mousedown', { 294 | target: thumb, 295 | clientY: top + 1, 296 | }) 297 | simulant.fire(document, 'mousemove', { 298 | clientY: top + 100, 299 | }) 300 | setTimeout(() => { 301 | expect(track.style.opacity).toEqual('1') 302 | done() 303 | }, 100) 304 | }) 305 | }) 306 | it('should hide tracks on end', done => { 307 | render(( 308 | 313 |
314 | 315 | ), node, function callback() { 316 | const { thumbVertical: thumb, trackVertical: track } = this.refs 317 | const { top } = thumb.getBoundingClientRect() 318 | simulant.fire(thumb, 'mousedown', { 319 | target: thumb, 320 | clientY: top + 1, 321 | }) 322 | simulant.fire(document, 'mouseup') 323 | setTimeout(() => { 324 | expect(track.style.opacity).toEqual('0') 325 | done() 326 | }, 100) 327 | }) 328 | }) 329 | describe('and leaving track', () => { 330 | it('should not hide tracks', done => { 331 | render(( 332 | 337 |
338 | 339 | ), node, function callback() { 340 | setTimeout(() => { 341 | const { thumbVertical: thumb, trackVertical: track } = this.refs 342 | const { top } = thumb.getBoundingClientRect() 343 | simulant.fire(thumb, 'mousedown', { 344 | target: thumb, 345 | clientY: top + 1, 346 | }) 347 | simulant.fire(document, 'mousemove', { 348 | clientY: top + 100, 349 | }) 350 | simulant.fire(track, 'mouseleave') 351 | setTimeout(() => { 352 | expect(track.style.opacity).toEqual('1') 353 | done() 354 | }, 200) 355 | }, 100) 356 | }) 357 | }) 358 | }) 359 | }) 360 | }) 361 | 362 | describe('when autoHide is disabed', () => { 363 | describe('enter/leave track', () => { 364 | describe('when entering horizontal track', () => { 365 | it('should not call `showTracks`', done => { 366 | render(( 367 | 368 |
369 | 370 | ), node, function callback() { 371 | const spy = spyOn(this, 'showTracks') 372 | const { trackHorizontal: track } = this.refs 373 | simulant.fire(track, 'mouseenter') 374 | expect(spy.calls.length).toEqual(0) 375 | done() 376 | }) 377 | }) 378 | }) 379 | describe('when leaving horizontal track', () => { 380 | it('should not call `hideTracks`', done => { 381 | render(( 382 | 383 |
384 | 385 | ), node, function callback() { 386 | const spy = spyOn(this, 'hideTracks') 387 | const { trackHorizontal: track } = this.refs 388 | simulant.fire(track, 'mouseenter') 389 | simulant.fire(track, 'mouseleave') 390 | setTimeout(() => { 391 | expect(spy.calls.length).toEqual(0) 392 | done() 393 | }, 100) 394 | }) 395 | }) 396 | }) 397 | describe('when entering vertical track', () => { 398 | it('should not call `showTracks`', done => { 399 | render(( 400 | 401 |
402 | 403 | ), node, function callback() { 404 | const spy = spyOn(this, 'showTracks') 405 | const { trackVertical: track } = this.refs 406 | simulant.fire(track, 'mouseenter') 407 | expect(spy.calls.length).toEqual(0) 408 | done() 409 | }) 410 | }) 411 | }) 412 | describe('when leaving vertical track', () => { 413 | it('should not call `hideTracks`', done => { 414 | render(( 415 | 416 |
417 | 418 | ), node, function callback() { 419 | const spy = spyOn(this, 'hideTracks') 420 | const { trackVertical: track } = this.refs 421 | simulant.fire(track, 'mouseenter') 422 | simulant.fire(track, 'mouseleave') 423 | setTimeout(() => { 424 | expect(spy.calls.length).toEqual(0) 425 | done() 426 | }, 100) 427 | }) 428 | }) 429 | }) 430 | }) 431 | }) 432 | } 433 | -------------------------------------------------------------------------------- /test/Scrollbars/clickTrack.js: -------------------------------------------------------------------------------- 1 | import { Scrollbars } from 'preact-custom-scrollbars' 2 | import { render, unmountComponentAtNode } from 'preact-dom' 3 | import Preact from 'preact' 4 | import simulant from 'simulant' 5 | 6 | export default function createTests(scrollbarWidth) { 7 | // Not for mobile environment 8 | if (!scrollbarWidth) return 9 | 10 | let node 11 | beforeEach(() => { 12 | node = document.createElement('div') 13 | document.body.appendChild(node) 14 | }) 15 | afterEach(() => { 16 | unmountComponentAtNode(node) 17 | document.body.removeChild(node) 18 | }) 19 | 20 | describe('when clicking on horizontal track', () => { 21 | it('should scroll to the respective position', done => { 22 | render(( 23 | 24 |
25 | 26 | ), node, function callback() { 27 | setTimeout(() => { 28 | const { view, trackHorizontal: bar } = this.refs 29 | const { left, width } = bar.getBoundingClientRect() 30 | simulant.fire(bar, 'mousedown', { 31 | target: bar, 32 | clientX: left + (width / 2), 33 | }) 34 | expect(view.scrollLeft).toEqual(50) 35 | done() 36 | }, 100) 37 | }) 38 | }) 39 | }) 40 | 41 | describe('when clicking on vertical track', () => { 42 | it('should scroll to the respective position', done => { 43 | render(( 44 | 45 |
46 | 47 | ), node, function callback() { 48 | setTimeout(() => { 49 | const { view, trackVertical: bar } = this.refs 50 | const { top, height } = bar.getBoundingClientRect() 51 | simulant.fire(bar, 'mousedown', { 52 | target: bar, 53 | clientY: top + (height / 2), 54 | }) 55 | expect(view.scrollTop).toEqual(50) 56 | done() 57 | }, 100) 58 | }) 59 | }) 60 | }) 61 | } 62 | -------------------------------------------------------------------------------- /test/Scrollbars/dragThumb.js: -------------------------------------------------------------------------------- 1 | import { Scrollbars } from 'preact-custom-scrollbars' 2 | import { render, unmountComponentAtNode } from 'preact-dom' 3 | import Preact from 'preact' 4 | import simulant from 'simulant' 5 | 6 | export default function createTests(scrollbarWidth) { 7 | // Not for mobile environment 8 | if (!scrollbarWidth) return 9 | 10 | let node 11 | beforeEach(() => { 12 | node = document.createElement('div') 13 | document.body.appendChild(node) 14 | }) 15 | afterEach(() => { 16 | unmountComponentAtNode(node) 17 | document.body.removeChild(node) 18 | }) 19 | describe('when dragging horizontal thumb', () => { 20 | it('should scroll to the respective position', done => { 21 | render(( 22 | 23 |
24 | 25 | ), node, function callback() { 26 | setTimeout(() => { 27 | const { view, thumbHorizontal: thumb } = this.refs 28 | const { left } = thumb.getBoundingClientRect() 29 | simulant.fire(thumb, 'mousedown', { 30 | target: thumb, 31 | clientX: left + 1, 32 | }) 33 | simulant.fire(document, 'mousemove', { 34 | clientX: left + 100, 35 | }) 36 | simulant.fire(document, 'mouseup') 37 | expect(view.scrollLeft).toEqual(100) 38 | done() 39 | }, 100) 40 | }) 41 | }) 42 | 43 | it('should disable selection', done => { 44 | render(( 45 | 46 |
47 | 48 | ), node, function callback() { 49 | setTimeout(() => { 50 | const { thumbHorizontal: thumb } = this.refs 51 | const { left } = thumb.getBoundingClientRect() 52 | simulant.fire(thumb, 'mousedown', { 53 | target: thumb, 54 | clientX: left + 1, 55 | }) 56 | expect(document.body.style.webkitUserSelect).toEqual('none') 57 | simulant.fire(document, 'mouseup') 58 | expect(document.body.style.webkitUserSelect).toEqual('') 59 | done() 60 | }, 100) 61 | }) 62 | }) 63 | }) 64 | 65 | describe('when dragging vertical thumb', () => { 66 | it('should scroll to the respective position', done => { 67 | render(( 68 | 69 |
70 | 71 | ), node, function callback() { 72 | setTimeout(() => { 73 | const { view, thumbVertical: thumb } = this.refs 74 | const { top } = thumb.getBoundingClientRect() 75 | simulant.fire(thumb, 'mousedown', { 76 | target: thumb, 77 | clientY: top + 1, 78 | }) 79 | simulant.fire(document, 'mousemove', { 80 | clientY: top + 100, 81 | }) 82 | simulant.fire(document, 'mouseup') 83 | expect(view.scrollTop).toEqual(100) 84 | done() 85 | }, 100) 86 | }) 87 | }) 88 | 89 | it('should disable selection', done => { 90 | render(( 91 | 92 |
93 | 94 | ), node, function callback() { 95 | setTimeout(() => { 96 | const { thumbVertical: thumb } = this.refs 97 | const { top } = thumb.getBoundingClientRect() 98 | simulant.fire(thumb, 'mousedown', { 99 | target: thumb, 100 | clientY: top + 1, 101 | }) 102 | expect(document.body.style.webkitUserSelect).toEqual('none') 103 | simulant.fire(document, 'mouseup') 104 | expect(document.body.style.webkitUserSelect).toEqual('') 105 | done() 106 | }, 100) 107 | }) 108 | }) 109 | }) 110 | } 111 | -------------------------------------------------------------------------------- /test/Scrollbars/flexbox.js: -------------------------------------------------------------------------------- 1 | import { Scrollbars } from 'preact-custom-scrollbars' 2 | import { render, unmountComponentAtNode, findDOMNode } from 'preact-dom' 3 | import Preact, { createClass } from 'preact' 4 | 5 | export default function createTests() { 6 | let node 7 | beforeEach(() => { 8 | node = document.createElement('div') 9 | document.body.appendChild(node) 10 | }) 11 | afterEach(() => { 12 | unmountComponentAtNode(node) 13 | document.body.removeChild(node) 14 | }) 15 | describe('when scrollbars are in flexbox environment', () => { 16 | it('should still work', done => { 17 | const Root = createClass({ 18 | render() { 19 | return ( 20 |
21 | 22 |
23 | 24 |
25 | ) 26 | }, 27 | }) 28 | render(, node, function callback() { 29 | setTimeout(() => { 30 | const { scrollbars } = this.refs 31 | const $scrollbars = findDOMNode(scrollbars) 32 | const $view = scrollbars.refs.view 33 | expect($scrollbars.clientHeight).toBeGreaterThan(0) 34 | expect($view.clientHeight).toBeGreaterThan(0) 35 | done() 36 | }, 100) 37 | }) 38 | }) 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /test/Scrollbars/gettersSetters.js: -------------------------------------------------------------------------------- 1 | import { Scrollbars } from 'preact-custom-scrollbars' 2 | import { render, unmountComponentAtNode } from 'preact-dom' 3 | import Preact from 'preact' 4 | 5 | export default function createTests(scrollbarWidth, envScrollbarWidth) { 6 | let node 7 | beforeEach(() => { 8 | node = document.createElement('div') 9 | document.body.appendChild(node) 10 | }) 11 | afterEach(() => { 12 | unmountComponentAtNode(node) 13 | document.body.removeChild(node) 14 | }) 15 | 16 | describe('getters', () => { 17 | function renderScrollbars(callback) { 18 | render(( 19 | 20 |
21 | 22 | ), node, callback) 23 | } 24 | describe('getScrollLeft', () => { 25 | it('should return scrollLeft', done => { 26 | renderScrollbars(function callback() { 27 | this.scrollLeft(50) 28 | expect(this.getScrollLeft()).toEqual(50) 29 | done() 30 | }) 31 | }) 32 | }) 33 | describe('getScrollTop', () => { 34 | it('should return scrollTop', done => { 35 | renderScrollbars(function callback() { 36 | this.scrollTop(50) 37 | expect(this.getScrollTop()).toEqual(50) 38 | done() 39 | }) 40 | }) 41 | }) 42 | describe('getScrollWidth', () => { 43 | it('should return scrollWidth', done => { 44 | renderScrollbars(function callback() { 45 | expect(this.getScrollWidth()).toEqual(200) 46 | done() 47 | }) 48 | }) 49 | }) 50 | describe('getScrollHeight', () => { 51 | it('should return scrollHeight', done => { 52 | renderScrollbars(function callback() { 53 | expect(this.getScrollHeight()).toEqual(200) 54 | done() 55 | }) 56 | }) 57 | }) 58 | describe('getClientWidth', () => { 59 | it('should return scrollWidth', done => { 60 | renderScrollbars(function callback() { 61 | expect(this.getClientWidth()).toEqual(100 + (scrollbarWidth - envScrollbarWidth)) 62 | done() 63 | }) 64 | }) 65 | }) 66 | describe('getClientHeight', () => { 67 | it('should return scrollHeight', done => { 68 | renderScrollbars(function callback() { 69 | expect(this.getClientHeight()).toEqual(100 + (scrollbarWidth - envScrollbarWidth)) 70 | done() 71 | }) 72 | }) 73 | }) 74 | }) 75 | 76 | describe('setters', () => { 77 | function renderScrollbars(callback) { 78 | render(( 79 | 80 |
81 | 82 | ), node, callback) 83 | } 84 | describe('scrollLeft/scrollToLeft', () => { 85 | it('should scroll to given left value', done => { 86 | renderScrollbars(function callback() { 87 | this.scrollLeft(50) 88 | expect(this.getScrollLeft()).toEqual(50) 89 | this.scrollToLeft() 90 | expect(this.getScrollLeft()).toEqual(0) 91 | this.scrollLeft(50) 92 | this.scrollLeft() 93 | expect(this.getScrollLeft()).toEqual(0) 94 | done() 95 | }) 96 | }) 97 | }) 98 | describe('scrollTop/scrollToTop', () => { 99 | it('should scroll to given top value', done => { 100 | renderScrollbars(function callback() { 101 | this.scrollTop(50) 102 | expect(this.getScrollTop()).toEqual(50) 103 | this.scrollToTop() 104 | expect(this.getScrollTop()).toEqual(0) 105 | this.scrollTop(50) 106 | this.scrollTop() 107 | expect(this.getScrollTop()).toEqual(0) 108 | done() 109 | }) 110 | }) 111 | }) 112 | describe('scrollToRight', () => { 113 | it('should scroll to right', done => { 114 | renderScrollbars(function callback() { 115 | this.scrollToRight() 116 | expect(this.getScrollLeft()).toEqual(100 + (envScrollbarWidth - scrollbarWidth)) 117 | done() 118 | }) 119 | }) 120 | }) 121 | describe('scrollToBottom', () => { 122 | it('should scroll to bottom', done => { 123 | renderScrollbars(function callback() { 124 | this.scrollToBottom() 125 | expect(this.getScrollTop()).toEqual(100 + (envScrollbarWidth - scrollbarWidth)) 126 | done() 127 | }) 128 | }) 129 | }) 130 | }) 131 | } 132 | -------------------------------------------------------------------------------- /test/Scrollbars/hideTracks.js: -------------------------------------------------------------------------------- 1 | import { Scrollbars } from 'preact-custom-scrollbars' 2 | import { render, unmountComponentAtNode } from 'preact-dom' 3 | import Preact from 'preact' 4 | 5 | export default function createTests(scrollbarWidth) { 6 | describe('hide tracks', () => { 7 | let node 8 | beforeEach(() => { 9 | node = document.createElement('div') 10 | document.body.appendChild(node) 11 | }) 12 | afterEach(() => { 13 | unmountComponentAtNode(node) 14 | document.body.removeChild(node) 15 | }) 16 | 17 | describe('when native scrollbars have a width', () => { 18 | if (!scrollbarWidth) return 19 | describe('when content is greater than wrapper', () => { 20 | it('should show tracks', done => { 21 | render(( 22 | 25 |
26 | 27 | ), node, function callback() { 28 | setTimeout(() => { 29 | const { trackHorizontal, trackVertical } = this.refs 30 | expect(trackHorizontal.style.visibility).toEqual('visible') 31 | expect(trackVertical.style.visibility).toEqual('visible') 32 | done() 33 | }, 100) 34 | }) 35 | }) 36 | }) 37 | describe('when content is smaller than wrapper', () => { 38 | it('should hide tracks', done => { 39 | render(( 40 | 43 |
44 | 45 | ), node, function callback() { 46 | setTimeout(() => { 47 | const { trackHorizontal, trackVertical } = this.refs 48 | expect(trackHorizontal.style.visibility).toEqual('hidden') 49 | expect(trackVertical.style.visibility).toEqual('hidden') 50 | done() 51 | }, 100) 52 | }) 53 | }) 54 | }) 55 | }) 56 | }) 57 | } 58 | -------------------------------------------------------------------------------- /test/Scrollbars/index.js: -------------------------------------------------------------------------------- 1 | import rendering from './rendering' 2 | import gettersSetters from './gettersSetters' 3 | import scrolling from './scrolling' 4 | import resizing from './resizing' 5 | import clickTrack from './clickTrack' 6 | import dragThumb from './dragThumb' 7 | import flexbox from './flexbox' 8 | import autoHide from './autoHide' 9 | import autoHeight from './autoHeight' 10 | import hideTracks from './hideTracks' 11 | import universal from './universal' 12 | import onUpdate from './onUpdate' 13 | 14 | export default function createTests(scrollbarWidth, envScrollbarWidth) { 15 | rendering(scrollbarWidth, envScrollbarWidth) 16 | gettersSetters(scrollbarWidth, envScrollbarWidth) 17 | scrolling(scrollbarWidth, envScrollbarWidth) 18 | resizing(scrollbarWidth, envScrollbarWidth) 19 | clickTrack(scrollbarWidth, envScrollbarWidth) 20 | dragThumb(scrollbarWidth, envScrollbarWidth) 21 | flexbox(scrollbarWidth, envScrollbarWidth) 22 | autoHide(scrollbarWidth, envScrollbarWidth) 23 | autoHeight(scrollbarWidth, envScrollbarWidth) 24 | hideTracks(scrollbarWidth, envScrollbarWidth) 25 | universal(scrollbarWidth, envScrollbarWidth) 26 | onUpdate(scrollbarWidth, envScrollbarWidth) 27 | } 28 | -------------------------------------------------------------------------------- /test/Scrollbars/onUpdate.js: -------------------------------------------------------------------------------- 1 | import { Scrollbars } from 'preact-custom-scrollbars' 2 | import { render, unmountComponentAtNode } from 'preact-dom' 3 | import Preact from 'preact' 4 | 5 | export default function createTests() { 6 | let node 7 | beforeEach(() => { 8 | node = document.createElement('div') 9 | document.body.appendChild(node) 10 | }) 11 | afterEach(() => { 12 | unmountComponentAtNode(node) 13 | document.body.removeChild(node) 14 | }) 15 | 16 | describe('onUpdate', () => { 17 | describe('when scrolling x-axis', () => { 18 | it('should call `onUpdate`', done => { 19 | const spy = createSpy() 20 | render(( 21 | 22 |
23 | 24 | ), node, function callback() { 25 | this.scrollLeft(50) 26 | setTimeout(() => { 27 | expect(spy.calls.length).toEqual(1) 28 | done() 29 | }, 100) 30 | }) 31 | }) 32 | }) 33 | describe('when scrolling y-axis', () => { 34 | it('should call `onUpdate`', done => { 35 | const spy = createSpy() 36 | render(( 37 | 38 |
39 | 40 | ), node, function callback() { 41 | this.scrollTop(50) 42 | setTimeout(() => { 43 | expect(spy.calls.length).toEqual(1) 44 | done() 45 | }, 100) 46 | }) 47 | }) 48 | }) 49 | 50 | describe('when resizing window', () => { 51 | it('should call onUpdate', done => { 52 | const spy = createSpy() 53 | render(( 54 | 55 |
56 | 57 | ), node, function callback() { 58 | setTimeout(() => { 59 | expect(spy.calls.length).toEqual(1) 60 | done() 61 | }, 100) 62 | }) 63 | }) 64 | }) 65 | }) 66 | } 67 | -------------------------------------------------------------------------------- /test/Scrollbars/rendering.js: -------------------------------------------------------------------------------- 1 | import { Scrollbars } from 'preact-custom-scrollbars' 2 | import { render, unmountComponentAtNode, findDOMNode } from 'preact-dom' 3 | import Preact from 'preact' 4 | 5 | export default function createTests(scrollbarWidth) { 6 | describe('rendering', () => { 7 | let node 8 | beforeEach(() => { 9 | node = document.createElement('div') 10 | document.body.appendChild(node) 11 | }) 12 | afterEach(() => { 13 | unmountComponentAtNode(node) 14 | document.body.removeChild(node) 15 | }) 16 | 17 | describe('when Scrollbars are rendered', () => { 18 | it('takes className', done => { 19 | render(( 20 | 21 |
22 | 23 | ), node, function callback() { 24 | expect(findDOMNode(this).className).toEqual('foo') 25 | done() 26 | }) 27 | }) 28 | 29 | it('takes styles', done => { 30 | render(( 31 | 32 |
33 | 34 | ), node, function callback() { 35 | expect(findDOMNode(this).style.width).toEqual('100px') 36 | expect(findDOMNode(this).style.height).toEqual('100px') 37 | expect(findDOMNode(this).style.overflow).toEqual('hidden') 38 | done() 39 | }) 40 | }) 41 | 42 | it('renders view', done => { 43 | render(( 44 | 45 |
46 | 47 | ), node, function callback() { 48 | expect(this.refs.view).toBeA(Node) 49 | done() 50 | }) 51 | }) 52 | 53 | describe('when using custom tagName', () => { 54 | it('should use the defined tagName', done => { 55 | render(( 56 | 59 |
60 | 61 | ), node, function callback() { 62 | const el = findDOMNode(this) 63 | expect(el.tagName.toLowerCase()).toEqual('nav') 64 | done() 65 | }) 66 | }) 67 | }) 68 | 69 | describe('when custom `renderView` is passed', () => { 70 | it('should render custom element', done => { 71 | render(( 72 |
}> 75 |
76 | 77 | ), node, function callback() { 78 | expect(this.refs.view.tagName).toEqual('SECTION') 79 | expect(this.refs.view.style.color).toEqual('red') 80 | expect(this.refs.view.style.position).toEqual('absolute') 81 | done() 82 | }) 83 | }) 84 | }) 85 | 86 | describe('when native scrollbars have a width', () => { 87 | if (!scrollbarWidth) return 88 | 89 | it('hides native scrollbars', done => { 90 | render(( 91 | 92 |
93 | 94 | ), node, function callback() { 95 | const width = `-${scrollbarWidth}px` 96 | expect(this.refs.view.style.marginRight).toEqual(width) 97 | expect(this.refs.view.style.marginBottom).toEqual(width) 98 | done() 99 | }) 100 | }) 101 | 102 | it('renders bars', done => { 103 | render(( 104 | 105 |
106 | 107 | ), node, function callback() { 108 | expect(this.refs.trackHorizontal).toBeA(Node) 109 | expect(this.refs.trackVertical).toBeA(Node) 110 | done() 111 | }) 112 | }) 113 | 114 | it('renders thumbs', done => { 115 | render(( 116 | 117 |
118 | 119 | ), node, function callback() { 120 | expect(this.refs.thumbHorizontal).toBeA(Node) 121 | expect(this.refs.thumbVertical).toBeA(Node) 122 | done() 123 | }) 124 | }) 125 | 126 | it('renders thumbs with correct size', done => { 127 | render(( 128 | 129 |
130 | 131 | ), node, function callback() { 132 | setTimeout(() => { 133 | // 100 / 200 * 96 = 48 134 | expect(this.refs.thumbVertical.style.height).toEqual('48px') 135 | expect(this.refs.thumbHorizontal.style.width).toEqual('48px') 136 | done() 137 | }, 100) 138 | }) 139 | }) 140 | 141 | it('the thumbs size should not be less than the given `thumbMinSize`', done => { 142 | render(( 143 | 144 |
145 | 146 | ), node, function callback() { 147 | setTimeout(() => { 148 | // 100 / 200 * 96 = 48 149 | expect(this.refs.thumbVertical.style.height).toEqual('30px') 150 | expect(this.refs.thumbHorizontal.style.width).toEqual('30px') 151 | done() 152 | }, 100) 153 | }) 154 | }) 155 | 156 | describe('when thumbs have a fixed size', () => { 157 | it('thumbs should have the given fixed size', done => { 158 | render(( 159 | 160 |
161 | 162 | ), node, function callback() { 163 | setTimeout(() => { 164 | // 100 / 200 * 96 = 48 165 | expect(this.refs.thumbVertical.style.height).toEqual('50px') 166 | expect(this.refs.thumbHorizontal.style.width).toEqual('50px') 167 | done() 168 | }, 100) 169 | }) 170 | }) 171 | }) 172 | 173 | describe('when custom `renderTrackHorizontal` is passed', () => { 174 | it('should render custom element', done => { 175 | render(( 176 |
}> 179 |
180 | 181 | ), node, function callback() { 182 | expect(this.refs.trackHorizontal.tagName).toEqual('SECTION') 183 | expect(this.refs.trackHorizontal.style.position).toEqual('absolute') 184 | expect(this.refs.trackHorizontal.style.color).toEqual('red') 185 | done() 186 | }) 187 | }) 188 | }) 189 | 190 | describe('when custom `renderTrackVertical` is passed', () => { 191 | it('should render custom element', done => { 192 | render(( 193 |
}> 196 |
197 | 198 | ), node, function callback() { 199 | expect(this.refs.trackVertical.tagName).toEqual('SECTION') 200 | expect(this.refs.trackVertical.style.position).toEqual('absolute') 201 | expect(this.refs.trackVertical.style.color).toEqual('red') 202 | done() 203 | }) 204 | }) 205 | }) 206 | 207 | describe('when custom `renderThumbHorizontal` is passed', () => { 208 | it('should render custom element', done => { 209 | render(( 210 |
}> 213 |
214 | 215 | ), node, function callback() { 216 | expect(this.refs.thumbHorizontal.tagName).toEqual('SECTION') 217 | expect(this.refs.thumbHorizontal.style.position).toEqual('relative') 218 | expect(this.refs.thumbHorizontal.style.color).toEqual('red') 219 | done() 220 | }) 221 | }) 222 | }) 223 | 224 | describe('when custom `renderThumbVertical` is passed', () => { 225 | it('should render custom element', done => { 226 | render(( 227 |
}> 230 |
231 | 232 | ), node, function callback() { 233 | expect(this.refs.thumbVertical.tagName).toEqual('SECTION') 234 | expect(this.refs.thumbVertical.style.position).toEqual('relative') 235 | expect(this.refs.thumbVertical.style.color).toEqual('red') 236 | done() 237 | }) 238 | }) 239 | }) 240 | 241 | it('positions view absolute', done => { 242 | render(( 243 | 244 |
245 | 246 | ), node, function callback() { 247 | expect(this.refs.view.style.position).toEqual('absolute') 248 | expect(this.refs.view.style.top).toEqual('0px') 249 | expect(this.refs.view.style.left).toEqual('0px') 250 | done() 251 | }) 252 | }) 253 | 254 | it('should not override the scrollbars width/height values', done => { 255 | render(( 256 | 259 |
} 260 | renderTrackVertical={({ style, ...props }) => 261 |
}> 262 |
263 | 264 | ), node, function callback() { 265 | setTimeout(() => { 266 | expect(this.refs.trackHorizontal.style.height).toEqual('10px') 267 | expect(this.refs.trackVertical.style.width).toEqual('10px') 268 | done() 269 | }, 100) 270 | }) 271 | }) 272 | 273 | describe('when view does not overflow container', () => { 274 | it('should hide scrollbars', done => { 275 | render(( 276 | 279 |
} 280 | renderTrackVertical={({ style, ...props }) => 281 |
}> 282 |
283 | 284 | ), node, function callback() { 285 | setTimeout(() => { 286 | expect(this.refs.thumbHorizontal.style.width).toEqual('0px') 287 | expect(this.refs.thumbVertical.style.height).toEqual('0px') 288 | done() 289 | }, 100) 290 | }) 291 | }) 292 | }) 293 | }) 294 | 295 | describe('when native scrollbars have no width', () => { 296 | if (scrollbarWidth) return 297 | 298 | it('hides bars', done => { 299 | render(( 300 | 301 |
302 | 303 | ), node, function callback() { 304 | setTimeout(() => { 305 | expect(this.refs.trackVertical.style.display).toEqual('none') 306 | expect(this.refs.trackHorizontal.style.display).toEqual('none') 307 | done() 308 | }, 100) 309 | }) 310 | }) 311 | }) 312 | }) 313 | 314 | describe('when rerendering Scrollbars', () => { 315 | function renderScrollbars(callback) { 316 | render(( 317 | 318 |
319 | 320 | ), node, callback) 321 | } 322 | it('should update scrollbars', done => { 323 | renderScrollbars(function callback() { 324 | const spy = spyOn(this, 'update').andCallThrough() 325 | renderScrollbars(function rerenderCallback() { 326 | expect(spy.calls.length).toEqual(1) 327 | spy.restore() 328 | done() 329 | }) 330 | }) 331 | }) 332 | }) 333 | }) 334 | } 335 | -------------------------------------------------------------------------------- /test/Scrollbars/resizing.js: -------------------------------------------------------------------------------- 1 | import { Scrollbars } from 'preact-custom-scrollbars' 2 | import { render, unmountComponentAtNode } from 'preact-dom' 3 | import Preact from 'preact' 4 | import simulant from 'simulant' 5 | 6 | export default function createTests(scrollbarWidth) { 7 | // Not for mobile environment 8 | if (!scrollbarWidth) return 9 | 10 | let node 11 | beforeEach(() => { 12 | node = document.createElement('div') 13 | document.body.appendChild(node) 14 | }) 15 | afterEach(() => { 16 | unmountComponentAtNode(node) 17 | document.body.removeChild(node) 18 | }) 19 | 20 | describe('when resizing window', () => { 21 | it('should update scrollbars', done => { 22 | render(( 23 | 24 |
25 | 26 | ), node, function callback() { 27 | setTimeout(() => { 28 | const spy = spyOn(this, 'update') 29 | simulant.fire(window, 'resize') 30 | expect(spy.calls.length).toEqual(1) 31 | done() 32 | }, 100) 33 | }) 34 | }) 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /test/Scrollbars/scrolling.js: -------------------------------------------------------------------------------- 1 | import { Scrollbars } from 'preact-custom-scrollbars' 2 | import { render, unmountComponentAtNode } from 'preact-dom' 3 | import Preact from 'preact' 4 | 5 | export default function createTests(scrollbarWidth, envScrollbarWidth) { 6 | let node 7 | beforeEach(() => { 8 | node = document.createElement('div') 9 | document.body.appendChild(node) 10 | }) 11 | afterEach(() => { 12 | unmountComponentAtNode(node) 13 | document.body.removeChild(node) 14 | }) 15 | 16 | describe('when scrolling', () => { 17 | describe('when native scrollbars have a width', () => { 18 | if (!scrollbarWidth) return 19 | it('should update thumbs position', done => { 20 | render(( 21 | 22 |
23 | 24 | ), node, function callback() { 25 | this.scrollTop(50) 26 | this.scrollLeft(50) 27 | setTimeout(() => { 28 | if (scrollbarWidth) { 29 | // 50 / (200 - 100) * (96 - 48) = 24 30 | expect(this.refs.thumbVertical.style.transform).toEqual('translateY(24px)') 31 | expect(this.refs.thumbHorizontal.style.transform).toEqual('translateX(24px)') 32 | } else { 33 | expect(this.refs.thumbVertical.style.transform).toEqual('') 34 | expect(this.refs.thumbHorizontal.style.transform).toEqual('') 35 | } 36 | done() 37 | }, 100) 38 | }) 39 | }) 40 | }) 41 | 42 | it('should not trigger a rerender', () => { 43 | render(( 44 | 45 |
46 | 47 | ), node, function callback() { 48 | const spy = spyOn(this, 'render').andCallThrough() 49 | this.scrollTop(50) 50 | expect(spy.calls.length).toEqual(0) 51 | spy.restore() 52 | }) 53 | }) 54 | 55 | describe('when scrolling x-axis', () => { 56 | it('should call `onScroll`', done => { 57 | const spy = createSpy() 58 | render(( 59 | 60 |
61 | 62 | ), node, function callback() { 63 | this.scrollLeft(50) 64 | setTimeout(() => { 65 | expect(spy.calls.length).toEqual(1) 66 | const args = spy.calls[0].arguments 67 | const event = args[0] 68 | expect(event).toBeA(Event) 69 | done() 70 | }, 100) 71 | }) 72 | }) 73 | it('should call `onScrollFrame`', done => { 74 | const spy = createSpy() 75 | render(( 76 | 77 |
78 | 79 | ), node, function callback() { 80 | this.scrollLeft(50) 81 | setTimeout(() => { 82 | expect(spy.calls.length).toEqual(1) 83 | const args = spy.calls[0].arguments 84 | const values = args[0] 85 | expect(values).toBeA(Object) 86 | 87 | if (scrollbarWidth) { 88 | expect(values).toEqual({ 89 | left: 0.5, 90 | top: 0, 91 | scrollLeft: 50, 92 | scrollTop: 0, 93 | scrollWidth: 200, 94 | scrollHeight: 200, 95 | clientWidth: 100, 96 | clientHeight: 100, 97 | }) 98 | } else { 99 | expect(values).toEqual({ 100 | left: values.scrollLeft / (values.scrollWidth - (values.clientWidth)), 101 | top: 0, 102 | scrollLeft: 50, 103 | scrollTop: 0, 104 | scrollWidth: 200, 105 | scrollHeight: 200, 106 | clientWidth: 100 - envScrollbarWidth, 107 | clientHeight: 100 - envScrollbarWidth, 108 | }) 109 | } 110 | done() 111 | }, 100) 112 | }) 113 | }) 114 | it('should call `onScrollStart` once', done => { 115 | const spy = createSpy() 116 | render(( 117 | 118 |
119 | 120 | ), node, function callback() { 121 | let left = 0 122 | const interval = setInterval(() => { 123 | this.scrollLeft(++left) 124 | if (left >= 50) { 125 | clearInterval(interval) 126 | expect(spy.calls.length).toEqual(1) 127 | done() 128 | } 129 | }, 10) 130 | }) 131 | }) 132 | it('should call `onScrollStop` once when scrolling stops', done => { 133 | const spy = createSpy() 134 | render(( 135 | 136 |
137 | 138 | ), node, function callback() { 139 | let left = 0 140 | const interval = setInterval(() => { 141 | this.scrollLeft(++left) 142 | if (left >= 50) { 143 | clearInterval(interval) 144 | setTimeout(() => { 145 | expect(spy.calls.length).toEqual(1) 146 | done() 147 | }, 300) 148 | } 149 | }, 10) 150 | }) 151 | }) 152 | }) 153 | 154 | describe('when scrolling y-axis', () => { 155 | it('should call `onScroll`', done => { 156 | const spy = createSpy() 157 | render(( 158 | 159 |
160 | 161 | ), node, function callback() { 162 | this.scrollTop(50) 163 | setTimeout(() => { 164 | expect(spy.calls.length).toEqual(1) 165 | const args = spy.calls[0].arguments 166 | const event = args[0] 167 | expect(event).toBeA(Event) 168 | done() 169 | }, 100) 170 | }) 171 | }) 172 | it('should call `onScrollFrame`', done => { 173 | const spy = createSpy() 174 | render(( 175 | 176 |
177 | 178 | ), node, function callback() { 179 | this.scrollTop(50) 180 | setTimeout(() => { 181 | expect(spy.calls.length).toEqual(1) 182 | const args = spy.calls[0].arguments 183 | const values = args[0] 184 | expect(values).toBeA(Object) 185 | 186 | if (scrollbarWidth) { 187 | expect(values).toEqual({ 188 | left: 0, 189 | top: 0.5, 190 | scrollLeft: 0, 191 | scrollTop: 50, 192 | scrollWidth: 200, 193 | scrollHeight: 200, 194 | clientWidth: 100, 195 | clientHeight: 100, 196 | }) 197 | } else { 198 | expect(values).toEqual({ 199 | left: 0, 200 | top: values.scrollTop / (values.scrollHeight - (values.clientHeight)), 201 | scrollLeft: 0, 202 | scrollTop: 50, 203 | scrollWidth: 200, 204 | scrollHeight: 200, 205 | clientWidth: 100 - envScrollbarWidth, 206 | clientHeight: 100 - envScrollbarWidth, 207 | }) 208 | } 209 | done() 210 | }, 100) 211 | }) 212 | }) 213 | it('should call `onScrollStart` once', done => { 214 | const spy = createSpy() 215 | render(( 216 | 217 |
218 | 219 | ), node, function callback() { 220 | let top = 0 221 | const interval = setInterval(() => { 222 | this.scrollTop(++top) 223 | if (top >= 50) { 224 | clearInterval(interval) 225 | expect(spy.calls.length).toEqual(1) 226 | done() 227 | } 228 | }, 10) 229 | }) 230 | }) 231 | it('should call `onScrollStop` once when scrolling stops', done => { 232 | const spy = createSpy() 233 | render(( 234 | 235 |
236 | 237 | ), node, function callback() { 238 | let top = 0 239 | const interval = setInterval(() => { 240 | this.scrollTop(++top) 241 | if (top >= 50) { 242 | clearInterval(interval) 243 | setTimeout(() => { 244 | expect(spy.calls.length).toEqual(1) 245 | done() 246 | }, 300) 247 | } 248 | }, 10) 249 | }) 250 | }) 251 | }) 252 | }) 253 | } 254 | -------------------------------------------------------------------------------- /test/Scrollbars/universal.js: -------------------------------------------------------------------------------- 1 | import { Scrollbars } from 'preact-custom-scrollbars' 2 | import { render, unmountComponentAtNode } from 'preact-dom' 3 | import Preact from 'preact' 4 | 5 | export default function createTests(scrollbarWidth) { 6 | let node 7 | beforeEach(() => { 8 | node = document.createElement('div') 9 | document.body.appendChild(node) 10 | }) 11 | afterEach(() => { 12 | unmountComponentAtNode(node) 13 | document.body.removeChild(node) 14 | }) 15 | 16 | describe('universal', () => { 17 | describe('default', () => { 18 | describe('when rendered', () => { 19 | it('should hide overflow', done => { 20 | class ScrollbarsTest extends Scrollbars { 21 | // Override componentDidMount, so we can check, how the markup 22 | // looks like on the first rendering 23 | componentDidMount() {} 24 | } 25 | render(( 26 | 27 |
28 | 29 | ), node, function callback() { 30 | const { view, trackHorizontal, trackVertical } = this.refs 31 | expect(view.style.position).toEqual('absolute') 32 | expect(view.style.overflow).toEqual('hidden') 33 | expect(view.style.top).toEqual('0px') 34 | expect(view.style.bottom).toEqual('0px') 35 | expect(view.style.left).toEqual('0px') 36 | expect(view.style.right).toEqual('0px') 37 | expect(view.style.marginBottom).toEqual('0px') 38 | expect(view.style.marginRight).toEqual('0px') 39 | expect(trackHorizontal.style.display).toEqual('none') 40 | expect(trackVertical.style.display).toEqual('none') 41 | done() 42 | }) 43 | }) 44 | }) 45 | describe('when componentDidMount', () => { 46 | it('should rerender', done => { 47 | render(( 48 | 49 |
50 | 51 | ), node, function callback() { 52 | const { view } = this.refs 53 | expect(view.style.overflow).toEqual('scroll') 54 | expect(view.style.marginBottom).toEqual(`${-scrollbarWidth}px`) 55 | expect(view.style.marginRight).toEqual(`${-scrollbarWidth}px`) 56 | done() 57 | }) 58 | }) 59 | }) 60 | }) 61 | describe('when using autoHeight', () => { 62 | describe('when rendered', () => { 63 | it('should hide overflow', done => { 64 | class ScrollbarsTest extends Scrollbars { 65 | // Override componentDidMount, so we can check, how the markup 66 | // looks like on the first rendering 67 | componentDidMount() {} 68 | } 69 | render(( 70 | 71 |
72 | 73 | ), node, function callback() { 74 | const { view, trackHorizontal, trackVertical } = this.refs 75 | expect(view.style.position).toEqual('relative') 76 | expect(view.style.overflow).toEqual('hidden') 77 | expect(view.style.marginBottom).toEqual('0px') 78 | expect(view.style.marginRight).toEqual('0px') 79 | expect(view.style.minHeight).toEqual('0px') 80 | expect(view.style.maxHeight).toEqual('100px') 81 | expect(trackHorizontal.style.display).toEqual('none') 82 | expect(trackVertical.style.display).toEqual('none') 83 | done() 84 | }) 85 | }) 86 | }) 87 | describe('when componentDidMount', () => { 88 | it('should rerender', done => { 89 | render(( 90 | 91 |
92 | 93 | ), node, function callback() { 94 | const { view } = this.refs 95 | expect(view.style.overflow).toEqual('scroll') 96 | expect(view.style.marginBottom).toEqual(`${-scrollbarWidth}px`) 97 | expect(view.style.marginRight).toEqual(`${-scrollbarWidth}px`) 98 | expect(view.style.minHeight).toEqual(`${scrollbarWidth}px`) 99 | expect(view.style.maxHeight).toEqual(`${100 + scrollbarWidth}px`) 100 | done() 101 | }) 102 | }) 103 | }) 104 | }) 105 | }) 106 | } 107 | -------------------------------------------------------------------------------- /test/browser.spec.js: -------------------------------------------------------------------------------- 1 | import getScrollbarWidth from '../src/utils/getScrollbarWidth' 2 | import createTests from './Scrollbars' 3 | 4 | describe('Scrollbars (browser)', () => { 5 | createTests(getScrollbarWidth(), getScrollbarWidth()) 6 | }) 7 | -------------------------------------------------------------------------------- /test/mobile.spec.js: -------------------------------------------------------------------------------- 1 | const getScrollbarWidthModule = require('../src/utils/getScrollbarWidth') 2 | const envScrollbarWidth = getScrollbarWidthModule.default() 3 | import createTests from './Scrollbars' 4 | 5 | describe('Scrollbars (mobile)', () => { 6 | const mobileScrollbarsWidth = 0 7 | let getScrollbarWidthSpy 8 | 9 | before(() => { 10 | getScrollbarWidthSpy = spyOn(getScrollbarWidthModule, 'default') 11 | getScrollbarWidthSpy.andReturn(mobileScrollbarsWidth) 12 | }) 13 | 14 | after(() => { 15 | getScrollbarWidthSpy.restore() 16 | }) 17 | 18 | createTests(mobileScrollbarsWidth, envScrollbarWidth) 19 | }) 20 | -------------------------------------------------------------------------------- /test/utils.spec.js: -------------------------------------------------------------------------------- 1 | import returnFalse from '../src/utils/returnFalse' 2 | describe('utils', () => { 3 | describe('returnFalse', () => { 4 | it('should return false', done => { 5 | expect(returnFalse()).toEqual(false) 6 | done() 7 | }) 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var webpack = require('webpack'); 4 | 5 | var plugins = [ 6 | new webpack.optimize.OccurenceOrderPlugin(), 7 | new webpack.DefinePlugin({ 8 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV) 9 | }) 10 | ]; 11 | 12 | if (process.env.NODE_ENV === 'production') { 13 | plugins.push( 14 | new webpack.optimize.UglifyJsPlugin({ 15 | compressor: { 16 | screw_ie8: true, 17 | warnings: false 18 | } 19 | }) 20 | ); 21 | } 22 | 23 | module.exports = { 24 | externals: { 25 | preact: { 26 | root: 'Preact', 27 | commonjs2: 'preact', 28 | commonjs: 'preact', 29 | amd: 'preact' 30 | } 31 | }, 32 | module: { 33 | loaders: [{ 34 | test: /\.js$/, 35 | loaders: ['babel-loader'], 36 | exclude: /node_modules/ 37 | }] 38 | }, 39 | output: { 40 | library: 'PreactCustomScrollbars', 41 | libraryTarget: 'umd' 42 | }, 43 | plugins: plugins, 44 | resolve: { 45 | extensions: ['', '.js'] 46 | } 47 | }; 48 | --------------------------------------------------------------------------------