├── .nvmrc
├── .travis.yml
├── .npmignore
├── src
├── utils
│ ├── returnFalse.js
│ ├── isString.js
│ ├── getInnerWidth.js
│ ├── getInnerHeight.js
│ └── getScrollbarWidth.js
├── index.js
└── Scrollbars
│ ├── defaultRenderElements.js
│ ├── styles.js
│ └── index.js
├── .gitignore
├── .eslintignore
├── .eslintrc
├── .babelrc
├── CHANGELOG.md
├── test
├── browser.spec.js
├── utils.spec.js
├── .eslintrc
├── mobile.spec.js
└── Scrollbars
│ ├── index.js
│ ├── resizing.js
│ ├── flexbox.js
│ ├── clickTrack.js
│ ├── onUpdate.js
│ ├── hideTracks.js
│ ├── dragThumb.js
│ ├── gettersSetters.js
│ ├── universal.js
│ ├── autoHeight.js
│ ├── scrolling.js
│ ├── rendering.js
│ └── autoHide.js
├── test.js
├── docs
├── README.md
├── upgrade-guide-v2-v3.md
├── API.md
├── usage.md
├── customization.md
└── v2-documentation.md
├── prepublish.js
├── LICENSE.md
├── webpack.config.js
├── CODE_OF_CONDUCT.md
├── karma.conf.js
├── package.json
└── README.md
/.nvmrc:
--------------------------------------------------------------------------------
1 | 6
2 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "iojs"
4 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | *.log
3 | src
4 | test
5 | examples
6 | coverage
7 |
--------------------------------------------------------------------------------
/src/utils/returnFalse.js:
--------------------------------------------------------------------------------
1 | export default function returnFalse() {
2 | return false
3 | }
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | *.log
3 | .DS_Store
4 | dist
5 | lib
6 | coverage
7 | examples/simple/static
8 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | lib
2 | **/node_modules
3 | **/webpack.config.js
4 | **/prepublish.js
5 | examples/**/server.js
6 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import Scrollbars from './Scrollbars'
2 | export default Scrollbars
3 | export { Scrollbars }
4 |
--------------------------------------------------------------------------------
/src/utils/isString.js:
--------------------------------------------------------------------------------
1 | export default function isString(maybe) {
2 | return typeof maybe === 'string'
3 | }
4 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "extends": [
4 | "standard",
5 | ],
6 | "settings": {
7 | "react": {
8 | "pragma": "h",
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "sourceMaps": true,
3 | "presets": [
4 | "latest",
5 | "stage-0"
6 | ],
7 | "plugins": [
8 | ["transform-react-jsx", { "pragma": "h" }]
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | [](https://www.npmjs.com/package/preact-custom-scrollbars)
8 | [](https://www.npmjs.com/package/preact-custom-scrollbars)
9 | [](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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
121 | Scroll to top
122 |
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 |
--------------------------------------------------------------------------------
/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 |
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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------