├── .npmignore ├── src ├── index.ts └── body-scroll-lock.ts ├── examples ├── vue │ ├── src │ │ ├── vite-env.d.ts │ │ ├── main.ts │ │ ├── assets │ │ │ └── vue.svg │ │ ├── style.css │ │ ├── App.vue │ │ └── components │ │ │ └── HelloWorld.vue │ ├── .vscode │ │ └── extensions.json │ ├── vite.config.ts │ ├── tsconfig.node.json │ ├── .gitignore │ ├── index.html │ ├── package.json │ ├── tsconfig.json │ ├── public │ │ └── vite.svg │ └── README.md ├── react │ ├── src │ │ ├── vite-env.d.ts │ │ ├── main.tsx │ │ ├── App.css │ │ ├── index.css │ │ └── assets │ │ │ └── react.svg │ ├── vite.config.ts │ ├── tsconfig.node.json │ ├── .gitignore │ ├── index.html │ ├── package.json │ ├── tsconfig.json │ └── public │ │ └── vite.svg ├── next │ ├── app │ │ ├── favicon.ico │ │ ├── layout.tsx │ │ ├── globals.css │ │ └── page.module.css │ ├── next.config.js │ ├── components │ │ ├── dialog.module.css │ │ └── dialog.tsx │ ├── package.json │ ├── .gitignore │ ├── public │ │ ├── vercel.svg │ │ └── next.svg │ ├── tsconfig.json │ ├── hooks │ │ ├── use-body-scroll-lock-upgrade.ts │ │ └── body-scroll-lock.ts │ ├── README.md │ └── pnpm-lock.yaml └── vanilla-js │ ├── index-umd.html │ └── index.html ├── PUBLISH-ME.md ├── .changeset ├── config.json └── README.md ├── .gitignore ├── tsconfig.json ├── lib ├── index.d.ts ├── index.esm.js ├── index.js ├── index.umd.js ├── index.js.map ├── index.esm.js.map └── index.umd.js.map ├── CHANGELOG.md ├── vite.config.cjs ├── LICENSE ├── package.json └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | PUBLISH-ME.md -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./body-scroll-lock"; 2 | -------------------------------------------------------------------------------- /examples/vue/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/react/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/vue/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"] 3 | } 4 | -------------------------------------------------------------------------------- /examples/next/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rick-liruixin/body-scroll-lock-upgrade/HEAD/examples/next/app/favicon.ico -------------------------------------------------------------------------------- /examples/next/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {} 3 | 4 | module.exports = nextConfig 5 | -------------------------------------------------------------------------------- /examples/vue/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import './style.css' 3 | import App from './App.vue' 4 | 5 | createApp(App).mount('#app') 6 | -------------------------------------------------------------------------------- /examples/vue/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [vue()], 7 | }) 8 | -------------------------------------------------------------------------------- /examples/react/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /PUBLISH-ME.md: -------------------------------------------------------------------------------- 1 | # body-scroll-lock-upgrade发布 2 | 3 | 1,打包dist 4 | 5 | 2,更新版本号 6 | 7 | 3,示例代码中 cdn 包 和 readme.md中的示例 更新为新版本的地址 8 | 9 | 3,git 合并请求 打tag ,tag为新的版本号 10 | 11 | 4,更新 cdn 缓存 12 | 13 | 5,测试示例代码中的 cdn 链接是否可以访问 14 | 15 | 6,推送 npm 包 -------------------------------------------------------------------------------- /examples/react/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /examples/vue/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /examples/react/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App' 4 | import './index.css' 5 | 6 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "restricted", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist-ssr 12 | *.local 13 | 14 | # Editor directories and files 15 | .vscode/* 16 | !.vscode/extensions.json 17 | .idea 18 | .DS_Store 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /examples/vue/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /examples/react/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /examples/next/components/dialog.module.css: -------------------------------------------------------------------------------- 1 | .dialog { 2 | width: 70%; 3 | height: 70%; 4 | position: fixed; 5 | top: 15%; 6 | left: 15%; 7 | overflow-y: auto; 8 | z-index: 10; 9 | color: #000; 10 | background-color: #fff; 11 | } 12 | .mask { 13 | width: 100%; 14 | height: 100%; 15 | position: fixed; 16 | top: 0; 17 | left: 0; 18 | background-color: #0000005e; 19 | z-index: 9; 20 | } 21 | -------------------------------------------------------------------------------- /examples/vue/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + Vue + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/react/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/vue/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-project", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite --open --host", 8 | "build": "vue-tsc && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "vue": "^3.2.47" 13 | }, 14 | "devDependencies": { 15 | "@vitejs/plugin-vue": "^4.1.0", 16 | "typescript": "^4.9.3", 17 | "vite": "^4.2.0", 18 | "vue-tsc": "^1.2.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/vue/src/assets/vue.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/next/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@types/node": "20.3.1", 13 | "@types/react": "18.2.14", 14 | "@types/react-dom": "18.2.6", 15 | "next": "13.4.7", 16 | "react": "18.2.0", 17 | "react-dom": "18.2.0", 18 | "typescript": "5.1.3" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/next/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import './globals.css' 2 | import { Inter } from 'next/font/google' 3 | 4 | const inter = Inter({ subsets: ['latin'] }) 5 | 6 | export const metadata = { 7 | title: 'Create Next App', 8 | description: 'Generated by create next app', 9 | } 10 | 11 | export default function RootLayout({ 12 | children, 13 | }: { 14 | children: React.ReactNode 15 | }) { 16 | return ( 17 | 18 | {children} 19 | 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /examples/next/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | -------------------------------------------------------------------------------- /examples/react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-project", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite --open --host", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "react": "^18.2.0", 13 | "react-dom": "^18.2.0" 14 | }, 15 | "devDependencies": { 16 | "@types/react": "^18.0.28", 17 | "@types/react-dom": "^18.0.11", 18 | "@vitejs/plugin-react": "^3.1.0", 19 | "typescript": "^4.9.3", 20 | "vite": "^4.2.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/vue/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "moduleResolution": "Node", 7 | "strict": true, 8 | "jsx": "preserve", 9 | "resolveJsonModule": true, 10 | "isolatedModules": true, 11 | "esModuleInterop": true, 12 | "lib": ["ESNext", "DOM"], 13 | "skipLibCheck": true, 14 | "noEmit": true 15 | }, 16 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], 17 | "references": [{ "path": "./tsconfig.node.json" }] 18 | } 19 | -------------------------------------------------------------------------------- /examples/next/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "exclude": ["node_modules", "**/*.spec.ts", "vite.config.cjs"] 21 | } 22 | -------------------------------------------------------------------------------- /lib/index.d.ts: -------------------------------------------------------------------------------- 1 | export declare type BodyScrollOptions = { 2 | reserveScrollBarGap?: boolean | undefined; 3 | allowTouchMove?: ((el: EventTarget) => boolean) | undefined; 4 | }; 5 | 6 | export declare const clearAllBodyScrollLocks: () => void; 7 | 8 | /** 9 | * 10 | * @param targetElement HTMLElement 11 | * @param options BodyScrollOptions 12 | * @returns void 13 | */ 14 | export declare const disableBodyScroll: (targetElement: HTMLElement, options?: BodyScrollOptions) => void; 15 | 16 | /** 17 | * @param targetElement 18 | * @returns void 19 | */ 20 | export declare const enableBodyScroll: (targetElement: HTMLElement) => void; 21 | 22 | export { } 23 | -------------------------------------------------------------------------------- /examples/next/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "paths": { 23 | "@/*": ["./*"] 24 | } 25 | }, 26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 27 | "exclude": ["node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # body-scroll-lock-upgrade 2 | 3 | ## 1.1.0 4 | 5 | ### Minor Changes 6 | 7 | - optimization:document.documentElement.clientWidth -> document.documentElement.getBoundingClientRect().width 8 | https://github.com/rick-liruixin/body-scroll-lock-upgrade/issues/16 9 | 10 | ## 1.0.4 11 | 12 | ### Patch Changes 13 | 14 | - add umd 15 | 16 | ## 1.0.3 17 | 18 | ### Patch Changes 19 | 20 | - Added a vue3 code example 21 | 22 | ## 1.0.1 23 | 24 | ### Patch Changes 25 | 26 | - #### Bug 27 | 28 | Fix width issues on iOS Safari 29 | Fix Scrolling enabled in browser as soon as enableBodyScroll is called once, regardless of number of locks 30 | Fix When enabled, scrolls to top of document in iOS Safari on Version: 4.0.0-beta.0 31 | Fix safari browser scroll bottom Unable to scroll up 32 | 33 | #### New feature 34 | 35 | with a new version of typeScript 36 | Added a react hook code example 37 | -------------------------------------------------------------------------------- /vite.config.cjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import path from "path"; 3 | import dts from "vite-plugin-dts"; 4 | import banner from "vite-plugin-banner"; 5 | const { version, name, author } = require("./package.json"); 6 | 7 | const resolvePath = (str) => path.resolve(__dirname, str); 8 | const outDir = "lib"; 9 | 10 | export default defineConfig({ 11 | build: { 12 | outDir, 13 | minify: false, 14 | sourcemap: true, 15 | lib: { 16 | entry: resolvePath("./src/index.ts"), 17 | name: "bodyScrollLockUpgrade", 18 | formats: ["es", "cjs", "umd"], 19 | fileName: (format) => 20 | format === "es" 21 | ? `index.esm.js` 22 | : format === "umd" 23 | ? `index.umd.js` 24 | : `index.js`, 25 | }, 26 | }, 27 | plugins: [ 28 | banner({ 29 | outDir, 30 | content: `/**\n * name: ${name}\n * version: v${version}\n * author: ${author}\n */`, 31 | }), 32 | dts({ copyDtsFiles: false, rollupTypes: true }), 33 | ], 34 | }); 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Will Po 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 | -------------------------------------------------------------------------------- /examples/react/src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | transition: filter 300ms; 13 | } 14 | .logo:hover { 15 | filter: drop-shadow(0 0 2em #646cffaa); 16 | } 17 | .logo.react:hover { 18 | filter: drop-shadow(0 0 2em #61dafbaa); 19 | } 20 | 21 | @keyframes logo-spin { 22 | from { 23 | transform: rotate(0deg); 24 | } 25 | to { 26 | transform: rotate(360deg); 27 | } 28 | } 29 | 30 | @media (prefers-reduced-motion: no-preference) { 31 | a:nth-of-type(2) .logo { 32 | animation: logo-spin infinite 20s linear; 33 | } 34 | } 35 | 36 | .card { 37 | padding: 2em; 38 | } 39 | 40 | .read-the-docs { 41 | color: #888; 42 | } 43 | .dialog { 44 | width: 70%; 45 | height: 70%; 46 | position: fixed; 47 | top: 15%; 48 | left: 15%; 49 | overflow-y: auto; 50 | z-index: 10; 51 | color: #000; 52 | background-color: #fff; 53 | } 54 | .mask { 55 | width: 100%; 56 | height: 100%; 57 | position: fixed; 58 | top: 0; 59 | left: 0; 60 | background-color: #0000005e; 61 | z-index: 9; 62 | } 63 | 64 | .dialogTwo { 65 | z-index: 12; 66 | } 67 | .maskTwo { 68 | z-index: 11; 69 | } 70 | -------------------------------------------------------------------------------- /examples/next/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/react/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/vue/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/next/hooks/use-body-scroll-lock-upgrade.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from "react"; 2 | import { 3 | enableBodyScroll, 4 | disableBodyScroll, 5 | clearAllBodyScrollLocks, 6 | } from "./body-scroll-lock"; 7 | 8 | export function useBodyScrollLockUpgrade(isLock: boolean) { 9 | const scrollLockerUpgradeRef = useRef(null); 10 | 11 | useEffect(() => { 12 | if (!scrollLockerUpgradeRef.current) { 13 | clearAllBodyScrollLocks(); 14 | return; 15 | } 16 | if (isLock) { 17 | disableBodyScroll(scrollLockerUpgradeRef.current, { 18 | reserveScrollBarGap: false, 19 | allowTouchMove: (el: any) => { 20 | if (!el) return false; 21 | while (el && el !== document.body) { 22 | if (typeof el.className === "string") { 23 | // 弹框内部需要滚动时给该部分添上此类名,即可恢复滚动 24 | if (el.className.indexOf("body-scroll-lock-ignore") > -1) { 25 | return true; 26 | } 27 | } 28 | 29 | el = el.parentElement; 30 | } 31 | return false; 32 | }, 33 | }); 34 | console.log("disableBodyScroll"); 35 | } else { 36 | enableBodyScroll(scrollLockerUpgradeRef.current); 37 | } 38 | }, [isLock]); 39 | 40 | useEffect(() => { 41 | const ele = scrollLockerUpgradeRef.current; 42 | return () => { 43 | if (ele) { 44 | enableBodyScroll(ele); 45 | } 46 | }; 47 | }, []); 48 | 49 | return scrollLockerUpgradeRef; 50 | } 51 | -------------------------------------------------------------------------------- /examples/next/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | ``` 14 | 15 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 16 | 17 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 18 | 19 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 20 | 21 | ## Learn More 22 | 23 | To learn more about Next.js, take a look at the following resources: 24 | 25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 27 | 28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 29 | 30 | ## Deploy on Vercel 31 | 32 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 33 | 34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 35 | -------------------------------------------------------------------------------- /examples/react/src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | color-scheme: light dark; 7 | color: rgba(255, 255, 255, 0.87); 8 | background-color: #242424; 9 | 10 | font-synthesis: none; 11 | text-rendering: optimizeLegibility; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | -webkit-text-size-adjust: 100%; 15 | } 16 | 17 | a { 18 | font-weight: 500; 19 | color: #646cff; 20 | text-decoration: inherit; 21 | } 22 | a:hover { 23 | color: #535bf2; 24 | } 25 | 26 | body { 27 | margin: 0; 28 | display: flex; 29 | place-items: center; 30 | min-width: 320px; 31 | min-height: 100vh; 32 | } 33 | 34 | h1 { 35 | font-size: 3.2em; 36 | line-height: 1.1; 37 | } 38 | 39 | button { 40 | border-radius: 8px; 41 | border: 1px solid transparent; 42 | padding: 0.6em 1.2em; 43 | font-size: 1em; 44 | font-weight: 500; 45 | font-family: inherit; 46 | background-color: #1a1a1a; 47 | cursor: pointer; 48 | transition: border-color 0.25s; 49 | } 50 | button:hover { 51 | border-color: #646cff; 52 | } 53 | button:focus, 54 | button:focus-visible { 55 | outline: 4px auto -webkit-focus-ring-color; 56 | } 57 | 58 | @media (prefers-color-scheme: light) { 59 | :root { 60 | color: #213547; 61 | background-color: #ffffff; 62 | } 63 | a:hover { 64 | color: #747bff; 65 | } 66 | button { 67 | background-color: #f9f9f9; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /examples/vue/README.md: -------------------------------------------------------------------------------- 1 | # Vue 3 + TypeScript + Vite 2 | 3 | This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 ` 4 | 5 | 150 | 151 | 165 | -------------------------------------------------------------------------------- /examples/next/components/dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import styles from "./dialog.module.css"; 3 | import { useBodyScrollLockUpgrade } from "@/hooks/use-body-scroll-lock-upgrade"; 4 | 5 | export function Dialog({ 6 | isVisible, 7 | onClose, 8 | }: { 9 | isVisible: boolean; 10 | onClose: () => void; 11 | }) { 12 | const bodyScrollLockUpgradeRef = useBodyScrollLockUpgrade(isVisible); 13 | return ( 14 |
19 |
20 |
21 | 28 |
29 |
30 |

31 | Click on the Vite and React logos to learn more 32 |
33 | Click on the Vite and React logos to learn more 34 |
35 | Click on the Vite and React logos to learn more 36 |
37 | Click on the Vite and React logos to learn more 38 |
39 | Click on the Vite and React logos to learn more 40 |
41 | Click on the Vite and React logos to learn more 42 |
43 | Click on the Vite and React logos to learn more 44 |
45 | Click on the Vite and React logos to learn more 46 |
47 | Click on the Vite and React logos to learn more 48 |
49 | Click on the Vite and React logos to learn more 50 |
51 | Click on the Vite and React logos to learn more 52 |
53 | Click on the Vite and React logos to learn more 54 |
55 | Click on the Vite and React logos to learn more 56 |
57 | Click on the Vite and React logos to learn more 58 |
59 | Click on the Vite and React logos to learn more 60 |
61 | Click on the Vite and React logos to learn more 62 |
63 | Click on the Vite and React logos to learn more 64 |
65 | Click on the Vite and React logos to learn more 66 |
67 | Click on the Vite and React logos to learn more 68 |
69 | Click on the Vite and React logos to learn more 70 |
71 | Click on the Vite and React logos to learn more 72 |
73 | Click on the Vite and React logos to learn more 74 |
75 | Click on the Vite and React logos to learn more 76 |
77 | Click on the Vite and React logos to learn more 78 |
79 | Click on the Vite and React logos to learn more 80 |
81 | Click on the Vite and React logos to learn more 82 |
83 | Click on the Vite and React logos to learn more 84 |
85 | Click on the Vite and React logos to learn more 86 |
87 | Click on the Vite and React logos to learn more 88 |
89 | Click on the Vite and React logos to learn more 90 |
91 | Click on the Vite and React logos to learn more 92 |
93 | Click on the Vite and React logos to learn more 94 |
95 | Click on the Vite and React logos to learn more 96 |
97 | Click on the Vite and React logos to learn more 98 |
99 | Click on the Vite and React logos to learn more 100 |
101 | Click on the Vite and React logos to learn more 102 |
103 | Click on the Vite and React logos to learn more 104 |
105 | Click on the Vite and React logos to learn more 106 |
107 | Click on the Vite and React logos to learn more 108 |
109 | Click on the Vite and React logos to learn more 110 |
111 | Click on the Vite and React logos to learn more 112 |
113 | Click on the Vite and React logos to learn more 114 |
115 | Click on the Vite and React logos to learn more 116 |
117 | Click on the Vite and React logos to learn more 118 |
119 | Click on the Vite and React logos to learn more 120 |
121 | Click on the Vite and React logos to learn more 122 |
123 | Click on the Vite and React logos to learn more 124 |
125 | Click on the Vite and React logos to learn more 126 |
127 | Click on the Vite and React logos to learn more 128 |
129 | Click on the Vite and React logos to learn more 130 |
131 | Click on the Vite and React logos to learn more 132 |
133 | Click on the Vite and React logos to learn more 134 |
135 | Click on the Vite and React logos to learn more 136 |
137 | Click on the Vite and React logos to learn more 138 |
139 | Click on the Vite and React logos to learn more 140 |
141 | Click on the Vite and React logos to learn more 142 |
143 | Click on the Vite and React logos to learn more 144 |
145 | Click on the Vite and React logos to learn more 146 |
147 | Click on the Vite and React logos to learn more 148 |
149 | Click on the Vite and React logos to learn more 150 |
151 | Click on the Vite and React logos to learn more 152 |
153 | Click on the Vite and React logos to learn more 154 |
155 | Click on the Vite and React logos to learn more 156 |
157 | Click on the Vite and React logos to learn more 158 |
159 | Click on the Vite and React logos to learn more 160 |
161 | Click on the Vite and React logos to learn more 162 |
163 |

