├── .eslintignore
├── .babelrc
├── typings.d.ts
├── src
├── types.d.ts
├── utils
│ ├── returnFalse.ts
│ ├── isString.ts
│ ├── getInnerHeight.ts
│ ├── getInnerWidth.ts
│ ├── index.ts
│ ├── mergeClasses.ts
│ └── getScrollbarWidth.ts
├── index.ts
└── Scrollbars
│ ├── styles.ts
│ ├── types.ts
│ └── index.tsx
├── .npmignore
├── public
├── favicon.ico
├── scrollbar-macos.png
├── scrollbar-components.png
└── logo.svg
├── .gitignore
├── CHANGELOG.md
├── .prettierrc
├── test
├── browser.spec.js
├── utils.spec.js
├── .eslintrc
├── mobile.spec.js
└── Scrollbars
│ ├── resizing.js
│ ├── index.js
│ ├── flexbox.js
│ ├── hideTracks.js
│ ├── clickTrack.js
│ ├── onUpdate.js
│ ├── dragThumb.js
│ ├── gettersSetters.js
│ ├── universal.js
│ ├── autoHeight.js
│ ├── scrolling.js
│ ├── rendering.js
│ └── autoHide.js
├── test.js
├── .umirc.ts
├── .eslintrc
├── prepublish.js
├── tsconfig.json
├── docs
├── components
│ ├── Lorem.tsx
│ ├── SpringScrollbars
│ │ ├── SpringScrollbarsExample.js
│ │ └── SpringScrollbars.js
│ ├── ColoredScrollbars.tsx
│ └── ShadowScrollbars
│ │ └── ShadowScrollbars.tsx
├── usage.md
├── API.md
├── README.md
├── demo.md
└── customization.md
├── LICENSE
├── CODE_OF_CONDUCT.md
├── karma.conf.js
├── package.json
└── README.md
/.eslintignore:
--------------------------------------------------------------------------------
1 | lib
2 | **/node_modules
3 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["react", "es2015", "stage-1"]
3 | }
4 |
--------------------------------------------------------------------------------
/typings.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.css';
2 | declare module '*.less';
3 |
--------------------------------------------------------------------------------
/src/types.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'dom-css';
2 | declare module 'classnames';
3 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | *.log
3 | src
4 | test
5 | examples
6 | coverage
7 | .idea
8 | docs
9 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sakhnyuk/rc-scrollbars/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/src/utils/returnFalse.ts:
--------------------------------------------------------------------------------
1 | export default function returnFalse() {
2 | return false;
3 | }
4 |
--------------------------------------------------------------------------------
/public/scrollbar-macos.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sakhnyuk/rc-scrollbars/HEAD/public/scrollbar-macos.png
--------------------------------------------------------------------------------
/src/utils/isString.ts:
--------------------------------------------------------------------------------
1 | export default function isString(maybe: any) {
2 | return typeof maybe === 'string';
3 | }
4 |
--------------------------------------------------------------------------------
/public/scrollbar-components.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sakhnyuk/rc-scrollbars/HEAD/public/scrollbar-components.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | *.log
3 | .DS_Store
4 | dist
5 | lib
6 | coverage
7 | examples/simple/static
8 | .idea/
9 |
10 | # umi
11 | .umi
12 | .umi-production
13 | .env.local
14 |
--------------------------------------------------------------------------------
/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/sakhnyuk/rc-scrollbars/releases)
6 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import { Scrollbars } from './Scrollbars';
2 |
3 | export { ScrollValues, StyleClasses, ScrollbarsProps } from './Scrollbars/types';
4 | export { Scrollbars };
5 |
6 | export default Scrollbars;
7 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "useTabs": false,
3 | "tabWidth": 2,
4 | "singleQuote": true,
5 | "semi": true,
6 | "printWidth": 100,
7 | "trailingComma": "all",
8 | "proseWrap": "never",
9 | "endOfLine": "auto"
10 | }
11 |
--------------------------------------------------------------------------------
/test/browser.spec.js:
--------------------------------------------------------------------------------
1 | import getScrollbarWidth from '../src/utils/getScrollbarWidth';
2 | import createTests from './Scrollbars';
3 |
4 | describe('Scrollbars (browser)', () => {
5 | createTests(getScrollbarWidth(), getScrollbarWidth());
6 | });
7 |
--------------------------------------------------------------------------------
/test.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 |
--------------------------------------------------------------------------------
/src/utils/getInnerHeight.ts:
--------------------------------------------------------------------------------
1 | export default function getInnerHeight(el?: HTMLElement) {
2 | if (!el) return 0;
3 | const { clientHeight } = el;
4 | const { paddingTop, paddingBottom } = getComputedStyle(el);
5 | return clientHeight - parseFloat(paddingTop) - parseFloat(paddingBottom);
6 | }
7 |
--------------------------------------------------------------------------------
/src/utils/getInnerWidth.ts:
--------------------------------------------------------------------------------
1 | export default function getInnerWidth(el?: HTMLDivElement) {
2 | if (!el) return 0;
3 | const { clientWidth } = el;
4 | const { paddingLeft, paddingRight } = getComputedStyle(el);
5 | return clientWidth - parseFloat(paddingLeft) - parseFloat(paddingRight);
6 | }
7 |
--------------------------------------------------------------------------------
/.umirc.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'dumi';
2 |
3 | export default defineConfig({
4 | title: 'rc-scrollbar',
5 | mode: 'doc',
6 | logo: '/logo.svg',
7 | favicon: '/favicon.ico',
8 | exportStatic: {},
9 | hash: true,
10 | resolve: {
11 | includes: ['docs'],
12 | },
13 | });
14 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "@typescript-eslint/parser",
3 | "extends": ["react-app", "plugin:prettier/recommended", "prettier"],
4 | "plugins": ["prettier"],
5 | "env": {
6 | "browser": true,
7 | "mocha": true,
8 | "node": true
9 | },
10 | "rules": {
11 | "prettier/prettier": "error"
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | export { default as getInnerHeight } from './getInnerHeight';
2 | export { default as getInnerWidth } from './getInnerWidth';
3 | export { default as getScrollbarWidth } from './getScrollbarWidth';
4 | export { default as isString } from './isString';
5 | export { default as getFinalClasses } from './mergeClasses';
6 | export { default as returnFalse } from './returnFalse';
7 |
--------------------------------------------------------------------------------
/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 | import createTests from './Scrollbars';
2 | const getScrollbarWidthModule = require('../src/utils/getScrollbarWidth');
3 | const envScrollbarWidth = getScrollbarWidthModule.default();
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 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "module": "commonjs",
5 | "lib": [
6 | "ES2015",
7 | "ES2016",
8 | "ES2017",
9 | "ES2018",
10 | "ES2019",
11 | "ES2020",
12 | "dom",
13 | "dom.iterable",
14 | "esnext"
15 | ],
16 | "allowJs": true,
17 | "jsx": "react",
18 | "declaration": true,
19 | "outDir": "lib",
20 | "removeComments": true,
21 | "strict": true,
22 | "noImplicitAny": false,
23 | "rootDirs": [
24 | "src"
25 | ],
26 | "esModuleInterop": true,
27 | "skipLibCheck": true,
28 | "forceConsistentCasingInFileNames": true,
29 | "baseUrl": "./",
30 | "allowSyntheticDefaultImports": true
31 | },
32 | "include": [
33 | "src"
34 | ],
35 | "exclude": [
36 | "node_modules",
37 | "test",
38 | "example",
39 | "lib"
40 | ]
41 | }
42 |
--------------------------------------------------------------------------------
/docs/components/Lorem.tsx:
--------------------------------------------------------------------------------
1 | export const Lorem = () => {
2 | const lorem = `Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam
3 | nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam
4 | erat, sed diam voluptua. At vero eos et accusam et justo duo dolores
5 | et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est
6 | Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur
7 | sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore
8 | et dolore magna aliquyam erat, sed diam voluptua. At vero eos et
9 | accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren,
10 | no sea takimata sanctus est Lorem ipsum dolor sit amet.`;
11 |
12 | return (
13 | <>
14 |
{lorem}
15 | {lorem}
16 | {lorem}
17 | {lorem}
18 | {lorem}
19 | {lorem}
20 | {lorem}
21 | {lorem}
22 | >
23 | );
24 | };
25 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Mikhail Sakhniuk
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 |
--------------------------------------------------------------------------------
/src/utils/mergeClasses.ts:
--------------------------------------------------------------------------------
1 | import { ScrollbarsProps, StyleClasses } from '..';
2 |
3 | const defaultClasses: StyleClasses = {
4 | root: 'rc-scrollbars-container',
5 | view: 'rc-scrollbars-view',
6 | trackVertical: 'rc-scrollbars-track rc-scrollbars-track-v',
7 | trackHorizontal: 'rc-scrollbars-track rc-scrollbars-track-h',
8 | thumbVertical: 'rc-scrollbars-thumb rc-scrollbars-thumb-v',
9 | thumbHorizontal: 'rc-scrollbars-thumb rc-scrollbars-thumb-h',
10 | };
11 |
12 | function mergeClasses(defaultClasses, providedClasses) {
13 | return providedClasses
14 | ? Object.keys(defaultClasses).reduce((result, classKey) => {
15 | result[classKey] = `${defaultClasses[classKey]} ${providedClasses[classKey] || ''}`;
16 | return result;
17 | }, {})
18 | : defaultClasses;
19 | }
20 |
21 | export default function getFinalClasses(props: ScrollbarsProps): StyleClasses {
22 | const { className, classes } = props;
23 | const { root: defaultRootClass, ...rest } = defaultClasses;
24 |
25 | return {
26 | root: [defaultRootClass, className, classes?.root].filter(Boolean).join(' '),
27 | ...mergeClasses(rest, props.classes),
28 | };
29 | }
30 |
--------------------------------------------------------------------------------
/test/Scrollbars/resizing.js:
--------------------------------------------------------------------------------
1 | import { Scrollbars } from 'react-custom-scrollbars';
2 | import { render, unmountComponentAtNode } from 'react-dom';
3 | import simulant from 'simulant';
4 |
5 | export default function createTests(scrollbarWidth) {
6 | // Not for mobile environment
7 | if (!scrollbarWidth) return;
8 |
9 | let node;
10 | beforeEach(() => {
11 | node = document.createElement('div');
12 | document.body.appendChild(node);
13 | });
14 | afterEach(() => {
15 | unmountComponentAtNode(node);
16 | document.body.removeChild(node);
17 | });
18 |
19 | describe('when resizing window', () => {
20 | it('should update scrollbars', (done) => {
21 | render(
22 |
23 |
24 | ,
25 | node,
26 | 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 | }
38 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/utils/getScrollbarWidth.ts:
--------------------------------------------------------------------------------
1 | import css from 'dom-css';
2 |
3 | let scrollbarWidth: number | undefined = undefined;
4 | let pxRatio: number = getPxRatio();
5 |
6 | export default function getScrollbarWidth() {
7 | /**
8 | * Check zoom ratio. If it was changed, then it would update scrollbatWidth
9 | */
10 | const newPxRatio = getPxRatio();
11 |
12 | if (pxRatio !== newPxRatio) {
13 | scrollbarWidth = getScrollbarWidthFromDom();
14 | pxRatio = newPxRatio;
15 | }
16 |
17 | if (typeof scrollbarWidth === 'number') return scrollbarWidth;
18 |
19 | /* istanbul ignore else */
20 | if (typeof document !== 'undefined') {
21 | scrollbarWidth = getScrollbarWidthFromDom();
22 | } else {
23 | scrollbarWidth = 0;
24 | }
25 |
26 | return scrollbarWidth || 0;
27 | }
28 |
29 | function getScrollbarWidthFromDom() {
30 | const div = document.createElement('div');
31 |
32 | css(div, {
33 | width: 100,
34 | height: 100,
35 | position: 'absolute',
36 | top: -9999,
37 | overflow: 'scroll',
38 | MsOverflowStyle: 'scrollbar',
39 | });
40 |
41 | document.body.appendChild(div);
42 | const result = div.offsetWidth - div.clientWidth;
43 | document.body.removeChild(div);
44 |
45 | return result;
46 | }
47 |
48 | function getPxRatio() {
49 | if (typeof window === 'undefined') return 1;
50 | return window.screen.availWidth / document.documentElement.clientWidth;
51 | }
52 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | 'react-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 |
--------------------------------------------------------------------------------
/docs/components/SpringScrollbars/SpringScrollbarsExample.js:
--------------------------------------------------------------------------------
1 | import random from 'lodash/random';
2 | import { Component } from 'react';
3 | import SpringScrollbars from './SpringScrollbars';
4 | import { Lorem } from '../Lorem';
5 |
6 | export default class SpringScrollbarsExample extends Component {
7 | constructor(props, ...rest) {
8 | super(props, ...rest);
9 | this.handleClickRandomPosition = this.handleClickRandomPosition.bind(this);
10 | }
11 |
12 | handleClickRandomPosition() {
13 | const { scrollbars } = this.refs;
14 | const scrollHeight = scrollbars.getScrollHeight();
15 | scrollbars.scrollTop(random(scrollHeight));
16 | }
17 |
18 | render() {
19 | return (
20 |
21 |
22 |
23 |
24 |
25 |
28 |
29 |
30 |
31 | The Scrollbars are animated with{' '}
32 |
38 | Rebound
39 |
40 | . You can simply animate the Scrollbars with scrollbars.scrollTop(x).
41 |
42 |
43 | Don't forget to wrap your steps with requestAnimationFrame.
44 |
45 |
46 |
47 | );
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/test/Scrollbars/flexbox.js:
--------------------------------------------------------------------------------
1 | import { Scrollbars } from 'react-custom-scrollbars';
2 | import { render, unmountComponentAtNode, findDOMNode } from 'react-dom';
3 | import { Component } from 'react';
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 | class Root extends Component {
18 | render() {
19 | return (
20 |
31 |
{
33 | this.scrollbars = ref;
34 | }}
35 | >
36 |
37 |
38 |
39 | );
40 | }
41 | }
42 | render(, node, function callback() {
43 | setTimeout(() => {
44 | const { scrollbars } = this;
45 | const $scrollbars = findDOMNode(scrollbars);
46 | const $view = scrollbars.view;
47 | expect($scrollbars.clientHeight).toBeGreaterThan(0);
48 | expect($view.clientHeight).toBeGreaterThan(0);
49 | done();
50 | }, 100);
51 | });
52 | });
53 | });
54 | }
55 |
--------------------------------------------------------------------------------
/docs/components/SpringScrollbars/SpringScrollbars.js:
--------------------------------------------------------------------------------
1 | import { Component } from 'react';
2 | import { Scrollbars } from 'rc-scrollbars';
3 | import { SpringSystem, MathUtil } from 'rebound';
4 |
5 | export default class SpringScrollbars extends Component {
6 | constructor(props, ...rest) {
7 | super(props, ...rest);
8 | this.handleSpringUpdate = this.handleSpringUpdate.bind(this);
9 | }
10 |
11 | componentDidMount() {
12 | this.springSystem = new SpringSystem();
13 | this.spring = this.springSystem.createSpring();
14 | this.spring.addListener({ onSpringUpdate: this.handleSpringUpdate });
15 | }
16 |
17 | componentWillUnmount() {
18 | this.springSystem.deregisterSpring(this.spring);
19 | this.springSystem.removeAllListeners();
20 | this.springSystem = undefined;
21 | this.spring.destroy();
22 | this.spring = undefined;
23 | }
24 |
25 | getScrollTop() {
26 | return this.refs.scrollbars.getScrollTop();
27 | }
28 |
29 | getScrollHeight() {
30 | return this.refs.scrollbars.getScrollHeight();
31 | }
32 |
33 | getHeight() {
34 | return this.refs.scrollbars.getHeight();
35 | }
36 |
37 | scrollTop(top) {
38 | const { scrollbars } = this.refs;
39 | const scrollTop = scrollbars.getScrollTop();
40 | const scrollHeight = scrollbars.getScrollHeight();
41 | const val = MathUtil.mapValueInRange(
42 | top,
43 | 0,
44 | scrollHeight,
45 | scrollHeight * 0.2,
46 | scrollHeight * 0.8,
47 | );
48 | this.spring.setCurrentValue(scrollTop).setAtRest();
49 | this.spring.setEndValue(val);
50 | }
51 |
52 | handleSpringUpdate(spring) {
53 | const { scrollbars } = this.refs;
54 | const val = spring.getCurrentValue();
55 | scrollbars.scrollTop(val);
56 | }
57 |
58 | render() {
59 | return ;
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/docs/components/ColoredScrollbars.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component, HTMLAttributes } from 'react';
2 | import { Scrollbars, ScrollValues } from 'rc-scrollbars';
3 |
4 | type State = {
5 | top: number;
6 | };
7 |
8 | export default class ColoredScrollbars extends Component {
9 | state: State;
10 |
11 | constructor(props) {
12 | super(props);
13 | this.state = { top: 0 };
14 | this.handleUpdate = this.handleUpdate.bind(this);
15 | this.renderView = this.renderView.bind(this);
16 | this.renderThumb = this.renderThumb.bind(this);
17 | }
18 |
19 | handleUpdate = (values: ScrollValues) => {
20 | const { top } = values;
21 | this.setState({ top });
22 | };
23 |
24 | renderView = ({ style, ...props }: HTMLAttributes) => {
25 | const { top } = this.state;
26 | const viewStyle = {
27 | padding: 15,
28 | backgroundColor: `rgb(${Math.round(255 - top * 255)}, ${Math.round(top * 255)}, ${Math.round(
29 | 255,
30 | )})`,
31 | color: `rgb(${Math.round(255 - top * 255)}, ${Math.round(255 - top * 255)}, ${Math.round(
32 | 255 - top * 255,
33 | )})`,
34 | };
35 | return ;
36 | };
37 |
38 | renderThumb = ({ style, ...props }: HTMLAttributes) => {
39 | const { top } = this.state;
40 | const thumbStyle = {
41 | backgroundColor: `rgb(${Math.round(255 - top * 255)}, ${Math.round(
42 | 255 - top * 255,
43 | )}, ${Math.round(255 - top * 255)})`,
44 | borderRadius: 'inherit',
45 | };
46 | return ;
47 | };
48 |
49 | render() {
50 | return (
51 |
58 | );
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/Scrollbars/styles.ts:
--------------------------------------------------------------------------------
1 | import { CSSProperties } from 'react';
2 | import { StyleKeys } from './types';
3 |
4 | export function createStyles(disableDefaultStyles: boolean): Record {
5 | const trackStyleDefault: CSSProperties = {
6 | position: 'absolute',
7 | right: 2,
8 | bottom: 2,
9 | zIndex: 100,
10 | ...(!disableDefaultStyles && { borderRadius: 3 }),
11 | };
12 |
13 | return {
14 | containerStyleDefault: {
15 | position: 'relative',
16 | overflow: 'hidden',
17 | width: '100%',
18 | height: '100%',
19 | },
20 |
21 | containerStyleAutoHeight: {
22 | height: 'auto',
23 | },
24 |
25 | viewStyleDefault: {
26 | position: 'absolute',
27 | top: 0,
28 | left: 0,
29 | right: 0,
30 | bottom: 0,
31 | overflow: 'scroll',
32 | WebkitOverflowScrolling: 'touch',
33 | },
34 |
35 | // Overrides viewStyleDefault properties
36 | viewStyleAutoHeight: {
37 | position: 'relative',
38 | top: undefined,
39 | left: undefined,
40 | right: undefined,
41 | bottom: undefined,
42 | },
43 |
44 | viewStyleUniversalInitial: {
45 | overflow: 'hidden',
46 | marginRight: 0,
47 | marginBottom: 0,
48 | },
49 |
50 | trackHorizontalStyleDefault: {
51 | ...trackStyleDefault,
52 | left: 2,
53 | height: 6,
54 | },
55 |
56 | trackVerticalStyleDefault: {
57 | ...trackStyleDefault,
58 | top: 2,
59 | width: 6,
60 | },
61 |
62 | thumbStyleDefault: {
63 | position: 'relative',
64 | display: 'block',
65 | height: '100%',
66 | cursor: 'pointer',
67 | borderRadius: 'inherit',
68 | ...(!disableDefaultStyles && { backgroundColor: 'rgba(0,0,0,.2)' }),
69 | },
70 |
71 | disableSelectStyle: {
72 | userSelect: 'none',
73 | },
74 |
75 | disableSelectStyleReset: {
76 | userSelect: 'auto',
77 | },
78 | };
79 | }
80 |
--------------------------------------------------------------------------------
/docs/components/ShadowScrollbars/ShadowScrollbars.tsx:
--------------------------------------------------------------------------------
1 | import { Component, CSSProperties } from 'react';
2 | import { Scrollbars } from 'rc-scrollbars';
3 | import css from 'dom-css';
4 |
5 | type Props = {
6 | style: CSSProperties;
7 | };
8 |
9 | class ShadowScrollbars extends Component {
10 | constructor(props) {
11 | super(props);
12 | this.state = {
13 | scrollTop: 0,
14 | scrollHeight: 0,
15 | clientHeight: 0,
16 | };
17 | this.handleUpdate = this.handleUpdate.bind(this);
18 | }
19 |
20 | handleUpdate(values) {
21 | const { shadowTop, shadowBottom } = this.refs;
22 | const { scrollTop, scrollHeight, clientHeight } = values;
23 | const shadowTopOpacity = (1 / 20) * Math.min(scrollTop, 20);
24 | const bottomScrollTop = scrollHeight - clientHeight;
25 | const shadowBottomOpacity =
26 | (1 / 20) * (bottomScrollTop - Math.max(scrollTop, bottomScrollTop - 20));
27 | css(shadowTop, { opacity: shadowTopOpacity });
28 | css(shadowBottom, { opacity: shadowBottomOpacity });
29 | }
30 |
31 | render() {
32 | const { style, ...props } = this.props;
33 | const containerStyle: CSSProperties = {
34 | ...style,
35 | position: 'relative',
36 | };
37 | const shadowTopStyle: CSSProperties = {
38 | position: 'absolute',
39 | top: 0,
40 | left: 0,
41 | right: 0,
42 | height: 10,
43 | background: 'linear-gradient(to bottom, rgba(0, 0, 0, 0.2) 0%, rgba(0, 0, 0, 0) 100%)',
44 | };
45 | const shadowBottomStyle: CSSProperties = {
46 | position: 'absolute',
47 | bottom: 0,
48 | left: 0,
49 | right: 0,
50 | height: 10,
51 | background: 'linear-gradient(to top, rgba(0, 0, 0, 0.2) 0%, rgba(0, 0, 0, 0) 100%)',
52 | };
53 | return (
54 |
59 | );
60 | }
61 | }
62 |
63 | export default ShadowScrollbars;
64 |
--------------------------------------------------------------------------------
/test/Scrollbars/hideTracks.js:
--------------------------------------------------------------------------------
1 | import { Scrollbars } from 'react-custom-scrollbars';
2 | import { render, unmountComponentAtNode } from 'react-dom';
3 |
4 | export default function createTests(scrollbarWidth) {
5 | describe('hide tracks', () => {
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 native scrollbars have a width', () => {
17 | if (!scrollbarWidth) return;
18 | describe('when content is greater than wrapper', () => {
19 | it('should show tracks', (done) => {
20 | render(
21 |
22 |
23 | ,
24 | node,
25 | function callback() {
26 | setTimeout(() => {
27 | const { trackHorizontal, trackVertical } = this;
28 | expect(trackHorizontal.style.visibility).toEqual('visible');
29 | expect(trackVertical.style.visibility).toEqual('visible');
30 | done();
31 | }, 100);
32 | },
33 | );
34 | });
35 | });
36 | describe('when content is smaller than wrapper', () => {
37 | it('should hide tracks', (done) => {
38 | render(
39 |
40 |
41 | ,
42 | node,
43 | function callback() {
44 | setTimeout(() => {
45 | const { trackHorizontal, trackVertical } = this;
46 | expect(trackHorizontal.style.visibility).toEqual('hidden');
47 | expect(trackVertical.style.visibility).toEqual('hidden');
48 | done();
49 | }, 100);
50 | },
51 | );
52 | });
53 | });
54 | });
55 | });
56 | }
57 |
--------------------------------------------------------------------------------
/test/Scrollbars/clickTrack.js:
--------------------------------------------------------------------------------
1 | import { Scrollbars } from 'react-custom-scrollbars';
2 | import { render, unmountComponentAtNode } from 'react-dom';
3 | import simulant from 'simulant';
4 |
5 | export default function createTests(scrollbarWidth) {
6 | // Not for mobile environment
7 | if (!scrollbarWidth) return;
8 |
9 | let node;
10 | beforeEach(() => {
11 | node = document.createElement('div');
12 | document.body.appendChild(node);
13 | });
14 | afterEach(() => {
15 | unmountComponentAtNode(node);
16 | document.body.removeChild(node);
17 | });
18 |
19 | describe('when clicking on horizontal track', () => {
20 | it('should scroll to the respective position', (done) => {
21 | render(
22 |
23 |
24 | ,
25 | node,
26 | function callback() {
27 | setTimeout(() => {
28 | const { view, trackHorizontal: bar } = this;
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 |
42 | describe('when clicking on vertical track', () => {
43 | it('should scroll to the respective position', (done) => {
44 | render(
45 |
46 |
47 | ,
48 | node,
49 | function callback() {
50 | setTimeout(() => {
51 | const { view, trackVertical: bar } = this;
52 | const { top, height } = bar.getBoundingClientRect();
53 | simulant.fire(bar, 'mousedown', {
54 | target: bar,
55 | clientY: top + height / 2,
56 | });
57 | expect(view.scrollTop).toEqual(50);
58 | done();
59 | }, 100);
60 | },
61 | );
62 | });
63 | });
64 | }
65 |
--------------------------------------------------------------------------------
/src/Scrollbars/types.ts:
--------------------------------------------------------------------------------
1 | import { CSSProperties, HTMLAttributes } from 'react';
2 | import * as React from 'react';
3 |
4 | export interface ScrollbarsProps {
5 | children?: React.ReactNode;
6 | autoHeight: boolean;
7 | autoHeightMax: number | string;
8 | autoHeightMin: number | string;
9 | autoHide: boolean;
10 | autoHideDuration: number;
11 | autoHideTimeout: number;
12 | /* class applied to the root element */
13 | className?: string;
14 | classes?: Partial;
15 | disableDefaultStyles: boolean;
16 | hideTracksWhenNotNeeded?: boolean;
17 | id?: string;
18 | onScroll?: (e: React.UIEvent) => void;
19 | onScrollFrame?: (values: ScrollValues) => void;
20 | onScrollStart?: () => void;
21 | onScrollStop?: () => void;
22 | onUpdate?: (values: ScrollValues) => void;
23 | renderThumbHorizontal: CustomRenderer;
24 | renderThumbVertical: CustomRenderer;
25 | renderTrackHorizontal: CustomRenderer;
26 | renderTrackVertical: CustomRenderer;
27 | renderView: CustomRenderer;
28 | style?: React.CSSProperties;
29 | tagName: string;
30 | thumbMinSize: number;
31 | thumbSize?: number;
32 | universal: boolean;
33 | }
34 |
35 | export interface ScrollValues {
36 | left: number;
37 | top: number;
38 | scrollLeft: number;
39 | scrollTop: number;
40 | scrollWidth: number;
41 | scrollHeight: number;
42 | clientWidth: number;
43 | clientHeight: number;
44 | }
45 |
46 | export interface StyleClasses {
47 | root: string;
48 | view: string;
49 | trackHorizontal: string;
50 | thumbHorizontal: string;
51 | trackVertical: string;
52 | thumbVertical: string;
53 | }
54 |
55 | export type StyleKeys =
56 | | 'containerStyleDefault'
57 | | 'containerStyleAutoHeight'
58 | | 'viewStyleDefault'
59 | | 'viewStyleAutoHeight'
60 | | 'viewStyleUniversalInitial'
61 | | 'trackHorizontalStyleDefault'
62 | | 'trackVerticalStyleDefault'
63 | | 'thumbStyleDefault'
64 | | 'disableSelectStyle'
65 | | 'disableSelectStyleReset';
66 |
67 | export interface CustomRenderProps extends HTMLAttributes {
68 | style: CSSProperties;
69 | className: string;
70 | }
71 |
72 | export interface CustomRenderer {
73 | (props: CustomRenderProps): JSX.Element;
74 | }
75 |
--------------------------------------------------------------------------------
/test/Scrollbars/onUpdate.js:
--------------------------------------------------------------------------------
1 | import { Scrollbars } from 'react-custom-scrollbars';
2 | import { render, unmountComponentAtNode } from 'react-dom';
3 |
4 | export default function createTests() {
5 | let node;
6 | beforeEach(() => {
7 | node = document.createElement('div');
8 | document.body.appendChild(node);
9 | });
10 | afterEach(() => {
11 | unmountComponentAtNode(node);
12 | document.body.removeChild(node);
13 | });
14 |
15 | describe('onUpdate', () => {
16 | describe('when scrolling x-axis', () => {
17 | it('should call `onUpdate`', (done) => {
18 | const spy = createSpy();
19 | render(
20 |
21 |
22 | ,
23 | node,
24 | function callback() {
25 | this.scrollLeft(50);
26 | setTimeout(() => {
27 | expect(spy.calls.length).toEqual(1);
28 | done();
29 | }, 100);
30 | },
31 | );
32 | });
33 | });
34 | describe('when scrolling y-axis', () => {
35 | it('should call `onUpdate`', (done) => {
36 | const spy = createSpy();
37 | render(
38 |
39 |
40 | ,
41 | node,
42 | function callback() {
43 | this.scrollTop(50);
44 | setTimeout(() => {
45 | expect(spy.calls.length).toEqual(1);
46 | done();
47 | }, 100);
48 | },
49 | );
50 | });
51 | });
52 |
53 | describe('when resizing window', () => {
54 | it('should call onUpdate', (done) => {
55 | const spy = createSpy();
56 | render(
57 |
58 |
59 | ,
60 | node,
61 | function callback() {
62 | setTimeout(() => {
63 | expect(spy.calls.length).toEqual(1);
64 | done();
65 | }, 100);
66 | },
67 | );
68 | });
69 | });
70 | });
71 | }
72 |
--------------------------------------------------------------------------------
/public/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "rc-scrollbars",
3 | "version": "1.1.6",
4 | "description": "React scrollbars component",
5 | "main": "lib/index.js",
6 | "scripts": {
7 | "clean": "rimraf lib",
8 | "dev:lib": "tsc -w",
9 | "dev:docs": "dumi dev",
10 | "dev": "concurrently -k -p \"[{name}]\" -n \"LIB,DOCS\" -c \"bgMagenta.bold,bgGreen.bold\" \"npm run dev:lib\" \"npm run dev:docs\"",
11 | "build:lib": "tsc",
12 | "build:docs": "dumi build",
13 | "lint": "eslint src test",
14 | "test": "cross-env NODE_ENV=test karma start",
15 | "test:watch": "cross-env NODE_ENV=test karma start --auto-watch --no-single-run",
16 | "test:cov": "cross-env NODE_ENV=test COVERAGE=true karma start --single-run",
17 | "prepublish": "npm run lint && npm run clean && npm run build:lib"
18 | },
19 | "repository": {
20 | "type": "git",
21 | "url": "https://github.com/sakhnyuk/rc-scrollbars.git",
22 | "branch": "main"
23 | },
24 | "keywords": [
25 | "scroll",
26 | "scroller",
27 | "scrollbars",
28 | "react-component",
29 | "react",
30 | "custom",
31 | "rc-scrollbars"
32 | ],
33 | "author": "Mikhail Sakhniuk",
34 | "license": "MIT",
35 | "bugs": {
36 | "url": "https://github.com/sakhnyuk/rc-scrollbars/issues"
37 | },
38 | "homepage": "https://github.com/sakhnyuk/rc-scrollbars",
39 | "devDependencies": {
40 | "@types/raf": "^3.4.0",
41 | "@types/react": "^18.0.9",
42 | "@types/react-dom": "^18.0.4",
43 | "@typescript-eslint/eslint-plugin": "^5.23.0",
44 | "@typescript-eslint/parser": "^5.23.0",
45 | "concurrently": "^7.1.0",
46 | "cross-env": "^7.0.3",
47 | "dumi": "^1.1.40",
48 | "eslint": "^8.15.0",
49 | "eslint-config-prettier": "^8.5.0",
50 | "eslint-config-react-app": "^7.0.1",
51 | "eslint-plugin-flowtype": "^8.0.3",
52 | "eslint-plugin-import": "^2.26.0",
53 | "eslint-plugin-jsx-a11y": "^6.5.1",
54 | "eslint-plugin-prettier": "^4.0.0",
55 | "eslint-plugin-react": "^7.29.4",
56 | "eslint-plugin-react-hooks": "^4.5.0",
57 | "expect": "^26.6.2",
58 | "fork-ts-checker-webpack-plugin": "^7.2.11",
59 | "glob": "^7.1.6",
60 | "istanbul-instrumenter-loader": "^3.0.1",
61 | "karma": "^6.3.3",
62 | "karma-chrome-launcher": "^3.1.0",
63 | "karma-cli": "^2.0.0",
64 | "karma-coverage": "^2.0.3",
65 | "karma-mocha": "^2.0.1",
66 | "karma-mocha-reporter": "^2.2.5",
67 | "karma-sourcemap-loader": "^0.3.8",
68 | "karma-webpack": "^4.0.2",
69 | "mocha": "^8.2.1",
70 | "prettier": "^2.6.2",
71 | "react": "^18.1.0",
72 | "react-dom": "^18.1.0",
73 | "rebound": "^0.1.0",
74 | "rimraf": "^3.0.2",
75 | "simulant": "^0.2.2",
76 | "typescript": "^4.6.4"
77 | },
78 | "peerDependencies": {
79 | "react": "^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0",
80 | "react-dom": "^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0"
81 | },
82 | "dependencies": {
83 | "dom-css": "^2.1.0",
84 | "raf": "^3.4.1"
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/docs/usage.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Usage
3 | order: 2
4 | ---
5 |
6 | # Usage
7 |
8 | ## Default Scrollbars
9 |
10 | 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`:
11 |
12 | ```jsx | pure
13 | import React from 'react';
14 | import { Scrollbars } from 'rc-scrollbars';
15 | import { Lorem } from './components/Lorem';
16 |
17 | export default () => (
18 |
19 |
20 |
21 | );
22 | ```
23 |
24 | Also don't forget to set the `viewport` meta tag, if you want to **support mobile devices**
25 |
26 | ```html
27 |
31 | ```
32 |
33 | ## Events
34 |
35 | There are several events you can listen to:
36 |
37 | ```jsx | pure
38 | import { Scrollbars } from 'rc-scrollbars';
39 |
40 | class App extends Component {
41 | render() {
42 | return (
43 |
55 | Some great content...
56 |
57 | );
58 | }
59 | }
60 | ```
61 |
62 | ## Auto-hide
63 |
64 | You can activate auto-hide by setting the `autoHide` property.
65 |
66 | Check out [demo](/demo#auto-hide)
67 |
68 | ```jsx | pure
69 | import { Scrollbars } from 'rc-scrollbars';
70 |
71 | class App extends Component {
72 | render() {
73 | return (
74 |
82 | Some great content...
83 |
84 | );
85 | }
86 | }
87 | ```
88 |
89 | ## Auto-height
90 |
91 | You can activate auto-height by setting the `autoHeight` property.
92 |
93 | ```jsx | pure
94 | import React from 'react';
95 | import { Scrollbars } from 'rc-scrollbars';
96 | import { Lorem } from './components/Lorem';
97 |
98 | export default () => {
99 | return (
100 |
101 |
102 |
103 | );
104 | };
105 | ```
106 |
107 | ## Universal rendering
108 |
109 | 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:
110 |
111 | ```jsx | pure
112 | import { Scrollbars } from 'rc-scrollbars';
113 |
114 | export const Component = () => {
115 | return (
116 |
117 | Some great content...
118 |
119 | );
120 | };
121 | ```
122 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | rc-scrollbars
2 | =========================
3 | rejuvenated project of react-custom-scrollbars
4 |
5 | [](https://www.npmjs.com/package/rc-scrollbars)
6 | [](https://www.npmjs.com/package/rc-scrollbars)
7 | [](https://www.npmjs.com/package/rc-scrollbars)
8 |
9 |
10 | * frictionless native browser scrolling
11 | * native scrollbars for mobile devices
12 | * [fully customizable](https://github.com/sakhnyuk/rc-scrollbars/blob/main/docs/customization.md)
13 | * [auto hide](https://github.com/sakhnyuk/rc-scrollbars/blob/main/docs/usage.md#auto-hide)
14 | * [auto height](https://github.com/sakhnyuk/rc-scrollbars/blob/main/docs/usage.md#auto-height)
15 | * [universal](https://github.com/sakhnyuk/rc-scrollbars/blob/main/docs/usage.md#universal-rendering) (runs on client & server)
16 | * `requestAnimationFrame` for 60fps
17 | * no extra stylesheets
18 | * well tested, 100% code coverage
19 |
20 | **[Documentation](https://rc-scrollbars.vercel.app) · [Demos](https://rc-scrollbars.vercel.app/demo)**
21 |
22 | ## Installation
23 | ```bash
24 | npm install rc-scrollbars --save
25 |
26 | # OR
27 |
28 | yarn add rc-scrollbars
29 | ```
30 |
31 | This assumes that you’re using [npm](http://npmjs.com/) 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).
32 |
33 | ## Usage
34 |
35 | This is the minimal configuration. [Check out the Documentation for advanced usage](https://rc-scrollbars.vercel.app/usage).
36 |
37 | ```javascript
38 | import { Scrollbars } from 'rc-scrollbars';
39 |
40 | class App extends Component {
41 | render() {
42 | return (
43 |
44 | Some great content...
45 |
46 | );
47 | }
48 | }
49 | ```
50 |
51 | The `` component is completely customizable. Check out the following code:
52 |
53 | ```javascript
54 | import { Scrollbars } from 'rc-scrollbars';
55 |
56 | class CustomScrollbars extends Component {
57 | render() {
58 | return (
59 |
79 | );
80 | }
81 | }
82 | ```
83 |
84 | All properties are documented in the [API docs](https://rc-scrollbars.vercel.app/api)
85 |
86 | ## Run project locally
87 |
88 | Run the simple example:
89 | ```bash
90 | # Make sure that you've installed the dependencies
91 | yarn
92 | # Run tsc of Scrollbars in watch mode and the documentation project in dev env
93 | yarn dev
94 | ```
95 |
96 | ## License
97 |
98 | MIT
99 |
--------------------------------------------------------------------------------
/docs/API.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: API
3 | order: 4
4 | ---
5 |
6 | # API
7 | ## ``
8 |
9 | | Property | Description | Type | Default |
10 | | --- | --- | --- | --- |
11 | | autoHeight | Enable auto-height mode. When `true` container grows with content | `boolean` | `false` |
12 | | autoHeightMax | Set a minimum height for auto-height mode. Ignoring if **autoHeight** is `false` | `number` | 0 |
13 | | autoHeightMin | Set a maximum height for auto-height mode. Ignoring if **autoHeight** is `false`| `number` | 200 |
14 | | autoHide | Enable auto-hide mode. When `true` tracks will hide automatically and are only visible while scrolling. | `boolean` | `false` |
15 | | autoHideDuration | Duration for hide animation in ms. | `number` | 200 |
16 | | autoHideTimeout | Hide delay in ms. | `number` | 1000 |
17 | | classes | extra custom className/s to any of the subcomponents | Partial | `{}`
18 | | className | className for the root component | `string` | `undefined`
19 | | disableDefaultStyles | removes basic styling to ease visual customization | `boolean` | `false`
20 | | hideTracksWhenNotNeeded | Hide tracks (`visibility: hidden`) when content does not overflow container. | `boolean` | `false` |
21 | | id | The `id` to apply to the root element | `string` | `undefined` |
22 | | onScroll | Event handler | `(e: React.UIEvent) => void` | `undefined` |
23 | | onScrollFrame | Runs inside the animation frame. Type of `ScrollValues` you can check below | `(values: ScrollValues) => void` | `undefined` |
24 | | onScrollStart | Called when scrolling starts | `() => void` | `undefined` |
25 | | onScrollStop | Called when scrolling stops | `() => void` | `undefined` |
26 | | onUpdate | Called when ever the component is updated. Runs inside the animation frame | `(values: ScrollValues) => void` | `undefined` |
27 | | renderThumbHorizontal | Horizontal thumb element | `(props: HTMLAttributes) => JSX.Element` | `undefined` |
28 | | renderThumbVertical | Vertical thumb element | `(props: HTMLAttributes) => JSX.Element` | `undefined` |
29 | | renderTrackHorizontal | Horizontal track element | `(props: HTMLAttributes) => JSX.Element` | `undefined` |
30 | | renderTrackVertical | Vertical track element | `(props: HTMLAttributes) => JSX.Element` | `undefined` |
31 | | renderView | The element your content will be rendered in | `(props: HTMLAttributes) => JSX.Element` | `undefined` |
32 | | thumbMinSize | The element your content will be rendered in | `number` | 30 |
33 | | thumbSize | The element your content will be rendered in | `number or undefined` | `undefined` |
34 | | universal | Enable universal rendering (SSR). [Learn how to use universal rendering](/usage#universal-rendering) | `boolean` | `false` |
35 |
36 | ### ScrollValues
37 | ```typescript
38 | export interface ScrollValues {
39 | left: number; // scrollLeft progess, from 0 to 1
40 | top: number; // scrollTop progess, from 0 to 1
41 | scrollLeft: number; // Native scrollLeft
42 | scrollTop: number; // Native scrollTop
43 | scrollWidth: number; // Width of the view
44 | scrollHeight: number; // Native scrollHeight
45 | clientWidth: number; // Width of the view
46 | clientHeight: number; // Height of the view
47 | }
48 | ```
49 |
50 | ### Methods
51 |
52 | - `scrollTop(top = 0)`: scroll to the top value
53 | - `scrollLeft(left = 0)`: scroll to the left value
54 | - `scrollToTop()`: scroll to top
55 | - `scrollToBottom()`: scroll to bottom
56 | - `scrollToLeft()`: scroll to left
57 | - `scrollToRight()`: scroll to right
58 | - `getScrollLeft()`: get scrollLeft value
59 | - `getScrollTop()`: get scrollTop value
60 | - `getScrollWidth()`: get scrollWidth value
61 | - `getScrollHeight()`: get scrollHeight value
62 | - `getClientWidth()`: get view client width
63 | - `getClientHeight()`: get view client height
64 | - `getValues()`: get an object with values about the current position.
65 |
--------------------------------------------------------------------------------
/test/Scrollbars/dragThumb.js:
--------------------------------------------------------------------------------
1 | import { Scrollbars } from 'react-custom-scrollbars';
2 | import { render, unmountComponentAtNode } from 'react-dom';
3 | import simulant from 'simulant';
4 |
5 | export default function createTests(scrollbarWidth) {
6 | // Not for mobile environment
7 | if (!scrollbarWidth) return;
8 |
9 | let node;
10 | beforeEach(() => {
11 | node = document.createElement('div');
12 | document.body.appendChild(node);
13 | });
14 | afterEach(() => {
15 | unmountComponentAtNode(node);
16 | document.body.removeChild(node);
17 | });
18 | describe('when dragging horizontal thumb', () => {
19 | it('should scroll to the respective position', (done) => {
20 | render(
21 |
22 |
23 | ,
24 | node,
25 | function callback() {
26 | setTimeout(() => {
27 | const { view, thumbHorizontal: thumb } = this;
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 |
44 | it('should disable selection', (done) => {
45 | render(
46 |
47 |
48 | ,
49 | node,
50 | function callback() {
51 | setTimeout(() => {
52 | const { thumbHorizontal: thumb } = this;
53 | const { left } = thumb.getBoundingClientRect();
54 | simulant.fire(thumb, 'mousedown', {
55 | target: thumb,
56 | clientX: left + 1,
57 | });
58 | expect(document.body.style.webkitUserSelect).toEqual('none');
59 | simulant.fire(document, 'mouseup');
60 | expect(document.body.style.webkitUserSelect).toEqual('');
61 | done();
62 | }, 100);
63 | },
64 | );
65 | });
66 | });
67 |
68 | describe('when dragging vertical thumb', () => {
69 | it('should scroll to the respective position', (done) => {
70 | render(
71 |
72 |
73 | ,
74 | node,
75 | function callback() {
76 | setTimeout(() => {
77 | const { view, thumbVertical: thumb } = this;
78 | const { top } = thumb.getBoundingClientRect();
79 | simulant.fire(thumb, 'mousedown', {
80 | target: thumb,
81 | clientY: top + 1,
82 | });
83 | simulant.fire(document, 'mousemove', {
84 | clientY: top + 100,
85 | });
86 | simulant.fire(document, 'mouseup');
87 | expect(view.scrollTop).toEqual(100);
88 | done();
89 | }, 100);
90 | },
91 | );
92 | });
93 |
94 | it('should disable selection', (done) => {
95 | render(
96 |
97 |
98 | ,
99 | node,
100 | function callback() {
101 | setTimeout(() => {
102 | const { thumbVertical: thumb } = this;
103 | const { top } = thumb.getBoundingClientRect();
104 | simulant.fire(thumb, 'mousedown', {
105 | target: thumb,
106 | clientY: top + 1,
107 | });
108 | expect(document.body.style.webkitUserSelect).toEqual('none');
109 | simulant.fire(document, 'mouseup');
110 | expect(document.body.style.webkitUserSelect).toEqual('');
111 | done();
112 | }, 100);
113 | },
114 | );
115 | });
116 | });
117 | }
118 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Getting started
3 | order: 1
4 | ---
5 |
6 | # rc-scrollbars
7 |
8 | React scrollbars component.
9 |
10 | `rc-scrollbars` is rejuvenated project of react-custom-scrollbars
11 |
12 | [](https://www.npmjs.com/package/rc-scrollbars)
13 | [](https://www.npmjs.com/package/rc-scrollbars)
14 | [](https://www.npmjs.com/package/rc-scrollbars)
15 |
16 | * frictionless native browser scrolling
17 | * native scrollbars for mobile devices
18 | * [fully customizable](/customization)
19 | * [auto hide](/usage#auto-hide)
20 | * [auto height](/usage#auto-height)
21 | * [universal](/usage#universal-rendering) (runs on client & server)
22 | * `requestAnimationFrame` for 60fps
23 | * no extra stylesheets
24 | * well tested, 100% code coverage
25 |
26 | #### **[Demos](/demo) · [API](/api) · [GitHub](https://github.com/sakhnyuk/rc-scrollbars)**
27 |
28 | ```jsx
29 | /**
30 | * title: Basic DEMO of scrollbar
31 | * hideActions: ['CSB', 'EXTERNAL']
32 | */
33 | import React from 'react';
34 | import { Scrollbars } from 'rc-scrollbars';
35 | import { Lorem } from './components/Lorem';
36 |
37 | export default () => (
38 |
39 |
40 |
41 | );
42 | ```
43 |
44 | ## Installation
45 | ```bash
46 | npm install rc-scrollbars --save
47 |
48 | # OR
49 |
50 | yarn add rc-scrollbars
51 | ```
52 |
53 | This assumes that you’re using [npm](http://npmjs.com/) 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).
54 |
55 | ## MacOS scrollbars explained
56 |
57 | 
58 |
59 | MacOS have 2 options of scrollbars visibility:
60 | - **Automatically based on mouse and trackpad** and **When scrolling** - Scrollbar thumb visible only while scrolling. Browsers don't add scrollbar blocks and only scrollbar thumbs placed over scroll block. **rc-scrollbars v1 don't render scroll tracks and thumbs**
61 | - **Always** - Show scrollbar always. In that option, browsers add system scrollbars like on Windows and Linux. **rc-scrollbars** working as expected.
62 |
63 | ## Usage
64 |
65 | This is the minimal configuration. [Check out the **Usage** page for advanced examples](/usage).
66 |
67 | ```javascript
68 | import { Scrollbars } from 'rc-scrollbars';
69 |
70 | class App extends Component {
71 | render() {
72 | return (
73 |
74 | Some great content...
75 |
76 | );
77 | }
78 | }
79 | ```
80 |
81 | The `` component is completely customizable. Check out the following code:
82 |
83 | ```javascript
84 | import { Scrollbars } from 'rc-scrollbars';
85 |
86 | class CustomScrollbars extends Component {
87 | render() {
88 | return (
89 |
109 | );
110 | }
111 | }
112 | ```
113 |
114 | All properties are documented in the [API docs](/api)
115 |
116 | ## Run project locally
117 |
118 | ```bash
119 | # Make sure that you've installed the dependencies
120 | yarn
121 | # Run tsc of Scrollbars in watch mode and the documentation project in dev env
122 | yarn dev
123 | ```
124 |
125 | ## License
126 |
127 | MIT
128 |
--------------------------------------------------------------------------------
/docs/demo.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Demo
3 | order: 5
4 | ---
5 |
6 | # Demo of rc-scrollbars
7 |
8 | ## Default usage
9 |
10 | ```jsx
11 | /**
12 | * title: Default vertical content
13 | * desc: Default usage of rc-scrollbars with vertical scroll
14 | * hideActions: ['CSB']
15 | */
16 | import React from 'react';
17 | import { Scrollbars } from 'rc-scrollbars';
18 | import { Lorem } from './components/Lorem';
19 |
20 | export default () => (
21 |
22 |
23 |
24 | );
25 | ```
26 |
27 | ```jsx
28 | /**
29 | * title: Default hozintal content
30 | * desc: Default usage of rc-scrollbars with horizontal and vertical scroll
31 | * hideActions: ['CSB']
32 | */
33 | import React from 'react';
34 | import { Scrollbars } from 'rc-scrollbars';
35 | import { Lorem } from './components/Lorem';
36 |
37 | export default () => (
38 |
39 |
40 |
41 |
42 |
43 | );
44 | ```
45 |
46 | ## Auto-hide
47 |
48 | ```jsx
49 | /**
50 | * hideActions: ['CSB']
51 | */
52 | import React from 'react';
53 | import { Scrollbars } from 'rc-scrollbars';
54 | import { Lorem } from './components/Lorem';
55 |
56 | export default class App extends React.Component {
57 | render() {
58 | return (
59 |
60 |
61 |
62 | );
63 | }
64 | }
65 | ```
66 |
67 | # Scrollbar customization
68 |
69 | ## Colored Scrollbar view
70 |
71 | ```jsx
72 | /**
73 | * title: Colored Scrollbar
74 | * desc: Example type customization of scrollbar and view
75 | * hideActions: ['CSB']
76 | */
77 | import React from 'react';
78 | import ColoredScrollbars from './components/ColoredScrollbars';
79 | import { Lorem } from './components/Lorem';
80 |
81 | export default () => (
82 |
83 |
84 |
85 | );
86 | ```
87 |
88 | ## Scrollbar View with shadow
89 |
90 | ```jsx
91 | /**
92 | * title: Shadow view
93 | * hideActions: ['CSB']
94 | */
95 | import React from 'react';
96 | import { Scrollbars } from 'rc-scrollbars';
97 | import { Lorem } from './components/Lorem';
98 | import ShadowScrollbars from './components/ShadowScrollbars/ShadowScrollbars';
99 |
100 | export default class App extends React.Component {
101 | render() {
102 | return (
103 |
104 |
105 |
106 | );
107 | }
108 | }
109 | ```
110 |
111 | ## Custom scrollbar
112 |
113 | ```tsx
114 | /**
115 | * title: Styled scrollbar
116 | * hideActions: ['CSB']
117 | */
118 | import React from 'react';
119 | import { Scrollbars } from 'rc-scrollbars';
120 | import { Lorem } from './components/Lorem';
121 |
122 | export default class App extends React.Component {
123 | thumbVertical({ style, ...props }: HTMLAttributes) {
124 | const finalStyle = {
125 | ...style,
126 | cursor: 'pointer',
127 | backgroundColor: 'rgba(0,255,0,.6)',
128 | };
129 |
130 | return ;
131 | }
132 |
133 | thumbHorizontal({ style, ...props }: HTMLAttributes) {
134 | const finalStyle = {
135 | ...style,
136 | cursor: 'pointer',
137 | backgroundColor: 'rgba(255,0,0,.6)',
138 | };
139 |
140 | return ;
141 | }
142 |
143 | render() {
144 | return (
145 |
150 |
151 |
152 |
153 |
154 | );
155 | }
156 | }
157 | ```
158 |
159 | ## With SpringSystem by `rebound`
160 |
161 | ```jsx
162 | /**
163 | * title: Spring Scrollbar
164 | * hideActions: ['CSB']
165 | */
166 | import React from 'react';
167 | import SpringScrollbarsExample from './components/SpringScrollbars/SpringScrollbarsExample';
168 |
169 | export default class App extends React.Component {
170 | render() {
171 | return ;
172 | }
173 | }
174 | ```
175 |
--------------------------------------------------------------------------------
/test/Scrollbars/gettersSetters.js:
--------------------------------------------------------------------------------
1 | import { Scrollbars } from 'react-custom-scrollbars';
2 | import { render, unmountComponentAtNode } from 'react-dom';
3 |
4 | export default function createTests(scrollbarWidth, envScrollbarWidth) {
5 | let node;
6 | beforeEach(() => {
7 | node = document.createElement('div');
8 | document.body.appendChild(node);
9 | });
10 | afterEach(() => {
11 | unmountComponentAtNode(node);
12 | document.body.removeChild(node);
13 | });
14 |
15 | describe('getters', () => {
16 | function renderScrollbars(callback) {
17 | render(
18 |
19 |
20 | ,
21 | node,
22 | callback,
23 | );
24 | }
25 | describe('getScrollLeft', () => {
26 | it('should return scrollLeft', (done) => {
27 | renderScrollbars(function callback() {
28 | this.scrollLeft(50);
29 | expect(this.getScrollLeft()).toEqual(50);
30 | done();
31 | });
32 | });
33 | });
34 | describe('getScrollTop', () => {
35 | it('should return scrollTop', (done) => {
36 | renderScrollbars(function callback() {
37 | this.scrollTop(50);
38 | expect(this.getScrollTop()).toEqual(50);
39 | done();
40 | });
41 | });
42 | });
43 | describe('getScrollWidth', () => {
44 | it('should return scrollWidth', (done) => {
45 | renderScrollbars(function callback() {
46 | expect(this.getScrollWidth()).toEqual(200);
47 | done();
48 | });
49 | });
50 | });
51 | describe('getScrollHeight', () => {
52 | it('should return scrollHeight', (done) => {
53 | renderScrollbars(function callback() {
54 | expect(this.getScrollHeight()).toEqual(200);
55 | done();
56 | });
57 | });
58 | });
59 | describe('getClientWidth', () => {
60 | it('should return scrollWidth', (done) => {
61 | renderScrollbars(function callback() {
62 | expect(this.getClientWidth()).toEqual(100 + (scrollbarWidth - envScrollbarWidth));
63 | done();
64 | });
65 | });
66 | });
67 | describe('getClientHeight', () => {
68 | it('should return scrollHeight', (done) => {
69 | renderScrollbars(function callback() {
70 | expect(this.getClientHeight()).toEqual(100 + (scrollbarWidth - envScrollbarWidth));
71 | done();
72 | });
73 | });
74 | });
75 | });
76 |
77 | describe('setters', () => {
78 | function renderScrollbars(callback) {
79 | render(
80 |
81 |
82 | ,
83 | node,
84 | callback,
85 | );
86 | }
87 | describe('scrollLeft/scrollToLeft', () => {
88 | it('should scroll to given left value', (done) => {
89 | renderScrollbars(function callback() {
90 | this.scrollLeft(50);
91 | expect(this.getScrollLeft()).toEqual(50);
92 | this.scrollToLeft();
93 | expect(this.getScrollLeft()).toEqual(0);
94 | this.scrollLeft(50);
95 | this.scrollLeft();
96 | expect(this.getScrollLeft()).toEqual(0);
97 | done();
98 | });
99 | });
100 | });
101 | describe('scrollTop/scrollToTop', () => {
102 | it('should scroll to given top value', (done) => {
103 | renderScrollbars(function callback() {
104 | this.scrollTop(50);
105 | expect(this.getScrollTop()).toEqual(50);
106 | this.scrollToTop();
107 | expect(this.getScrollTop()).toEqual(0);
108 | this.scrollTop(50);
109 | this.scrollTop();
110 | expect(this.getScrollTop()).toEqual(0);
111 | done();
112 | });
113 | });
114 | });
115 | describe('scrollToRight', () => {
116 | it('should scroll to right', (done) => {
117 | renderScrollbars(function callback() {
118 | this.scrollToRight();
119 | expect(this.getScrollLeft()).toEqual(100 + (envScrollbarWidth - scrollbarWidth));
120 | done();
121 | });
122 | });
123 | });
124 | describe('scrollToBottom', () => {
125 | it('should scroll to bottom', (done) => {
126 | renderScrollbars(function callback() {
127 | this.scrollToBottom();
128 | expect(this.getScrollTop()).toEqual(100 + (envScrollbarWidth - scrollbarWidth));
129 | done();
130 | });
131 | });
132 | });
133 | });
134 | }
135 |
--------------------------------------------------------------------------------
/test/Scrollbars/universal.js:
--------------------------------------------------------------------------------
1 | import { Scrollbars } from 'react-custom-scrollbars';
2 | import { render, unmountComponentAtNode } from 'react-dom';
3 |
4 | export default function createTests(scrollbarWidth) {
5 | let node;
6 | beforeEach(() => {
7 | node = document.createElement('div');
8 | document.body.appendChild(node);
9 | });
10 | afterEach(() => {
11 | unmountComponentAtNode(node);
12 | document.body.removeChild(node);
13 | });
14 |
15 | describe('universal', () => {
16 | describe('default', () => {
17 | describe('when rendered', () => {
18 | it('should hide overflow', (done) => {
19 | class ScrollbarsTest extends Scrollbars {
20 | // Override componentDidMount, so we can check, how the markup
21 | // looks like on the first rendering
22 | componentDidMount() {}
23 | }
24 | render(
25 |
26 |
27 | ,
28 | node,
29 | function callback() {
30 | const { view, trackHorizontal, trackVertical } = this;
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 | });
46 | describe('when componentDidMount', () => {
47 | it('should rerender', (done) => {
48 | render(
49 |
50 |
51 | ,
52 | node,
53 | function callback() {
54 | setTimeout(() => {
55 | const { view } = this;
56 | expect(view.style.overflow).toEqual('scroll');
57 | expect(view.style.marginBottom).toEqual(`${-scrollbarWidth}px`);
58 | expect(view.style.marginRight).toEqual(`${-scrollbarWidth}px`);
59 | done();
60 | }, 100);
61 | },
62 | );
63 | });
64 | });
65 | });
66 | describe('when using autoHeight', () => {
67 | describe('when rendered', () => {
68 | it('should hide overflow', (done) => {
69 | class ScrollbarsTest extends Scrollbars {
70 | // Override componentDidMount, so we can check, how the markup
71 | // looks like on the first rendering
72 | componentDidMount() {}
73 | }
74 | render(
75 |
76 |
77 | ,
78 | node,
79 | function callback() {
80 | const { view, trackHorizontal, trackVertical } = this;
81 | expect(view.style.position).toEqual('relative');
82 | expect(view.style.overflow).toEqual('hidden');
83 | expect(view.style.marginBottom).toEqual('0px');
84 | expect(view.style.marginRight).toEqual('0px');
85 | expect(view.style.minHeight).toEqual('0px');
86 | expect(view.style.maxHeight).toEqual('100px');
87 | expect(trackHorizontal.style.display).toEqual('none');
88 | expect(trackVertical.style.display).toEqual('none');
89 | done();
90 | },
91 | );
92 | });
93 | });
94 | describe('when componentDidMount', () => {
95 | it('should rerender', (done) => {
96 | render(
97 |
98 |
99 | ,
100 | node,
101 | function callback() {
102 | setTimeout(() => {
103 | const { view } = this;
104 | expect(view.style.overflow).toEqual('scroll');
105 | expect(view.style.marginBottom).toEqual(`${-scrollbarWidth}px`);
106 | expect(view.style.marginRight).toEqual(`${-scrollbarWidth}px`);
107 | expect(view.style.minHeight).toEqual(`${scrollbarWidth}px`);
108 | expect(view.style.maxHeight).toEqual(`${100 + scrollbarWidth}px`);
109 | done();
110 | });
111 | },
112 | );
113 | });
114 | });
115 | });
116 | });
117 | }
118 |
--------------------------------------------------------------------------------
/docs/customization.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Customization
3 | order: 3
4 | ---
5 |
6 | # Customization
7 |
8 | The `` component consists of the following elements:
9 |
10 | - `root` The root element that covering scrolled view with track and thumb placed
11 | - `view` The element your content is rendered in
12 | - `trackHorizontal` The horizontal scrollbars track
13 | - `trackVertical` The vertical scrollbars track
14 | - `thumbHorizontal` The horizontal thumb
15 | - `thumbVertical` The vertical thumb
16 |
17 | 
18 |
19 | ## `rc-scrollbars` provide three ways to get your components styled
20 |
21 | ## Render props
22 |
23 | Each element can be **rendered individually** with a function that you pass to the component.
24 |
25 | Say, you want use your own `className` for each element:
26 |
27 | ```typescript jsx
28 | import { Scrollbars } from 'rc-scrollbars';
29 | import './styles.css';
30 |
31 | class CustomScrollbars extends Component {
32 | render() {
33 | return (
34 | }
36 | renderTrackVertical={(props) => }
37 | renderThumbHorizontal={(props) => }
38 | renderThumbVertical={(props) => }
39 | renderView={(props) => }
40 | >
41 | {this.props.children}
42 |
43 | );
44 | }
45 | }
46 |
47 | class App extends Component {
48 | render() {
49 | return (
50 |
51 | Some great content...
52 |
53 | );
54 | }
55 | }
56 | ```
57 |
58 | **Important**: **You will always need to pass through the given props** for the respective element like in the example above: ``. This is because we need to pass some default `styles` down to the element in order to make the component work.
59 |
60 | If you are working with **inline styles**, you could do something like this:
61 |
62 | ```jsx | pure
63 | import { Scrollbars } from 'rc-scrollbars';
64 |
65 | const myOwnStyles = { backgroundColor: 'blue' };
66 |
67 | class CustomScrollbars extends Component {
68 | render() {
69 | return (
70 | (
72 |
73 | )}
74 | >
75 | {this.props.children}
76 |
77 | );
78 | }
79 | }
80 | ```
81 |
82 | ## Default classNames
83 |
84 | For convenience, some 'marker' classes are provided for each of the subcomponents:
85 |
86 | `root`: 'rc-scrollbars-container'
87 | `view`: 'rc-scrollbars-view'
88 | `trackVertical`: 'rc-scrollbars-track rc-scrollbars-track-v'
89 | `trackHorizontal`: 'rc-scrollbars-track rc-scrollbars-track-h'
90 | `thumbVertical`: 'rc-scrollbars-thumb rc-scrollbars-thumb-v'
91 | `thumbHorizontal`: 'rc-scrollbars-thumb rc-scrollbars-thumb-h'
92 |
93 | There's very little 'beautifying' styles applied by default, however if you'd like to change the `background-color` of the **thumbs** or `border-radius` of the **tracks** you can easily disable their default styling by passing a single prop `disableDefaultStyles`.
94 |
95 | ## Classes prop
96 |
97 | You can pass `classes` prop and set your own className for every provided element
98 |
99 | ```jsx | pure
100 | import { Scrollbars } from 'rc-scrollbars';
101 | import css from './styles.module.css';
102 |
103 | const StyledScrollbars = ({ children }) => {
104 | return (
105 |
115 | {children}
116 |
117 | );
118 | };
119 | ```
120 |
121 | # Respond to scroll events
122 |
123 | If you want to change the appearance in respond to the scrolling position, you could do that like:
124 |
125 | ```javascript
126 | import { Scrollbars } from 'rc-scrollbars';
127 | class CustomScrollbars extends Component {
128 | constructor(props, context) {
129 | super(props, context);
130 | this.state = { top: 0 };
131 | this.handleScrollFrame = this.handleScrollFrame.bind(this);
132 | this.renderView = this.renderView.bind(this);
133 | }
134 |
135 | handleScrollFrame(values) {
136 | const { top } = values;
137 | this.setState({ top });
138 | }
139 |
140 | renderView({ style, ...props }) {
141 | const { top } = this.state;
142 | const color = top * 255;
143 | const customStyle = {
144 | backgroundColor: `rgb(${color}, ${color}, ${color})`,
145 | };
146 | return ;
147 | }
148 |
149 | render() {
150 | return (
151 |
156 | );
157 | }
158 | }
159 | ```
160 |
161 | ### Check out [**DEMO**](/demo) for some inspiration
162 |
--------------------------------------------------------------------------------
/test/Scrollbars/autoHeight.js:
--------------------------------------------------------------------------------
1 | import { Scrollbars } from 'react-custom-scrollbars';
2 | import { render, unmountComponentAtNode, findDOMNode } from 'react-dom';
3 | import { Component } from 'react';
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 |
21 |
22 | ,
23 | node,
24 | function callback() {
25 | const scrollbars = findDOMNode(this);
26 | expect(scrollbars.style.position).toEqual('relative');
27 | expect(scrollbars.style.minHeight).toEqual('0px');
28 | expect(scrollbars.style.maxHeight).toEqual('100px');
29 | expect(this.view.style.position).toEqual('relative');
30 | expect(this.view.style.minHeight).toEqual(`${scrollbarWidth}px`);
31 | expect(this.view.style.maxHeight).toEqual(`${100 + scrollbarWidth}px`);
32 | done();
33 | },
34 | );
35 | });
36 | });
37 |
38 | describe('when native scrollbars have a width', () => {
39 | if (!scrollbarWidth) return;
40 | it('hides native scrollbars', (done) => {
41 | render(
42 |
43 |
44 | ,
45 | node,
46 | function callback() {
47 | const width = `-${scrollbarWidth}px`;
48 | expect(this.view.style.marginRight).toEqual(width);
49 | expect(this.view.style.marginBottom).toEqual(width);
50 | done();
51 | },
52 | );
53 | });
54 | });
55 |
56 | describe('when native scrollbars have no width', () => {
57 | if (scrollbarWidth) return;
58 | it('hides bars', (done) => {
59 | render(
60 |
61 |
62 | ,
63 | node,
64 | function callback() {
65 | setTimeout(() => {
66 | expect(this.trackVertical.style.display).toEqual('none');
67 | expect(this.trackHorizontal.style.display).toEqual('none');
68 | done();
69 | }, 100);
70 | },
71 | );
72 | });
73 | });
74 |
75 | describe('when content is smaller than maxHeight', () => {
76 | it("should have the content's height", (done) => {
77 | render(
78 |
79 |
80 | ,
81 | node,
82 | function callback() {
83 | setTimeout(() => {
84 | const scrollbars = findDOMNode(this);
85 | expect(scrollbars.clientHeight).toEqual(50 + (envScrollbarWidth - scrollbarWidth));
86 | expect(this.view.clientHeight).toEqual(50);
87 | expect(this.view.scrollHeight).toEqual(50);
88 | expect(this.thumbVertical.clientHeight).toEqual(0);
89 | done();
90 | }, 100);
91 | },
92 | );
93 | });
94 | });
95 |
96 | describe('when content is larger than maxHeight', () => {
97 | it('should show scrollbars', (done) => {
98 | render(
99 |
100 |
101 | ,
102 | node,
103 | function callback() {
104 | setTimeout(() => {
105 | const scrollbars = findDOMNode(this);
106 | expect(scrollbars.clientHeight).toEqual(100);
107 | expect(this.view.clientHeight).toEqual(100 - (envScrollbarWidth - scrollbarWidth));
108 | expect(this.view.scrollHeight).toEqual(200);
109 | if (scrollbarWidth) {
110 | // 100 / 200 * 96 = 48
111 | expect(this.thumbVertical.clientHeight).toEqual(48);
112 | }
113 | done();
114 | }, 100);
115 | },
116 | );
117 | });
118 | });
119 |
120 | describe('when minHeight is greater than 0', () => {
121 | it('should have height greater than 0', (done) => {
122 | render(
123 |
124 |
125 | ,
126 | node,
127 | function callback() {
128 | setTimeout(() => {
129 | const scrollbars = findDOMNode(this);
130 | expect(scrollbars.clientHeight).toEqual(100);
131 | expect(this.view.clientHeight).toEqual(100 - (envScrollbarWidth - scrollbarWidth));
132 | expect(this.thumbVertical.clientHeight).toEqual(0);
133 | done();
134 | }, 100);
135 | },
136 | );
137 | });
138 | });
139 |
140 | describe('when using perecentages', () => {
141 | it('should use calc', (done) => {
142 | class Root extends Component {
143 | render() {
144 | return (
145 |
146 |
{
148 | this.scrollbars = ref;
149 | }}
150 | autoHeight
151 | autoHeightMin="50%"
152 | autoHeightMax="100%"
153 | >
154 |
155 |
156 |
157 | );
158 | }
159 | }
160 | render(, node, function callback() {
161 | setTimeout(() => {
162 | const $scrollbars = findDOMNode(this.scrollbars);
163 | const view = this.scrollbars.view;
164 | expect($scrollbars.clientWidth).toEqual(500);
165 | expect($scrollbars.clientHeight).toEqual(250);
166 | expect($scrollbars.style.position).toEqual('relative');
167 | expect($scrollbars.style.minHeight).toEqual('50%');
168 | expect($scrollbars.style.maxHeight).toEqual('100%');
169 | expect(view.style.position).toEqual('relative');
170 | expect(view.style.minHeight).toEqual(`calc(50% + ${scrollbarWidth}px)`);
171 | expect(view.style.maxHeight).toEqual(`calc(100% + ${scrollbarWidth}px)`);
172 | done();
173 | }, 100);
174 | });
175 | });
176 | });
177 |
178 | describe('when using other units', () => {
179 | it('should use calc', (done) => {
180 | render(
181 |
182 |
183 | ,
184 | node,
185 | function callback() {
186 | const scrollbars = findDOMNode(this);
187 | expect(scrollbars.style.position).toEqual('relative');
188 | expect(scrollbars.style.minHeight).toEqual('10em');
189 | expect(scrollbars.style.maxHeight).toEqual('100em');
190 | expect(this.view.style.position).toEqual('relative');
191 | expect(this.view.style.minHeight).toEqual(`calc(10em + ${scrollbarWidth}px)`);
192 | expect(this.view.style.maxHeight).toEqual(`calc(100em + ${scrollbarWidth}px)`);
193 | done();
194 | },
195 | );
196 | });
197 | });
198 | });
199 | }
200 |
--------------------------------------------------------------------------------
/test/Scrollbars/scrolling.js:
--------------------------------------------------------------------------------
1 | import { Scrollbars } from 'react-custom-scrollbars';
2 | import { render, unmountComponentAtNode } from 'react-dom';
3 |
4 | export default function createTests(scrollbarWidth, envScrollbarWidth) {
5 | let node;
6 | beforeEach(() => {
7 | node = document.createElement('div');
8 | document.body.appendChild(node);
9 | });
10 | afterEach(() => {
11 | unmountComponentAtNode(node);
12 | document.body.removeChild(node);
13 | });
14 |
15 | describe('when scrolling', () => {
16 | describe('when native scrollbars have a width', () => {
17 | if (!scrollbarWidth) return;
18 | it('should update thumbs position', (done) => {
19 | render(
20 |
21 |
22 | ,
23 | node,
24 | 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.thumbVertical.style.transform).toEqual('translateY(24px)');
31 | expect(this.thumbHorizontal.style.transform).toEqual('translateX(24px)');
32 | } else {
33 | expect(this.thumbVertical.style.transform).toEqual('');
34 | expect(this.thumbHorizontal.style.transform).toEqual('');
35 | }
36 | done();
37 | }, 100);
38 | },
39 | );
40 | });
41 | });
42 |
43 | it('should not trigger a rerender', () => {
44 | render(
45 |
46 |
47 | ,
48 | node,
49 | function callback() {
50 | const spy = spyOn(this, 'render').andCallThrough();
51 | this.scrollTop(50);
52 | expect(spy.calls.length).toEqual(0);
53 | spy.restore();
54 | },
55 | );
56 | });
57 |
58 | describe('when scrolling x-axis', () => {
59 | it('should call `onScroll`', (done) => {
60 | const spy = createSpy();
61 | render(
62 |
63 |
64 | ,
65 | node,
66 | function callback() {
67 | this.scrollLeft(50);
68 | setTimeout(() => {
69 | expect(spy.calls.length).toEqual(1);
70 | const args = spy.calls[0].arguments;
71 | const event = args[0];
72 | expect(event).toBeA(Event);
73 | done();
74 | }, 100);
75 | },
76 | );
77 | });
78 | it('should call `onScrollFrame`', (done) => {
79 | const spy = createSpy();
80 | render(
81 |
82 |
83 | ,
84 | node,
85 | function callback() {
86 | this.scrollLeft(50);
87 | setTimeout(() => {
88 | expect(spy.calls.length).toEqual(1);
89 | const args = spy.calls[0].arguments;
90 | const values = args[0];
91 | expect(values).toBeA(Object);
92 |
93 | if (scrollbarWidth) {
94 | expect(values).toEqual({
95 | left: 0.5,
96 | top: 0,
97 | scrollLeft: 50,
98 | scrollTop: 0,
99 | scrollWidth: 200,
100 | scrollHeight: 200,
101 | clientWidth: 100,
102 | clientHeight: 100,
103 | });
104 | } else {
105 | expect(values).toEqual({
106 | left: values.scrollLeft / (values.scrollWidth - values.clientWidth),
107 | top: 0,
108 | scrollLeft: 50,
109 | scrollTop: 0,
110 | scrollWidth: 200,
111 | scrollHeight: 200,
112 | clientWidth: 100 - envScrollbarWidth,
113 | clientHeight: 100 - envScrollbarWidth,
114 | });
115 | }
116 | done();
117 | }, 100);
118 | },
119 | );
120 | });
121 | it('should call `onScrollStart` once', (done) => {
122 | const spy = createSpy();
123 | render(
124 |
125 |
126 | ,
127 | node,
128 | function callback() {
129 | let left = 0;
130 | const interval = setInterval(() => {
131 | this.scrollLeft(++left);
132 | if (left >= 50) {
133 | clearInterval(interval);
134 | expect(spy.calls.length).toEqual(1);
135 | done();
136 | }
137 | }, 10);
138 | },
139 | );
140 | });
141 | it('should call `onScrollStop` once when scrolling stops', (done) => {
142 | const spy = createSpy();
143 | render(
144 |
145 |
146 | ,
147 | node,
148 | function callback() {
149 | let left = 0;
150 | const interval = setInterval(() => {
151 | this.scrollLeft(++left);
152 | if (left >= 50) {
153 | clearInterval(interval);
154 | setTimeout(() => {
155 | expect(spy.calls.length).toEqual(1);
156 | done();
157 | }, 300);
158 | }
159 | }, 10);
160 | },
161 | );
162 | });
163 | });
164 |
165 | describe('when scrolling y-axis', () => {
166 | it('should call `onScroll`', (done) => {
167 | const spy = createSpy();
168 | render(
169 |
170 |
171 | ,
172 | node,
173 | function callback() {
174 | this.scrollTop(50);
175 | setTimeout(() => {
176 | expect(spy.calls.length).toEqual(1);
177 | const args = spy.calls[0].arguments;
178 | const event = args[0];
179 | expect(event).toBeA(Event);
180 | done();
181 | }, 100);
182 | },
183 | );
184 | });
185 | it('should call `onScrollFrame`', (done) => {
186 | const spy = createSpy();
187 | render(
188 |
189 |
190 | ,
191 | node,
192 | function callback() {
193 | this.scrollTop(50);
194 | setTimeout(() => {
195 | expect(spy.calls.length).toEqual(1);
196 | const args = spy.calls[0].arguments;
197 | const values = args[0];
198 | expect(values).toBeA(Object);
199 |
200 | if (scrollbarWidth) {
201 | expect(values).toEqual({
202 | left: 0,
203 | top: 0.5,
204 | scrollLeft: 0,
205 | scrollTop: 50,
206 | scrollWidth: 200,
207 | scrollHeight: 200,
208 | clientWidth: 100,
209 | clientHeight: 100,
210 | });
211 | } else {
212 | expect(values).toEqual({
213 | left: 0,
214 | top: values.scrollTop / (values.scrollHeight - values.clientHeight),
215 | scrollLeft: 0,
216 | scrollTop: 50,
217 | scrollWidth: 200,
218 | scrollHeight: 200,
219 | clientWidth: 100 - envScrollbarWidth,
220 | clientHeight: 100 - envScrollbarWidth,
221 | });
222 | }
223 | done();
224 | }, 100);
225 | },
226 | );
227 | });
228 | it('should call `onScrollStart` once', (done) => {
229 | const spy = createSpy();
230 | render(
231 |
232 |
233 | ,
234 | node,
235 | function callback() {
236 | let top = 0;
237 | const interval = setInterval(() => {
238 | this.scrollTop(++top);
239 | if (top >= 50) {
240 | clearInterval(interval);
241 | expect(spy.calls.length).toEqual(1);
242 | done();
243 | }
244 | }, 10);
245 | },
246 | );
247 | });
248 | it('should call `onScrollStop` once when scrolling stops', (done) => {
249 | const spy = createSpy();
250 | render(
251 |
252 |
253 | ,
254 | node,
255 | function callback() {
256 | let top = 0;
257 | const interval = setInterval(() => {
258 | this.scrollTop(++top);
259 | if (top >= 50) {
260 | clearInterval(interval);
261 | setTimeout(() => {
262 | expect(spy.calls.length).toEqual(1);
263 | done();
264 | }, 300);
265 | }
266 | }, 10);
267 | },
268 | );
269 | });
270 | });
271 | });
272 | }
273 |
--------------------------------------------------------------------------------
/test/Scrollbars/rendering.js:
--------------------------------------------------------------------------------
1 | import { Scrollbars } from 'react-custom-scrollbars';
2 | import { render, unmountComponentAtNode, findDOMNode } from 'react-dom';
3 |
4 | export default function createTests(scrollbarWidth) {
5 | describe('rendering', () => {
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 Scrollbars are rendered', () => {
17 | it('takes className', (done) => {
18 | render(
19 |
20 |
21 | ,
22 | node,
23 | function callback() {
24 | expect(findDOMNode(this).className).toEqual('foo');
25 | done();
26 | },
27 | );
28 | });
29 |
30 | it('takes id', (done) => {
31 | render(
32 |
33 |
34 | ,
35 | node,
36 | function callback() {
37 | expect(findDOMNode(this).id).toEqual('foo');
38 | done();
39 | },
40 | );
41 | });
42 |
43 | it('takes styles', (done) => {
44 | render(
45 |
46 |
47 | ,
48 | node,
49 | function callback() {
50 | expect(findDOMNode(this).style.width).toEqual('100px');
51 | expect(findDOMNode(this).style.height).toEqual('100px');
52 | expect(findDOMNode(this).style.overflow).toEqual('hidden');
53 | done();
54 | },
55 | );
56 | });
57 |
58 | it('renders view', (done) => {
59 | render(
60 |
61 |
62 | ,
63 | node,
64 | function callback() {
65 | expect(this.view).toBeA(Node);
66 | done();
67 | },
68 | );
69 | });
70 |
71 | describe('when using custom tagName', () => {
72 | it('should use the defined tagName', (done) => {
73 | render(
74 |
75 |
76 | ,
77 | node,
78 | function callback() {
79 | const el = findDOMNode(this);
80 | expect(el.tagName.toLowerCase()).toEqual('nav');
81 | done();
82 | },
83 | );
84 | });
85 | });
86 |
87 | describe('when custom `renderView` is passed', () => {
88 | it('should render custom element', (done) => {
89 | render(
90 | (
93 |
94 | )}
95 | >
96 |
97 | ,
98 | node,
99 | function callback() {
100 | expect(this.view.tagName).toEqual('SECTION');
101 | expect(this.view.style.color).toEqual('red');
102 | expect(this.view.style.position).toEqual('absolute');
103 | done();
104 | },
105 | );
106 | });
107 | });
108 |
109 | describe('when native scrollbars have a width', () => {
110 | if (!scrollbarWidth) return;
111 |
112 | it('hides native scrollbars', (done) => {
113 | render(
114 |
115 |
116 | ,
117 | node,
118 | function callback() {
119 | setTimeout(() => {
120 | const width = `-${scrollbarWidth}px`;
121 | expect(this.view.style.marginRight).toEqual(width);
122 | expect(this.view.style.marginBottom).toEqual(width);
123 | done();
124 | }, 100);
125 | },
126 | );
127 | });
128 |
129 | it('renders bars', (done) => {
130 | render(
131 |
132 |
133 | ,
134 | node,
135 | function callback() {
136 | expect(this.trackHorizontal).toBeA(Node);
137 | expect(this.trackVertical).toBeA(Node);
138 | done();
139 | },
140 | );
141 | });
142 |
143 | it('renders thumbs', (done) => {
144 | render(
145 |
146 |
147 | ,
148 | node,
149 | function callback() {
150 | expect(this.thumbHorizontal).toBeA(Node);
151 | expect(this.thumbVertical).toBeA(Node);
152 | done();
153 | },
154 | );
155 | });
156 |
157 | it('renders thumbs with correct size', (done) => {
158 | render(
159 |
160 |
161 | ,
162 | node,
163 | function callback() {
164 | setTimeout(() => {
165 | // 100 / 200 * 96 = 48
166 | expect(this.thumbVertical.style.height).toEqual('48px');
167 | expect(this.thumbHorizontal.style.width).toEqual('48px');
168 | done();
169 | }, 100);
170 | },
171 | );
172 | });
173 |
174 | it('the thumbs size should not be less than the given `thumbMinSize`', (done) => {
175 | render(
176 |
177 |
178 | ,
179 | node,
180 | function callback() {
181 | setTimeout(() => {
182 | // 100 / 200 * 96 = 48
183 | expect(this.thumbVertical.style.height).toEqual('30px');
184 | expect(this.thumbHorizontal.style.width).toEqual('30px');
185 | done();
186 | }, 100);
187 | },
188 | );
189 | });
190 |
191 | describe('when thumbs have a fixed size', () => {
192 | it('thumbs should have the given fixed size', (done) => {
193 | render(
194 |
195 |
196 | ,
197 | node,
198 | function callback() {
199 | setTimeout(() => {
200 | // 100 / 200 * 96 = 48
201 | expect(this.thumbVertical.style.height).toEqual('50px');
202 | expect(this.thumbHorizontal.style.width).toEqual('50px');
203 | done();
204 | }, 100);
205 | },
206 | );
207 | });
208 | });
209 |
210 | describe('when custom `renderTrackHorizontal` is passed', () => {
211 | it('should render custom element', (done) => {
212 | render(
213 | (
216 |
217 | )}
218 | >
219 |
220 | ,
221 | node,
222 | function callback() {
223 | expect(this.trackHorizontal.tagName).toEqual('SECTION');
224 | expect(this.trackHorizontal.style.position).toEqual('absolute');
225 | expect(this.trackHorizontal.style.color).toEqual('red');
226 | done();
227 | },
228 | );
229 | });
230 | });
231 |
232 | describe('when custom `renderTrackVertical` is passed', () => {
233 | it('should render custom element', (done) => {
234 | render(
235 | (
238 |
239 | )}
240 | >
241 |
242 | ,
243 | node,
244 | function callback() {
245 | expect(this.trackVertical.tagName).toEqual('SECTION');
246 | expect(this.trackVertical.style.position).toEqual('absolute');
247 | expect(this.trackVertical.style.color).toEqual('red');
248 | done();
249 | },
250 | );
251 | });
252 | });
253 |
254 | describe('when custom `renderThumbHorizontal` is passed', () => {
255 | it('should render custom element', (done) => {
256 | render(
257 | (
260 |
261 | )}
262 | >
263 |
264 | ,
265 | node,
266 | function callback() {
267 | expect(this.thumbHorizontal.tagName).toEqual('SECTION');
268 | expect(this.thumbHorizontal.style.position).toEqual('relative');
269 | expect(this.thumbHorizontal.style.color).toEqual('red');
270 | done();
271 | },
272 | );
273 | });
274 | });
275 |
276 | describe('when custom `renderThumbVertical` is passed', () => {
277 | it('should render custom element', (done) => {
278 | render(
279 | (
282 |
283 | )}
284 | >
285 |
286 | ,
287 | node,
288 | function callback() {
289 | expect(this.thumbVertical.tagName).toEqual('SECTION');
290 | expect(this.thumbVertical.style.position).toEqual('relative');
291 | expect(this.thumbVertical.style.color).toEqual('red');
292 | done();
293 | },
294 | );
295 | });
296 | });
297 |
298 | it('positions view absolute', (done) => {
299 | render(
300 |
301 |
302 | ,
303 | node,
304 | function callback() {
305 | expect(this.view.style.position).toEqual('absolute');
306 | expect(this.view.style.top).toEqual('0px');
307 | expect(this.view.style.left).toEqual('0px');
308 | done();
309 | },
310 | );
311 | });
312 |
313 | it('should not override the scrollbars width/height values', (done) => {
314 | render(
315 | (
318 |
319 | )}
320 | renderTrackVertical={({ style, ...props }) => (
321 |
322 | )}
323 | >
324 |
325 | ,
326 | node,
327 | function callback() {
328 | setTimeout(() => {
329 | expect(this.trackHorizontal.style.height).toEqual('10px');
330 | expect(this.trackVertical.style.width).toEqual('10px');
331 | done();
332 | }, 100);
333 | },
334 | );
335 | });
336 |
337 | describe('when view does not overflow container', () => {
338 | it('should hide scrollbars', (done) => {
339 | render(
340 | (
343 |
344 | )}
345 | renderTrackVertical={({ style, ...props }) => (
346 |
347 | )}
348 | >
349 |
350 | ,
351 | node,
352 | function callback() {
353 | setTimeout(() => {
354 | expect(this.thumbHorizontal.style.width).toEqual('0px');
355 | expect(this.thumbVertical.style.height).toEqual('0px');
356 | done();
357 | }, 100);
358 | },
359 | );
360 | });
361 | });
362 | });
363 |
364 | describe('when native scrollbars have no width', () => {
365 | if (scrollbarWidth) return;
366 |
367 | it('hides bars', (done) => {
368 | render(
369 |
370 |
371 | ,
372 | node,
373 | function callback() {
374 | setTimeout(() => {
375 | expect(this.trackVertical.style.display).toEqual('none');
376 | expect(this.trackHorizontal.style.display).toEqual('none');
377 | done();
378 | }, 100);
379 | },
380 | );
381 | });
382 | });
383 | });
384 |
385 | describe('when rerendering Scrollbars', () => {
386 | function renderScrollbars(callback) {
387 | render(
388 |
389 |
390 | ,
391 | node,
392 | callback,
393 | );
394 | }
395 | it('should update scrollbars', (done) => {
396 | renderScrollbars(function callback() {
397 | const spy = spyOn(this, 'update').andCallThrough();
398 | renderScrollbars(function rerenderCallback() {
399 | expect(spy.calls.length).toEqual(1);
400 | spy.restore();
401 | done();
402 | });
403 | });
404 | });
405 | });
406 | });
407 | }
408 |
--------------------------------------------------------------------------------
/test/Scrollbars/autoHide.js:
--------------------------------------------------------------------------------
1 | import { Scrollbars } from 'react-custom-scrollbars';
2 | import { render, unmountComponentAtNode } from 'react-dom';
3 | import simulant from 'simulant';
4 |
5 | export default function createTests(scrollbarWidth) {
6 | // Not for mobile environment
7 | if (!scrollbarWidth) return;
8 |
9 | let node;
10 | beforeEach(() => {
11 | node = document.createElement('div');
12 | document.body.appendChild(node);
13 | });
14 | afterEach(() => {
15 | unmountComponentAtNode(node);
16 | document.body.removeChild(node);
17 | });
18 |
19 | describe('autoHide', () => {
20 | describe('when Scrollbars are rendered', () => {
21 | it('should hide tracks', (done) => {
22 | render(
23 |
24 |
25 | ,
26 | node,
27 | function callback() {
28 | expect(this.trackHorizontal.style.opacity).toEqual('0');
29 | expect(this.trackVertical.style.opacity).toEqual('0');
30 | done();
31 | },
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,
43 | function callback() {
44 | const { trackHorizontal: track } = this;
45 | simulant.fire(track, 'mouseenter');
46 | expect(track.style.opacity).toEqual('1');
47 | done();
48 | },
49 | );
50 | });
51 | it('should not hide tracks', (done) => {
52 | render(
53 |
54 |
55 | ,
56 | node,
57 | function callback() {
58 | const { trackHorizontal: track } = this;
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 | });
69 | describe('when leaving horizontal track', () => {
70 | it('should hide tracks', (done) => {
71 | render(
72 |
78 |
79 | ,
80 | node,
81 | function callback() {
82 | const { trackHorizontal: track } = this;
83 | simulant.fire(track, 'mouseenter');
84 | simulant.fire(track, 'mouseleave');
85 | setTimeout(() => {
86 | expect(track.style.opacity).toEqual('0');
87 | done();
88 | }, 100);
89 | },
90 | );
91 | });
92 | });
93 | describe('when entering vertical track', () => {
94 | it('should show tracks', (done) => {
95 | render(
96 |
97 |
98 | ,
99 | node,
100 | function callback() {
101 | const { trackVertical: track } = this;
102 | simulant.fire(track, 'mouseenter');
103 | expect(track.style.opacity).toEqual('1');
104 | done();
105 | },
106 | );
107 | });
108 | it('should not hide tracks', (done) => {
109 | render(
110 |
111 |
112 | ,
113 | node,
114 | function callback() {
115 | const { trackVertical: track } = this;
116 | simulant.fire(track, 'mouseenter');
117 | setTimeout(() => this.hideTracks(), 10);
118 | setTimeout(() => {
119 | expect(track.style.opacity).toEqual('1');
120 | }, 100);
121 | done();
122 | },
123 | );
124 | });
125 | });
126 | describe('when leaving vertical track', () => {
127 | it('should hide tracks', (done) => {
128 | render(
129 |
135 |
136 | ,
137 | node,
138 | function callback() {
139 | const { trackVertical: track } = this;
140 | simulant.fire(track, 'mouseenter');
141 | simulant.fire(track, 'mouseleave');
142 | setTimeout(() => {
143 | expect(track.style.opacity).toEqual('0');
144 | done();
145 | }, 100);
146 | },
147 | );
148 | });
149 | });
150 | });
151 | describe('when scrolling', () => {
152 | it('should show tracks', (done) => {
153 | render(
154 |
155 |
156 | ,
157 | node,
158 | function callback() {
159 | this.scrollTop(50);
160 | setTimeout(() => {
161 | const { trackHorizontal, trackVertical } = this;
162 | expect(trackHorizontal.style.opacity).toEqual('1');
163 | expect(trackVertical.style.opacity).toEqual('1');
164 | done();
165 | }, 100);
166 | },
167 | );
168 | });
169 | it('should hide tracks after scrolling', (done) => {
170 | render(
171 |
177 |
178 | ,
179 | node,
180 | function callback() {
181 | this.scrollTop(50);
182 | setTimeout(() => {
183 | const { trackHorizontal, trackVertical } = this;
184 | expect(trackHorizontal.style.opacity).toEqual('0');
185 | expect(trackVertical.style.opacity).toEqual('0');
186 | done();
187 | }, 300);
188 | },
189 | );
190 | });
191 | it('should not hide tracks', (done) => {
192 | render(
193 |
194 |
195 | ,
196 | node,
197 | function callback() {
198 | this.scrollTop(50);
199 | setTimeout(() => this.hideTracks());
200 | setTimeout(() => {
201 | const { trackHorizontal, trackVertical } = this;
202 | expect(trackHorizontal.style.opacity).toEqual('1');
203 | expect(trackVertical.style.opacity).toEqual('1');
204 | done();
205 | }, 50);
206 | },
207 | );
208 | });
209 | });
210 | describe('when dragging x-axis', () => {
211 | it('should show tracks', (done) => {
212 | render(
213 |
219 |
220 | ,
221 | node,
222 | function callback() {
223 | const { thumbHorizontal: thumb, trackHorizontal: track } = this;
224 | const { left } = thumb.getBoundingClientRect();
225 | simulant.fire(thumb, 'mousedown', {
226 | target: thumb,
227 | clientX: left + 1,
228 | });
229 | simulant.fire(document, 'mousemove', {
230 | clientX: left + 100,
231 | });
232 | setTimeout(() => {
233 | expect(track.style.opacity).toEqual('1');
234 | done();
235 | }, 100);
236 | },
237 | );
238 | });
239 |
240 | it('should hide tracks on end', (done) => {
241 | render(
242 |
248 |
249 | ,
250 | node,
251 | function callback() {
252 | const { thumbHorizontal: thumb, trackHorizontal: track } = this;
253 | const { left } = thumb.getBoundingClientRect();
254 | simulant.fire(thumb, 'mousedown', {
255 | target: thumb,
256 | clientX: left + 1,
257 | });
258 | simulant.fire(document, 'mouseup');
259 | setTimeout(() => {
260 | expect(track.style.opacity).toEqual('0');
261 | done();
262 | }, 100);
263 | },
264 | );
265 | });
266 |
267 | describe('and leaving track', () => {
268 | it('should not hide tracks', (done) => {
269 | render(
270 |
276 |
277 | ,
278 | node,
279 | function callback() {
280 | setTimeout(() => {
281 | const { thumbHorizontal: thumb, trackHorizontal: track } = this;
282 | const { left } = thumb.getBoundingClientRect();
283 | simulant.fire(thumb, 'mousedown', {
284 | target: thumb,
285 | clientX: left + 1,
286 | });
287 | simulant.fire(document, 'mousemove', {
288 | clientX: left + 100,
289 | });
290 | simulant.fire(track, 'mouseleave');
291 | setTimeout(() => {
292 | expect(track.style.opacity).toEqual('1');
293 | done();
294 | }, 200);
295 | }, 100);
296 | },
297 | );
298 | });
299 | });
300 | });
301 | describe('when dragging y-axis', () => {
302 | it('should show tracks', (done) => {
303 | render(
304 |
310 |
311 | ,
312 | node,
313 | function callback() {
314 | const { thumbVertical: thumb, trackVertical: track } = this;
315 | const { top } = thumb.getBoundingClientRect();
316 | simulant.fire(thumb, 'mousedown', {
317 | target: thumb,
318 | clientY: top + 1,
319 | });
320 | simulant.fire(document, 'mousemove', {
321 | clientY: top + 100,
322 | });
323 | setTimeout(() => {
324 | expect(track.style.opacity).toEqual('1');
325 | done();
326 | }, 100);
327 | },
328 | );
329 | });
330 | it('should hide tracks on end', (done) => {
331 | render(
332 |
338 |
339 | ,
340 | node,
341 | function callback() {
342 | const { thumbVertical: thumb, trackVertical: track } = this;
343 | const { top } = thumb.getBoundingClientRect();
344 | simulant.fire(thumb, 'mousedown', {
345 | target: thumb,
346 | clientY: top + 1,
347 | });
348 | simulant.fire(document, 'mouseup');
349 | setTimeout(() => {
350 | expect(track.style.opacity).toEqual('0');
351 | done();
352 | }, 100);
353 | },
354 | );
355 | });
356 | describe('and leaving track', () => {
357 | it('should not hide tracks', (done) => {
358 | render(
359 |
365 |
366 | ,
367 | node,
368 | function callback() {
369 | setTimeout(() => {
370 | const { thumbVertical: thumb, trackVertical: track } = this;
371 | const { top } = thumb.getBoundingClientRect();
372 | simulant.fire(thumb, 'mousedown', {
373 | target: thumb,
374 | clientY: top + 1,
375 | });
376 | simulant.fire(document, 'mousemove', {
377 | clientY: top + 100,
378 | });
379 | simulant.fire(track, 'mouseleave');
380 | setTimeout(() => {
381 | expect(track.style.opacity).toEqual('1');
382 | done();
383 | }, 200);
384 | }, 100);
385 | },
386 | );
387 | });
388 | });
389 | });
390 | });
391 |
392 | describe('when autoHide is disabed', () => {
393 | describe('enter/leave track', () => {
394 | describe('when entering horizontal track', () => {
395 | it('should not call `showTracks`', (done) => {
396 | render(
397 |
398 |
399 | ,
400 | node,
401 | function callback() {
402 | const spy = spyOn(this, 'showTracks');
403 | const { trackHorizontal: track } = this;
404 | simulant.fire(track, 'mouseenter');
405 | expect(spy.calls.length).toEqual(0);
406 | done();
407 | },
408 | );
409 | });
410 | });
411 | describe('when leaving horizontal track', () => {
412 | it('should not call `hideTracks`', (done) => {
413 | render(
414 |
415 |
416 | ,
417 | node,
418 | function callback() {
419 | const spy = spyOn(this, 'hideTracks');
420 | const { trackHorizontal: track } = this;
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 | describe('when entering vertical track', () => {
432 | it('should not call `showTracks`', (done) => {
433 | render(
434 |
435 |
436 | ,
437 | node,
438 | function callback() {
439 | const spy = spyOn(this, 'showTracks');
440 | const { trackVertical: track } = this;
441 | simulant.fire(track, 'mouseenter');
442 | expect(spy.calls.length).toEqual(0);
443 | done();
444 | },
445 | );
446 | });
447 | });
448 | describe('when leaving vertical track', () => {
449 | it('should not call `hideTracks`', (done) => {
450 | render(
451 |
452 |
453 | ,
454 | node,
455 | function callback() {
456 | const spy = spyOn(this, 'hideTracks');
457 | const { trackVertical: track } = this;
458 | simulant.fire(track, 'mouseenter');
459 | simulant.fire(track, 'mouseleave');
460 | setTimeout(() => {
461 | expect(spy.calls.length).toEqual(0);
462 | done();
463 | }, 100);
464 | },
465 | );
466 | });
467 | });
468 | });
469 | });
470 | }
471 |
--------------------------------------------------------------------------------
/src/Scrollbars/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { cloneElement, Component, createElement, CSSProperties } from 'react';
3 | import raf, { cancel as caf } from 'raf';
4 | import css from 'dom-css';
5 | //
6 | import { ScrollValues, ScrollbarsProps, StyleKeys } from './types';
7 | import {
8 | getFinalClasses,
9 | getScrollbarWidth,
10 | returnFalse,
11 | getInnerWidth,
12 | getInnerHeight,
13 | } from '../utils';
14 | import { createStyles } from './styles';
15 |
16 | interface State {
17 | didMountUniversal: boolean;
18 | scrollbarWidth: number;
19 | }
20 |
21 | export class Scrollbars extends Component {
22 | container: Element | null = null;
23 | detectScrollingInterval: any; // Node timeout bug
24 | dragging: boolean = false;
25 | hideTracksTimeout: any; // Node timeout bug
26 | lastViewScrollLeft?: number;
27 | lastViewScrollTop?: number;
28 | prevPageX?: number;
29 | prevPageY?: number;
30 | requestFrame?: number;
31 | scrolling: boolean = false;
32 | styles: Record;
33 | thumbHorizontal?: HTMLDivElement;
34 | thumbVertical?: HTMLDivElement;
35 | trackHorizontal?: HTMLDivElement;
36 | trackMouseOver: boolean = false;
37 | trackVertical?: HTMLDivElement;
38 | view?: HTMLElement;
39 | viewScrollLeft?: number;
40 | viewScrollTop?: number;
41 |
42 | static defaultProps = {
43 | autoHeight: false,
44 | autoHeightMax: 200,
45 | autoHeightMin: 0,
46 | autoHide: false,
47 | autoHideDuration: 200,
48 | autoHideTimeout: 1000,
49 | disableDefaultStyles: false,
50 | hideTracksWhenNotNeeded: false,
51 | renderThumbHorizontal: (props) => ,
52 | renderThumbVertical: (props) => ,
53 | renderTrackHorizontal: (props) => ,
54 | renderTrackVertical: (props) => ,
55 | renderView: (props) => ,
56 | tagName: 'div',
57 | thumbMinSize: 30,
58 | universal: false,
59 | };
60 |
61 | constructor(props: ScrollbarsProps) {
62 | super(props);
63 |
64 | this.styles = createStyles(this.props.disableDefaultStyles);
65 |
66 | this.getScrollLeft = this.getScrollLeft.bind(this);
67 | this.getScrollTop = this.getScrollTop.bind(this);
68 | this.getScrollWidth = this.getScrollWidth.bind(this);
69 | this.getScrollHeight = this.getScrollHeight.bind(this);
70 | this.getClientWidth = this.getClientWidth.bind(this);
71 | this.getClientHeight = this.getClientHeight.bind(this);
72 | this.getValues = this.getValues.bind(this);
73 | this.getThumbHorizontalWidth = this.getThumbHorizontalWidth.bind(this);
74 | this.getThumbVerticalHeight = this.getThumbVerticalHeight.bind(this);
75 | this.getScrollLeftForOffset = this.getScrollLeftForOffset.bind(this);
76 | this.getScrollTopForOffset = this.getScrollTopForOffset.bind(this);
77 |
78 | this.scrollLeft = this.scrollLeft.bind(this);
79 | this.scrollTop = this.scrollTop.bind(this);
80 | this.scrollToLeft = this.scrollToLeft.bind(this);
81 | this.scrollToTop = this.scrollToTop.bind(this);
82 | this.scrollToRight = this.scrollToRight.bind(this);
83 | this.scrollToBottom = this.scrollToBottom.bind(this);
84 |
85 | this.handleTrackMouseEnter = this.handleTrackMouseEnter.bind(this);
86 | this.handleTrackMouseLeave = this.handleTrackMouseLeave.bind(this);
87 | this.handleHorizontalTrackMouseDown = this.handleHorizontalTrackMouseDown.bind(this);
88 | this.handleVerticalTrackMouseDown = this.handleVerticalTrackMouseDown.bind(this);
89 | this.handleHorizontalThumbMouseDown = this.handleHorizontalThumbMouseDown.bind(this);
90 | this.handleVerticalThumbMouseDown = this.handleVerticalThumbMouseDown.bind(this);
91 | this.handleWindowResize = this.handleWindowResize.bind(this);
92 | this.handleScroll = this.handleScroll.bind(this);
93 | this.handleDrag = this.handleDrag.bind(this);
94 | this.handleDragEnd = this.handleDragEnd.bind(this);
95 |
96 | this.state = {
97 | didMountUniversal: false,
98 | scrollbarWidth: getScrollbarWidth(),
99 | };
100 | }
101 |
102 | componentDidMount() {
103 | this.addListeners();
104 | this.update();
105 | this.componentDidMountUniversal();
106 | }
107 |
108 | componentDidMountUniversal() {
109 | // eslint-disable-line react/sort-comp
110 | const { universal } = this.props;
111 | if (!universal) return;
112 | this.setState({ didMountUniversal: true });
113 | }
114 |
115 | componentDidUpdate() {
116 | this.update();
117 | }
118 |
119 | componentWillUnmount() {
120 | this.removeListeners();
121 | this.requestFrame && caf(this.requestFrame);
122 | clearTimeout(this.hideTracksTimeout);
123 | clearInterval(this.detectScrollingInterval);
124 | }
125 |
126 | getScrollLeft() {
127 | if (!this.view) return 0;
128 | return this.view.scrollLeft;
129 | }
130 |
131 | getScrollTop() {
132 | if (!this.view) return 0;
133 | return this.view.scrollTop;
134 | }
135 |
136 | getScrollWidth() {
137 | if (!this.view) return 0;
138 | return this.view.scrollWidth;
139 | }
140 |
141 | getScrollHeight() {
142 | if (!this.view) return 0;
143 | return this.view.scrollHeight;
144 | }
145 |
146 | getClientWidth() {
147 | if (!this.view) return 0;
148 | return this.view.clientWidth;
149 | }
150 |
151 | getClientHeight() {
152 | if (!this.view) return 0;
153 | return this.view.clientHeight;
154 | }
155 |
156 | getValues() {
157 | const {
158 | scrollLeft = 0,
159 | scrollTop = 0,
160 | scrollWidth = 0,
161 | scrollHeight = 0,
162 | clientWidth = 0,
163 | clientHeight = 0,
164 | } = this.view || {};
165 |
166 | return {
167 | left: scrollLeft / (scrollWidth - clientWidth) || 0,
168 | top: scrollTop / (scrollHeight - clientHeight) || 0,
169 | scrollLeft,
170 | scrollTop,
171 | scrollWidth,
172 | scrollHeight,
173 | clientWidth,
174 | clientHeight,
175 | };
176 | }
177 |
178 | getThumbHorizontalWidth() {
179 | if (!this.view || !this.trackHorizontal) return 0;
180 | const { thumbSize, thumbMinSize } = this.props;
181 | const { scrollWidth, clientWidth } = this.view;
182 | const trackWidth = getInnerWidth(this.trackHorizontal);
183 | const width = Math.ceil((clientWidth / scrollWidth) * trackWidth);
184 | if (trackWidth === width) return 0;
185 | if (thumbSize) return thumbSize;
186 | return Math.max(width, thumbMinSize);
187 | }
188 |
189 | getThumbVerticalHeight() {
190 | if (!this.view || !this.trackVertical) return 0;
191 | const { thumbSize, thumbMinSize } = this.props;
192 | const { scrollHeight, clientHeight } = this.view;
193 | const trackHeight = getInnerHeight(this.trackVertical);
194 | const height = Math.ceil((clientHeight / scrollHeight) * trackHeight);
195 | if (trackHeight === height) return 0;
196 | if (thumbSize) return thumbSize;
197 | return Math.max(height, thumbMinSize);
198 | }
199 |
200 | getScrollLeftForOffset(offset) {
201 | if (!this.view || !this.trackHorizontal) return 0;
202 | const { scrollWidth, clientWidth } = this.view;
203 | const trackWidth = getInnerWidth(this.trackHorizontal);
204 | const thumbWidth = this.getThumbHorizontalWidth();
205 | return (offset / (trackWidth - thumbWidth)) * (scrollWidth - clientWidth);
206 | }
207 |
208 | getScrollTopForOffset(offset) {
209 | if (!this.view || !this.trackVertical) return 0;
210 | const { scrollHeight, clientHeight } = this.view;
211 | const trackHeight = getInnerHeight(this.trackVertical);
212 | const thumbHeight = this.getThumbVerticalHeight();
213 | return (offset / (trackHeight - thumbHeight)) * (scrollHeight - clientHeight);
214 | }
215 |
216 | scrollLeft(left = 0) {
217 | if (!this.view) return;
218 | this.view.scrollLeft = left;
219 | }
220 |
221 | scrollTop(top = 0) {
222 | if (!this.view) return;
223 | this.view.scrollTop = top;
224 | }
225 |
226 | scrollToLeft() {
227 | if (!this.view) return;
228 | this.view.scrollLeft = 0;
229 | }
230 |
231 | scrollToTop() {
232 | if (!this.view) return;
233 | this.view.scrollTop = 0;
234 | }
235 |
236 | scrollToRight() {
237 | if (!this.view) return;
238 | this.view.scrollLeft = this.view.scrollWidth;
239 | }
240 |
241 | scrollToBottom() {
242 | if (!this.view) return;
243 | this.view.scrollTop = this.view.scrollHeight;
244 | }
245 |
246 | scrollToY(y: number) {
247 | if (!this.view) return;
248 | this.view.scrollTop = y;
249 | }
250 |
251 | addListeners() {
252 | /* istanbul ignore if */
253 | if (
254 | typeof document === 'undefined' ||
255 | !this.view ||
256 | !this.trackHorizontal ||
257 | !this.trackVertical ||
258 | !this.thumbVertical ||
259 | !this.thumbHorizontal
260 | )
261 | return;
262 |
263 | const { view, trackHorizontal, trackVertical, thumbHorizontal, thumbVertical } = this;
264 | view.addEventListener('scroll', this.handleScroll);
265 |
266 | if (!this.state.scrollbarWidth) return;
267 |
268 | trackHorizontal.addEventListener('mouseenter', this.handleTrackMouseEnter);
269 | trackHorizontal.addEventListener('mouseleave', this.handleTrackMouseLeave);
270 | trackHorizontal.addEventListener('mousedown', this.handleHorizontalTrackMouseDown);
271 | trackVertical.addEventListener('mouseenter', this.handleTrackMouseEnter);
272 | trackVertical.addEventListener('mouseleave', this.handleTrackMouseLeave);
273 | trackVertical.addEventListener('mousedown', this.handleVerticalTrackMouseDown);
274 | thumbHorizontal.addEventListener('mousedown', this.handleHorizontalThumbMouseDown);
275 | thumbVertical.addEventListener('mousedown', this.handleVerticalThumbMouseDown);
276 | window.addEventListener('resize', this.handleWindowResize);
277 | }
278 |
279 | removeListeners() {
280 | /* istanbul ignore if */
281 | if (
282 | typeof document === 'undefined' ||
283 | !this.view ||
284 | !this.trackHorizontal ||
285 | !this.trackVertical ||
286 | !this.thumbVertical ||
287 | !this.thumbHorizontal
288 | )
289 | return;
290 | const { view, trackHorizontal, trackVertical, thumbHorizontal, thumbVertical } = this;
291 | view.removeEventListener('scroll', this.handleScroll);
292 |
293 | if (!this.state.scrollbarWidth) return;
294 |
295 | trackHorizontal.removeEventListener('mouseenter', this.handleTrackMouseEnter);
296 | trackHorizontal.removeEventListener('mouseleave', this.handleTrackMouseLeave);
297 | trackHorizontal.removeEventListener('mousedown', this.handleHorizontalTrackMouseDown);
298 | trackVertical.removeEventListener('mouseenter', this.handleTrackMouseEnter);
299 | trackVertical.removeEventListener('mouseleave', this.handleTrackMouseLeave);
300 | trackVertical.removeEventListener('mousedown', this.handleVerticalTrackMouseDown);
301 | thumbHorizontal.removeEventListener('mousedown', this.handleHorizontalThumbMouseDown);
302 | thumbVertical.removeEventListener('mousedown', this.handleVerticalThumbMouseDown);
303 | window.removeEventListener('resize', this.handleWindowResize);
304 | // Possibly setup by `handleDragStart`
305 | this.teardownDragging();
306 | }
307 |
308 | handleScroll(event) {
309 | const { onScroll, onScrollFrame } = this.props;
310 | if (onScroll) onScroll(event);
311 | this.update((values: ScrollValues) => {
312 | const { scrollLeft, scrollTop } = values;
313 | this.viewScrollLeft = scrollLeft;
314 | this.viewScrollTop = scrollTop;
315 | if (onScrollFrame) onScrollFrame(values);
316 | });
317 | this.detectScrolling();
318 | }
319 |
320 | handleScrollStart() {
321 | const { onScrollStart } = this.props;
322 | if (onScrollStart) onScrollStart();
323 | this.handleScrollStartAutoHide();
324 | }
325 |
326 | handleScrollStartAutoHide() {
327 | const { autoHide } = this.props;
328 | if (!autoHide) return;
329 | this.showTracks();
330 | }
331 |
332 | handleScrollStop() {
333 | const { onScrollStop } = this.props;
334 | if (onScrollStop) onScrollStop();
335 | this.handleScrollStopAutoHide();
336 | }
337 |
338 | handleScrollStopAutoHide() {
339 | const { autoHide } = this.props;
340 | if (!autoHide) return;
341 | this.hideTracks();
342 | }
343 |
344 | handleWindowResize() {
345 | this.update();
346 | }
347 |
348 | handleHorizontalTrackMouseDown(event) {
349 | if (!this.view) return;
350 | event.preventDefault();
351 | const { target, clientX } = event;
352 | const { left: targetLeft } = target.getBoundingClientRect();
353 | const thumbWidth = this.getThumbHorizontalWidth();
354 | const offset = Math.abs(targetLeft - clientX) - thumbWidth / 2;
355 | this.view.scrollLeft = this.getScrollLeftForOffset(offset);
356 | }
357 |
358 | handleVerticalTrackMouseDown(event) {
359 | if (!this.view) return;
360 | event.preventDefault();
361 | const { target, clientY } = event;
362 | const { top: targetTop } = target.getBoundingClientRect();
363 | const thumbHeight = this.getThumbVerticalHeight();
364 | const offset = Math.abs(targetTop - clientY) - thumbHeight / 2;
365 | this.view.scrollTop = this.getScrollTopForOffset(offset);
366 | }
367 |
368 | handleHorizontalThumbMouseDown(event) {
369 | event.preventDefault();
370 | this.handleDragStart(event);
371 | const { target, clientX } = event;
372 | const { offsetWidth } = target;
373 | const { left } = target.getBoundingClientRect();
374 | this.prevPageX = offsetWidth - (clientX - left);
375 | }
376 |
377 | handleVerticalThumbMouseDown(event) {
378 | event.preventDefault();
379 | this.handleDragStart(event);
380 | const { target, clientY } = event;
381 | const { offsetHeight } = target;
382 | const { top } = target.getBoundingClientRect();
383 | this.prevPageY = offsetHeight - (clientY - top);
384 | }
385 |
386 | setupDragging() {
387 | css(document.body, this.styles.disableSelectStyle);
388 | document.addEventListener('mousemove', this.handleDrag);
389 | document.addEventListener('mouseup', this.handleDragEnd);
390 | document.onselectstart = returnFalse;
391 | }
392 |
393 | teardownDragging() {
394 | css(document.body, this.styles.disableSelectStyleReset);
395 | document.removeEventListener('mousemove', this.handleDrag);
396 | document.removeEventListener('mouseup', this.handleDragEnd);
397 | document.onselectstart = null;
398 | }
399 |
400 | handleDragStart(event) {
401 | this.dragging = true;
402 | event.stopImmediatePropagation();
403 | this.setupDragging();
404 | }
405 |
406 | handleDrag(event) {
407 | if (this.prevPageX && this.trackHorizontal && this.view) {
408 | const { clientX } = event;
409 | const { left: trackLeft } = this.trackHorizontal.getBoundingClientRect();
410 | const thumbWidth = this.getThumbHorizontalWidth();
411 | const clickPosition = thumbWidth - this.prevPageX;
412 | const offset = -trackLeft + clientX - clickPosition;
413 | this.view.scrollLeft = this.getScrollLeftForOffset(offset);
414 | }
415 | if (this.prevPageY && this.trackVertical && this.view) {
416 | const { clientY } = event;
417 | const { top: trackTop } = this.trackVertical.getBoundingClientRect();
418 | const thumbHeight = this.getThumbVerticalHeight();
419 | const clickPosition = thumbHeight - this.prevPageY;
420 | const offset = -trackTop + clientY - clickPosition;
421 | this.view.scrollTop = this.getScrollTopForOffset(offset);
422 | }
423 | return false;
424 | }
425 |
426 | handleDragEnd() {
427 | this.dragging = false;
428 | this.prevPageX = this.prevPageY = 0;
429 | this.teardownDragging();
430 | this.handleDragEndAutoHide();
431 | }
432 |
433 | handleDragEndAutoHide() {
434 | const { autoHide } = this.props;
435 | if (!autoHide) return;
436 | this.hideTracks();
437 | }
438 |
439 | handleTrackMouseEnter() {
440 | this.trackMouseOver = true;
441 | this.handleTrackMouseEnterAutoHide();
442 | }
443 |
444 | handleTrackMouseEnterAutoHide() {
445 | const { autoHide } = this.props;
446 | if (!autoHide) return;
447 | this.showTracks();
448 | }
449 |
450 | handleTrackMouseLeave() {
451 | this.trackMouseOver = false;
452 | this.handleTrackMouseLeaveAutoHide();
453 | }
454 |
455 | handleTrackMouseLeaveAutoHide() {
456 | const { autoHide } = this.props;
457 | if (!autoHide) return;
458 | this.hideTracks();
459 | }
460 |
461 | showTracks() {
462 | clearTimeout(this.hideTracksTimeout);
463 | css(this.trackHorizontal, { opacity: 1 });
464 | css(this.trackVertical, { opacity: 1 });
465 | }
466 |
467 | hideTracks() {
468 | if (this.dragging) return;
469 | if (this.scrolling) return;
470 | if (this.trackMouseOver) return;
471 | const { autoHideTimeout } = this.props;
472 | clearTimeout(this.hideTracksTimeout);
473 | this.hideTracksTimeout = setTimeout(() => {
474 | css(this.trackHorizontal, { opacity: 0 });
475 | css(this.trackVertical, { opacity: 0 });
476 | }, autoHideTimeout);
477 | }
478 |
479 | detectScrolling() {
480 | if (this.scrolling) return;
481 | this.scrolling = true;
482 | this.handleScrollStart();
483 | this.detectScrollingInterval = setInterval(() => {
484 | if (
485 | this.lastViewScrollLeft === this.viewScrollLeft &&
486 | this.lastViewScrollTop === this.viewScrollTop
487 | ) {
488 | clearInterval(this.detectScrollingInterval);
489 | this.scrolling = false;
490 | this.handleScrollStop();
491 | }
492 | this.lastViewScrollLeft = this.viewScrollLeft;
493 | this.lastViewScrollTop = this.viewScrollTop;
494 | }, 100);
495 | }
496 |
497 | raf(callback) {
498 | if (this.requestFrame) raf.cancel(this.requestFrame);
499 | this.requestFrame = raf(() => {
500 | this.requestFrame = undefined;
501 | callback();
502 | });
503 | }
504 |
505 | update(callback?: (values: ScrollValues) => void) {
506 | this.raf(() => this._update(callback));
507 | }
508 |
509 | _update(callback) {
510 | const { onUpdate, hideTracksWhenNotNeeded } = this.props;
511 | const values = this.getValues();
512 |
513 | const freshScrollbarWidth = getScrollbarWidth();
514 |
515 | if (this.state.scrollbarWidth !== freshScrollbarWidth) {
516 | this.setState({ scrollbarWidth: freshScrollbarWidth });
517 | }
518 |
519 | if (this.state.scrollbarWidth) {
520 | const { scrollLeft, clientWidth, scrollWidth } = values;
521 | const trackHorizontalWidth = getInnerWidth(this.trackHorizontal);
522 |
523 | const thumbHorizontalWidth = this.getThumbHorizontalWidth();
524 | const thumbHorizontalX =
525 | (scrollLeft / (scrollWidth - clientWidth)) * (trackHorizontalWidth - thumbHorizontalWidth);
526 | const thumbHorizontalStyle = {
527 | width: thumbHorizontalWidth,
528 | transform: `translateX(${thumbHorizontalX}px)`,
529 | };
530 | const { scrollTop, clientHeight, scrollHeight } = values;
531 | const trackVerticalHeight = getInnerHeight(this.trackVertical);
532 | const thumbVerticalHeight = this.getThumbVerticalHeight();
533 | const thumbVerticalY =
534 | (scrollTop / (scrollHeight - clientHeight)) * (trackVerticalHeight - thumbVerticalHeight);
535 | const thumbVerticalStyle = {
536 | height: thumbVerticalHeight,
537 | transform: `translateY(${thumbVerticalY}px)`,
538 | };
539 | if (hideTracksWhenNotNeeded) {
540 | const trackHorizontalStyle = {
541 | visibility: scrollWidth > clientWidth ? 'visible' : 'hidden',
542 | };
543 | const trackVerticalStyle = {
544 | visibility: scrollHeight > clientHeight ? 'visible' : 'hidden',
545 | };
546 | css(this.trackHorizontal, trackHorizontalStyle);
547 | css(this.trackVertical, trackVerticalStyle);
548 | }
549 | css(this.thumbHorizontal, thumbHorizontalStyle);
550 | css(this.thumbVertical, thumbVerticalStyle);
551 | }
552 | if (onUpdate) onUpdate(values);
553 | if (typeof callback !== 'function') return;
554 | callback(values);
555 | }
556 |
557 | render() {
558 | const { scrollbarWidth, didMountUniversal } = this.state;
559 |
560 | /* eslint-disable no-unused-vars */
561 | const {
562 | autoHeight,
563 | autoHeightMax,
564 | autoHeightMin,
565 | autoHide,
566 | autoHideDuration,
567 | autoHideTimeout,
568 | children,
569 | classes,
570 | hideTracksWhenNotNeeded,
571 | onScroll,
572 | onScrollFrame,
573 | onScrollStart,
574 | onScrollStop,
575 | onUpdate,
576 | renderThumbHorizontal,
577 | renderThumbVertical,
578 | renderTrackHorizontal,
579 | renderTrackVertical,
580 | renderView,
581 | style,
582 | tagName,
583 | thumbMinSize,
584 | thumbSize,
585 | universal,
586 | disableDefaultStyles,
587 | ...props
588 | } = this.props;
589 | /* eslint-enable no-unused-vars */
590 |
591 | const {
592 | containerStyleAutoHeight,
593 | containerStyleDefault,
594 | thumbStyleDefault,
595 | trackHorizontalStyleDefault,
596 | trackVerticalStyleDefault,
597 | viewStyleAutoHeight,
598 | viewStyleDefault,
599 | viewStyleUniversalInitial,
600 | } = this.styles;
601 |
602 | const containerStyle = {
603 | ...containerStyleDefault,
604 | ...(autoHeight && {
605 | ...containerStyleAutoHeight,
606 | minHeight: autoHeightMin,
607 | maxHeight: autoHeightMax,
608 | }),
609 | ...style,
610 | };
611 |
612 | const viewStyle = {
613 | ...viewStyleDefault,
614 | // Hide scrollbars by setting a negative margin
615 | marginRight: scrollbarWidth ? -scrollbarWidth : 0,
616 | marginBottom: scrollbarWidth ? -scrollbarWidth : 0,
617 | ...(autoHeight && {
618 | ...viewStyleAutoHeight,
619 | // Add scrollbarWidth to autoHeight in order to compensate negative margins
620 | minHeight:
621 | typeof autoHeightMin === 'string'
622 | ? `calc(${autoHeightMin} + ${scrollbarWidth}px)`
623 | : autoHeightMin + scrollbarWidth,
624 | maxHeight:
625 | typeof autoHeightMax === 'string'
626 | ? `calc(${autoHeightMax} + ${scrollbarWidth}px)`
627 | : autoHeightMax + scrollbarWidth,
628 | }),
629 | // Override min/max height for initial universal rendering
630 | ...(autoHeight &&
631 | universal &&
632 | !didMountUniversal && {
633 | minHeight: autoHeightMin,
634 | maxHeight: autoHeightMax,
635 | }),
636 | // Override
637 | ...(universal && !didMountUniversal && viewStyleUniversalInitial),
638 | };
639 |
640 | const trackAutoHeightStyle = {
641 | transition: `opacity ${autoHideDuration}ms`,
642 | opacity: 0,
643 | };
644 |
645 | const trackHorizontalStyle = {
646 | ...trackHorizontalStyleDefault,
647 | ...(autoHide && trackAutoHeightStyle),
648 | ...((!scrollbarWidth || (universal && !didMountUniversal)) && {
649 | display: 'none',
650 | }),
651 | };
652 |
653 | const trackVerticalStyle = {
654 | ...trackVerticalStyleDefault,
655 | ...(autoHide && trackAutoHeightStyle),
656 | ...((!scrollbarWidth || (universal && !didMountUniversal)) && {
657 | display: 'none',
658 | }),
659 | };
660 |
661 | const mergedClasses = getFinalClasses(this.props);
662 |
663 | return createElement(
664 | tagName,
665 | {
666 | ...props,
667 | className: mergedClasses.root,
668 | style: containerStyle,
669 | ref: (ref) => {
670 | this.container = ref;
671 | },
672 | },
673 | [
674 | cloneElement(
675 | renderView({
676 | style: viewStyle,
677 | className: mergedClasses.view,
678 | }),
679 | {
680 | key: 'view',
681 | ref: (ref) => {
682 | this.view = ref;
683 | },
684 | },
685 | children,
686 | ),
687 | cloneElement(
688 | renderTrackHorizontal({
689 | style: trackHorizontalStyle,
690 | className: mergedClasses.trackHorizontal,
691 | }),
692 | {
693 | key: 'trackHorizontal',
694 | ref: (ref) => {
695 | this.trackHorizontal = ref;
696 | },
697 | },
698 | cloneElement(
699 | renderThumbHorizontal({
700 | style: thumbStyleDefault,
701 | className: mergedClasses.thumbHorizontal,
702 | }),
703 | {
704 | ref: (ref) => {
705 | this.thumbHorizontal = ref;
706 | },
707 | },
708 | ),
709 | ),
710 | cloneElement(
711 | renderTrackVertical({
712 | style: trackVerticalStyle,
713 | className: mergedClasses.trackVertical,
714 | }),
715 | {
716 | key: 'trackVertical',
717 | ref: (ref) => {
718 | this.trackVertical = ref;
719 | },
720 | },
721 | cloneElement(
722 | renderThumbVertical({
723 | style: thumbStyleDefault,
724 | className: mergedClasses.thumbVertical,
725 | }),
726 | {
727 | ref: (ref) => {
728 | this.thumbVertical = ref;
729 | },
730 | },
731 | ),
732 | ),
733 | ],
734 | );
735 | }
736 | }
737 |
--------------------------------------------------------------------------------