├── .eslintrc.js ├── .github └── stale.yml ├── .gitignore ├── .npmignore ├── .npmrc ├── .nvmrc ├── .size-limit ├── .size.json ├── .storybook ├── .babelrc ├── main.js └── preview.js ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── __tests__ └── index.tsx ├── constants └── package.json ├── example ├── app.tsx ├── assets │ └── .gitkeep ├── index.html ├── index.tsx └── utils.ts ├── jest.config.js ├── package.json ├── src ├── component.tsx ├── constants.ts ├── index.ts └── utils.ts ├── stories └── cases.stories.tsx ├── tsconfig.json └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['plugin:@typescript-eslint/recommended', 'plugin:import/typescript', 'plugin:react-hooks/recommended'], 3 | parser: '@typescript-eslint/parser', 4 | plugins: ['@typescript-eslint', 'prettier', 'import'], 5 | rules: { 6 | '@typescript-eslint/ban-ts-comment': 0, 7 | '@typescript-eslint/ban-ts-ignore': 0, 8 | '@typescript-eslint/no-var-requires': 0, 9 | '@typescript-eslint/camelcase': 0, 10 | 'import/order': [ 11 | 'error', 12 | { 13 | 'newlines-between': 'always-and-inside-groups', 14 | alphabetize: { 15 | order: 'asc', 16 | }, 17 | groups: ['builtin', 'external', 'internal', ['parent', 'index', 'sibling']], 18 | }, 19 | ], 20 | 'padding-line-between-statements': [ 21 | 'error', 22 | // IMPORT 23 | { 24 | blankLine: 'always', 25 | prev: 'import', 26 | next: '*', 27 | }, 28 | { 29 | blankLine: 'any', 30 | prev: 'import', 31 | next: 'import', 32 | }, 33 | // EXPORT 34 | { 35 | blankLine: 'always', 36 | prev: '*', 37 | next: 'export', 38 | }, 39 | { 40 | blankLine: 'any', 41 | prev: 'export', 42 | next: 'export', 43 | }, 44 | { 45 | blankLine: 'always', 46 | prev: '*', 47 | next: ['const', 'let'], 48 | }, 49 | { 50 | blankLine: 'any', 51 | prev: ['const', 'let'], 52 | next: ['const', 'let'], 53 | }, 54 | // BLOCKS 55 | { 56 | blankLine: 'always', 57 | prev: ['block', 'block-like', 'class', 'function', 'multiline-expression'], 58 | next: '*', 59 | }, 60 | { 61 | blankLine: 'always', 62 | prev: '*', 63 | next: ['block', 'block-like', 'class', 'function', 'return', 'multiline-expression'], 64 | }, 65 | ], 66 | }, 67 | settings: { 68 | 'import/parsers': { 69 | '@typescript-eslint/parser': ['.ts', '.tsx'], 70 | }, 71 | 'import/resolver': { 72 | typescript: { 73 | alwaysTryTypes: true, 74 | }, 75 | }, 76 | }, 77 | }; 78 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Label to use when marking an issue as stale 6 | staleLabel: state 7 | # Comment to post when marking an issue as stale. Set to `false` to disable 8 | markComment: > 9 | This issue has been marked as "stale" because there has been no activity for 2 months. 10 | If you have any new information or would like to continue the discussion, please feel free to do so. 11 | If this issue got buried among other tasks, maybe this message will reignite the conversation. 12 | Otherwise, this issue will be closed in 7 days. Thank you for your contributions so far. 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | /dist/ 3 | .DS_Store 4 | coverage/ 5 | yarn-error.log -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | .DS_Store 4 | example -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 18 -------------------------------------------------------------------------------- /.size-limit: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "path": "dist/es2019/index.js", 4 | "ignore": ["react-dom", "tslib"], 5 | "limit": "1350 B" 6 | } 7 | ] 8 | -------------------------------------------------------------------------------- /.size.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "dist/es2019/index.js", 4 | "passed": true, 5 | "size": 1285, 6 | "sizeLimit": 1350 7 | } 8 | ] 9 | -------------------------------------------------------------------------------- /.storybook/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "sourceType": "unambiguous", 3 | "presets": [ 4 | [ 5 | "@babel/preset-env", 6 | { 7 | "shippedProposals": true, 8 | "loose": true 9 | } 10 | ], 11 | "@babel/preset-typescript", 12 | "@babel/preset-react" 13 | ] 14 | } -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | stories: ['../stories/*.stories.@(js|jsx|ts|tsx)'], 3 | framework: '@storybook/react', 4 | typescript: { 5 | check: false, 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | export const parameters = { 2 | actions: { argTypesRegex: '^on[A-Z].*' }, 3 | controls: { 4 | matchers: { 5 | color: /(background|color)$/i, 6 | date: /Date$/, 7 | }, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '16' 4 | cache: yarn 5 | script: 6 | - yarn 7 | - yarn test:ci 8 | - codecov 9 | notifications: 10 | email: true -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [2.3.6](https://github.com/theKashey/react-remove-scroll-bar/compare/v2.3.5...v2.3.6) (2024-03-14) 2 | 3 | ### Bug Fixes 4 | 5 | - correct lock behavior for nested invocations, fixes [#57](https://github.com/theKashey/react-remove-scroll-bar/issues/57) ([bd44ebe](https://github.com/theKashey/react-remove-scroll-bar/commit/bd44ebeea9af047816dbb66c42eab57cbd937fcb)) 6 | 7 | ## [2.3.5](https://github.com/theKashey/react-remove-scroll-bar/compare/v2.3.4...v2.3.5) (2024-02-19) 8 | 9 | ## [2.3.4](https://github.com/theKashey/react-remove-scroll-bar/compare/v2.3.3...v2.3.4) (2022-10-12) 10 | 11 | ### Bug Fixes 12 | 13 | - remove measure time error reporting, fixes [#35](https://github.com/theKashey/react-remove-scroll-bar/issues/35) ([3f4bf5d](https://github.com/theKashey/react-remove-scroll-bar/commit/3f4bf5d26713b18e3161dce34b7a07265d763864)) 14 | 15 | ## [2.3.3](https://github.com/theKashey/react-remove-scroll-bar/compare/v2.3.2...v2.3.3) (2022-06-06) 16 | 17 | ## [2.3.2](https://github.com/theKashey/react-remove-scroll-bar/compare/v2.3.1...v2.3.2) (2022-06-06) 18 | 19 | ### Bug Fixes 20 | 21 | - update style-singlenton. fixes [#32](https://github.com/theKashey/react-remove-scroll-bar/issues/32) ([f8b9a22](https://github.com/theKashey/react-remove-scroll-bar/commit/f8b9a22ae5ba8a0868caa15faf1a83ee06534f17)) 22 | 23 | ## [2.3.1](https://github.com/theKashey/react-remove-scroll-bar/compare/v2.3.0...v2.3.1) (2022-05-01) 24 | 25 | ### Bug Fixes 26 | 27 | - configure overscroll-behavior to contain scroll within body ([a29ad5d](https://github.com/theKashey/react-remove-scroll-bar/commit/a29ad5d237f9c696f430f18dbd4cc0ae0a37b617)) 28 | 29 | # [2.3.0](https://github.com/theKashey/react-remove-scroll-bar/compare/v2.2.0...v2.3.0) (2022-04-17) 30 | 31 | # [2.2.0](https://github.com/theKashey/react-remove-scroll-bar/compare/v2.1.1...v2.2.0) (2021-02-10) 32 | 33 | ### Features 34 | 35 | - expose removed scroll bar size via css variable ([e357989](https://github.com/theKashey/react-remove-scroll-bar/commit/e357989a92fb8acc83466f8371d0fa558f1a5492)) 36 | 37 | ## [2.1.1](https://github.com/theKashey/react-remove-scroll-bar/compare/v2.1.0...v2.1.1) (2020-11-27) 38 | 39 | # [2.1.0](https://github.com/theKashey/react-remove-scroll-bar/compare/v2.0.0...v2.1.0) (2020-04-16) 40 | 41 | ### Features 42 | 43 | - support CSP ([d805e33](https://github.com/theKashey/react-remove-scroll-bar/commit/d805e332d8525a7b39bc19765f52ca89fb415dff)) 44 | 45 | # [2.0.0](https://github.com/theKashey/react-remove-scroll-bar/compare/v1.2.0...v2.0.0) (2019-06-07) 46 | 47 | # [1.2.0](https://github.com/theKashey/react-remove-scroll-bar/compare/v1.1.5...v1.2.0) (2019-06-04) 48 | 49 | ### Features 50 | 51 | - extract constants as a separate entrypoint ([2a569b5](https://github.com/theKashey/react-remove-scroll-bar/commit/2a569b50643c251604e360a1cb366aa5a68aedda)) 52 | 53 | ## [1.1.5](https://github.com/theKashey/react-remove-scroll-bar/compare/v1.1.4...v1.1.5) (2019-05-03) 54 | 55 | ### Bug Fixes 56 | 57 | - always add overflow hidden, even for scroll-bar-free systems ([9540463](https://github.com/theKashey/react-remove-scroll-bar/commit/95404631661026008772f2c43c298ff05971dd21)) 58 | 59 | ## [1.1.4](https://github.com/theKashey/react-remove-scroll-bar/compare/v1.1.3...v1.1.4) (2019-03-02) 60 | 61 | ## [1.1.3](https://github.com/theKashey/react-remove-scroll-bar/compare/v1.1.2...v1.1.3) (2019-01-21) 62 | 63 | ### Features 64 | 65 | - support non-zero body margins ([0828cf6](https://github.com/theKashey/react-remove-scroll-bar/commit/0828cf6d4fcbe142c7b487b941768db64f7b4508)) 66 | 67 | ## [1.1.2](https://github.com/theKashey/react-remove-scroll-bar/compare/v1.1.1...v1.1.2) (2019-01-18) 68 | 69 | ## [1.1.1](https://github.com/theKashey/react-remove-scroll-bar/compare/v1.1.0...v1.1.1) (2019-01-18) 70 | 71 | # 1.1.0 (2019-01-18) 72 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Anton Korzunov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

react-remove-scroll-bar

2 | 3 | [![npm](https://img.shields.io/npm/v/react-remove-scroll-bar.svg)](https://www.npmjs.com/package/react-remove-scroll-bar) 4 | [![bundle size](https://badgen.net/bundlephobia/minzip/react-remove-scroll-bar)](https://bundlephobia.com/result?p=react-remove-scroll-bar) 5 | [![downloads](https://badgen.net/npm/dm/react-remove-scroll-bar)](https://www.npmtrends.com/react-remove-scroll-bar) 6 | 7 |
8 | 9 | > v1+ for React 15, v2+ requires React 16.8+ 10 | 11 | Removes scroll bar (by setting `overflow: hidden` on body), and preserves the scroll bar "gap". 12 | 13 | Read - it just makes scroll bar invisible. 14 | 15 | Does nothing if scroll bar does not consume any space. 16 | 17 | # Usage 18 | 19 | ```js 20 | import {RemoveScrollBar} from 'react-remove-scroll-bar'; 21 | 22 | -> no scroll bar 23 | ``` 24 | 25 | ### The Right Border 26 | To prevent content jumps __position:fixed__ elements with `right:0` should have additional classname applied. 27 | It will just provide a _non-zero_ right, when it needed, to maintain the right "gap". 28 | ```js 29 | import {zeroRightClassName,fullWidthClassName, noScrollbarsClassName} from 'react-remove-scroll-bar'; 30 | 31 | // to set `right:0` on an element 32 |
33 | 34 | // to set `width:100%` on an element 35 |
36 | 37 | // to remove scrollbar from an element 38 |
39 | 40 | ``` 41 | 42 | # Size 43 | 500b after compression (excluding tslib). 44 | 45 | # Scroll-Locky 46 | All code is a result of a [react-scroll-locky](https://github.com/theKashey/react-scroll-locky) refactoring. 47 | 48 | # Article 49 | There is a medium article about preventing the body scroll - [How to fight the scroll](https://medium.com/@antonkorzunov/how-to-fight-the-body-scroll-2b00267b37ac) 50 | 51 | # License 52 | MIT 53 | -------------------------------------------------------------------------------- /__tests__/index.tsx: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/react'; 2 | 3 | import React, { useState } from 'react'; 4 | 5 | import { RemoveScrollBar } from '../src'; 6 | import { lockAttribute } from '../src/component'; 7 | 8 | const renderTest = (actionName = 'toggle') => { 9 | const Test = () => { 10 | const [lock, setLock] = useState(false); 11 | 12 | return ( 13 | <> 14 | 15 | {lock ? : null} 16 | 17 | ); 18 | }; 19 | 20 | return render(); 21 | }; 22 | 23 | describe('RemoveScrollBar', () => { 24 | it('should toggle the lock attribute on mount/unmount', async () => { 25 | const { getByRole } = renderTest(); 26 | const button = getByRole('button'); 27 | 28 | expect(document.body.getAttribute(lockAttribute)).toBeNull(); 29 | expect(window.getComputedStyle(document.body).overflow).toBe(''); 30 | 31 | button.click(); 32 | expect(document.body.getAttribute(lockAttribute)).toBeDefined(); 33 | await new Promise((resolve) => setTimeout(resolve, 1)); 34 | expect(window.getComputedStyle(document.body).overflow).toBe('hidden'); 35 | 36 | button.click(); 37 | expect(document.body.getAttribute(lockAttribute)).toBeNull(); 38 | expect(window.getComputedStyle(document.body).overflow).toBe(''); 39 | }); 40 | 41 | it('should handle nested cases', () => { 42 | const t1 = renderTest('toggle1'); 43 | const t2 = renderTest('toggle2'); 44 | const button1 = t1.getByRole('button', { name: 'toggle1' }); 45 | const button2 = t2.getByRole('button', { name: 'toggle2' }); 46 | 47 | expect(document.body.getAttribute(lockAttribute)).toBeNull(); 48 | 49 | button1.click(); 50 | button2.click(); 51 | expect(document.body.getAttribute(lockAttribute)).toBeDefined(); 52 | 53 | button1.click(); 54 | expect(document.body.getAttribute(lockAttribute)).toBeDefined(); 55 | 56 | button2.click(); 57 | expect(document.body.getAttribute(lockAttribute)).toBeNull(); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /constants/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "separate entrypoint for constants only", 3 | "private": true, 4 | "main": "../dist/es5/constants.js", 5 | "jsnext:main": "../dist/es2015/constants.js", 6 | "module": "../dist/es2015/constants.js", 7 | "sideEffects": false 8 | } 9 | -------------------------------------------------------------------------------- /example/app.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import {Component} from 'react'; 3 | import {RemoveScrollBar, fullWidthClassName} from "../src"; 4 | 5 | export interface AppState { 6 | counter: number; 7 | } 8 | 9 | const fill = (x: number, y: number) => { 10 | const a: number[] = []; 11 | for (let i = 0; i < x; ++i) { 12 | a.push(y) 13 | } 14 | return a; 15 | } 16 | 17 | export default class App extends Component <{}, AppState> { 18 | state: AppState = { 19 | counter: 0 20 | }; 21 | 22 | componentDidMount() { 23 | setInterval(() => { 24 | // this.setState({counter: this.state.counter ? 0 : 1}) 25 | }, 1000); 26 | 27 | setTimeout(() => { 28 | this.setState({counter: this.state.counter ? 0 : 1}) 29 | }, 1000); 30 | } 31 | 32 | render() { 33 | const gapMode = 'margin'; 34 | return ( 35 |
36 | {this.state.counter ? : undefined} 37 | {/*
floating*/} 45 | {/*
*/} 46 | 47 |
60 | XXX 61 | XXX 62 | XXX 63 | {fill(1000, 1).map(x =>

{x}****

)} 64 |
65 | 66 |
79 | XXX 80 | XXX 81 | XXX 82 | {fill(1000, 1).map(x =>

{x}****

)} 83 |
84 | 85 |
98 | XXX 99 | XXX 100 | XXX 101 | {fill(1000, 1).map(x =>

{x}****

)} 102 |
103 | 104 | 105 |
115 | XXX 116 | XXX 117 | XXX 118 | 119 |
126 | ZZZ 127 | ZZZ 128 | {fill(1000, 1).map(x =>

{x}****

)} 129 |
130 | 131 | {fill(1000, 1).map(x =>

{x}****

)} 132 |
133 | 134 | {fill(1000, 1).map((x, index) => {index}**** )} 135 |
136 | ) 137 | } 138 | } -------------------------------------------------------------------------------- /example/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theKashey/react-remove-scroll-bar/8ca9ba5ea52de03308fe8ced94f7b159a44d28ff/example/assets/.gitkeep -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Example 6 | 11 | 12 | 13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /example/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import App from './app'; 4 | 5 | ReactDOM.render(, document.getElementById('app')); 6 | -------------------------------------------------------------------------------- /example/utils.ts: -------------------------------------------------------------------------------- 1 | import {Component} from 'react'; 2 | 3 | export class ToolboxApp extends Component { 4 | onCheckboxChange = (propName: any) => () => { 5 | const currentValue = (this.state as any)[propName]; 6 | this.setState({ [propName]: !currentValue } as any); 7 | } 8 | 9 | onFieldTextChange = (propName: any) => (e: any) => { 10 | const value = e.target.value; 11 | 12 | (this as any).setState({ 13 | [propName]: value 14 | }); 15 | } 16 | } -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | }; 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-remove-scroll-bar", 3 | "version": "2.3.7", 4 | "description": "Removes body scroll without content _shake_", 5 | "main": "dist/es5/index.js", 6 | "jsnext:main": "dist/es2015/index.js", 7 | "module": "dist/es2015/index.js", 8 | "types": "dist/es5/index.d.ts", 9 | "scripts": { 10 | "dev": "lib-builder dev", 11 | "test": "jest", 12 | "test:ci": "jest --runInBand --coverage", 13 | "build": "lib-builder build && yarn size:report", 14 | "release": "yarn build && yarn test", 15 | "size": "yarn size-limit", 16 | "size:report": "yarn --silent size-limit --json > .size.json", 17 | "lint": "lib-builder lint", 18 | "format": "lib-builder format", 19 | "update": "lib-builder update", 20 | "prepublish": "yarn build && yarn changelog", 21 | "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s", 22 | "changelog:rewrite": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0", 23 | "storybook": "start-storybook -p 6006" 24 | }, 25 | "keywords": [ 26 | "scroll" 27 | ], 28 | "author": "Anton Korzunov ", 29 | "license": "MIT", 30 | "devDependencies": { 31 | "@size-limit/preset-small-lib": "^11.0.2", 32 | "size-limit": "^11.0.2", 33 | "@storybook/react": "^6.4.22", 34 | "@testing-library/react": "^12.1.5", 35 | "@types/react": "^16.14.56", 36 | "@theuiteam/lib-builder": "^0.1.4", 37 | "react": "^16.8.6", 38 | "react-dom": "^16.8.6" 39 | }, 40 | "engines": { 41 | "node": ">=10" 42 | }, 43 | "peerDependencies": { 44 | "@types/react": "*", 45 | "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" 46 | }, 47 | "peerDependenciesMeta": { 48 | "@types/react": { 49 | "optional": true 50 | } 51 | }, 52 | "files": [ 53 | "dist", 54 | "constants" 55 | ], 56 | "repository": "https://github.com/theKashey/react-remove-scroll-bar", 57 | "dependencies": { 58 | "react-style-singleton": "^2.2.1", 59 | "tslib": "^2.0.0" 60 | }, 61 | "module:es2019": "dist/es2019/index.js", 62 | "husky": { 63 | "hooks": { 64 | "pre-commit": "lint-staged" 65 | } 66 | }, 67 | "lint-staged": { 68 | "*.{ts,tsx}": [ 69 | "prettier --write", 70 | "eslint --fix", 71 | "git add" 72 | ], 73 | "*.{js,css,json,md}": [ 74 | "prettier --write", 75 | "git add" 76 | ] 77 | }, 78 | "prettier": { 79 | "printWidth": 120, 80 | "trailingComma": "es5", 81 | "tabWidth": 2, 82 | "semi": true, 83 | "singleQuote": true 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/component.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { styleSingleton } from 'react-style-singleton'; 3 | 4 | import { fullWidthClassName, zeroRightClassName, noScrollbarsClassName, removedBarSizeVariable } from './constants'; 5 | import { GapMode, GapOffset, getGapWidth } from './utils'; 6 | 7 | export interface BodyScroll { 8 | noRelative?: boolean; 9 | noImportant?: boolean; 10 | gapMode?: GapMode; 11 | } 12 | 13 | const Style = styleSingleton(); 14 | 15 | export const lockAttribute = 'data-scroll-locked'; 16 | 17 | // important tip - once we measure scrollBar width and remove them 18 | // we could not repeat this operation 19 | // thus we are using style-singleton - only the first "yet correct" style will be applied. 20 | const getStyles = ( 21 | { left, top, right, gap }: GapOffset, 22 | allowRelative: boolean, 23 | gapMode: GapMode = 'margin', 24 | important: string 25 | ) => ` 26 | .${noScrollbarsClassName} { 27 | overflow: hidden ${important}; 28 | padding-right: ${gap}px ${important}; 29 | } 30 | body[${lockAttribute}] { 31 | overflow: hidden ${important}; 32 | overscroll-behavior: contain; 33 | ${[ 34 | allowRelative && `position: relative ${important};`, 35 | gapMode === 'margin' && 36 | ` 37 | padding-left: ${left}px; 38 | padding-top: ${top}px; 39 | padding-right: ${right}px; 40 | margin-left:0; 41 | margin-top:0; 42 | margin-right: ${gap}px ${important}; 43 | `, 44 | gapMode === 'padding' && `padding-right: ${gap}px ${important};`, 45 | ] 46 | .filter(Boolean) 47 | .join('')} 48 | } 49 | 50 | .${zeroRightClassName} { 51 | right: ${gap}px ${important}; 52 | } 53 | 54 | .${fullWidthClassName} { 55 | margin-right: ${gap}px ${important}; 56 | } 57 | 58 | .${zeroRightClassName} .${zeroRightClassName} { 59 | right: 0 ${important}; 60 | } 61 | 62 | .${fullWidthClassName} .${fullWidthClassName} { 63 | margin-right: 0 ${important}; 64 | } 65 | 66 | body[${lockAttribute}] { 67 | ${removedBarSizeVariable}: ${gap}px; 68 | } 69 | `; 70 | 71 | const getCurrentUseCounter = () => { 72 | const counter = parseInt(document.body.getAttribute(lockAttribute) || '0', 10); 73 | 74 | return isFinite(counter) ? counter : 0; 75 | }; 76 | 77 | export const useLockAttribute = () => { 78 | React.useEffect(() => { 79 | document.body.setAttribute(lockAttribute, (getCurrentUseCounter() + 1).toString()); 80 | 81 | return () => { 82 | const newCounter = getCurrentUseCounter() - 1; 83 | 84 | if (newCounter <= 0) { 85 | document.body.removeAttribute(lockAttribute); 86 | } else { 87 | document.body.setAttribute(lockAttribute, newCounter.toString()); 88 | } 89 | }; 90 | }, []); 91 | }; 92 | 93 | /** 94 | * Removes page scrollbar and blocks page scroll when mounted 95 | */ 96 | export const RemoveScrollBar: React.FC = ({ noRelative, noImportant, gapMode = 'margin' }) => { 97 | useLockAttribute(); 98 | 99 | /* 100 | gap will be measured on every component mount 101 | however it will be used only by the "first" invocation 102 | due to singleton nature of