164 |
165 |
{ 168 | onClose(); 169 | }} 170 | >
171 |
172 | ); 173 | } 174 | -------------------------------------------------------------------------------- /examples/vue/src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 205 | 206 | 232 | -------------------------------------------------------------------------------- /lib/index.esm.js: -------------------------------------------------------------------------------- 1 | /** 2 | * name: body-scroll-lock-upgrade 3 | * version: v1.1.0 4 | * author: Rick.li 5 | */ 6 | let hasPassiveEvents = false; 7 | if (typeof window !== "undefined") { 8 | const passiveTestOptions = { 9 | get passive() { 10 | hasPassiveEvents = true; 11 | return void 0; 12 | } 13 | }; 14 | window.addEventListener("testPassive", null, passiveTestOptions); 15 | window.removeEventListener("testPassive", null, passiveTestOptions); 16 | } 17 | const isIosDevice = typeof window !== "undefined" && window.navigator && window.navigator.platform && (/iP(ad|hone|od)/.test(window.navigator.platform) || window.navigator.platform === "MacIntel" && window.navigator.maxTouchPoints > 1); 18 | let locks = []; 19 | let locksIndex = /* @__PURE__ */ new Map(); 20 | let documentListenerAdded = false; 21 | let initialClientY = -1; 22 | let previousBodyOverflowSetting; 23 | let htmlStyle; 24 | let bodyStyle; 25 | let previousBodyPaddingRight; 26 | const allowTouchMove = (el) => locks.some((lock) => { 27 | if (lock.options.allowTouchMove && lock.options.allowTouchMove(el)) { 28 | return true; 29 | } 30 | return false; 31 | }); 32 | const preventDefault = (rawEvent) => { 33 | const e = rawEvent || window.event; 34 | if (allowTouchMove(e.target)) { 35 | return true; 36 | } 37 | if (e.touches.length > 1) 38 | return true; 39 | if (e.preventDefault) 40 | e.preventDefault(); 41 | return false; 42 | }; 43 | const setOverflowHidden = (options) => { 44 | if (previousBodyPaddingRight === void 0) { 45 | const reserveScrollBarGap = !!options && options.reserveScrollBarGap === true; 46 | const scrollBarGap = window.innerWidth - document.documentElement.getBoundingClientRect().width; 47 | if (reserveScrollBarGap && scrollBarGap > 0) { 48 | const computedBodyPaddingRight = parseInt( 49 | window.getComputedStyle(document.body).getPropertyValue("padding-right"), 50 | 10 51 | ); 52 | previousBodyPaddingRight = document.body.style.paddingRight; 53 | document.body.style.paddingRight = `${computedBodyPaddingRight + scrollBarGap}px`; 54 | } 55 | } 56 | if (previousBodyOverflowSetting === void 0) { 57 | previousBodyOverflowSetting = document.body.style.overflow; 58 | document.body.style.overflow = "hidden"; 59 | } 60 | }; 61 | const restoreOverflowSetting = () => { 62 | if (previousBodyPaddingRight !== void 0) { 63 | document.body.style.paddingRight = previousBodyPaddingRight; 64 | previousBodyPaddingRight = void 0; 65 | } 66 | if (previousBodyOverflowSetting !== void 0) { 67 | document.body.style.overflow = previousBodyOverflowSetting; 68 | previousBodyOverflowSetting = void 0; 69 | } 70 | }; 71 | const setPositionFixed = () => window.requestAnimationFrame(() => { 72 | const $html = document.documentElement; 73 | const $body = document.body; 74 | if (bodyStyle === void 0) { 75 | htmlStyle = { ...$html.style }; 76 | bodyStyle = { ...$body.style }; 77 | const { scrollY, scrollX, innerHeight } = window; 78 | $html.style.height = "100%"; 79 | $html.style.overflow = "hidden"; 80 | $body.style.position = "fixed"; 81 | $body.style.top = `${-scrollY}px`; 82 | $body.style.left = `${-scrollX}px`; 83 | $body.style.width = "100%"; 84 | $body.style.height = "auto"; 85 | $body.style.overflow = "hidden"; 86 | setTimeout( 87 | () => window.requestAnimationFrame(() => { 88 | const bottomBarHeight = innerHeight - window.innerHeight; 89 | if (bottomBarHeight && scrollY >= innerHeight) { 90 | $body.style.top = -(scrollY + bottomBarHeight) + "px"; 91 | } 92 | }), 93 | 300 94 | ); 95 | } 96 | }); 97 | const restorePositionSetting = () => { 98 | if (bodyStyle !== void 0) { 99 | const y = -parseInt(document.body.style.top, 10); 100 | const x = -parseInt(document.body.style.left, 10); 101 | const $html = document.documentElement; 102 | const $body = document.body; 103 | $html.style.height = (htmlStyle == null ? void 0 : htmlStyle.height) || ""; 104 | $html.style.overflow = (htmlStyle == null ? void 0 : htmlStyle.overflow) || ""; 105 | $body.style.position = bodyStyle.position || ""; 106 | $body.style.top = bodyStyle.top || ""; 107 | $body.style.left = bodyStyle.left || ""; 108 | $body.style.width = bodyStyle.width || ""; 109 | $body.style.height = bodyStyle.height || ""; 110 | $body.style.overflow = bodyStyle.overflow || ""; 111 | window.scrollTo(x, y); 112 | bodyStyle = void 0; 113 | } 114 | }; 115 | const isTargetElementTotallyScrolled = (targetElement) => targetElement ? targetElement.scrollHeight - targetElement.scrollTop <= targetElement.clientHeight : false; 116 | const handleScroll = (event, targetElement) => { 117 | const clientY = event.targetTouches[0].clientY - initialClientY; 118 | if (allowTouchMove(event.target)) { 119 | return false; 120 | } 121 | if (targetElement && targetElement.scrollTop === 0 && clientY > 0) { 122 | return preventDefault(event); 123 | } 124 | if (isTargetElementTotallyScrolled(targetElement) && clientY < 0) { 125 | return preventDefault(event); 126 | } 127 | event.stopPropagation(); 128 | return true; 129 | }; 130 | const disableBodyScroll = (targetElement, options) => { 131 | if (!targetElement) { 132 | console.error( 133 | "disableBodyScroll unsuccessful - targetElement must be provided when calling disableBodyScroll on IOS devices." 134 | ); 135 | return; 136 | } 137 | locksIndex.set( 138 | targetElement, 139 | (locksIndex == null ? void 0 : locksIndex.get(targetElement)) ? (locksIndex == null ? void 0 : locksIndex.get(targetElement)) + 1 : 1 140 | ); 141 | if (locks.some((lock2) => lock2.targetElement === targetElement)) { 142 | return; 143 | } 144 | const lock = { 145 | targetElement, 146 | options: options || {} 147 | }; 148 | locks = [...locks, lock]; 149 | if (isIosDevice) { 150 | setPositionFixed(); 151 | } else { 152 | setOverflowHidden(options); 153 | } 154 | if (isIosDevice) { 155 | targetElement.ontouchstart = (event) => { 156 | if (event.targetTouches.length === 1) { 157 | initialClientY = event.targetTouches[0].clientY; 158 | } 159 | }; 160 | targetElement.ontouchmove = (event) => { 161 | if (event.targetTouches.length === 1) { 162 | handleScroll(event, targetElement); 163 | } 164 | }; 165 | if (!documentListenerAdded) { 166 | document.addEventListener( 167 | "touchmove", 168 | preventDefault, 169 | hasPassiveEvents ? { passive: false } : void 0 170 | ); 171 | documentListenerAdded = true; 172 | } 173 | } 174 | }; 175 | const clearAllBodyScrollLocks = () => { 176 | if (isIosDevice) { 177 | locks.forEach((lock) => { 178 | lock.targetElement.ontouchstart = null; 179 | lock.targetElement.ontouchmove = null; 180 | }); 181 | if (documentListenerAdded) { 182 | document.removeEventListener( 183 | "touchmove", 184 | preventDefault, 185 | hasPassiveEvents ? { passive: false } : void 0 186 | ); 187 | documentListenerAdded = false; 188 | } 189 | initialClientY = -1; 190 | } 191 | if (isIosDevice) { 192 | restorePositionSetting(); 193 | } else { 194 | restoreOverflowSetting(); 195 | } 196 | locks = []; 197 | locksIndex.clear(); 198 | }; 199 | const enableBodyScroll = (targetElement) => { 200 | if (!targetElement) { 201 | console.error( 202 | "enableBodyScroll unsuccessful - targetElement must be provided when calling enableBodyScroll on IOS devices." 203 | ); 204 | return; 205 | } 206 | locksIndex.set( 207 | targetElement, 208 | (locksIndex == null ? void 0 : locksIndex.get(targetElement)) ? (locksIndex == null ? void 0 : locksIndex.get(targetElement)) - 1 : 0 209 | ); 210 | if ((locksIndex == null ? void 0 : locksIndex.get(targetElement)) === 0) { 211 | locks = locks.filter((lock) => lock.targetElement !== targetElement); 212 | locksIndex == null ? void 0 : locksIndex.delete(targetElement); 213 | } 214 | if (isIosDevice) { 215 | targetElement.ontouchstart = null; 216 | targetElement.ontouchmove = null; 217 | if (documentListenerAdded && locks.length === 0) { 218 | document.removeEventListener( 219 | "touchmove", 220 | preventDefault, 221 | hasPassiveEvents ? { passive: false } : void 0 222 | ); 223 | documentListenerAdded = false; 224 | } 225 | } 226 | if (locks.length === 0) { 227 | if (isIosDevice) { 228 | restorePositionSetting(); 229 | } else { 230 | restoreOverflowSetting(); 231 | } 232 | } 233 | }; 234 | export { 235 | clearAllBodyScrollLocks, 236 | disableBodyScroll, 237 | enableBodyScroll 238 | }; 239 | //# sourceMappingURL=index.esm.js.map 240 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * name: body-scroll-lock-upgrade 3 | * version: v1.1.0 4 | * author: Rick.li 5 | */ 6 | "use strict"; 7 | Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); 8 | let hasPassiveEvents = false; 9 | if (typeof window !== "undefined") { 10 | const passiveTestOptions = { 11 | get passive() { 12 | hasPassiveEvents = true; 13 | return void 0; 14 | } 15 | }; 16 | window.addEventListener("testPassive", null, passiveTestOptions); 17 | window.removeEventListener("testPassive", null, passiveTestOptions); 18 | } 19 | const isIosDevice = typeof window !== "undefined" && window.navigator && window.navigator.platform && (/iP(ad|hone|od)/.test(window.navigator.platform) || window.navigator.platform === "MacIntel" && window.navigator.maxTouchPoints > 1); 20 | let locks = []; 21 | let locksIndex = /* @__PURE__ */ new Map(); 22 | let documentListenerAdded = false; 23 | let initialClientY = -1; 24 | let previousBodyOverflowSetting; 25 | let htmlStyle; 26 | let bodyStyle; 27 | let previousBodyPaddingRight; 28 | const allowTouchMove = (el) => locks.some((lock) => { 29 | if (lock.options.allowTouchMove && lock.options.allowTouchMove(el)) { 30 | return true; 31 | } 32 | return false; 33 | }); 34 | const preventDefault = (rawEvent) => { 35 | const e = rawEvent || window.event; 36 | if (allowTouchMove(e.target)) { 37 | return true; 38 | } 39 | if (e.touches.length > 1) 40 | return true; 41 | if (e.preventDefault) 42 | e.preventDefault(); 43 | return false; 44 | }; 45 | const setOverflowHidden = (options) => { 46 | if (previousBodyPaddingRight === void 0) { 47 | const reserveScrollBarGap = !!options && options.reserveScrollBarGap === true; 48 | const scrollBarGap = window.innerWidth - document.documentElement.getBoundingClientRect().width; 49 | if (reserveScrollBarGap && scrollBarGap > 0) { 50 | const computedBodyPaddingRight = parseInt( 51 | window.getComputedStyle(document.body).getPropertyValue("padding-right"), 52 | 10 53 | ); 54 | previousBodyPaddingRight = document.body.style.paddingRight; 55 | document.body.style.paddingRight = `${computedBodyPaddingRight + scrollBarGap}px`; 56 | } 57 | } 58 | if (previousBodyOverflowSetting === void 0) { 59 | previousBodyOverflowSetting = document.body.style.overflow; 60 | document.body.style.overflow = "hidden"; 61 | } 62 | }; 63 | const restoreOverflowSetting = () => { 64 | if (previousBodyPaddingRight !== void 0) { 65 | document.body.style.paddingRight = previousBodyPaddingRight; 66 | previousBodyPaddingRight = void 0; 67 | } 68 | if (previousBodyOverflowSetting !== void 0) { 69 | document.body.style.overflow = previousBodyOverflowSetting; 70 | previousBodyOverflowSetting = void 0; 71 | } 72 | }; 73 | const setPositionFixed = () => window.requestAnimationFrame(() => { 74 | const $html = document.documentElement; 75 | const $body = document.body; 76 | if (bodyStyle === void 0) { 77 | htmlStyle = { ...$html.style }; 78 | bodyStyle = { ...$body.style }; 79 | const { scrollY, scrollX, innerHeight } = window; 80 | $html.style.height = "100%"; 81 | $html.style.overflow = "hidden"; 82 | $body.style.position = "fixed"; 83 | $body.style.top = `${-scrollY}px`; 84 | $body.style.left = `${-scrollX}px`; 85 | $body.style.width = "100%"; 86 | $body.style.height = "auto"; 87 | $body.style.overflow = "hidden"; 88 | setTimeout( 89 | () => window.requestAnimationFrame(() => { 90 | const bottomBarHeight = innerHeight - window.innerHeight; 91 | if (bottomBarHeight && scrollY >= innerHeight) { 92 | $body.style.top = -(scrollY + bottomBarHeight) + "px"; 93 | } 94 | }), 95 | 300 96 | ); 97 | } 98 | }); 99 | const restorePositionSetting = () => { 100 | if (bodyStyle !== void 0) { 101 | const y = -parseInt(document.body.style.top, 10); 102 | const x = -parseInt(document.body.style.left, 10); 103 | const $html = document.documentElement; 104 | const $body = document.body; 105 | $html.style.height = (htmlStyle == null ? void 0 : htmlStyle.height) || ""; 106 | $html.style.overflow = (htmlStyle == null ? void 0 : htmlStyle.overflow) || ""; 107 | $body.style.position = bodyStyle.position || ""; 108 | $body.style.top = bodyStyle.top || ""; 109 | $body.style.left = bodyStyle.left || ""; 110 | $body.style.width = bodyStyle.width || ""; 111 | $body.style.height = bodyStyle.height || ""; 112 | $body.style.overflow = bodyStyle.overflow || ""; 113 | window.scrollTo(x, y); 114 | bodyStyle = void 0; 115 | } 116 | }; 117 | const isTargetElementTotallyScrolled = (targetElement) => targetElement ? targetElement.scrollHeight - targetElement.scrollTop <= targetElement.clientHeight : false; 118 | const handleScroll = (event, targetElement) => { 119 | const clientY = event.targetTouches[0].clientY - initialClientY; 120 | if (allowTouchMove(event.target)) { 121 | return false; 122 | } 123 | if (targetElement && targetElement.scrollTop === 0 && clientY > 0) { 124 | return preventDefault(event); 125 | } 126 | if (isTargetElementTotallyScrolled(targetElement) && clientY < 0) { 127 | return preventDefault(event); 128 | } 129 | event.stopPropagation(); 130 | return true; 131 | }; 132 | const disableBodyScroll = (targetElement, options) => { 133 | if (!targetElement) { 134 | console.error( 135 | "disableBodyScroll unsuccessful - targetElement must be provided when calling disableBodyScroll on IOS devices." 136 | ); 137 | return; 138 | } 139 | locksIndex.set( 140 | targetElement, 141 | (locksIndex == null ? void 0 : locksIndex.get(targetElement)) ? (locksIndex == null ? void 0 : locksIndex.get(targetElement)) + 1 : 1 142 | ); 143 | if (locks.some((lock2) => lock2.targetElement === targetElement)) { 144 | return; 145 | } 146 | const lock = { 147 | targetElement, 148 | options: options || {} 149 | }; 150 | locks = [...locks, lock]; 151 | if (isIosDevice) { 152 | setPositionFixed(); 153 | } else { 154 | setOverflowHidden(options); 155 | } 156 | if (isIosDevice) { 157 | targetElement.ontouchstart = (event) => { 158 | if (event.targetTouches.length === 1) { 159 | initialClientY = event.targetTouches[0].clientY; 160 | } 161 | }; 162 | targetElement.ontouchmove = (event) => { 163 | if (event.targetTouches.length === 1) { 164 | handleScroll(event, targetElement); 165 | } 166 | }; 167 | if (!documentListenerAdded) { 168 | document.addEventListener( 169 | "touchmove", 170 | preventDefault, 171 | hasPassiveEvents ? { passive: false } : void 0 172 | ); 173 | documentListenerAdded = true; 174 | } 175 | } 176 | }; 177 | const clearAllBodyScrollLocks = () => { 178 | if (isIosDevice) { 179 | locks.forEach((lock) => { 180 | lock.targetElement.ontouchstart = null; 181 | lock.targetElement.ontouchmove = null; 182 | }); 183 | if (documentListenerAdded) { 184 | document.removeEventListener( 185 | "touchmove", 186 | preventDefault, 187 | hasPassiveEvents ? { passive: false } : void 0 188 | ); 189 | documentListenerAdded = false; 190 | } 191 | initialClientY = -1; 192 | } 193 | if (isIosDevice) { 194 | restorePositionSetting(); 195 | } else { 196 | restoreOverflowSetting(); 197 | } 198 | locks = []; 199 | locksIndex.clear(); 200 | }; 201 | const enableBodyScroll = (targetElement) => { 202 | if (!targetElement) { 203 | console.error( 204 | "enableBodyScroll unsuccessful - targetElement must be provided when calling enableBodyScroll on IOS devices." 205 | ); 206 | return; 207 | } 208 | locksIndex.set( 209 | targetElement, 210 | (locksIndex == null ? void 0 : locksIndex.get(targetElement)) ? (locksIndex == null ? void 0 : locksIndex.get(targetElement)) - 1 : 0 211 | ); 212 | if ((locksIndex == null ? void 0 : locksIndex.get(targetElement)) === 0) { 213 | locks = locks.filter((lock) => lock.targetElement !== targetElement); 214 | locksIndex == null ? void 0 : locksIndex.delete(targetElement); 215 | } 216 | if (isIosDevice) { 217 | targetElement.ontouchstart = null; 218 | targetElement.ontouchmove = null; 219 | if (documentListenerAdded && locks.length === 0) { 220 | document.removeEventListener( 221 | "touchmove", 222 | preventDefault, 223 | hasPassiveEvents ? { passive: false } : void 0 224 | ); 225 | documentListenerAdded = false; 226 | } 227 | } 228 | if (locks.length === 0) { 229 | if (isIosDevice) { 230 | restorePositionSetting(); 231 | } else { 232 | restoreOverflowSetting(); 233 | } 234 | } 235 | }; 236 | exports.clearAllBodyScrollLocks = clearAllBodyScrollLocks; 237 | exports.disableBodyScroll = disableBodyScroll; 238 | exports.enableBodyScroll = enableBodyScroll; 239 | //# sourceMappingURL=index.js.map 240 | -------------------------------------------------------------------------------- /lib/index.umd.js: -------------------------------------------------------------------------------- 1 | /** 2 | * name: body-scroll-lock-upgrade 3 | * version: v1.1.0 4 | * author: Rick.li 5 | */ 6 | (function(global, factory) { 7 | typeof exports === "object" && typeof module !== "undefined" ? factory(exports) : typeof define === "function" && define.amd ? define(["exports"], factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self, factory(global.bodyScrollLockUpgrade = {})); 8 | })(this, function(exports2) { 9 | "use strict"; 10 | let hasPassiveEvents = false; 11 | if (typeof window !== "undefined") { 12 | const passiveTestOptions = { 13 | get passive() { 14 | hasPassiveEvents = true; 15 | return void 0; 16 | } 17 | }; 18 | window.addEventListener("testPassive", null, passiveTestOptions); 19 | window.removeEventListener("testPassive", null, passiveTestOptions); 20 | } 21 | const isIosDevice = typeof window !== "undefined" && window.navigator && window.navigator.platform && (/iP(ad|hone|od)/.test(window.navigator.platform) || window.navigator.platform === "MacIntel" && window.navigator.maxTouchPoints > 1); 22 | let locks = []; 23 | let locksIndex = /* @__PURE__ */ new Map(); 24 | let documentListenerAdded = false; 25 | let initialClientY = -1; 26 | let previousBodyOverflowSetting; 27 | let htmlStyle; 28 | let bodyStyle; 29 | let previousBodyPaddingRight; 30 | const allowTouchMove = (el) => locks.some((lock) => { 31 | if (lock.options.allowTouchMove && lock.options.allowTouchMove(el)) { 32 | return true; 33 | } 34 | return false; 35 | }); 36 | const preventDefault = (rawEvent) => { 37 | const e = rawEvent || window.event; 38 | if (allowTouchMove(e.target)) { 39 | return true; 40 | } 41 | if (e.touches.length > 1) 42 | return true; 43 | if (e.preventDefault) 44 | e.preventDefault(); 45 | return false; 46 | }; 47 | const setOverflowHidden = (options) => { 48 | if (previousBodyPaddingRight === void 0) { 49 | const reserveScrollBarGap = !!options && options.reserveScrollBarGap === true; 50 | const scrollBarGap = window.innerWidth - document.documentElement.getBoundingClientRect().width; 51 | if (reserveScrollBarGap && scrollBarGap > 0) { 52 | const computedBodyPaddingRight = parseInt( 53 | window.getComputedStyle(document.body).getPropertyValue("padding-right"), 54 | 10 55 | ); 56 | previousBodyPaddingRight = document.body.style.paddingRight; 57 | document.body.style.paddingRight = `${computedBodyPaddingRight + scrollBarGap}px`; 58 | } 59 | } 60 | if (previousBodyOverflowSetting === void 0) { 61 | previousBodyOverflowSetting = document.body.style.overflow; 62 | document.body.style.overflow = "hidden"; 63 | } 64 | }; 65 | const restoreOverflowSetting = () => { 66 | if (previousBodyPaddingRight !== void 0) { 67 | document.body.style.paddingRight = previousBodyPaddingRight; 68 | previousBodyPaddingRight = void 0; 69 | } 70 | if (previousBodyOverflowSetting !== void 0) { 71 | document.body.style.overflow = previousBodyOverflowSetting; 72 | previousBodyOverflowSetting = void 0; 73 | } 74 | }; 75 | const setPositionFixed = () => window.requestAnimationFrame(() => { 76 | const $html = document.documentElement; 77 | const $body = document.body; 78 | if (bodyStyle === void 0) { 79 | htmlStyle = { ...$html.style }; 80 | bodyStyle = { ...$body.style }; 81 | const { scrollY, scrollX, innerHeight } = window; 82 | $html.style.height = "100%"; 83 | $html.style.overflow = "hidden"; 84 | $body.style.position = "fixed"; 85 | $body.style.top = `${-scrollY}px`; 86 | $body.style.left = `${-scrollX}px`; 87 | $body.style.width = "100%"; 88 | $body.style.height = "auto"; 89 | $body.style.overflow = "hidden"; 90 | setTimeout( 91 | () => window.requestAnimationFrame(() => { 92 | const bottomBarHeight = innerHeight - window.innerHeight; 93 | if (bottomBarHeight && scrollY >= innerHeight) { 94 | $body.style.top = -(scrollY + bottomBarHeight) + "px"; 95 | } 96 | }), 97 | 300 98 | ); 99 | } 100 | }); 101 | const restorePositionSetting = () => { 102 | if (bodyStyle !== void 0) { 103 | const y = -parseInt(document.body.style.top, 10); 104 | const x = -parseInt(document.body.style.left, 10); 105 | const $html = document.documentElement; 106 | const $body = document.body; 107 | $html.style.height = (htmlStyle == null ? void 0 : htmlStyle.height) || ""; 108 | $html.style.overflow = (htmlStyle == null ? void 0 : htmlStyle.overflow) || ""; 109 | $body.style.position = bodyStyle.position || ""; 110 | $body.style.top = bodyStyle.top || ""; 111 | $body.style.left = bodyStyle.left || ""; 112 | $body.style.width = bodyStyle.width || ""; 113 | $body.style.height = bodyStyle.height || ""; 114 | $body.style.overflow = bodyStyle.overflow || ""; 115 | window.scrollTo(x, y); 116 | bodyStyle = void 0; 117 | } 118 | }; 119 | const isTargetElementTotallyScrolled = (targetElement) => targetElement ? targetElement.scrollHeight - targetElement.scrollTop <= targetElement.clientHeight : false; 120 | const handleScroll = (event, targetElement) => { 121 | const clientY = event.targetTouches[0].clientY - initialClientY; 122 | if (allowTouchMove(event.target)) { 123 | return false; 124 | } 125 | if (targetElement && targetElement.scrollTop === 0 && clientY > 0) { 126 | return preventDefault(event); 127 | } 128 | if (isTargetElementTotallyScrolled(targetElement) && clientY < 0) { 129 | return preventDefault(event); 130 | } 131 | event.stopPropagation(); 132 | return true; 133 | }; 134 | const disableBodyScroll = (targetElement, options) => { 135 | if (!targetElement) { 136 | console.error( 137 | "disableBodyScroll unsuccessful - targetElement must be provided when calling disableBodyScroll on IOS devices." 138 | ); 139 | return; 140 | } 141 | locksIndex.set( 142 | targetElement, 143 | (locksIndex == null ? void 0 : locksIndex.get(targetElement)) ? (locksIndex == null ? void 0 : locksIndex.get(targetElement)) + 1 : 1 144 | ); 145 | if (locks.some((lock2) => lock2.targetElement === targetElement)) { 146 | return; 147 | } 148 | const lock = { 149 | targetElement, 150 | options: options || {} 151 | }; 152 | locks = [...locks, lock]; 153 | if (isIosDevice) { 154 | setPositionFixed(); 155 | } else { 156 | setOverflowHidden(options); 157 | } 158 | if (isIosDevice) { 159 | targetElement.ontouchstart = (event) => { 160 | if (event.targetTouches.length === 1) { 161 | initialClientY = event.targetTouches[0].clientY; 162 | } 163 | }; 164 | targetElement.ontouchmove = (event) => { 165 | if (event.targetTouches.length === 1) { 166 | handleScroll(event, targetElement); 167 | } 168 | }; 169 | if (!documentListenerAdded) { 170 | document.addEventListener( 171 | "touchmove", 172 | preventDefault, 173 | hasPassiveEvents ? { passive: false } : void 0 174 | ); 175 | documentListenerAdded = true; 176 | } 177 | } 178 | }; 179 | const clearAllBodyScrollLocks = () => { 180 | if (isIosDevice) { 181 | locks.forEach((lock) => { 182 | lock.targetElement.ontouchstart = null; 183 | lock.targetElement.ontouchmove = null; 184 | }); 185 | if (documentListenerAdded) { 186 | document.removeEventListener( 187 | "touchmove", 188 | preventDefault, 189 | hasPassiveEvents ? { passive: false } : void 0 190 | ); 191 | documentListenerAdded = false; 192 | } 193 | initialClientY = -1; 194 | } 195 | if (isIosDevice) { 196 | restorePositionSetting(); 197 | } else { 198 | restoreOverflowSetting(); 199 | } 200 | locks = []; 201 | locksIndex.clear(); 202 | }; 203 | const enableBodyScroll = (targetElement) => { 204 | if (!targetElement) { 205 | console.error( 206 | "enableBodyScroll unsuccessful - targetElement must be provided when calling enableBodyScroll on IOS devices." 207 | ); 208 | return; 209 | } 210 | locksIndex.set( 211 | targetElement, 212 | (locksIndex == null ? void 0 : locksIndex.get(targetElement)) ? (locksIndex == null ? void 0 : locksIndex.get(targetElement)) - 1 : 0 213 | ); 214 | if ((locksIndex == null ? void 0 : locksIndex.get(targetElement)) === 0) { 215 | locks = locks.filter((lock) => lock.targetElement !== targetElement); 216 | locksIndex == null ? void 0 : locksIndex.delete(targetElement); 217 | } 218 | if (isIosDevice) { 219 | targetElement.ontouchstart = null; 220 | targetElement.ontouchmove = null; 221 | if (documentListenerAdded && locks.length === 0) { 222 | document.removeEventListener( 223 | "touchmove", 224 | preventDefault, 225 | hasPassiveEvents ? { passive: false } : void 0 226 | ); 227 | documentListenerAdded = false; 228 | } 229 | } 230 | if (locks.length === 0) { 231 | if (isIosDevice) { 232 | restorePositionSetting(); 233 | } else { 234 | restoreOverflowSetting(); 235 | } 236 | } 237 | }; 238 | exports2.clearAllBodyScrollLocks = clearAllBodyScrollLocks; 239 | exports2.disableBodyScroll = disableBodyScroll; 240 | exports2.enableBodyScroll = enableBodyScroll; 241 | Object.defineProperty(exports2, Symbol.toStringTag, { value: "Module" }); 242 | }); 243 | //# sourceMappingURL=index.umd.js.map 244 | -------------------------------------------------------------------------------- /examples/next/pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '6.0' 2 | 3 | dependencies: 4 | '@types/node': 5 | specifier: 20.3.1 6 | version: 20.3.1 7 | '@types/react': 8 | specifier: 18.2.14 9 | version: 18.2.14 10 | '@types/react-dom': 11 | specifier: 18.2.6 12 | version: 18.2.6 13 | next: 14 | specifier: 13.4.7 15 | version: 13.4.7(react-dom@18.2.0)(react@18.2.0) 16 | react: 17 | specifier: 18.2.0 18 | version: 18.2.0 19 | react-dom: 20 | specifier: 18.2.0 21 | version: 18.2.0(react@18.2.0) 22 | typescript: 23 | specifier: 5.1.3 24 | version: 5.1.3 25 | 26 | packages: 27 | 28 | /@next/env@13.4.7: 29 | resolution: {integrity: sha512-ZlbiFulnwiFsW9UV1ku1OvX/oyIPLtMk9p/nnvDSwI0s7vSoZdRtxXNsaO+ZXrLv/pMbXVGq4lL8TbY9iuGmVw==} 30 | dev: false 31 | 32 | /@next/swc-darwin-arm64@13.4.7: 33 | resolution: {integrity: sha512-VZTxPv1b59KGiv/pZHTO5Gbsdeoxcj2rU2cqJu03btMhHpn3vwzEK0gUSVC/XW96aeGO67X+cMahhwHzef24/w==} 34 | engines: {node: '>= 10'} 35 | cpu: [arm64] 36 | os: [darwin] 37 | requiresBuild: true 38 | dev: false 39 | optional: true 40 | 41 | /@next/swc-darwin-x64@13.4.7: 42 | resolution: {integrity: sha512-gO2bw+2Ymmga+QYujjvDz9955xvYGrWofmxTq7m70b9pDPvl7aDFABJOZ2a8SRCuSNB5mXU8eTOmVVwyp/nAew==} 43 | engines: {node: '>= 10'} 44 | cpu: [x64] 45 | os: [darwin] 46 | requiresBuild: true 47 | dev: false 48 | optional: true 49 | 50 | /@next/swc-linux-arm64-gnu@13.4.7: 51 | resolution: {integrity: sha512-6cqp3vf1eHxjIDhEOc7Mh/s8z1cwc/l5B6ZNkOofmZVyu1zsbEM5Hmx64s12Rd9AYgGoiCz4OJ4M/oRnkE16/Q==} 52 | engines: {node: '>= 10'} 53 | cpu: [arm64] 54 | os: [linux] 55 | requiresBuild: true 56 | dev: false 57 | optional: true 58 | 59 | /@next/swc-linux-arm64-musl@13.4.7: 60 | resolution: {integrity: sha512-T1kD2FWOEy5WPidOn1si0rYmWORNch4a/NR52Ghyp4q7KyxOCuiOfZzyhVC5tsLIBDH3+cNdB5DkD9afpNDaOw==} 61 | engines: {node: '>= 10'} 62 | cpu: [arm64] 63 | os: [linux] 64 | requiresBuild: true 65 | dev: false 66 | optional: true 67 | 68 | /@next/swc-linux-x64-gnu@13.4.7: 69 | resolution: {integrity: sha512-zaEC+iEiAHNdhl6fuwl0H0shnTzQoAoJiDYBUze8QTntE/GNPfTYpYboxF5LRYIjBwETUatvE0T64W6SKDipvg==} 70 | engines: {node: '>= 10'} 71 | cpu: [x64] 72 | os: [linux] 73 | requiresBuild: true 74 | dev: false 75 | optional: true 76 | 77 | /@next/swc-linux-x64-musl@13.4.7: 78 | resolution: {integrity: sha512-X6r12F8d8SKAtYJqLZBBMIwEqcTRvUdVm+xIq+l6pJqlgT2tNsLLf2i5Cl88xSsIytBICGsCNNHd+siD2fbWBA==} 79 | engines: {node: '>= 10'} 80 | cpu: [x64] 81 | os: [linux] 82 | requiresBuild: true 83 | dev: false 84 | optional: true 85 | 86 | /@next/swc-win32-arm64-msvc@13.4.7: 87 | resolution: {integrity: sha512-NPnmnV+vEIxnu6SUvjnuaWRglZzw4ox5n/MQTxeUhb5iwVWFedolPFebMNwgrWu4AELwvTdGtWjqof53AiWHcw==} 88 | engines: {node: '>= 10'} 89 | cpu: [arm64] 90 | os: [win32] 91 | requiresBuild: true 92 | dev: false 93 | optional: true 94 | 95 | /@next/swc-win32-ia32-msvc@13.4.7: 96 | resolution: {integrity: sha512-6Hxijm6/a8XqLQpOOf/XuwWRhcuc/g4rBB2oxjgCMuV9Xlr2bLs5+lXyh8w9YbAUMYR3iC9mgOlXbHa79elmXw==} 97 | engines: {node: '>= 10'} 98 | cpu: [ia32] 99 | os: [win32] 100 | requiresBuild: true 101 | dev: false 102 | optional: true 103 | 104 | /@next/swc-win32-x64-msvc@13.4.7: 105 | resolution: {integrity: sha512-sW9Yt36Db1nXJL+mTr2Wo0y+VkPWeYhygvcHj1FF0srVtV+VoDjxleKtny21QHaG05zdeZnw2fCtf2+dEqgwqA==} 106 | engines: {node: '>= 10'} 107 | cpu: [x64] 108 | os: [win32] 109 | requiresBuild: true 110 | dev: false 111 | optional: true 112 | 113 | /@swc/helpers@0.5.1: 114 | resolution: {integrity: sha512-sJ902EfIzn1Fa+qYmjdQqh8tPsoxyBz+8yBKC2HKUxyezKJFwPGOn7pv4WY6QuQW//ySQi5lJjA/ZT9sNWWNTg==} 115 | dependencies: 116 | tslib: 2.5.3 117 | dev: false 118 | 119 | /@types/node@20.3.1: 120 | resolution: {integrity: sha512-EhcH/wvidPy1WeML3TtYFGR83UzjxeWRen9V402T8aUGYsCHOmfoisV3ZSg03gAFIbLq8TnWOJ0f4cALtnSEUg==} 121 | dev: false 122 | 123 | /@types/prop-types@15.7.5: 124 | resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==} 125 | dev: false 126 | 127 | /@types/react-dom@18.2.6: 128 | resolution: {integrity: sha512-2et4PDvg6PVCyS7fuTc4gPoksV58bW0RwSxWKcPRcHZf0PRUGq03TKcD/rUHe3azfV6/5/biUBJw+HhCQjaP0A==} 129 | dependencies: 130 | '@types/react': 18.2.14 131 | dev: false 132 | 133 | /@types/react@18.2.14: 134 | resolution: {integrity: sha512-A0zjq+QN/O0Kpe30hA1GidzyFjatVvrpIvWLxD+xv67Vt91TWWgco9IvrJBkeyHm1trGaFS/FSGqPlhyeZRm0g==} 135 | dependencies: 136 | '@types/prop-types': 15.7.5 137 | '@types/scheduler': 0.16.3 138 | csstype: 3.1.2 139 | dev: false 140 | 141 | /@types/scheduler@0.16.3: 142 | resolution: {integrity: sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==} 143 | dev: false 144 | 145 | /busboy@1.6.0: 146 | resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} 147 | engines: {node: '>=10.16.0'} 148 | dependencies: 149 | streamsearch: 1.1.0 150 | dev: false 151 | 152 | /caniuse-lite@1.0.30001507: 153 | resolution: {integrity: sha512-SFpUDoSLCaE5XYL2jfqe9ova/pbQHEmbheDf5r4diNwbAgR3qxM9NQtfsiSscjqoya5K7kFcHPUQ+VsUkIJR4A==} 154 | dev: false 155 | 156 | /client-only@0.0.1: 157 | resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} 158 | dev: false 159 | 160 | /csstype@3.1.2: 161 | resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==} 162 | dev: false 163 | 164 | /glob-to-regexp@0.4.1: 165 | resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} 166 | dev: false 167 | 168 | /graceful-fs@4.2.11: 169 | resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} 170 | dev: false 171 | 172 | /js-tokens@4.0.0: 173 | resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} 174 | dev: false 175 | 176 | /loose-envify@1.4.0: 177 | resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} 178 | hasBin: true 179 | dependencies: 180 | js-tokens: 4.0.0 181 | dev: false 182 | 183 | /nanoid@3.3.6: 184 | resolution: {integrity: sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==} 185 | engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} 186 | hasBin: true 187 | dev: false 188 | 189 | /next@13.4.7(react-dom@18.2.0)(react@18.2.0): 190 | resolution: {integrity: sha512-M8z3k9VmG51SRT6v5uDKdJXcAqLzP3C+vaKfLIAM0Mhx1um1G7MDnO63+m52qPdZfrTFzMZNzfsgvm3ghuVHIQ==} 191 | engines: {node: '>=16.8.0'} 192 | hasBin: true 193 | peerDependencies: 194 | '@opentelemetry/api': ^1.1.0 195 | fibers: '>= 3.1.0' 196 | react: ^18.2.0 197 | react-dom: ^18.2.0 198 | sass: ^1.3.0 199 | peerDependenciesMeta: 200 | '@opentelemetry/api': 201 | optional: true 202 | fibers: 203 | optional: true 204 | sass: 205 | optional: true 206 | dependencies: 207 | '@next/env': 13.4.7 208 | '@swc/helpers': 0.5.1 209 | busboy: 1.6.0 210 | caniuse-lite: 1.0.30001507 211 | postcss: 8.4.14 212 | react: 18.2.0 213 | react-dom: 18.2.0(react@18.2.0) 214 | styled-jsx: 5.1.1(react@18.2.0) 215 | watchpack: 2.4.0 216 | zod: 3.21.4 217 | optionalDependencies: 218 | '@next/swc-darwin-arm64': 13.4.7 219 | '@next/swc-darwin-x64': 13.4.7 220 | '@next/swc-linux-arm64-gnu': 13.4.7 221 | '@next/swc-linux-arm64-musl': 13.4.7 222 | '@next/swc-linux-x64-gnu': 13.4.7 223 | '@next/swc-linux-x64-musl': 13.4.7 224 | '@next/swc-win32-arm64-msvc': 13.4.7 225 | '@next/swc-win32-ia32-msvc': 13.4.7 226 | '@next/swc-win32-x64-msvc': 13.4.7 227 | transitivePeerDependencies: 228 | - '@babel/core' 229 | - babel-plugin-macros 230 | dev: false 231 | 232 | /picocolors@1.0.0: 233 | resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} 234 | dev: false 235 | 236 | /postcss@8.4.14: 237 | resolution: {integrity: sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==} 238 | engines: {node: ^10 || ^12 || >=14} 239 | dependencies: 240 | nanoid: 3.3.6 241 | picocolors: 1.0.0 242 | source-map-js: 1.0.2 243 | dev: false 244 | 245 | /react-dom@18.2.0(react@18.2.0): 246 | resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} 247 | peerDependencies: 248 | react: ^18.2.0 249 | dependencies: 250 | loose-envify: 1.4.0 251 | react: 18.2.0 252 | scheduler: 0.23.0 253 | dev: false 254 | 255 | /react@18.2.0: 256 | resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} 257 | engines: {node: '>=0.10.0'} 258 | dependencies: 259 | loose-envify: 1.4.0 260 | dev: false 261 | 262 | /scheduler@0.23.0: 263 | resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} 264 | dependencies: 265 | loose-envify: 1.4.0 266 | dev: false 267 | 268 | /source-map-js@1.0.2: 269 | resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} 270 | engines: {node: '>=0.10.0'} 271 | dev: false 272 | 273 | /streamsearch@1.1.0: 274 | resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} 275 | engines: {node: '>=10.0.0'} 276 | dev: false 277 | 278 | /styled-jsx@5.1.1(react@18.2.0): 279 | resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==} 280 | engines: {node: '>= 12.0.0'} 281 | peerDependencies: 282 | '@babel/core': '*' 283 | babel-plugin-macros: '*' 284 | react: '>= 16.8.0 || 17.x.x || ^18.0.0-0' 285 | peerDependenciesMeta: 286 | '@babel/core': 287 | optional: true 288 | babel-plugin-macros: 289 | optional: true 290 | dependencies: 291 | client-only: 0.0.1 292 | react: 18.2.0 293 | dev: false 294 | 295 | /tslib@2.5.3: 296 | resolution: {integrity: sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w==} 297 | dev: false 298 | 299 | /typescript@5.1.3: 300 | resolution: {integrity: sha512-XH627E9vkeqhlZFQuL+UsyAXEnibT0kWR2FWONlr4sTjvxyJYnyefgrkyECLzM5NenmKzRAy2rR/OlYLA1HkZw==} 301 | engines: {node: '>=14.17'} 302 | hasBin: true 303 | dev: false 304 | 305 | /watchpack@2.4.0: 306 | resolution: {integrity: sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==} 307 | engines: {node: '>=10.13.0'} 308 | dependencies: 309 | glob-to-regexp: 0.4.1 310 | graceful-fs: 4.2.11 311 | dev: false 312 | 313 | /zod@3.21.4: 314 | resolution: {integrity: sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==} 315 | dev: false 316 | -------------------------------------------------------------------------------- /src/body-scroll-lock.ts: -------------------------------------------------------------------------------- 1 | export type BodyScrollOptions = { 2 | reserveScrollBarGap?: boolean | undefined; 3 | allowTouchMove?: ((el: EventTarget) => boolean) | undefined; 4 | }; 5 | 6 | type BodyStyleType = { 7 | position: string; 8 | top: string; 9 | left: string; 10 | width: string; 11 | height: string; 12 | overflow: string; 13 | }; 14 | 15 | interface Lock { 16 | targetElement: HTMLElement; 17 | options: BodyScrollOptions; 18 | } 19 | 20 | // Older browsers don't support event options, feature detect it. 21 | let hasPassiveEvents = false; 22 | if (typeof window !== 'undefined') { 23 | const passiveTestOptions: any = { 24 | get passive() { 25 | hasPassiveEvents = true; 26 | return undefined; 27 | }, 28 | }; 29 | (window as any).addEventListener('testPassive', null, passiveTestOptions); 30 | (window as any).removeEventListener('testPassive', null, passiveTestOptions); 31 | } 32 | 33 | const isIosDevice = 34 | typeof window !== 'undefined' && 35 | window.navigator && 36 | window.navigator.platform && 37 | (/iP(ad|hone|od)/.test(window.navigator.platform) || 38 | (window.navigator.platform === 'MacIntel' && 39 | window.navigator.maxTouchPoints > 1)); 40 | type HandleScrollEvent = TouchEvent; 41 | 42 | let locks: Array = []; 43 | let locksIndex: Map = new Map(); 44 | let documentListenerAdded: boolean = false; 45 | let initialClientY: number = -1; 46 | let previousBodyOverflowSetting: string | undefined; 47 | let htmlStyle: 48 | | { 49 | height: string; 50 | overflow: string; 51 | } 52 | | undefined; 53 | let bodyStyle: BodyStyleType | undefined; 54 | 55 | let previousBodyPaddingRight: string | undefined; 56 | 57 | // returns true if `el` should be allowed to receive touchmove events. 58 | const allowTouchMove = (el: EventTarget): boolean => 59 | locks.some((lock) => { 60 | if (lock.options.allowTouchMove && lock.options.allowTouchMove(el)) { 61 | return true; 62 | } 63 | 64 | return false; 65 | }); 66 | 67 | const preventDefault = (rawEvent: HandleScrollEvent): boolean => { 68 | const e: any = rawEvent || window.event; 69 | 70 | // For the case whereby consumers adds a touchmove event listener to document. 71 | // Recall that we do document.addEventListener('touchmove', preventDefault, { passive: false }) 72 | // in disableBodyScroll - so if we provide this opportunity to allowTouchMove, then 73 | // the touchmove event on document will break. 74 | if (allowTouchMove(e.target)) { 75 | return true; 76 | } 77 | 78 | // Do not prevent if the event has more than one touch (usually meaning this is a multi touch gesture like pinch to zoom). 79 | if (e.touches.length > 1) return true; 80 | 81 | if (e.preventDefault) e.preventDefault(); 82 | 83 | return false; 84 | }; 85 | 86 | const setOverflowHidden = (options?: BodyScrollOptions) => { 87 | // If previousBodyPaddingRight is already set, don't set it again. 88 | if (previousBodyPaddingRight === undefined) { 89 | const reserveScrollBarGap = 90 | !!options && options.reserveScrollBarGap === true; 91 | const scrollBarGap = 92 | window.innerWidth - 93 | document.documentElement.getBoundingClientRect().width; 94 | 95 | if (reserveScrollBarGap && scrollBarGap > 0) { 96 | const computedBodyPaddingRight = parseInt( 97 | window 98 | .getComputedStyle(document.body) 99 | .getPropertyValue('padding-right'), 100 | 10 101 | ); 102 | previousBodyPaddingRight = document.body.style.paddingRight; 103 | document.body.style.paddingRight = `${ 104 | computedBodyPaddingRight + scrollBarGap 105 | }px`; 106 | } 107 | } 108 | 109 | // If previousBodyOverflowSetting is already set, don't set it again. 110 | if (previousBodyOverflowSetting === undefined) { 111 | previousBodyOverflowSetting = document.body.style.overflow; 112 | document.body.style.overflow = 'hidden'; 113 | } 114 | }; 115 | 116 | const restoreOverflowSetting = () => { 117 | if (previousBodyPaddingRight !== undefined) { 118 | document.body.style.paddingRight = previousBodyPaddingRight; 119 | 120 | // Restore previousBodyPaddingRight to undefined so setOverflowHidden knows it 121 | // can be set again. 122 | previousBodyPaddingRight = undefined; 123 | } 124 | 125 | if (previousBodyOverflowSetting !== undefined) { 126 | document.body.style.overflow = previousBodyOverflowSetting; 127 | 128 | // Restore previousBodyOverflowSetting to undefined 129 | // so setOverflowHidden knows it can be set again. 130 | previousBodyOverflowSetting = undefined; 131 | } 132 | }; 133 | 134 | const setPositionFixed = () => 135 | window.requestAnimationFrame(() => { 136 | const $html = document.documentElement; 137 | const $body = document.body; 138 | // If bodyStyle is already set, don't set it again. 139 | if (bodyStyle === undefined) { 140 | htmlStyle = { ...$html.style }; 141 | bodyStyle = { ...$body.style }; 142 | 143 | // Update the dom inside an animation frame 144 | const { scrollY, scrollX, innerHeight } = window; 145 | 146 | $html.style.height = '100%'; 147 | $html.style.overflow = 'hidden'; 148 | 149 | $body.style.position = 'fixed'; 150 | $body.style.top = `${-scrollY}px`; 151 | $body.style.left = `${-scrollX}px`; 152 | $body.style.width = '100%'; 153 | $body.style.height = 'auto'; 154 | $body.style.overflow = 'hidden'; 155 | 156 | setTimeout( 157 | () => 158 | window.requestAnimationFrame(() => { 159 | // Attempt to check if the bottom bar appeared due to the position change 160 | const bottomBarHeight = innerHeight - window.innerHeight; 161 | if (bottomBarHeight && scrollY >= innerHeight) { 162 | // Move the content further up so that the bottom bar doesn't hide it 163 | $body.style.top = -(scrollY + bottomBarHeight) + 'px'; 164 | } 165 | }), 166 | 300 167 | ); 168 | } 169 | }); 170 | 171 | const restorePositionSetting = () => { 172 | if (bodyStyle !== undefined) { 173 | // Convert the position from "px" to Int 174 | const y = -parseInt(document.body.style.top, 10); 175 | const x = -parseInt(document.body.style.left, 10); 176 | 177 | // Restore styles 178 | const $html = document.documentElement; 179 | const $body = document.body; 180 | 181 | $html.style.height = htmlStyle?.height || ''; 182 | $html.style.overflow = htmlStyle?.overflow || ''; 183 | 184 | $body.style.position = bodyStyle.position || ''; 185 | $body.style.top = bodyStyle.top || ''; 186 | $body.style.left = bodyStyle.left || ''; 187 | $body.style.width = bodyStyle.width || ''; 188 | $body.style.height = bodyStyle.height || ''; 189 | $body.style.overflow = bodyStyle.overflow || ''; 190 | 191 | // Restore scroll 192 | window.scrollTo(x, y); 193 | 194 | bodyStyle = undefined; 195 | } 196 | }; 197 | 198 | // https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight#Problems_and_solutions 199 | const isTargetElementTotallyScrolled = (targetElement: HTMLElement): boolean => 200 | targetElement 201 | ? targetElement.scrollHeight - targetElement.scrollTop <= 202 | targetElement.clientHeight 203 | : false; 204 | 205 | const handleScroll = ( 206 | event: HandleScrollEvent, 207 | targetElement: HTMLElement 208 | ): boolean => { 209 | const clientY = event.targetTouches[0].clientY - initialClientY; 210 | 211 | if (allowTouchMove(event.target as EventTarget)) { 212 | return false; 213 | } 214 | 215 | if (targetElement && targetElement.scrollTop === 0 && clientY > 0) { 216 | // element is at the top of its scroll. 217 | return preventDefault(event); 218 | } 219 | 220 | if (isTargetElementTotallyScrolled(targetElement) && clientY < 0) { 221 | // element is at the bottom of its scroll. 222 | return preventDefault(event); 223 | } 224 | 225 | event.stopPropagation(); 226 | return true; 227 | }; 228 | 229 | /** 230 | * 231 | * @param targetElement HTMLElement 232 | * @param options BodyScrollOptions 233 | * @returns void 234 | */ 235 | export const disableBodyScroll = ( 236 | targetElement: HTMLElement, 237 | options?: BodyScrollOptions 238 | ): void => { 239 | // targetElement must be provided 240 | if (!targetElement) { 241 | // eslint-disable-next-line no-console 242 | console.error( 243 | 'disableBodyScroll unsuccessful - targetElement must be provided when calling disableBodyScroll on IOS devices.' 244 | ); 245 | return; 246 | } 247 | 248 | locksIndex.set( 249 | targetElement, 250 | locksIndex?.get(targetElement) 251 | ? (locksIndex?.get(targetElement) as number) + 1 252 | : 1 253 | ); 254 | // disableBodyScroll must not have been called on this targetElement before 255 | if (locks.some((lock) => lock.targetElement === targetElement)) { 256 | return; 257 | } 258 | 259 | const lock = { 260 | targetElement, 261 | options: options || {}, 262 | }; 263 | locks = [...locks, lock]; 264 | 265 | if (isIosDevice) { 266 | setPositionFixed(); 267 | } else { 268 | setOverflowHidden(options); 269 | } 270 | 271 | if (isIosDevice) { 272 | targetElement.ontouchstart = (event: HandleScrollEvent) => { 273 | if (event.targetTouches.length === 1) { 274 | // detect single touch. 275 | initialClientY = event.targetTouches[0].clientY; 276 | } 277 | }; 278 | targetElement.ontouchmove = (event: HandleScrollEvent) => { 279 | if (event.targetTouches.length === 1) { 280 | // detect single touch. 281 | handleScroll(event, targetElement); 282 | } 283 | }; 284 | 285 | if (!documentListenerAdded) { 286 | document.addEventListener( 287 | 'touchmove', 288 | preventDefault, 289 | hasPassiveEvents ? { passive: false } : undefined 290 | ); 291 | documentListenerAdded = true; 292 | } 293 | } 294 | }; 295 | 296 | export const clearAllBodyScrollLocks = (): void => { 297 | if (isIosDevice) { 298 | // Clear all locks ontouchstart/ontouchmove handlers, and the references. 299 | locks.forEach((lock: Lock) => { 300 | lock.targetElement.ontouchstart = null; 301 | lock.targetElement.ontouchmove = null; 302 | }); 303 | 304 | if (documentListenerAdded) { 305 | (document as any).removeEventListener( 306 | 'touchmove', 307 | preventDefault, 308 | hasPassiveEvents ? { passive: false } : undefined 309 | ); 310 | documentListenerAdded = false; 311 | } 312 | 313 | // Reset initial clientY. 314 | initialClientY = -1; 315 | } 316 | 317 | if (isIosDevice) { 318 | restorePositionSetting(); 319 | } else { 320 | restoreOverflowSetting(); 321 | } 322 | 323 | locks = []; 324 | locksIndex.clear(); 325 | }; 326 | 327 | /** 328 | * @param targetElement 329 | * @returns void 330 | */ 331 | export const enableBodyScroll = (targetElement: HTMLElement): void => { 332 | if (!targetElement) { 333 | // eslint-disable-next-line no-console 334 | console.error( 335 | 'enableBodyScroll unsuccessful - targetElement must be provided when calling enableBodyScroll on IOS devices.' 336 | ); 337 | return; 338 | } 339 | 340 | locksIndex.set( 341 | targetElement, 342 | locksIndex?.get(targetElement) 343 | ? (locksIndex?.get(targetElement) as number) - 1 344 | : 0 345 | ); 346 | if (locksIndex?.get(targetElement) === 0) { 347 | locks = locks.filter((lock) => lock.targetElement !== targetElement); 348 | locksIndex?.delete(targetElement); 349 | } 350 | 351 | if (isIosDevice) { 352 | targetElement.ontouchstart = null; 353 | targetElement.ontouchmove = null; 354 | 355 | if (documentListenerAdded && locks.length === 0) { 356 | (document as any).removeEventListener( 357 | 'touchmove', 358 | preventDefault, 359 | hasPassiveEvents ? { passive: false } : undefined 360 | ); 361 | documentListenerAdded = false; 362 | } 363 | } 364 | 365 | if (locks.length === 0) { 366 | if (isIosDevice) { 367 | restorePositionSetting(); 368 | } else { 369 | restoreOverflowSetting(); 370 | } 371 | } 372 | }; 373 | -------------------------------------------------------------------------------- /examples/next/hooks/body-scroll-lock.ts: -------------------------------------------------------------------------------- 1 | export type BodyScrollOptions = { 2 | reserveScrollBarGap?: boolean | undefined; 3 | allowTouchMove?: ((el: EventTarget) => boolean) | undefined; 4 | }; 5 | 6 | type BodyStyleType = { 7 | position: string; 8 | top: string; 9 | left: string; 10 | width: string; 11 | height: string; 12 | overflow: string; 13 | }; 14 | 15 | interface Lock { 16 | targetElement: HTMLElement; 17 | options: BodyScrollOptions; 18 | } 19 | 20 | // Older browsers don't support event options, feature detect it. 21 | let hasPassiveEvents = false; 22 | if (typeof window !== 'undefined') { 23 | const passiveTestOptions: any = { 24 | get passive() { 25 | hasPassiveEvents = true; 26 | return undefined; 27 | }, 28 | }; 29 | (window as any).addEventListener('testPassive', null, passiveTestOptions); 30 | (window as any).removeEventListener('testPassive', null, passiveTestOptions); 31 | } 32 | 33 | const isIosDevice = 34 | typeof window !== 'undefined' && 35 | window.navigator && 36 | window.navigator.platform && 37 | (/iP(ad|hone|od)/.test(window.navigator.platform) || 38 | (window.navigator.platform === 'MacIntel' && 39 | window.navigator.maxTouchPoints > 1)); 40 | type HandleScrollEvent = TouchEvent; 41 | 42 | let locks: Array = []; 43 | let locksIndex: Map = new Map(); 44 | let documentListenerAdded: boolean = false; 45 | let initialClientY: number = -1; 46 | let previousBodyOverflowSetting: string | undefined; 47 | let htmlStyle: 48 | | { 49 | height: string; 50 | overflow: string; 51 | } 52 | | undefined; 53 | let bodyStyle: BodyStyleType | undefined; 54 | 55 | let previousBodyPaddingRight: string | undefined; 56 | 57 | // returns true if `el` should be allowed to receive touchmove events. 58 | const allowTouchMove = (el: EventTarget): boolean => 59 | locks.some((lock) => { 60 | if (lock.options.allowTouchMove && lock.options.allowTouchMove(el)) { 61 | return true; 62 | } 63 | 64 | return false; 65 | }); 66 | 67 | const preventDefault = (rawEvent: HandleScrollEvent): boolean => { 68 | const e: any = rawEvent || window.event; 69 | 70 | // For the case whereby consumers adds a touchmove event listener to document. 71 | // Recall that we do document.addEventListener('touchmove', preventDefault, { passive: false }) 72 | // in disableBodyScroll - so if we provide this opportunity to allowTouchMove, then 73 | // the touchmove event on document will break. 74 | if (allowTouchMove(e.target)) { 75 | return true; 76 | } 77 | 78 | // Do not prevent if the event has more than one touch (usually meaning this is a multi touch gesture like pinch to zoom). 79 | if (e.touches.length > 1) return true; 80 | 81 | if (e.preventDefault) e.preventDefault(); 82 | 83 | return false; 84 | }; 85 | 86 | const setOverflowHidden = (options?: BodyScrollOptions) => { 87 | // If previousBodyPaddingRight is already set, don't set it again. 88 | if (previousBodyPaddingRight === undefined) { 89 | const reserveScrollBarGap = 90 | !!options && options.reserveScrollBarGap === true; 91 | const scrollBarGap = 92 | window.innerWidth - 93 | document.documentElement.getBoundingClientRect().width; 94 | 95 | if (reserveScrollBarGap && scrollBarGap > 0) { 96 | const computedBodyPaddingRight = parseInt( 97 | window 98 | .getComputedStyle(document.body) 99 | .getPropertyValue('padding-right'), 100 | 10 101 | ); 102 | previousBodyPaddingRight = document.body.style.paddingRight; 103 | document.body.style.paddingRight = `${ 104 | computedBodyPaddingRight + scrollBarGap 105 | }px`; 106 | } 107 | } 108 | 109 | // If previousBodyOverflowSetting is already set, don't set it again. 110 | if (previousBodyOverflowSetting === undefined) { 111 | previousBodyOverflowSetting = document.body.style.overflow; 112 | document.body.style.overflow = 'hidden'; 113 | } 114 | }; 115 | 116 | const restoreOverflowSetting = () => { 117 | if (previousBodyPaddingRight !== undefined) { 118 | document.body.style.paddingRight = previousBodyPaddingRight; 119 | 120 | // Restore previousBodyPaddingRight to undefined so setOverflowHidden knows it 121 | // can be set again. 122 | previousBodyPaddingRight = undefined; 123 | } 124 | 125 | if (previousBodyOverflowSetting !== undefined) { 126 | document.body.style.overflow = previousBodyOverflowSetting; 127 | 128 | // Restore previousBodyOverflowSetting to undefined 129 | // so setOverflowHidden knows it can be set again. 130 | previousBodyOverflowSetting = undefined; 131 | } 132 | }; 133 | 134 | const setPositionFixed = () => 135 | window.requestAnimationFrame(() => { 136 | const $html = document.documentElement; 137 | const $body = document.body; 138 | // If bodyStyle is already set, don't set it again. 139 | if (bodyStyle === undefined) { 140 | htmlStyle = { ...$html.style }; 141 | bodyStyle = { ...$body.style }; 142 | 143 | // Update the dom inside an animation frame 144 | const { scrollY, scrollX, innerHeight } = window; 145 | 146 | $html.style.height = '100%'; 147 | $html.style.overflow = 'hidden'; 148 | 149 | $body.style.position = 'fixed'; 150 | $body.style.top = `${-scrollY}px`; 151 | $body.style.left = `${-scrollX}px`; 152 | $body.style.width = '100%'; 153 | $body.style.height = 'auto'; 154 | $body.style.overflow = 'hidden'; 155 | 156 | setTimeout( 157 | () => 158 | window.requestAnimationFrame(() => { 159 | // Attempt to check if the bottom bar appeared due to the position change 160 | const bottomBarHeight = innerHeight - window.innerHeight; 161 | if (bottomBarHeight && scrollY >= innerHeight) { 162 | // Move the content further up so that the bottom bar doesn't hide it 163 | $body.style.top = -(scrollY + bottomBarHeight) + 'px'; 164 | } 165 | }), 166 | 300 167 | ); 168 | } 169 | }); 170 | 171 | const restorePositionSetting = () => { 172 | if (bodyStyle !== undefined) { 173 | // Convert the position from "px" to Int 174 | const y = -parseInt(document.body.style.top, 10); 175 | const x = -parseInt(document.body.style.left, 10); 176 | 177 | // Restore styles 178 | const $html = document.documentElement; 179 | const $body = document.body; 180 | 181 | $html.style.height = htmlStyle?.height || ''; 182 | $html.style.overflow = htmlStyle?.overflow || ''; 183 | 184 | $body.style.position = bodyStyle.position || ''; 185 | $body.style.top = bodyStyle.top || ''; 186 | $body.style.left = bodyStyle.left || ''; 187 | $body.style.width = bodyStyle.width || ''; 188 | $body.style.height = bodyStyle.height || ''; 189 | $body.style.overflow = bodyStyle.overflow || ''; 190 | 191 | // Restore scroll 192 | window.scrollTo(x, y); 193 | 194 | bodyStyle = undefined; 195 | } 196 | }; 197 | 198 | // https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight#Problems_and_solutions 199 | const isTargetElementTotallyScrolled = (targetElement: HTMLElement): boolean => 200 | targetElement 201 | ? targetElement.scrollHeight - targetElement.scrollTop <= 202 | targetElement.clientHeight 203 | : false; 204 | 205 | const handleScroll = ( 206 | event: HandleScrollEvent, 207 | targetElement: HTMLElement 208 | ): boolean => { 209 | const clientY = event.targetTouches[0].clientY - initialClientY; 210 | 211 | if (allowTouchMove(event.target as EventTarget)) { 212 | return false; 213 | } 214 | 215 | if (targetElement && targetElement.scrollTop === 0 && clientY > 0) { 216 | // element is at the top of its scroll. 217 | return preventDefault(event); 218 | } 219 | 220 | if (isTargetElementTotallyScrolled(targetElement) && clientY < 0) { 221 | // element is at the bottom of its scroll. 222 | return preventDefault(event); 223 | } 224 | 225 | event.stopPropagation(); 226 | return true; 227 | }; 228 | 229 | /** 230 | * 231 | * @param targetElement HTMLElement 232 | * @param options BodyScrollOptions 233 | * @returns void 234 | */ 235 | export const disableBodyScroll = ( 236 | targetElement: HTMLElement, 237 | options?: BodyScrollOptions 238 | ): void => { 239 | // targetElement must be provided 240 | if (!targetElement) { 241 | // eslint-disable-next-line no-console 242 | console.error( 243 | 'disableBodyScroll unsuccessful - targetElement must be provided when calling disableBodyScroll on IOS devices.' 244 | ); 245 | return; 246 | } 247 | 248 | locksIndex.set( 249 | targetElement, 250 | locksIndex?.get(targetElement) 251 | ? (locksIndex?.get(targetElement) as number) + 1 252 | : 1 253 | ); 254 | // disableBodyScroll must not have been called on this targetElement before 255 | if (locks.some((lock) => lock.targetElement === targetElement)) { 256 | return; 257 | } 258 | 259 | const lock = { 260 | targetElement, 261 | options: options || {}, 262 | }; 263 | locks = [...locks, lock]; 264 | 265 | if (isIosDevice) { 266 | setPositionFixed(); 267 | } else { 268 | setOverflowHidden(options); 269 | } 270 | 271 | if (isIosDevice) { 272 | targetElement.ontouchstart = (event: HandleScrollEvent) => { 273 | if (event.targetTouches.length === 1) { 274 | // detect single touch. 275 | initialClientY = event.targetTouches[0].clientY; 276 | } 277 | }; 278 | targetElement.ontouchmove = (event: HandleScrollEvent) => { 279 | if (event.targetTouches.length === 1) { 280 | // detect single touch. 281 | handleScroll(event, targetElement); 282 | } 283 | }; 284 | 285 | if (!documentListenerAdded) { 286 | document.addEventListener( 287 | 'touchmove', 288 | preventDefault, 289 | hasPassiveEvents ? { passive: false } : undefined 290 | ); 291 | documentListenerAdded = true; 292 | } 293 | } 294 | }; 295 | 296 | export const clearAllBodyScrollLocks = (): void => { 297 | if (isIosDevice) { 298 | // Clear all locks ontouchstart/ontouchmove handlers, and the references. 299 | locks.forEach((lock: Lock) => { 300 | lock.targetElement.ontouchstart = null; 301 | lock.targetElement.ontouchmove = null; 302 | }); 303 | 304 | if (documentListenerAdded) { 305 | (document as any).removeEventListener( 306 | 'touchmove', 307 | preventDefault, 308 | hasPassiveEvents ? { passive: false } : undefined 309 | ); 310 | documentListenerAdded = false; 311 | } 312 | 313 | // Reset initial clientY. 314 | initialClientY = -1; 315 | } 316 | 317 | if (isIosDevice) { 318 | restorePositionSetting(); 319 | } else { 320 | restoreOverflowSetting(); 321 | } 322 | 323 | locks = []; 324 | locksIndex.clear(); 325 | }; 326 | 327 | /** 328 | * @param targetElement 329 | * @returns void 330 | */ 331 | export const enableBodyScroll = (targetElement: HTMLElement): void => { 332 | if (!targetElement) { 333 | // eslint-disable-next-line no-console 334 | console.error( 335 | 'enableBodyScroll unsuccessful - targetElement must be provided when calling enableBodyScroll on IOS devices.' 336 | ); 337 | return; 338 | } 339 | 340 | locksIndex.set( 341 | targetElement, 342 | locksIndex?.get(targetElement) 343 | ? (locksIndex?.get(targetElement) as number) - 1 344 | : 0 345 | ); 346 | if (locksIndex?.get(targetElement) === 0) { 347 | locks = locks.filter((lock) => lock.targetElement !== targetElement); 348 | locksIndex?.delete(targetElement); 349 | } 350 | 351 | if (isIosDevice) { 352 | targetElement.ontouchstart = null; 353 | targetElement.ontouchmove = null; 354 | 355 | if (documentListenerAdded && locks.length === 0) { 356 | (document as any).removeEventListener( 357 | 'touchmove', 358 | preventDefault, 359 | hasPassiveEvents ? { passive: false } : undefined 360 | ); 361 | documentListenerAdded = false; 362 | } 363 | } 364 | 365 | if (locks.length === 0) { 366 | if (isIosDevice) { 367 | restorePositionSetting(); 368 | } else { 369 | restoreOverflowSetting(); 370 | } 371 | } 372 | }; 373 | -------------------------------------------------------------------------------- /examples/vanilla-js/index-umd.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 31 | 32 | 33 | 34 |
35 |
36 | 37 |
38 |
39 |

