diff --git a/docs/useLockBodyScroll.md b/docs/useLockBodyScroll.md index d050deb44d..ee2f819713 100644 --- a/docs/useLockBodyScroll.md +++ b/docs/useLockBodyScroll.md @@ -2,7 +2,12 @@ React side-effect hook that locks scrolling on the body element. Useful for modal and other overlay components. -## Usage +Accepts ref object pointing to any HTML element as second parameter. Parent body element will be found and it's scroll will be locked/unlocked. It is needed to proper iFrame handling. +By default it uses body element of script's parent window. + +>Note: To improve performance you can pass body's or iframe's ref object, thus no parent lookup will be performed + +## Usage ```jsx import {useLockBodyScroll, useToggle} from 'react-use'; @@ -25,7 +30,8 @@ const Demo = () => { ## Reference ```ts -useLockBodyScroll(enabled?: boolean = true); +useLockBodyScroll(locked: boolean = true, elementRef?: RefObject); ``` -- `enabled` — Hook will lock scrolling on the body element if `true`, defaults to `true` +- `locked` — Hook will lock scrolling on the body element if `true`, defaults to `true` +- `elementRef` — The element ref object to find the body element. Can be either a ref to body or iframe element. diff --git a/package.json b/package.json index 3502ce1581..2c08aaa9ec 100644 --- a/package.json +++ b/package.json @@ -89,6 +89,7 @@ "raf-stub": "3.0.0", "react": "16.9.0", "react-dom": "16.9.0", + "react-frame-component": "^4.1.1", "react-spring": "8.0.27", "react-test-renderer": "16.9.0", "rebound": "0.1.0", diff --git a/src/__stories__/useLockBodyScroll.story.tsx b/src/__stories__/useLockBodyScroll.story.tsx index 6d8b3c484e..4b66dee256 100644 --- a/src/__stories__/useLockBodyScroll.story.tsx +++ b/src/__stories__/useLockBodyScroll.story.tsx @@ -1,5 +1,7 @@ import { storiesOf } from '@storybook/react'; import * as React from 'react'; +import { useRef } from 'react'; +import Frame from 'react-frame-component'; import { useLockBodyScroll, useToggle } from '..'; import ShowDocs from './util/ShowDocs'; @@ -27,6 +29,30 @@ const AnotherComponent = () => { ); }; +const IframeComponent = () => { + const [mainLocked, toggleMainLocked] = useToggle(false); + const [iframeLocked, toggleIframeLocked] = useToggle(false); + const iframeElementRef = useRef(null); + + useLockBodyScroll(mainLocked); + useLockBodyScroll(iframeLocked, iframeElementRef); + + return ( +
+ +
+ + +
+ +
+ ); +}; + storiesOf('Side effects|useLockBodyScroll', module) .add('Docs', () => ) .add('Demo', () => ) @@ -34,5 +60,7 @@ storiesOf('Side effects|useLockBodyScroll', module) <> + - )); + )) + .add('Iframe', () => ); diff --git a/src/useLockBodyScroll.ts b/src/useLockBodyScroll.ts index a42cdcbf28..e63bd6cfad 100644 --- a/src/useLockBodyScroll.ts +++ b/src/useLockBodyScroll.ts @@ -1,34 +1,58 @@ -import { useEffect } from 'react'; +import { RefObject, useEffect, useRef } from 'react'; -let counter = 0; -let originalOverflow: string | null = null; +export function getClosestBody(el: Element | HTMLElement | HTMLIFrameElement | null): HTMLElement | null { + if (!el) { + return null; + } else if (el.tagName === 'BODY') { + return el as HTMLElement; + } else if (el.tagName === 'IFRAME') { + const document = (el as HTMLIFrameElement).contentDocument; + return document ? document.body : null; + } else if (!(el as HTMLElement).offsetParent) { + return null; + } -const lock = () => { - originalOverflow = window.getComputedStyle(document.body).overflow; - document.body.style.overflow = 'hidden'; -}; + return getClosestBody((el as HTMLElement).offsetParent!); +} -const unlock = () => { - document.body.style.overflow = originalOverflow; - originalOverflow = null; -}; +export interface BodyInfoItem { + counter: number; + initialOverflow: string | null; +} -const increment = () => { - counter++; - if (counter === 1) { - lock(); - } -}; +const bodies: Map = new Map(); -const decrement = () => { - counter--; - if (counter === 0) { - unlock(); - } -}; +const doc: Document | undefined = typeof document === 'object' ? document : undefined; + +export default !doc + ? function useLockBodyMock(_locked: boolean = true, _elementRef?: RefObject) {} + : function useLockBody(locked: boolean = true, elementRef?: RefObject) { + elementRef = elementRef || useRef(doc!.body); + + useEffect(() => { + const body = getClosestBody(elementRef!.current); + if (!body) { + return; + } -const useLockBodyScroll = (enabled: boolean = true) => { - useEffect(() => (enabled ? (increment(), decrement) : undefined), [enabled]); -}; + const bodyInfo = bodies.get(body); -export default useLockBodyScroll; + if (locked) { + if (!bodyInfo) { + bodies.set(body, { counter: 1, initialOverflow: body.style.overflow }); + body.style.overflow = 'hidden'; + } else { + bodies.set(body, { counter: bodyInfo.counter + 1, initialOverflow: bodyInfo.initialOverflow }); + } + } else { + if (bodyInfo) { + if (bodyInfo.counter === 1) { + bodies.delete(body); + body.style.overflow = bodyInfo.initialOverflow; + } else { + bodies.set(body, { counter: bodyInfo.counter - 1, initialOverflow: bodyInfo.initialOverflow }); + } + } + } + }, [locked, elementRef.current]); + }; diff --git a/yarn.lock b/yarn.lock index aa47340600..c179ee0834 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10140,6 +10140,11 @@ react-focus-lock@^1.18.3: prop-types "^15.6.2" react-clientside-effect "^1.2.0" +react-frame-component@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/react-frame-component/-/react-frame-component-4.1.1.tgz#ea8f7c518ef6b5ad72146dd1f648752369826894" + integrity sha512-NfJp90AvYA1R6+uSYafQ+n+UM2HjHqi4WGHeprVXa6quU9d8o6ZFRzQ36uemY82dlkZFzf2jigFx6E4UzNFajA== + react-helmet-async@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/react-helmet-async/-/react-helmet-async-1.0.2.tgz#bb55dd8268f7b15aac69c6b22e2f950abda8cc44"