├── .babelrc ├── .codeclimate.yml ├── .codesandbox └── ci.json ├── .editorconfig ├── .eslintrc ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── demo ├── .prettierrc ├── package.json ├── public │ └── index.html ├── src │ ├── App.tsx │ ├── GitHubRepo.tsx │ ├── components.tsx │ ├── index.tsx │ └── styles.css └── tsconfig.json ├── jest.config.ts ├── package-lock.json ├── package.json ├── src └── index.ts ├── test ├── __setup__ │ └── index.ts └── index.spec.ts └── tsconfig.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "corejs": 3, 7 | "useBuiltIns": "entry", 8 | "modules": false 9 | } 10 | ] 11 | ], 12 | "plugins": [ 13 | "@babel/plugin-proposal-class-properties", 14 | [ 15 | "@babel/plugin-proposal-decorators", 16 | { 17 | "legacy": true 18 | } 19 | ], 20 | "@babel/plugin-proposal-do-expressions", 21 | "@babel/plugin-proposal-export-default-from", 22 | "@babel/plugin-proposal-export-namespace-from", 23 | "@babel/plugin-proposal-function-sent", 24 | "@babel/plugin-proposal-json-strings", 25 | "@babel/plugin-proposal-logical-assignment-operators", 26 | "@babel/plugin-proposal-nullish-coalescing-operator", 27 | "@babel/plugin-proposal-numeric-separator", 28 | "@babel/plugin-proposal-optional-chaining", 29 | [ 30 | "@babel/plugin-proposal-pipeline-operator", 31 | { 32 | "proposal": "minimal" 33 | } 34 | ], 35 | "@babel/plugin-proposal-throw-expressions", 36 | "@babel/plugin-syntax-dynamic-import", 37 | "@babel/plugin-syntax-import-meta" 38 | ], 39 | "compact": false, 40 | "env": { 41 | "production": { 42 | "plugins": [ 43 | "array-includes" 44 | ] 45 | }, 46 | "commonjs": { 47 | "plugins": [ 48 | [ 49 | "@babel/plugin-transform-modules-commonjs", 50 | { 51 | "loose": true 52 | } 53 | ] 54 | ] 55 | }, 56 | "test": { 57 | "plugins": [ 58 | "@babel/plugin-transform-modules-commonjs" 59 | ], 60 | "sourceMaps": "both" 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: "2" 3 | checks: 4 | argument-count: 5 | config: 6 | threshold: 4 7 | complex-logic: 8 | config: 9 | threshold: 4 10 | file-lines: 11 | config: 12 | threshold: 500 13 | method-complexity: 14 | config: 15 | threshold: 20 16 | method-count: 17 | config: 18 | threshold: 20 19 | method-lines: 20 | config: 21 | threshold: 50 22 | nested-control-flow: 23 | config: 24 | threshold: 4 25 | return-statements: 26 | config: 27 | threshold: 4 28 | similar-code: 29 | config: 30 | threshold: 100 31 | identical-code: 32 | config: 33 | threshold: 20 34 | exclude_patterns: 35 | - coverage/**/* 36 | - demo/**/* 37 | - esm/**/* 38 | - lib/**/* 39 | - node_modules/**/* 40 | - test/**/* 41 | -------------------------------------------------------------------------------- /.codesandbox/ci.json: -------------------------------------------------------------------------------- 1 | { 2 | "sandboxes": ["/demo"] 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | # change these settings to your own preference 9 | indent_style = space 10 | indent_size = 2 11 | 12 | # we recommend you to keep these unchanged 13 | end_of_line = lf 14 | charset = utf-8 15 | trim_trailing_whitespace = true 16 | insert_final_newline = true 17 | 18 | [*.md] 19 | trim_trailing_whitespace = false 20 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "airbnb-base", 4 | "plugin:@typescript-eslint/recommended", 5 | "plugin:import/typescript", 6 | "plugin:prettier/recommended", 7 | "prettier/standard" 8 | ], 9 | "parser": "@typescript-eslint/parser", 10 | "env": { 11 | "browser": true, 12 | "jest": true 13 | }, 14 | "plugins": ["@typescript-eslint/eslint-plugin", "prettier"], 15 | "overrides": [ 16 | { 17 | "files": ["test/**/*.ts?(x)"], 18 | "rules": { 19 | "@typescript-eslint/ban-ts-comment": "off", 20 | "@typescript-eslint/no-explicit-any": "off" 21 | } 22 | } 23 | ], 24 | "rules": { 25 | "lines-between-class-members": "off", 26 | "import/extensions": ["warn", "never"] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | esm/ 3 | lib/ 4 | node_modules 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '12' 4 | cache: 5 | directories: 6 | - "node_modules" 7 | before_script: 8 | - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter 9 | - chmod +x ./cc-test-reporter 10 | - ./cc-test-reporter before-build 11 | script: 12 | - npm run validate 13 | after_success: 14 | - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Gil Barbara 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | disable scroll 2 | === 3 | 4 | [![NPM version](https://badge.fury.io/js/disable-scroll.svg)](https://www.npmjs.com/package/disable-scroll) [![build status](https://travis-ci.org/gilbarbara/disable-scroll.svg)](https://travis-ci.org/gilbarbara/disable-scroll) [![Maintainability](https://api.codeclimate.com/v1/badges/6a4bbda8255037dca13f/maintainability)](https://codeclimate.com/github/gilbarbara/disable-scroll/maintainability) [![Test Coverage](https://api.codeclimate.com/v1/badges/6a4bbda8255037dca13f/test_coverage)](https://codeclimate.com/github/gilbarbara/disable-scroll/test_coverage) 5 | 6 | **Prevent page scrolling like a boss.** 7 | Supports `scroll, wheel, touchmove, keydown` events. 8 | 9 | [Demo](https://codesandbox.io/s/github/gilbarbara/disable-scroll/tree/master/demo) 10 | 11 | ## Setup 12 | 13 | ### npm 14 | ```bash 15 | npm install --save disable-scroll 16 | ``` 17 | 18 | and import it 19 | 20 | ```typescript 21 | import disableScroll from 'disable-scroll'; 22 | ``` 23 | 24 | ## Usage 25 | 26 | ```typescript 27 | disableScroll.on(); // prevent scrolling 28 | ... 29 | disableScroll.off(); // re-enable scroll 30 | ``` 31 | 32 | > If you need to support legacy browsers you need to include the scrollingelement polyfill. 33 | 34 | ## API 35 | 36 | ### .on(element?: Element, options?: Options) 37 | Disable page scroll by adding event listeners and locking the scroll position. 38 | 39 | Options defaults to: 40 | 41 | ```typescript 42 | { 43 | authorizedInInputs: [32, 37, 38, 39, 40], 44 | disableKeys: true, 45 | disableScroll: true, 46 | disableWheel: true, 47 | keyboardKeys: [32, 33, 34, 35, 36, 37, 38, 39, 40], 48 | } 49 | ``` 50 | 51 | ### .off() 52 | Re-enable page scrolling and remove the listeners. 53 | 54 | ## Issues 55 | 56 | If you find a bug, please file an issue on [our issue tracker on GitHub](https://github.com/gilbarbara/disable-scroll/issues). 57 | 58 | ## License 59 | 60 | MIT 61 | 62 | --- 63 | 64 | Inspired by [jquery-disablescroll](https://github.com/ultrapasty/jquery-disablescroll) 65 | -------------------------------------------------------------------------------- /demo/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "printWidth": 100, 4 | "singleQuote": true, 5 | "trailingComma": "all" 6 | } -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "disable-scroll-demo", 3 | "version": "1.0.0", 4 | "description": "", 5 | "keywords": [], 6 | "main": "src/index.tsx", 7 | "dependencies": { 8 | "disable-scroll": "0.6.0", 9 | "react": "17.0.1", 10 | "react-dom": "17.0.1", 11 | "react-scripts": "4.0.1", 12 | "styled-components": "5.2.1" 13 | }, 14 | "devDependencies": { 15 | "@types/react": "16.9.19", 16 | "@types/react-dom": "16.9.5", 17 | "@types/styled-components": "5.1.4", 18 | "typescript": "3.7.5" 19 | }, 20 | "scripts": { 21 | "start": "react-scripts start", 22 | "build": "react-scripts build", 23 | "test": "react-scripts test --env=jsdom", 24 | "eject": "react-scripts eject" 25 | }, 26 | "browserslist": [ 27 | ">0.2%", 28 | "not dead", 29 | "not ie <= 11", 30 | "not op_mini all" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /demo/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 23 | React App 24 | 25 | 26 | 27 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /demo/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import disableScroll from 'disable-scroll'; 3 | 4 | import { Button, Header, H1, Input, Step, Steps, Wrapper } from './components'; 5 | import GitHubRepo from "./GitHubRepo"; 6 | 7 | import './styles.css'; 8 | 9 | class App extends React.Component { 10 | state = { 11 | isDisabled: false, 12 | }; 13 | 14 | handleClick = () => { 15 | const { isDisabled } = this.state; 16 | 17 | disableScroll[isDisabled ? 'off' : 'on'](); 18 | this.setState({ isDisabled: !isDisabled }); 19 | }; 20 | 21 | renderSteps() { 22 | const colors = [ 23 | '#5bc0eb', 24 | '#fde74c', 25 | '#9bc53d', 26 | '#e55934', 27 | '#fa7921', 28 | '#8F7892', 29 | '#f25f5c', 30 | '#ffe066', 31 | '#247ba0', 32 | '#98A584', 33 | ]; 34 | const steps = []; 35 | let colorIdx = 0; 36 | 37 | for (let i = 200; i <= 5000; ) { 38 | steps.push( 39 | 45 | {i} 46 | , 47 | ); 48 | i += 200; 49 | colorIdx++; 50 | } 51 | 52 | return {steps}; 53 | } 54 | 55 | render() { 56 | const { isDisabled } = this.state; 57 | 58 | return ( 59 | 60 | 61 | 62 |
63 |

disable-scroll

64 |
65 | 71 | 78 |
79 | 80 |
81 | {this.renderSteps()} 82 |
83 | ); 84 | } 85 | } 86 | 87 | export default App; 88 | -------------------------------------------------------------------------------- /demo/src/GitHubRepo.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | const Wrapper = styled.a` 5 | position: fixed; 6 | top: 0; 7 | right: 0; 8 | z-index: 100; 9 | 10 | &:hover { 11 | .octo-arm { 12 | animation: octocat-wave 560ms ease-in-out; 13 | } 14 | } 15 | 16 | svg { 17 | fill: #000; 18 | color: #fff; 19 | } 20 | 21 | .octo-arm { 22 | transform-origin: 130px 106px; 23 | } 24 | 25 | @keyframes octocat-wave { 26 | 0%, 27 | 100% { 28 | transform: rotate(0); 29 | } 30 | 20%, 31 | 60% { 32 | transform: rotate(-25deg); 33 | } 34 | 40%, 35 | 80% { 36 | transform: rotate(10deg); 37 | } 38 | } 39 | `; 40 | 41 | const GitHubRepo = () => { 42 | return ( 43 | 50 | 63 | 64 | ); 65 | }; 66 | 67 | export default GitHubRepo; 68 | -------------------------------------------------------------------------------- /demo/src/components.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Wrapper = styled.main` 4 | font-family: sans-serif; 5 | text-align: center; 6 | `; 7 | 8 | export const Steps = styled.div` 9 | margin-top: 200px; 10 | `; 11 | 12 | export const Step = styled.div` 13 | align-items: center; 14 | color: rgba(0, 0, 0, 0.4); 15 | display: flex; 16 | font-size: 60px; 17 | height: 200px; 18 | justify-content: center; 19 | padding: 20px; 20 | `; 21 | 22 | export const Button = styled.button` 23 | -webkit-appearance: none; 24 | background-color: #000; 25 | border-radius: 3px; 26 | border: 0; 27 | color: #fff; 28 | font-size: 12px; 29 | font-weight: bold; 30 | line-height: 1; 31 | outline: none; 32 | padding: 6px 10px; 33 | white-space: nowrap; 34 | 35 | &:disabled { 36 | background-color: #999; 37 | font-weight: normal; 38 | pointer-events: none; 39 | } 40 | `; 41 | 42 | export const Header = styled.header` 43 | height: 200px; 44 | left: 0; 45 | padding-top: 50px; 46 | position: fixed; 47 | text-align: center; 48 | top: 0; 49 | right: 0; 50 | `; 51 | 52 | export const H1 = styled.h1` 53 | margin-top: 0; 54 | `; 55 | 56 | export const Input = styled.input` 57 | border: 1px solid #ccc; 58 | display: block; 59 | font-size: 16px; 60 | line-height: 1; 61 | margin: 15px auto 0; 62 | padding: 4px; 63 | text-align: center; 64 | width: 200px; 65 | 66 | &::placeholder { 67 | color: #ccc; 68 | } 69 | `; 70 | -------------------------------------------------------------------------------- /demo/src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { render } from "react-dom"; 3 | 4 | import App from "./App"; 5 | 6 | const rootElement = document.getElementById("root"); 7 | render(, rootElement); 8 | -------------------------------------------------------------------------------- /demo/src/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #f7f7f7; 3 | font-family: Helvetica Neue, Helvetica, Arial, sans-serif; 4 | margin: 0; 5 | padding: 0; 6 | } 7 | 8 | * { 9 | box-sizing: border-box; 10 | } 11 | -------------------------------------------------------------------------------- /demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "./src/**/*" 4 | ], 5 | "compilerOptions": { 6 | "strict": true, 7 | "esModuleInterop": true, 8 | "lib": [ 9 | "dom", 10 | "es2015" 11 | ], 12 | "jsx": "react" 13 | } 14 | } -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | collectCoverage: false, 3 | collectCoverageFrom: ['src/**/*.ts'], 4 | coverageThreshold: { 5 | global: { 6 | branches: 80, 7 | functions: 90, 8 | lines: 90, 9 | statements: 90, 10 | }, 11 | }, 12 | globals: { 13 | 'ts-jest': { 14 | tsconfig: 'tsconfig.json', 15 | }, 16 | }, 17 | moduleDirectories: ['node_modules', 'src', './'], 18 | moduleFileExtensions: ['js', 'json', 'ts'], 19 | preset: 'ts-jest', 20 | setupFiles: ['/test/__setup__/index.ts'], 21 | testRegex: '/test/.*?\\.(test|spec)\\.ts$', 22 | testURL: 'http://localhost:3000', 23 | verbose: false, 24 | watchPlugins: ['jest-watch-typeahead/filename', 'jest-watch-typeahead/testname'], 25 | }; 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "disable-scroll", 3 | "version": "0.6.0", 4 | "description": "Prevent page scroll", 5 | "author": "Gil Barbara ", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/gilbarbara/disable-scroll.git" 9 | }, 10 | "bugs": { 11 | "url": "https://github.com/gilbarbara/disable-scroll/issues" 12 | }, 13 | "main": "lib/index.js", 14 | "module": "esm/index.js", 15 | "files": [ 16 | "esm", 17 | "lib", 18 | "src" 19 | ], 20 | "types": "esm", 21 | "sideEffects": true, 22 | "license": "MIT", 23 | "keywords": [ 24 | "scroll" 25 | ], 26 | "dependencies": {}, 27 | "devDependencies": { 28 | "@gilbarbara/tsconfig": "^0.1.0", 29 | "@size-limit/preset-small-lib": "^4.9.1", 30 | "@types/jest": "^26.0.16", 31 | "@types/node": "^14.14.10", 32 | "@typescript-eslint/eslint-plugin": "^4.9.0", 33 | "@typescript-eslint/parser": "^4.9.0", 34 | "del-cli": "^3.0.1", 35 | "eslint": "^7.14.0", 36 | "eslint-config-airbnb-base": "^14.2.1", 37 | "eslint-config-prettier": "^6.15.0", 38 | "eslint-plugin-babel": "^5.3.1", 39 | "eslint-plugin-import": "^2.22.1", 40 | "eslint-plugin-prettier": "^3.1.4", 41 | "husky": "^4.3.0", 42 | "jest": "^26.6.3", 43 | "jest-extended": "^0.11.5", 44 | "jest-watch-typeahead": "^0.6.1", 45 | "prettier": "^2.2.1", 46 | "repo-tools": "^0.2.0", 47 | "size-limit": "^4.9.1", 48 | "ts-jest": "^26.4.4", 49 | "ts-node": "^9.1.0", 50 | "typescript": "^4.1.2" 51 | }, 52 | "scripts": { 53 | "build": "npm run clean && npm run build:cjs && npm run build:esm", 54 | "build:cjs": "tsc", 55 | "build:esm": "tsc -m es6 --outDir esm", 56 | "clean": "del lib/* && del esm/*", 57 | "watch:cjs": "npm run build:cjs -- -w", 58 | "watch:esm": "npm run build:esm -- -w", 59 | "lint": "eslint --ext .ts src test", 60 | "test": "jest --bail --coverage", 61 | "test:watch": "jest --watchAll --verbose", 62 | "validate": "npm run lint && npm test && npm run build && npm run size", 63 | "size": "size-limit", 64 | "prepublishOnly": "npm run validate" 65 | }, 66 | "prettier": { 67 | "jsxBracketSameLine": false, 68 | "printWidth": 100, 69 | "singleQuote": true, 70 | "trailingComma": "all" 71 | }, 72 | "size-limit": [ 73 | { 74 | "path": "./esm/index.js", 75 | "limit": "1 kB" 76 | }, 77 | { 78 | "path": "./lib/index.js", 79 | "limit": "1 kB" 80 | } 81 | ], 82 | "husky": { 83 | "hooks": { 84 | "post-merge": "repo-tools install-packages", 85 | "pre-commit": "repo-tools check-remote && npm run validate" 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | const canUseDOM = () => 2 | typeof window !== 'undefined' && !!window.document && !!window.document.createElement; 3 | 4 | interface Options { 5 | authorizedInInputs: number[]; 6 | disableKeys: boolean; 7 | disableScroll: boolean; 8 | disableWheel: boolean; 9 | keyboardKeys: number[]; 10 | } 11 | 12 | class DisableScroll { 13 | element: Element | null; 14 | lockToScrollPos: [number, number]; 15 | options: Options; 16 | 17 | constructor() { 18 | this.element = null; 19 | this.lockToScrollPos = [0, 0]; 20 | this.options = { 21 | authorizedInInputs: [32, 37, 38, 39, 40], 22 | disableKeys: true, 23 | disableScroll: true, 24 | disableWheel: true, 25 | keyboardKeys: [32, 33, 34, 35, 36, 37, 38, 39, 40], 26 | // space: 32, page up: 33, page down: 34, end: 35, home: 36 27 | // left: 37, up: 38, right: 39, down: 40 28 | }; 29 | 30 | /* istanbul ignore else */ 31 | if (canUseDOM()) { 32 | this.element = document.scrollingElement; 33 | } 34 | } 35 | 36 | /** 37 | * Disable Page Scroll 38 | */ 39 | on(element?: Element, options?: Partial) { 40 | if (!canUseDOM()) return; 41 | 42 | this.element = element || this.element; 43 | this.options = { 44 | ...this.options, 45 | ...options, 46 | }; 47 | 48 | const { disableKeys, disableScroll, disableWheel } = this.options; 49 | 50 | /* istanbul ignore else */ 51 | if (disableWheel) { 52 | document.addEventListener('wheel', this.handleWheel, { passive: false }); 53 | document.addEventListener('touchmove', this.handleWheel, { passive: false }); 54 | } 55 | 56 | /* istanbul ignore else */ 57 | if (disableScroll) { 58 | this.lockToScrollPos = [this.element?.scrollLeft ?? 0, this.element?.scrollTop ?? 0]; 59 | document.addEventListener('scroll', this.handleScroll, { passive: false }); 60 | } 61 | 62 | /* istanbul ignore else */ 63 | if (disableKeys) { 64 | document.addEventListener('keydown', this.handleKeydown, { passive: false }); 65 | } 66 | } 67 | 68 | /** 69 | * Re-enable page scrolls 70 | */ 71 | off() { 72 | if (!canUseDOM()) return; 73 | 74 | document.removeEventListener('wheel', this.handleWheel); 75 | document.removeEventListener('touchmove', this.handleWheel); 76 | document.removeEventListener('scroll', this.handleScroll); 77 | document.removeEventListener('keydown', this.handleKeydown); 78 | } 79 | 80 | handleWheel = (e: WheelEvent | TouchEvent) => { 81 | e.preventDefault(); 82 | }; 83 | 84 | handleScroll = () => { 85 | window.scrollTo(...this.lockToScrollPos); 86 | }; 87 | 88 | handleKeydown = (e: KeyboardEvent) => { 89 | let keys = this.options.keyboardKeys; 90 | 91 | /* istanbul ignore else */ 92 | if ( 93 | ['INPUT', 'TEXTAREA'].includes((e.target as HTMLInputElement | HTMLTextAreaElement).tagName) 94 | ) { 95 | keys = keys.filter((key) => !this.options.authorizedInInputs.includes(key)); 96 | } 97 | 98 | /* istanbul ignore else */ 99 | if (keys.includes(e.keyCode)) { 100 | e.preventDefault(); 101 | } 102 | }; 103 | } 104 | 105 | export default new DisableScroll(); 106 | -------------------------------------------------------------------------------- /test/__setup__/index.ts: -------------------------------------------------------------------------------- 1 | declare const global: any; 2 | 3 | global.requestAnimationFrame = (callback: () => void) => { 4 | setTimeout(callback, 0); 5 | }; 6 | 7 | global.matchMedia = () => ({ 8 | matches: false, 9 | addListener: () => undefined, 10 | removeListener: () => undefined, 11 | }); 12 | 13 | export {}; 14 | -------------------------------------------------------------------------------- /test/index.spec.ts: -------------------------------------------------------------------------------- 1 | import disableScroll from '../src'; 2 | 3 | global.scrollTo = () => undefined; 4 | 5 | function dispatchEvents() { 6 | document.dispatchEvent(new Event('scroll')); 7 | document.dispatchEvent(new Event('wheel')); 8 | document.dispatchEvent(new KeyboardEvent('keydown', { keyCode: 32 })); 9 | } 10 | 11 | describe('disable-scroll', () => { 12 | const scrollListener = jest.spyOn(disableScroll, 'handleScroll'); 13 | const wheelListener = jest.spyOn(disableScroll, 'handleWheel'); 14 | const keyboardListener = jest.spyOn(disableScroll, 'handleKeydown'); 15 | 16 | describe('with DOM', () => { 17 | afterEach(() => { 18 | jest.clearAllMocks(); 19 | disableScroll.off(); 20 | }); 21 | 22 | it('should have initialized', () => { 23 | expect(disableScroll.element).toBe(document.scrollingElement); 24 | }); 25 | 26 | it('should have added the listeners when turning it ON', () => { 27 | disableScroll.on(); 28 | dispatchEvents(); 29 | 30 | expect(scrollListener).toHaveBeenCalledTimes(1); 31 | expect(wheelListener).toHaveBeenCalledTimes(1); 32 | expect(keyboardListener).toHaveBeenCalledTimes(1); 33 | }); 34 | 35 | it('should have removed the listeners ', () => { 36 | disableScroll.off(); 37 | dispatchEvents(); 38 | 39 | expect(scrollListener).toHaveBeenCalledTimes(0); 40 | expect(wheelListener).toHaveBeenCalledTimes(0); 41 | expect(keyboardListener).toHaveBeenCalledTimes(0); 42 | }); 43 | 44 | it('should not prevent space in input', () => { 45 | disableScroll.on(); 46 | 47 | const input = document.createElement('input'); 48 | input.setAttribute('type', 'text'); 49 | input.setAttribute('name', 'test-input'); 50 | document.body.appendChild(input); 51 | 52 | const event = input.dispatchEvent( 53 | new KeyboardEvent('keydown', { bubbles: true, cancelable: true, keyCode: 32 }), 54 | ); 55 | 56 | expect(keyboardListener).toHaveBeenCalledTimes(1); 57 | expect(event).toEqual(true); 58 | }); 59 | 60 | it('should prevent page down in input', () => { 61 | disableScroll.on(); 62 | 63 | const input = document.createElement('input'); 64 | input.setAttribute('type', 'text'); 65 | input.setAttribute('name', 'test-input'); 66 | document.body.appendChild(input); 67 | 68 | const event = input.dispatchEvent( 69 | new KeyboardEvent('keydown', { bubbles: true, cancelable: true, keyCode: 34 }), 70 | ); 71 | 72 | expect(keyboardListener).toHaveBeenCalledTimes(1); 73 | expect(event).toEqual(false); 74 | }); 75 | }); 76 | 77 | describe('without DOM', () => { 78 | const { createElement } = window.document; 79 | 80 | beforeAll(() => { 81 | jest.clearAllMocks(); 82 | 83 | // @ts-ignore 84 | window.document.createElement = null; 85 | }); 86 | 87 | afterAll(() => { 88 | window.document.createElement = createElement; 89 | }); 90 | 91 | it('should not have added the listeners when turning it ON', () => { 92 | disableScroll.on(); 93 | dispatchEvents(); 94 | 95 | expect(scrollListener).toHaveBeenCalledTimes(0); 96 | expect(wheelListener).toHaveBeenCalledTimes(0); 97 | expect(keyboardListener).toHaveBeenCalledTimes(0); 98 | }); 99 | 100 | it('should not have removed the listeners when turning it OFF', () => { 101 | disableScroll.off(); 102 | dispatchEvents(); 103 | 104 | expect(scrollListener).toHaveBeenCalledTimes(0); 105 | expect(wheelListener).toHaveBeenCalledTimes(0); 106 | expect(keyboardListener).toHaveBeenCalledTimes(0); 107 | }); 108 | }); 109 | }); 110 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@gilbarbara/tsconfig", 3 | "compilerOptions": { 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "outDir": "./lib", 6 | "target": "es5", 7 | "skipLibCheck": true 8 | }, 9 | "include": ["src/**/*"] 10 | } 11 | --------------------------------------------------------------------------------