40 | Click on the Vite and React logos to learn more 41 |
42 | Click on the Vite and React logos to learn more 43 |
44 | Click on the Vite and React logos to learn more 45 |
46 | Click on the Vite and React logos to learn more 47 |
48 | Click on the Vite and React logos to learn more 49 |
50 | Click on the Vite and React logos to learn more 51 |
52 | Click on the Vite and React logos to learn more 53 |
54 | Click on the Vite and React logos to learn more 55 |
56 | Click on the Vite and React logos to learn more 57 |
58 | Click on the Vite and React logos to learn more 59 |
60 | Click on the Vite and React logos to learn more 61 |
62 | Click on the Vite and React logos to learn more 63 |
64 | Click on the Vite and React logos to learn more 65 |
66 | Click on the Vite and React logos to learn more 67 |
68 | Click on the Vite and React logos to learn more 69 |
70 | Click on the Vite and React logos to learn more 71 |
72 | Click on the Vite and React logos to learn more 73 |
74 | Click on the Vite and React logos to learn more 75 |
76 | Click on the Vite and React logos to learn more 77 |
78 | Click on the Vite and React logos to learn more 79 |
80 | Click on the Vite and React logos to learn more 81 |
82 | Click on the Vite and React logos to learn more 83 |
84 | Click on the Vite and React logos to learn more 85 |
86 | Click on the Vite and React logos to learn more 87 |
88 | Click on the Vite and React logos to learn more 89 |
90 | Click on the Vite and React logos to learn more 91 |
92 | Click on the Vite and React logos to learn more 93 |
94 | Click on the Vite and React logos to learn more 95 |
96 | Click on the Vite and React logos to learn more 97 |
98 | Click on the Vite and React logos to learn more 99 |
100 | Click on the Vite and React logos to learn more 101 |
102 | Click on the Vite and React logos to learn more 103 |
104 | Click on the Vite and React logos to learn more 105 |
106 | Click on the Vite and React logos to learn more 107 |
108 | Click on the Vite and React logos to learn more 109 |
110 | Click on the Vite and React logos to learn more 111 |
112 | Click on the Vite and React logos to learn more 113 |
114 | Click on the Vite and React logos to learn more 115 |
116 | Click on the Vite and React logos to learn more 117 |
118 | Click on the Vite and React logos to learn more 119 |
120 | Click on the Vite and React logos to learn more 121 |
122 | Click on the Vite and React logos to learn more 123 |
124 | Click on the Vite and React logos to learn more 125 |
126 | Click on the Vite and React logos to learn more 127 |
128 | Click on the Vite and React logos to learn more 129 |
130 | Click on the Vite and React logos to learn more 131 |
132 | Click on the Vite and React logos to learn more 133 |
134 | Click on the Vite and React logos to learn more 135 |
136 | Click on the Vite and React logos to learn more 137 |
138 | Click on the Vite and React logos to learn more 139 |
140 | Click on the Vite and React logos to learn more 141 |
142 | Click on the Vite and React logos to learn more 143 |
144 | Click on the Vite and React logos to learn more 145 |
146 | Click on the Vite and React logos to learn more 147 |
148 | Click on the Vite and React logos to learn more 149 |
150 | Click on the Vite and React logos to learn more 151 |
152 | Click on the Vite and React logos to learn more 153 |
154 | Click on the Vite and React logos to learn more 155 |
156 | Click on the Vite and React logos to learn more 157 |
158 | Click on the Vite and React logos to learn more 159 |
160 | Click on the Vite and React logos to learn more 161 |
162 | Click on the Vite and React logos to learn more 163 |
164 | Click on the Vite and React logos to learn more 165 |
166 | Click on the Vite and React logos to learn more 167 |
168 | Click on the Vite and React logos to learn more 169 |
170 | Click on the Vite and React logos to learn more 171 |
172 |

173 |
174 |
175 | 176 |

177 | Click on the Vite and React logos to learn more 178 |
179 | Click on the Vite and React logos to learn more 180 |
181 | Click on the Vite and React logos to learn more 182 |
183 | Click on the Vite and React logos to learn more 184 |
185 | Click on the Vite and React logos to learn more 186 |
187 | Click on the Vite and React logos to learn more 188 |
189 | Click on the Vite and React logos to learn more 190 |
191 | Click on the Vite and React logos to learn more 192 |
193 | Click on the Vite and React logos to learn more 194 |
195 | Click on the Vite and React logos to learn more 196 |
197 | Click on the Vite and React logos to learn more 198 |
199 | Click on the Vite and React logos to learn more 200 |
201 | Click on the Vite and React logos to learn more 202 |
203 | Click on the Vite and React logos to learn more 204 |
205 | Click on the Vite and React logos to learn more 206 |
207 | Click on the Vite and React logos to learn more 208 |
209 | Click on the Vite and React logos to learn more 210 |
211 | Click on the Vite and React logos to learn more 212 |
213 | Click on the Vite and React logos to learn more 214 |
215 | Click on the Vite and React logos to learn more 216 |
217 | Click on the Vite and React logos to learn more 218 |
219 | Click on the Vite and React logos to learn more 220 |
221 | Click on the Vite and React logos to learn more 222 |
223 | Click on the Vite and React logos to learn more 224 |
225 | Click on the Vite and React logos to learn more 226 |
227 | Click on the Vite and React logos to learn more 228 |
229 | Click on the Vite and React logos to learn more 230 |
231 | Click on the Vite and React logos to learn more 232 |
233 | Click on the Vite and React logos to learn more 234 |
235 | Click on the Vite and React logos to learn more 236 |
237 | Click on the Vite and React logos to learn more 238 |
239 | Click on the Vite and React logos to learn more 240 |
241 | Click on the Vite and React logos to learn more 242 |
243 | Click on the Vite and React logos to learn more 244 |
245 | Click on the Vite and React logos to learn more 246 |
247 | Click on the Vite and React logos to learn more 248 |
249 | Click on the Vite and React logos to learn more 250 |
251 | Click on the Vite and React logos to learn more 252 |
253 | Click on the Vite and React logos to learn more 254 |
255 | Click on the Vite and React logos to learn more 256 |
257 | Click on the Vite and React logos to learn more 258 |
259 | Click on the Vite and React logos to learn more 260 |
261 | Click on the Vite and React logos to learn more 262 |
263 | Click on the Vite and React logos to learn more 264 |
265 | Click on the Vite and React logos to learn more 266 |
267 | Click on the Vite and React logos to learn more 268 |
269 | Click on the Vite and React logos to learn more 270 |
271 | Click on the Vite and React logos to learn more 272 |
273 | Click on the Vite and React logos to learn more 274 |
275 | Click on the Vite and React logos to learn more 276 |
277 | Click on the Vite and React logos to learn more 278 |
279 | Click on the Vite and React logos to learn more 280 |
281 | Click on the Vite and React logos to learn more 282 |
283 | Click on the Vite and React logos to learn more 284 |
285 | Click on the Vite and React logos to learn more 286 |
287 | Click on the Vite and React logos to learn more 288 |
289 | Click on the Vite and React logos to learn more 290 |
291 | Click on the Vite and React logos to learn more 292 |
293 | Click on the Vite and React logos to learn more 294 |
295 | Click on the Vite and React logos to learn more 296 |
297 | Click on the Vite and React logos to learn more 298 |
299 | Click on the Vite and React logos to learn more 300 |
301 | Click on the Vite and React logos to learn more 302 |
303 | Click on the Vite and React logos to learn more 304 |
305 | Click on the Vite and React logos to learn more 306 |
307 | Click on the Vite and React logos to learn more 308 |
309 |

310 | 311 | 312 | 313 | 329 | 330 | 331 | -------------------------------------------------------------------------------- /examples/vanilla-js/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 31 | 32 | 33 | 34 |
35 |
36 | 37 |
38 |
39 |

40 | Click on the Vite and React logos to learn more 41 |
42 | Click on the Vite and React logos to learn more 43 |
44 | Click on the Vite and React logos to learn more 45 |
46 | Click on the Vite and React logos to learn more 47 |
48 | Click on the Vite and React logos to learn more 49 |
50 | Click on the Vite and React logos to learn more 51 |
52 | Click on the Vite and React logos to learn more 53 |
54 | Click on the Vite and React logos to learn more 55 |
56 | Click on the Vite and React logos to learn more 57 |
58 | Click on the Vite and React logos to learn more 59 |
60 | Click on the Vite and React logos to learn more 61 |
62 | Click on the Vite and React logos to learn more 63 |
64 | Click on the Vite and React logos to learn more 65 |
66 | Click on the Vite and React logos to learn more 67 |
68 | Click on the Vite and React logos to learn more 69 |
70 | Click on the Vite and React logos to learn more 71 |
72 | Click on the Vite and React logos to learn more 73 |
74 | Click on the Vite and React logos to learn more 75 |
76 | Click on the Vite and React logos to learn more 77 |
78 | Click on the Vite and React logos to learn more 79 |
80 | Click on the Vite and React logos to learn more 81 |
82 | Click on the Vite and React logos to learn more 83 |
84 | Click on the Vite and React logos to learn more 85 |
86 | Click on the Vite and React logos to learn more 87 |
88 | Click on the Vite and React logos to learn more 89 |
90 | Click on the Vite and React logos to learn more 91 |
92 | Click on the Vite and React logos to learn more 93 |
94 | Click on the Vite and React logos to learn more 95 |
96 | Click on the Vite and React logos to learn more 97 |
98 | Click on the Vite and React logos to learn more 99 |
100 | Click on the Vite and React logos to learn more 101 |
102 | Click on the Vite and React logos to learn more 103 |
104 | Click on the Vite and React logos to learn more 105 |
106 | Click on the Vite and React logos to learn more 107 |
108 | Click on the Vite and React logos to learn more 109 |
110 | Click on the Vite and React logos to learn more 111 |
112 | Click on the Vite and React logos to learn more 113 |
114 | Click on the Vite and React logos to learn more 115 |
116 | Click on the Vite and React logos to learn more 117 |
118 | Click on the Vite and React logos to learn more 119 |
120 | Click on the Vite and React logos to learn more 121 |
122 | Click on the Vite and React logos to learn more 123 |
124 | Click on the Vite and React logos to learn more 125 |
126 | Click on the Vite and React logos to learn more 127 |
128 | Click on the Vite and React logos to learn more 129 |
130 | Click on the Vite and React logos to learn more 131 |
132 | Click on the Vite and React logos to learn more 133 |
134 | Click on the Vite and React logos to learn more 135 |
136 | Click on the Vite and React logos to learn more 137 |
138 | Click on the Vite and React logos to learn more 139 |
140 | Click on the Vite and React logos to learn more 141 |
142 | Click on the Vite and React logos to learn more 143 |
144 | Click on the Vite and React logos to learn more 145 |
146 | Click on the Vite and React logos to learn more 147 |
148 | Click on the Vite and React logos to learn more 149 |
150 | Click on the Vite and React logos to learn more 151 |
152 | Click on the Vite and React logos to learn more 153 |
154 | Click on the Vite and React logos to learn more 155 |
156 | Click on the Vite and React logos to learn more 157 |
158 | Click on the Vite and React logos to learn more 159 |
160 | Click on the Vite and React logos to learn more 161 |
162 | Click on the Vite and React logos to learn more 163 |
164 | Click on the Vite and React logos to learn more 165 |
166 | Click on the Vite and React logos to learn more 167 |
168 | Click on the Vite and React logos to learn more 169 |
170 | Click on the Vite and React logos to learn more 171 |
172 |

173 |
174 |
175 | 176 |

177 | Click on the Vite and React logos to learn more 178 |
179 | Click on the Vite and React logos to learn more 180 |
181 | Click on the Vite and React logos to learn more 182 |
183 | Click on the Vite and React logos to learn more 184 |
185 | Click on the Vite and React logos to learn more 186 |
187 | Click on the Vite and React logos to learn more 188 |
189 | Click on the Vite and React logos to learn more 190 |
191 | Click on the Vite and React logos to learn more 192 |
193 | Click on the Vite and React logos to learn more 194 |
195 | Click on the Vite and React logos to learn more 196 |
197 | Click on the Vite and React logos to learn more 198 |
199 | Click on the Vite and React logos to learn more 200 |
201 | Click on the Vite and React logos to learn more 202 |
203 | Click on the Vite and React logos to learn more 204 |
205 | Click on the Vite and React logos to learn more 206 |
207 | Click on the Vite and React logos to learn more 208 |
209 | Click on the Vite and React logos to learn more 210 |
211 | Click on the Vite and React logos to learn more 212 |
213 | Click on the Vite and React logos to learn more 214 |
215 | Click on the Vite and React logos to learn more 216 |
217 | Click on the Vite and React logos to learn more 218 |
219 | Click on the Vite and React logos to learn more 220 |
221 | Click on the Vite and React logos to learn more 222 |
223 | Click on the Vite and React logos to learn more 224 |
225 | Click on the Vite and React logos to learn more 226 |
227 | Click on the Vite and React logos to learn more 228 |
229 | Click on the Vite and React logos to learn more 230 |
231 | Click on the Vite and React logos to learn more 232 |
233 | Click on the Vite and React logos to learn more 234 |
235 | Click on the Vite and React logos to learn more 236 |
237 | Click on the Vite and React logos to learn more 238 |
239 | Click on the Vite and React logos to learn more 240 |
241 | Click on the Vite and React logos to learn more 242 |
243 | Click on the Vite and React logos to learn more 244 |
245 | Click on the Vite and React logos to learn more 246 |
247 | Click on the Vite and React logos to learn more 248 |
249 | Click on the Vite and React logos to learn more 250 |
251 | Click on the Vite and React logos to learn more 252 |
253 | Click on the Vite and React logos to learn more 254 |
255 | Click on the Vite and React logos to learn more 256 |
257 | Click on the Vite and React logos to learn more 258 |
259 | Click on the Vite and React logos to learn more 260 |
261 | Click on the Vite and React logos to learn more 262 |
263 | Click on the Vite and React logos to learn more 264 |
265 | Click on the Vite and React logos to learn more 266 |
267 | Click on the Vite and React logos to learn more 268 |
269 | Click on the Vite and React logos to learn more 270 |
271 | Click on the Vite and React logos to learn more 272 |
273 | Click on the Vite and React logos to learn more 274 |
275 | Click on the Vite and React logos to learn more 276 |
277 | Click on the Vite and React logos to learn more 278 |
279 | Click on the Vite and React logos to learn more 280 |
281 | Click on the Vite and React logos to learn more 282 |
283 | Click on the Vite and React logos to learn more 284 |
285 | Click on the Vite and React logos to learn more 286 |
287 | Click on the Vite and React logos to learn more 288 |
289 | Click on the Vite and React logos to learn more 290 |
291 | Click on the Vite and React logos to learn more 292 |
293 | Click on the Vite and React logos to learn more 294 |
295 | Click on the Vite and React logos to learn more 296 |
297 | Click on the Vite and React logos to learn more 298 |
299 | Click on the Vite and React logos to learn more 300 |
301 | Click on the Vite and React logos to learn more 302 |
303 | Click on the Vite and React logos to learn more 304 |
305 | Click on the Vite and React logos to learn more 306 |
307 | Click on the Vite and React logos to learn more 308 |
309 |

310 | 311 | 337 | 338 | 339 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # body-scroll-lock-upgrade 2 | 3 | body-scroll-lock upgrade version, repair body-scroll-lock v4.0.0-beta.0 bug。 4 | 5 | They stopped maintenance. I had to work it out myself, using the same approach, with a new version of typeScript. And fix the original problem, available for everyone to use. 6 | 7 | # Changelog 8 | 9 | Refer to the [releases](https://github.com/rick-liruixin/body-scroll-lock-upgrade/releases) page. 10 | 11 | `If you think it works for you, please give me a star ⭐️ to encourage me` 12 | 13 | go to [github](https://github.com/rick-liruixin/body-scroll-lock-upgrade) ⭐️⭐️⭐️⭐️⭐️ 14 | 15 | # source document 16 | 17 |

Body scroll lock...just works with everything ;-)

18 | 19 | ## Why BSL? 20 | 21 | Enables body scroll locking (for iOS Mobile and Tablet, Android, desktop Safari/Chrome/Firefox) without breaking scrolling of a target element (eg. modal/lightbox/flyouts/nav-menus). 22 | 23 | _Features:_ 24 | 25 | - disables body scroll WITHOUT disabling scroll of a target element 26 | - works on iOS mobile/tablet (!!) 27 | - works on Android 28 | - works on Safari desktop 29 | - works on Chrome/Firefox 30 | - works with vanilla JS and frameworks such as React / Angular / VueJS 31 | - supports nested target elements (eg. a modal that appears on top of a flyout) 32 | - can reserve scrollbar width 33 | - `-webkit-overflow-scrolling: touch` still works 34 | 35 | _Aren't the alternative approaches sufficient?_ 36 | 37 | - the approach `document.body.ontouchmove = (e) => { e.preventDefault(); return false; };` locks the 38 | body scroll, but ALSO locks the scroll of a target element (eg. modal). 39 | - the approach `overflow: hidden` on the body or html elements doesn't work for all browsers 40 | - the `position: fixed` approach causes the body scroll to reset 41 | - some approaches break inertia/momentum/rubber-band scrolling on iOS 42 | 43 | ## Install 44 | 45 | $ yarn add body-scroll-lock-upgrade 46 | 47 | or 48 | 49 | $ npm install body-scroll-lock-upgrade 50 | 51 | You can also load via a `` tag (refer to the lib folder). 52 | 53 | ## Usage examples 54 | 55 | ##### Common JS 56 | 57 | ```javascript 58 | // 1. Import the functions 59 | const bodyScrollLock = require('body-scroll-lock-upgrade'); 60 | const disableBodyScroll = bodyScrollLock.disableBodyScroll; 61 | const enableBodyScroll = bodyScrollLock.enableBodyScroll; 62 | 63 | // 2. Get a target element that you want to persist scrolling for (such as a modal/lightbox/flyout/nav). 64 | // Specifically, the target element is the one we would like to allow scroll on (NOT a parent of that element). 65 | // This is also the element to apply the CSS '-webkit-overflow-scrolling: touch;' if desired. 66 | const targetElement = document.querySelector('#someElementId'); 67 | 68 | // 3. ...in some event handler after showing the target element...disable body scroll 69 | disableBodyScroll(targetElement); 70 | 71 | // 4. ...in some event handler after hiding the target element... 72 | enableBodyScroll(targetElement); 73 | ``` 74 | 75 | ##### React/ES6 76 | 77 | ```javascript 78 | // 1. Import the functions 79 | import { 80 | disableBodyScroll, 81 | enableBodyScroll, 82 | clearAllBodyScrollLocks, 83 | } from 'body-scroll-lock-upgrade'; 84 | 85 | class SomeComponent extends React.Component { 86 | targetElement = null; 87 | 88 | componentDidMount() { 89 | // 2. Get a target element that you want to persist scrolling for (such as a modal/lightbox/flyout/nav). 90 | // Specifically, the target element is the one we would like to allow scroll on (NOT a parent of that element). 91 | // This is also the element to apply the CSS '-webkit-overflow-scrolling: touch;' if desired. 92 | this.targetElement = document.querySelector('#targetElementId'); 93 | } 94 | 95 | showTargetElement = () => { 96 | // ... some logic to show target element 97 | 98 | // 3. Disable body scroll 99 | disableBodyScroll(this.targetElement); 100 | }; 101 | 102 | hideTargetElement = () => { 103 | // ... some logic to hide target element 104 | 105 | // 4. Re-enable body scroll 106 | enableBodyScroll(this.targetElement); 107 | }; 108 | 109 | componentWillUnmount() { 110 | // 5. Useful if we have called disableBodyScroll for multiple target elements, 111 | // and we just want a kill-switch to undo all that. 112 | // OR useful for if the `hideTargetElement()` function got circumvented eg. visitor 113 | // clicks a link which takes him/her to a different page within the app. 114 | clearAllBodyScrollLocks(); 115 | } 116 | 117 | render() { 118 | return
some JSX to go here
; 119 | } 120 | } 121 | ``` 122 | 123 | ##### React/ES6 with Refs 124 | 125 | ```javascript 126 | // 1. Import the functions 127 | import { 128 | disableBodyScroll, 129 | enableBodyScroll, 130 | clearAllBodyScrollLocks, 131 | } from 'body-scroll-lock-upgrade'; 132 | 133 | class SomeComponent extends React.Component { 134 | // 2. Initialise your ref and targetElement here 135 | targetRef = React.createRef(); 136 | targetElement = null; 137 | 138 | componentDidMount() { 139 | // 3. Get a target element that you want to persist scrolling for (such as a modal/lightbox/flyout/nav). 140 | // Specifically, the target element is the one we would like to allow scroll on (NOT a parent of that element). 141 | // This is also the element to apply the CSS '-webkit-overflow-scrolling: touch;' if desired. 142 | this.targetElement = this.targetRef.current; 143 | } 144 | 145 | showTargetElement = () => { 146 | // ... some logic to show target element 147 | 148 | // 4. Disable body scroll 149 | disableBodyScroll(this.targetElement); 150 | }; 151 | 152 | hideTargetElement = () => { 153 | // ... some logic to hide target element 154 | 155 | // 5. Re-enable body scroll 156 | enableBodyScroll(this.targetElement); 157 | }; 158 | 159 | componentWillUnmount() { 160 | // 5. Useful if we have called disableBodyScroll for multiple target elements, 161 | // and we just want a kill-switch to undo all that. 162 | // OR useful for if the `hideTargetElement()` function got circumvented eg. visitor 163 | // clicks a link which takes him/her to a different page within the app. 164 | clearAllBodyScrollLocks(); 165 | } 166 | 167 | render() { 168 | return ( 169 | // 6. Pass your ref with the reference to the targetElement to SomeOtherComponent 170 | 171 | some JSX to go here 172 | 173 | ); 174 | } 175 | } 176 | 177 | // 7. SomeOtherComponent needs to be a Class component to receive the ref (unless Hooks - https://reactjs.org/docs/hooks-faq.html#can-i-make-a-ref-to-a-function-component - are used). 178 | class SomeOtherComponent extends React.Component { 179 | componentDidMount() { 180 | // Your logic on mount goes here 181 | } 182 | 183 | // 8. BSL will be applied to div below in SomeOtherComponent and persist scrolling for the container 184 | render() { 185 | return
some JSX to go here
; 186 | } 187 | } 188 | ``` 189 | 190 | ##### Angular 191 | 192 | ```javascript 193 | import { Component, ElementRef, OnDestroy, ViewChild } from '@angular/core'; 194 | 195 | // 1. Import the functions 196 | import { 197 | disableBodyScroll, 198 | enableBodyScroll, 199 | clearAllBodyScrollLocks, 200 | } from 'body-scroll-lock-upgrade'; 201 | 202 | @Component({ 203 | selector: 'app-scroll-block', 204 | templateUrl: './scroll-block.component.html', 205 | styleUrls: ['./scroll-block.component.css'], 206 | }) 207 | export class SomeComponent implements OnDestroy { 208 | // 2. Get a target element that you want to persist scrolling for (such as a modal/lightbox/flyout/nav). 209 | // Specifically, the target element is the one we would like to allow scroll on (NOT a parent of that element). 210 | // This is also the element to apply the CSS '-webkit-overflow-scrolling: touch;' if desired. 211 | @ViewChild('scrollTarget') scrollTarget: ElementRef; 212 | 213 | showTargetElement() { 214 | // ... some logic to show target element 215 | 216 | // 3. Disable body scroll 217 | disableBodyScroll(this.scrollTarget.nativeElement); 218 | } 219 | 220 | hideTargetElement() { 221 | // ... some logic to hide target element 222 | 223 | // 4. Re-enable body scroll 224 | enableBodyScroll(this.scrollTarget.nativeElement); 225 | } 226 | 227 | ngOnDestroy() { 228 | // 5. Useful if we have called disableBodyScroll for multiple target elements, 229 | // and we just want a kill-switch to undo all that. 230 | // OR useful for if the `hideTargetElement()` function got circumvented eg. visitor 231 | // clicks a link which takes him/her to a different page within the app. 232 | clearAllBodyScrollLocks(); 233 | } 234 | } 235 | ``` 236 | 237 | ##### Vanilla JS 238 | 239 | Then in the javascript: 240 | 241 | ```javascript 242 | 243 | 267 | 268 | // UMD 269 | 270 | 286 | ``` 287 | 288 | ## Demo 289 | 290 | Check out the demo, powered by Vercel. 291 | 292 | - https://bodyscrolllock.vercel.app for a basic example 293 | - https://bodyscrolllock-modal.vercel.app for an example with a modal. 294 | 295 | ## Functions 296 | 297 | | Function | Arguments | Return | Description | 298 | | :------------------------ | :------------------------------------------------------------- | :----: | :----------------------------------------------------------- | 299 | | `disableBodyScroll` | `targetElement: HTMLElement`
`options: BodyScrollOptions` | `void` | Disables body scroll while enabling scroll on target element | 300 | | `enableBodyScroll` | `targetElement: HTMLElement` | `void` | Enables body scroll and removing listeners on target element | 301 | | `clearAllBodyScrollLocks` | `null` | `void` | Clears all scroll locks | 302 | 303 | ## Options 304 | 305 | ### reserveScrollBarGap 306 | 307 | **optional, default:** false 308 | 309 | If the overflow property of the body is set to hidden, the body widens by the width of the scrollbar. This produces an 310 | unpleasant flickering effect, especially on websites with centered content. If the `reserveScrollBarGap` option is set, 311 | this gap is filled by a `padding-right` on the body element. If `disableBodyScroll` is called for the last target element, 312 | or `clearAllBodyScrollLocks` is called, the `padding-right` is automatically reset to the previous value. 313 | 314 | ```js 315 | import { disableBodyScroll } from 'body-scroll-lock-upgrade'; 316 | import type { BodyScrollOptions } from 'body-scroll-lock-upgrade'; 317 | 318 | const options: BodyScrollOptions = { 319 | reserveScrollBarGap: true, 320 | }; 321 | 322 | disableBodyScroll(targetElement, options); 323 | ``` 324 | 325 | ### allowTouchMove 326 | 327 | **optional, default:** undefined 328 | 329 | To disable scrolling on iOS, `disableBodyScroll` prevents `touchmove` events. 330 | However, there are cases where you have called `disableBodyScroll` on an 331 | element, but its children still require `touchmove` events to function. 332 | 333 | See below for 2 use cases: 334 | 335 | ##### Simple 336 | 337 | ```javascript 338 | disableBodyScroll(container, { 339 | allowTouchMove: (el) => el.tagName === 'TEXTAREA', 340 | }); 341 | ``` 342 | 343 | ##### More Complex 344 | 345 | Javascript: 346 | 347 | ```javascript 348 | disableBodyScroll(container, { 349 | allowTouchMove: (el) => { 350 | while (el && el !== document.body) { 351 | if (el.getAttribute('body-scroll-lock-ignore') !== null) { 352 | return true; 353 | } 354 | 355 | el = el.parentElement; 356 | } 357 | }, 358 | }); 359 | ``` 360 | 361 | Html: 362 | 363 | ```html 364 |
365 |
...
366 |
367 | ``` 368 | 369 | ## References 370 | 371 | https://medium.com/jsdownunder/locking-body-scroll-for-all-devices-22def9615177 372 | https://stackoverflow.com/questions/41594997/ios-10-safari-prevent-scrolling-behind-a-fixed-overlay-and-maintain-scroll-posi 373 | -------------------------------------------------------------------------------- /lib/index.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.js","sources":["../src/body-scroll-lock.ts"],"sourcesContent":["export type BodyScrollOptions = {\n reserveScrollBarGap?: boolean | undefined;\n allowTouchMove?: ((el: EventTarget) => boolean) | undefined;\n};\n\ntype BodyStyleType = {\n position: string;\n top: string;\n left: string;\n width: string;\n height: string;\n overflow: string;\n};\n\ninterface Lock {\n targetElement: HTMLElement;\n options: BodyScrollOptions;\n}\n\n// Older browsers don't support event options, feature detect it.\nlet hasPassiveEvents = false;\nif (typeof window !== 'undefined') {\n const passiveTestOptions: any = {\n get passive() {\n hasPassiveEvents = true;\n return undefined;\n },\n };\n (window as any).addEventListener('testPassive', null, passiveTestOptions);\n (window as any).removeEventListener('testPassive', null, passiveTestOptions);\n}\n\nconst isIosDevice =\n typeof window !== 'undefined' &&\n window.navigator &&\n window.navigator.platform &&\n (/iP(ad|hone|od)/.test(window.navigator.platform) ||\n (window.navigator.platform === 'MacIntel' &&\n window.navigator.maxTouchPoints > 1));\ntype HandleScrollEvent = TouchEvent;\n\nlet locks: Array = [];\nlet locksIndex: Map = new Map();\nlet documentListenerAdded: boolean = false;\nlet initialClientY: number = -1;\nlet previousBodyOverflowSetting: string | undefined;\nlet htmlStyle:\n | {\n height: string;\n overflow: string;\n }\n | undefined;\nlet bodyStyle: BodyStyleType | undefined;\n\nlet previousBodyPaddingRight: string | undefined;\n\n// returns true if `el` should be allowed to receive touchmove events.\nconst allowTouchMove = (el: EventTarget): boolean =>\n locks.some((lock) => {\n if (lock.options.allowTouchMove && lock.options.allowTouchMove(el)) {\n return true;\n }\n\n return false;\n });\n\nconst preventDefault = (rawEvent: HandleScrollEvent): boolean => {\n const e: any = rawEvent || window.event;\n\n // For the case whereby consumers adds a touchmove event listener to document.\n // Recall that we do document.addEventListener('touchmove', preventDefault, { passive: false })\n // in disableBodyScroll - so if we provide this opportunity to allowTouchMove, then\n // the touchmove event on document will break.\n if (allowTouchMove(e.target)) {\n return true;\n }\n\n // Do not prevent if the event has more than one touch (usually meaning this is a multi touch gesture like pinch to zoom).\n if (e.touches.length > 1) return true;\n\n if (e.preventDefault) e.preventDefault();\n\n return false;\n};\n\nconst setOverflowHidden = (options?: BodyScrollOptions) => {\n // If previousBodyPaddingRight is already set, don't set it again.\n if (previousBodyPaddingRight === undefined) {\n const reserveScrollBarGap =\n !!options && options.reserveScrollBarGap === true;\n const scrollBarGap =\n window.innerWidth -\n document.documentElement.getBoundingClientRect().width;\n\n if (reserveScrollBarGap && scrollBarGap > 0) {\n const computedBodyPaddingRight = parseInt(\n window\n .getComputedStyle(document.body)\n .getPropertyValue('padding-right'),\n 10\n );\n previousBodyPaddingRight = document.body.style.paddingRight;\n document.body.style.paddingRight = `${\n computedBodyPaddingRight + scrollBarGap\n }px`;\n }\n }\n\n // If previousBodyOverflowSetting is already set, don't set it again.\n if (previousBodyOverflowSetting === undefined) {\n previousBodyOverflowSetting = document.body.style.overflow;\n document.body.style.overflow = 'hidden';\n }\n};\n\nconst restoreOverflowSetting = () => {\n if (previousBodyPaddingRight !== undefined) {\n document.body.style.paddingRight = previousBodyPaddingRight;\n\n // Restore previousBodyPaddingRight to undefined so setOverflowHidden knows it\n // can be set again.\n previousBodyPaddingRight = undefined;\n }\n\n if (previousBodyOverflowSetting !== undefined) {\n document.body.style.overflow = previousBodyOverflowSetting;\n\n // Restore previousBodyOverflowSetting to undefined\n // so setOverflowHidden knows it can be set again.\n previousBodyOverflowSetting = undefined;\n }\n};\n\nconst setPositionFixed = () =>\n window.requestAnimationFrame(() => {\n const $html = document.documentElement;\n const $body = document.body;\n // If bodyStyle is already set, don't set it again.\n if (bodyStyle === undefined) {\n htmlStyle = { ...$html.style };\n bodyStyle = { ...$body.style };\n\n // Update the dom inside an animation frame\n const { scrollY, scrollX, innerHeight } = window;\n\n $html.style.height = '100%';\n $html.style.overflow = 'hidden';\n\n $body.style.position = 'fixed';\n $body.style.top = `${-scrollY}px`;\n $body.style.left = `${-scrollX}px`;\n $body.style.width = '100%';\n $body.style.height = 'auto';\n $body.style.overflow = 'hidden';\n\n setTimeout(\n () =>\n window.requestAnimationFrame(() => {\n // Attempt to check if the bottom bar appeared due to the position change\n const bottomBarHeight = innerHeight - window.innerHeight;\n if (bottomBarHeight && scrollY >= innerHeight) {\n // Move the content further up so that the bottom bar doesn't hide it\n $body.style.top = -(scrollY + bottomBarHeight) + 'px';\n }\n }),\n 300\n );\n }\n });\n\nconst restorePositionSetting = () => {\n if (bodyStyle !== undefined) {\n // Convert the position from \"px\" to Int\n const y = -parseInt(document.body.style.top, 10);\n const x = -parseInt(document.body.style.left, 10);\n\n // Restore styles\n const $html = document.documentElement;\n const $body = document.body;\n\n $html.style.height = htmlStyle?.height || '';\n $html.style.overflow = htmlStyle?.overflow || '';\n\n $body.style.position = bodyStyle.position || '';\n $body.style.top = bodyStyle.top || '';\n $body.style.left = bodyStyle.left || '';\n $body.style.width = bodyStyle.width || '';\n $body.style.height = bodyStyle.height || '';\n $body.style.overflow = bodyStyle.overflow || '';\n\n // Restore scroll\n window.scrollTo(x, y);\n\n bodyStyle = undefined;\n }\n};\n\n// https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight#Problems_and_solutions\nconst isTargetElementTotallyScrolled = (targetElement: HTMLElement): boolean =>\n targetElement\n ? targetElement.scrollHeight - targetElement.scrollTop <=\n targetElement.clientHeight\n : false;\n\nconst handleScroll = (\n event: HandleScrollEvent,\n targetElement: HTMLElement\n): boolean => {\n const clientY = event.targetTouches[0].clientY - initialClientY;\n\n if (allowTouchMove(event.target as EventTarget)) {\n return false;\n }\n\n if (targetElement && targetElement.scrollTop === 0 && clientY > 0) {\n // element is at the top of its scroll.\n return preventDefault(event);\n }\n\n if (isTargetElementTotallyScrolled(targetElement) && clientY < 0) {\n // element is at the bottom of its scroll.\n return preventDefault(event);\n }\n\n event.stopPropagation();\n return true;\n};\n\n/**\n *\n * @param targetElement HTMLElement\n * @param options BodyScrollOptions\n * @returns void\n */\nexport const disableBodyScroll = (\n targetElement: HTMLElement,\n options?: BodyScrollOptions\n): void => {\n // targetElement must be provided\n if (!targetElement) {\n // eslint-disable-next-line no-console\n console.error(\n 'disableBodyScroll unsuccessful - targetElement must be provided when calling disableBodyScroll on IOS devices.'\n );\n return;\n }\n\n locksIndex.set(\n targetElement,\n locksIndex?.get(targetElement)\n ? (locksIndex?.get(targetElement) as number) + 1\n : 1\n );\n // disableBodyScroll must not have been called on this targetElement before\n if (locks.some((lock) => lock.targetElement === targetElement)) {\n return;\n }\n\n const lock = {\n targetElement,\n options: options || {},\n };\n locks = [...locks, lock];\n\n if (isIosDevice) {\n setPositionFixed();\n } else {\n setOverflowHidden(options);\n }\n\n if (isIosDevice) {\n targetElement.ontouchstart = (event: HandleScrollEvent) => {\n if (event.targetTouches.length === 1) {\n // detect single touch.\n initialClientY = event.targetTouches[0].clientY;\n }\n };\n targetElement.ontouchmove = (event: HandleScrollEvent) => {\n if (event.targetTouches.length === 1) {\n // detect single touch.\n handleScroll(event, targetElement);\n }\n };\n\n if (!documentListenerAdded) {\n document.addEventListener(\n 'touchmove',\n preventDefault,\n hasPassiveEvents ? { passive: false } : undefined\n );\n documentListenerAdded = true;\n }\n }\n};\n\nexport const clearAllBodyScrollLocks = (): void => {\n if (isIosDevice) {\n // Clear all locks ontouchstart/ontouchmove handlers, and the references.\n locks.forEach((lock: Lock) => {\n lock.targetElement.ontouchstart = null;\n lock.targetElement.ontouchmove = null;\n });\n\n if (documentListenerAdded) {\n (document as any).removeEventListener(\n 'touchmove',\n preventDefault,\n hasPassiveEvents ? { passive: false } : undefined\n );\n documentListenerAdded = false;\n }\n\n // Reset initial clientY.\n initialClientY = -1;\n }\n\n if (isIosDevice) {\n restorePositionSetting();\n } else {\n restoreOverflowSetting();\n }\n\n locks = [];\n locksIndex.clear();\n};\n\n/**\n * @param targetElement\n * @returns void\n */\nexport const enableBodyScroll = (targetElement: HTMLElement): void => {\n if (!targetElement) {\n // eslint-disable-next-line no-console\n console.error(\n 'enableBodyScroll unsuccessful - targetElement must be provided when calling enableBodyScroll on IOS devices.'\n );\n return;\n }\n\n locksIndex.set(\n targetElement,\n locksIndex?.get(targetElement)\n ? (locksIndex?.get(targetElement) as number) - 1\n : 0\n );\n if (locksIndex?.get(targetElement) === 0) {\n locks = locks.filter((lock) => lock.targetElement !== targetElement);\n locksIndex?.delete(targetElement);\n }\n\n if (isIosDevice) {\n targetElement.ontouchstart = null;\n targetElement.ontouchmove = null;\n\n if (documentListenerAdded && locks.length === 0) {\n (document as any).removeEventListener(\n 'touchmove',\n preventDefault,\n hasPassiveEvents ? { passive: false } : undefined\n );\n documentListenerAdded = false;\n }\n }\n\n if (locks.length === 0) {\n if (isIosDevice) {\n restorePositionSetting();\n } else {\n restoreOverflowSetting();\n }\n }\n};\n"],"names":["lock"],"mappings":"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"} -------------------------------------------------------------------------------- /lib/index.esm.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.esm.js","sources":["../src/body-scroll-lock.ts"],"sourcesContent":["export type BodyScrollOptions = {\n reserveScrollBarGap?: boolean | undefined;\n allowTouchMove?: ((el: EventTarget) => boolean) | undefined;\n};\n\ntype BodyStyleType = {\n position: string;\n top: string;\n left: string;\n width: string;\n height: string;\n overflow: string;\n};\n\ninterface Lock {\n targetElement: HTMLElement;\n options: BodyScrollOptions;\n}\n\n// Older browsers don't support event options, feature detect it.\nlet hasPassiveEvents = false;\nif (typeof window !== 'undefined') {\n const passiveTestOptions: any = {\n get passive() {\n hasPassiveEvents = true;\n return undefined;\n },\n };\n (window as any).addEventListener('testPassive', null, passiveTestOptions);\n (window as any).removeEventListener('testPassive', null, passiveTestOptions);\n}\n\nconst isIosDevice =\n typeof window !== 'undefined' &&\n window.navigator &&\n window.navigator.platform &&\n (/iP(ad|hone|od)/.test(window.navigator.platform) ||\n (window.navigator.platform === 'MacIntel' &&\n window.navigator.maxTouchPoints > 1));\ntype HandleScrollEvent = TouchEvent;\n\nlet locks: Array = [];\nlet locksIndex: Map = new Map();\nlet documentListenerAdded: boolean = false;\nlet initialClientY: number = -1;\nlet previousBodyOverflowSetting: string | undefined;\nlet htmlStyle:\n | {\n height: string;\n overflow: string;\n }\n | undefined;\nlet bodyStyle: BodyStyleType | undefined;\n\nlet previousBodyPaddingRight: string | undefined;\n\n// returns true if `el` should be allowed to receive touchmove events.\nconst allowTouchMove = (el: EventTarget): boolean =>\n locks.some((lock) => {\n if (lock.options.allowTouchMove && lock.options.allowTouchMove(el)) {\n return true;\n }\n\n return false;\n });\n\nconst preventDefault = (rawEvent: HandleScrollEvent): boolean => {\n const e: any = rawEvent || window.event;\n\n // For the case whereby consumers adds a touchmove event listener to document.\n // Recall that we do document.addEventListener('touchmove', preventDefault, { passive: false })\n // in disableBodyScroll - so if we provide this opportunity to allowTouchMove, then\n // the touchmove event on document will break.\n if (allowTouchMove(e.target)) {\n return true;\n }\n\n // Do not prevent if the event has more than one touch (usually meaning this is a multi touch gesture like pinch to zoom).\n if (e.touches.length > 1) return true;\n\n if (e.preventDefault) e.preventDefault();\n\n return false;\n};\n\nconst setOverflowHidden = (options?: BodyScrollOptions) => {\n // If previousBodyPaddingRight is already set, don't set it again.\n if (previousBodyPaddingRight === undefined) {\n const reserveScrollBarGap =\n !!options && options.reserveScrollBarGap === true;\n const scrollBarGap =\n window.innerWidth -\n document.documentElement.getBoundingClientRect().width;\n\n if (reserveScrollBarGap && scrollBarGap > 0) {\n const computedBodyPaddingRight = parseInt(\n window\n .getComputedStyle(document.body)\n .getPropertyValue('padding-right'),\n 10\n );\n previousBodyPaddingRight = document.body.style.paddingRight;\n document.body.style.paddingRight = `${\n computedBodyPaddingRight + scrollBarGap\n }px`;\n }\n }\n\n // If previousBodyOverflowSetting is already set, don't set it again.\n if (previousBodyOverflowSetting === undefined) {\n previousBodyOverflowSetting = document.body.style.overflow;\n document.body.style.overflow = 'hidden';\n }\n};\n\nconst restoreOverflowSetting = () => {\n if (previousBodyPaddingRight !== undefined) {\n document.body.style.paddingRight = previousBodyPaddingRight;\n\n // Restore previousBodyPaddingRight to undefined so setOverflowHidden knows it\n // can be set again.\n previousBodyPaddingRight = undefined;\n }\n\n if (previousBodyOverflowSetting !== undefined) {\n document.body.style.overflow = previousBodyOverflowSetting;\n\n // Restore previousBodyOverflowSetting to undefined\n // so setOverflowHidden knows it can be set again.\n previousBodyOverflowSetting = undefined;\n }\n};\n\nconst setPositionFixed = () =>\n window.requestAnimationFrame(() => {\n const $html = document.documentElement;\n const $body = document.body;\n // If bodyStyle is already set, don't set it again.\n if (bodyStyle === undefined) {\n htmlStyle = { ...$html.style };\n bodyStyle = { ...$body.style };\n\n // Update the dom inside an animation frame\n const { scrollY, scrollX, innerHeight } = window;\n\n $html.style.height = '100%';\n $html.style.overflow = 'hidden';\n\n $body.style.position = 'fixed';\n $body.style.top = `${-scrollY}px`;\n $body.style.left = `${-scrollX}px`;\n $body.style.width = '100%';\n $body.style.height = 'auto';\n $body.style.overflow = 'hidden';\n\n setTimeout(\n () =>\n window.requestAnimationFrame(() => {\n // Attempt to check if the bottom bar appeared due to the position change\n const bottomBarHeight = innerHeight - window.innerHeight;\n if (bottomBarHeight && scrollY >= innerHeight) {\n // Move the content further up so that the bottom bar doesn't hide it\n $body.style.top = -(scrollY + bottomBarHeight) + 'px';\n }\n }),\n 300\n );\n }\n });\n\nconst restorePositionSetting = () => {\n if (bodyStyle !== undefined) {\n // Convert the position from \"px\" to Int\n const y = -parseInt(document.body.style.top, 10);\n const x = -parseInt(document.body.style.left, 10);\n\n // Restore styles\n const $html = document.documentElement;\n const $body = document.body;\n\n $html.style.height = htmlStyle?.height || '';\n $html.style.overflow = htmlStyle?.overflow || '';\n\n $body.style.position = bodyStyle.position || '';\n $body.style.top = bodyStyle.top || '';\n $body.style.left = bodyStyle.left || '';\n $body.style.width = bodyStyle.width || '';\n $body.style.height = bodyStyle.height || '';\n $body.style.overflow = bodyStyle.overflow || '';\n\n // Restore scroll\n window.scrollTo(x, y);\n\n bodyStyle = undefined;\n }\n};\n\n// https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight#Problems_and_solutions\nconst isTargetElementTotallyScrolled = (targetElement: HTMLElement): boolean =>\n targetElement\n ? targetElement.scrollHeight - targetElement.scrollTop <=\n targetElement.clientHeight\n : false;\n\nconst handleScroll = (\n event: HandleScrollEvent,\n targetElement: HTMLElement\n): boolean => {\n const clientY = event.targetTouches[0].clientY - initialClientY;\n\n if (allowTouchMove(event.target as EventTarget)) {\n return false;\n }\n\n if (targetElement && targetElement.scrollTop === 0 && clientY > 0) {\n // element is at the top of its scroll.\n return preventDefault(event);\n }\n\n if (isTargetElementTotallyScrolled(targetElement) && clientY < 0) {\n // element is at the bottom of its scroll.\n return preventDefault(event);\n }\n\n event.stopPropagation();\n return true;\n};\n\n/**\n *\n * @param targetElement HTMLElement\n * @param options BodyScrollOptions\n * @returns void\n */\nexport const disableBodyScroll = (\n targetElement: HTMLElement,\n options?: BodyScrollOptions\n): void => {\n // targetElement must be provided\n if (!targetElement) {\n // eslint-disable-next-line no-console\n console.error(\n 'disableBodyScroll unsuccessful - targetElement must be provided when calling disableBodyScroll on IOS devices.'\n );\n return;\n }\n\n locksIndex.set(\n targetElement,\n locksIndex?.get(targetElement)\n ? (locksIndex?.get(targetElement) as number) + 1\n : 1\n );\n // disableBodyScroll must not have been called on this targetElement before\n if (locks.some((lock) => lock.targetElement === targetElement)) {\n return;\n }\n\n const lock = {\n targetElement,\n options: options || {},\n };\n locks = [...locks, lock];\n\n if (isIosDevice) {\n setPositionFixed();\n } else {\n setOverflowHidden(options);\n }\n\n if (isIosDevice) {\n targetElement.ontouchstart = (event: HandleScrollEvent) => {\n if (event.targetTouches.length === 1) {\n // detect single touch.\n initialClientY = event.targetTouches[0].clientY;\n }\n };\n targetElement.ontouchmove = (event: HandleScrollEvent) => {\n if (event.targetTouches.length === 1) {\n // detect single touch.\n handleScroll(event, targetElement);\n }\n };\n\n if (!documentListenerAdded) {\n document.addEventListener(\n 'touchmove',\n preventDefault,\n hasPassiveEvents ? { passive: false } : undefined\n );\n documentListenerAdded = true;\n }\n }\n};\n\nexport const clearAllBodyScrollLocks = (): void => {\n if (isIosDevice) {\n // Clear all locks ontouchstart/ontouchmove handlers, and the references.\n locks.forEach((lock: Lock) => {\n lock.targetElement.ontouchstart = null;\n lock.targetElement.ontouchmove = null;\n });\n\n if (documentListenerAdded) {\n (document as any).removeEventListener(\n 'touchmove',\n preventDefault,\n hasPassiveEvents ? { passive: false } : undefined\n );\n documentListenerAdded = false;\n }\n\n // Reset initial clientY.\n initialClientY = -1;\n }\n\n if (isIosDevice) {\n restorePositionSetting();\n } else {\n restoreOverflowSetting();\n }\n\n locks = [];\n locksIndex.clear();\n};\n\n/**\n * @param targetElement\n * @returns void\n */\nexport const enableBodyScroll = (targetElement: HTMLElement): void => {\n if (!targetElement) {\n // eslint-disable-next-line no-console\n console.error(\n 'enableBodyScroll unsuccessful - targetElement must be provided when calling enableBodyScroll on IOS devices.'\n );\n return;\n }\n\n locksIndex.set(\n targetElement,\n locksIndex?.get(targetElement)\n ? (locksIndex?.get(targetElement) as number) - 1\n : 0\n );\n if (locksIndex?.get(targetElement) === 0) {\n locks = locks.filter((lock) => lock.targetElement !== targetElement);\n locksIndex?.delete(targetElement);\n }\n\n if (isIosDevice) {\n targetElement.ontouchstart = null;\n targetElement.ontouchmove = null;\n\n if (documentListenerAdded && locks.length === 0) {\n (document as any).removeEventListener(\n 'touchmove',\n preventDefault,\n hasPassiveEvents ? { passive: false } : undefined\n );\n documentListenerAdded = false;\n }\n }\n\n if (locks.length === 0) {\n if (isIosDevice) {\n restorePositionSetting();\n } else {\n restoreOverflowSetting();\n }\n }\n};\n"],"names":["lock"],"mappings":"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"} -------------------------------------------------------------------------------- /lib/index.umd.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.umd.js","sources":["../src/body-scroll-lock.ts"],"sourcesContent":["export type BodyScrollOptions = {\n reserveScrollBarGap?: boolean | undefined;\n allowTouchMove?: ((el: EventTarget) => boolean) | undefined;\n};\n\ntype BodyStyleType = {\n position: string;\n top: string;\n left: string;\n width: string;\n height: string;\n overflow: string;\n};\n\ninterface Lock {\n targetElement: HTMLElement;\n options: BodyScrollOptions;\n}\n\n// Older browsers don't support event options, feature detect it.\nlet hasPassiveEvents = false;\nif (typeof window !== 'undefined') {\n const passiveTestOptions: any = {\n get passive() {\n hasPassiveEvents = true;\n return undefined;\n },\n };\n (window as any).addEventListener('testPassive', null, passiveTestOptions);\n (window as any).removeEventListener('testPassive', null, passiveTestOptions);\n}\n\nconst isIosDevice =\n typeof window !== 'undefined' &&\n window.navigator &&\n window.navigator.platform &&\n (/iP(ad|hone|od)/.test(window.navigator.platform) ||\n (window.navigator.platform === 'MacIntel' &&\n window.navigator.maxTouchPoints > 1));\ntype HandleScrollEvent = TouchEvent;\n\nlet locks: Array = [];\nlet locksIndex: Map = new Map();\nlet documentListenerAdded: boolean = false;\nlet initialClientY: number = -1;\nlet previousBodyOverflowSetting: string | undefined;\nlet htmlStyle:\n | {\n height: string;\n overflow: string;\n }\n | undefined;\nlet bodyStyle: BodyStyleType | undefined;\n\nlet previousBodyPaddingRight: string | undefined;\n\n// returns true if `el` should be allowed to receive touchmove events.\nconst allowTouchMove = (el: EventTarget): boolean =>\n locks.some((lock) => {\n if (lock.options.allowTouchMove && lock.options.allowTouchMove(el)) {\n return true;\n }\n\n return false;\n });\n\nconst preventDefault = (rawEvent: HandleScrollEvent): boolean => {\n const e: any = rawEvent || window.event;\n\n // For the case whereby consumers adds a touchmove event listener to document.\n // Recall that we do document.addEventListener('touchmove', preventDefault, { passive: false })\n // in disableBodyScroll - so if we provide this opportunity to allowTouchMove, then\n // the touchmove event on document will break.\n if (allowTouchMove(e.target)) {\n return true;\n }\n\n // Do not prevent if the event has more than one touch (usually meaning this is a multi touch gesture like pinch to zoom).\n if (e.touches.length > 1) return true;\n\n if (e.preventDefault) e.preventDefault();\n\n return false;\n};\n\nconst setOverflowHidden = (options?: BodyScrollOptions) => {\n // If previousBodyPaddingRight is already set, don't set it again.\n if (previousBodyPaddingRight === undefined) {\n const reserveScrollBarGap =\n !!options && options.reserveScrollBarGap === true;\n const scrollBarGap =\n window.innerWidth -\n document.documentElement.getBoundingClientRect().width;\n\n if (reserveScrollBarGap && scrollBarGap > 0) {\n const computedBodyPaddingRight = parseInt(\n window\n .getComputedStyle(document.body)\n .getPropertyValue('padding-right'),\n 10\n );\n previousBodyPaddingRight = document.body.style.paddingRight;\n document.body.style.paddingRight = `${\n computedBodyPaddingRight + scrollBarGap\n }px`;\n }\n }\n\n // If previousBodyOverflowSetting is already set, don't set it again.\n if (previousBodyOverflowSetting === undefined) {\n previousBodyOverflowSetting = document.body.style.overflow;\n document.body.style.overflow = 'hidden';\n }\n};\n\nconst restoreOverflowSetting = () => {\n if (previousBodyPaddingRight !== undefined) {\n document.body.style.paddingRight = previousBodyPaddingRight;\n\n // Restore previousBodyPaddingRight to undefined so setOverflowHidden knows it\n // can be set again.\n previousBodyPaddingRight = undefined;\n }\n\n if (previousBodyOverflowSetting !== undefined) {\n document.body.style.overflow = previousBodyOverflowSetting;\n\n // Restore previousBodyOverflowSetting to undefined\n // so setOverflowHidden knows it can be set again.\n previousBodyOverflowSetting = undefined;\n }\n};\n\nconst setPositionFixed = () =>\n window.requestAnimationFrame(() => {\n const $html = document.documentElement;\n const $body = document.body;\n // If bodyStyle is already set, don't set it again.\n if (bodyStyle === undefined) {\n htmlStyle = { ...$html.style };\n bodyStyle = { ...$body.style };\n\n // Update the dom inside an animation frame\n const { scrollY, scrollX, innerHeight } = window;\n\n $html.style.height = '100%';\n $html.style.overflow = 'hidden';\n\n $body.style.position = 'fixed';\n $body.style.top = `${-scrollY}px`;\n $body.style.left = `${-scrollX}px`;\n $body.style.width = '100%';\n $body.style.height = 'auto';\n $body.style.overflow = 'hidden';\n\n setTimeout(\n () =>\n window.requestAnimationFrame(() => {\n // Attempt to check if the bottom bar appeared due to the position change\n const bottomBarHeight = innerHeight - window.innerHeight;\n if (bottomBarHeight && scrollY >= innerHeight) {\n // Move the content further up so that the bottom bar doesn't hide it\n $body.style.top = -(scrollY + bottomBarHeight) + 'px';\n }\n }),\n 300\n );\n }\n });\n\nconst restorePositionSetting = () => {\n if (bodyStyle !== undefined) {\n // Convert the position from \"px\" to Int\n const y = -parseInt(document.body.style.top, 10);\n const x = -parseInt(document.body.style.left, 10);\n\n // Restore styles\n const $html = document.documentElement;\n const $body = document.body;\n\n $html.style.height = htmlStyle?.height || '';\n $html.style.overflow = htmlStyle?.overflow || '';\n\n $body.style.position = bodyStyle.position || '';\n $body.style.top = bodyStyle.top || '';\n $body.style.left = bodyStyle.left || '';\n $body.style.width = bodyStyle.width || '';\n $body.style.height = bodyStyle.height || '';\n $body.style.overflow = bodyStyle.overflow || '';\n\n // Restore scroll\n window.scrollTo(x, y);\n\n bodyStyle = undefined;\n }\n};\n\n// https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight#Problems_and_solutions\nconst isTargetElementTotallyScrolled = (targetElement: HTMLElement): boolean =>\n targetElement\n ? targetElement.scrollHeight - targetElement.scrollTop <=\n targetElement.clientHeight\n : false;\n\nconst handleScroll = (\n event: HandleScrollEvent,\n targetElement: HTMLElement\n): boolean => {\n const clientY = event.targetTouches[0].clientY - initialClientY;\n\n if (allowTouchMove(event.target as EventTarget)) {\n return false;\n }\n\n if (targetElement && targetElement.scrollTop === 0 && clientY > 0) {\n // element is at the top of its scroll.\n return preventDefault(event);\n }\n\n if (isTargetElementTotallyScrolled(targetElement) && clientY < 0) {\n // element is at the bottom of its scroll.\n return preventDefault(event);\n }\n\n event.stopPropagation();\n return true;\n};\n\n/**\n *\n * @param targetElement HTMLElement\n * @param options BodyScrollOptions\n * @returns void\n */\nexport const disableBodyScroll = (\n targetElement: HTMLElement,\n options?: BodyScrollOptions\n): void => {\n // targetElement must be provided\n if (!targetElement) {\n // eslint-disable-next-line no-console\n console.error(\n 'disableBodyScroll unsuccessful - targetElement must be provided when calling disableBodyScroll on IOS devices.'\n );\n return;\n }\n\n locksIndex.set(\n targetElement,\n locksIndex?.get(targetElement)\n ? (locksIndex?.get(targetElement) as number) + 1\n : 1\n );\n // disableBodyScroll must not have been called on this targetElement before\n if (locks.some((lock) => lock.targetElement === targetElement)) {\n return;\n }\n\n const lock = {\n targetElement,\n options: options || {},\n };\n locks = [...locks, lock];\n\n if (isIosDevice) {\n setPositionFixed();\n } else {\n setOverflowHidden(options);\n }\n\n if (isIosDevice) {\n targetElement.ontouchstart = (event: HandleScrollEvent) => {\n if (event.targetTouches.length === 1) {\n // detect single touch.\n initialClientY = event.targetTouches[0].clientY;\n }\n };\n targetElement.ontouchmove = (event: HandleScrollEvent) => {\n if (event.targetTouches.length === 1) {\n // detect single touch.\n handleScroll(event, targetElement);\n }\n };\n\n if (!documentListenerAdded) {\n document.addEventListener(\n 'touchmove',\n preventDefault,\n hasPassiveEvents ? { passive: false } : undefined\n );\n documentListenerAdded = true;\n }\n }\n};\n\nexport const clearAllBodyScrollLocks = (): void => {\n if (isIosDevice) {\n // Clear all locks ontouchstart/ontouchmove handlers, and the references.\n locks.forEach((lock: Lock) => {\n lock.targetElement.ontouchstart = null;\n lock.targetElement.ontouchmove = null;\n });\n\n if (documentListenerAdded) {\n (document as any).removeEventListener(\n 'touchmove',\n preventDefault,\n hasPassiveEvents ? { passive: false } : undefined\n );\n documentListenerAdded = false;\n }\n\n // Reset initial clientY.\n initialClientY = -1;\n }\n\n if (isIosDevice) {\n restorePositionSetting();\n } else {\n restoreOverflowSetting();\n }\n\n locks = [];\n locksIndex.clear();\n};\n\n/**\n * @param targetElement\n * @returns void\n */\nexport const enableBodyScroll = (targetElement: HTMLElement): void => {\n if (!targetElement) {\n // eslint-disable-next-line no-console\n console.error(\n 'enableBodyScroll unsuccessful - targetElement must be provided when calling enableBodyScroll on IOS devices.'\n );\n return;\n }\n\n locksIndex.set(\n targetElement,\n locksIndex?.get(targetElement)\n ? (locksIndex?.get(targetElement) as number) - 1\n : 0\n );\n if (locksIndex?.get(targetElement) === 0) {\n locks = locks.filter((lock) => lock.targetElement !== targetElement);\n locksIndex?.delete(targetElement);\n }\n\n if (isIosDevice) {\n targetElement.ontouchstart = null;\n targetElement.ontouchmove = null;\n\n if (documentListenerAdded && locks.length === 0) {\n (document as any).removeEventListener(\n 'touchmove',\n preventDefault,\n hasPassiveEvents ? { passive: false } : undefined\n );\n documentListenerAdded = false;\n }\n }\n\n if (locks.length === 0) {\n if (isIosDevice) {\n restorePositionSetting();\n } else {\n restoreOverflowSetting();\n }\n }\n};\n"],"names":["lock"],"mappings":"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"} --------------------------------------------------------------------------------