Skip to content

Commit 2617d74

Browse files
committed
1 parent a851e90 commit 2617d74

File tree

5 files changed

+95
-31
lines changed

5 files changed

+95
-31
lines changed

Diff for: docs/useLockBodyScroll.md

+9-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@
22

33
React side-effect hook that locks scrolling on the body element. Useful for modal and other overlay components.
44

5-
## Usage
5+
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.
6+
By default it uses body element of script's parent window.
7+
8+
>Note: To improve performance you can pass body's or iframe's ref object, thus no parent lookup will be performed
9+
10+
## Usage
611

712
```jsx
813
import {useLockBodyScroll, useToggle} from 'react-use';
@@ -25,7 +30,8 @@ const Demo = () => {
2530
## Reference
2631

2732
```ts
28-
useLockBodyScroll(enabled?: boolean = true);
33+
useLockBodyScroll(locked: boolean = true, elementRef?: RefObject<HTMLElement>);
2934
```
3035

31-
- `enabled` &mdash; Hook will lock scrolling on the body element if `true`, defaults to `true`
36+
- `locked` &mdash; Hook will lock scrolling on the body element if `true`, defaults to `true`
37+
- `elementRef` &mdash; The element ref object to find the body element. Can be either a ref to body or iframe element.

Diff for: package.json

+1
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@
8989
"raf-stub": "3.0.0",
9090
"react": "16.9.0",
9191
"react-dom": "16.9.0",
92+
"react-frame-component": "^4.1.1",
9293
"react-spring": "8.0.27",
9394
"react-test-renderer": "16.9.0",
9495
"rebound": "0.1.0",

Diff for: src/__stories__/useLockBodyScroll.story.tsx

+29-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { storiesOf } from '@storybook/react';
22
import * as React from 'react';
3+
import { useRef } from 'react';
4+
import Frame from 'react-frame-component';
35
import { useLockBodyScroll, useToggle } from '..';
46
import ShowDocs from './util/ShowDocs';
57

@@ -27,12 +29,38 @@ const AnotherComponent = () => {
2729
);
2830
};
2931

32+
const IframeComponent = () => {
33+
const [mainLocked, toggleMainLocked] = useToggle(false);
34+
const [iframeLocked, toggleIframeLocked] = useToggle(false);
35+
const iframeElementRef = useRef<HTMLIFrameElement>(null);
36+
37+
useLockBodyScroll(mainLocked);
38+
useLockBodyScroll(iframeLocked, iframeElementRef);
39+
40+
return (
41+
<div style={{ height: '200vh' }}>
42+
<Frame style={{ height: '50vh', width: '50vw' }}>
43+
<div style={{ height: '200vh' }} ref={iframeElementRef}>
44+
<button onClick={() => toggleMainLocked()} style={{ position: 'fixed', left: 0, top: 0 }}>
45+
{mainLocked ? 'Unlock' : 'Lock'} main window scroll
46+
</button>
47+
<button onClick={() => toggleIframeLocked()} style={{ position: 'fixed', left: 0, top: 64 }}>
48+
{iframeLocked ? 'Unlock' : 'Lock'} iframe window scroll
49+
</button>
50+
</div>
51+
</Frame>
52+
</div>
53+
);
54+
};
55+
3056
storiesOf('Side effects|useLockBodyScroll', module)
3157
.add('Docs', () => <ShowDocs md={require('../../docs/useLockBodyScroll.md')} />)
3258
.add('Demo', () => <Demo />)
3359
.add('Two hooks on page', () => (
3460
<>
3561
<AnotherComponent />
3662
<Demo />
63+
<IframeComponent />
3764
</>
38-
));
65+
))
66+
.add('Iframe', () => <IframeComponent />);

Diff for: src/useLockBodyScroll.ts

+51-27
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,58 @@
1-
import { useEffect } from 'react';
1+
import { RefObject, useEffect, useRef } from 'react';
22

3-
let counter = 0;
4-
let originalOverflow: string | null = null;
3+
export function getClosestBody(el: Element | HTMLElement | HTMLIFrameElement | null): HTMLElement | null {
4+
if (!el) {
5+
return null;
6+
} else if (el.tagName === 'BODY') {
7+
return el as HTMLElement;
8+
} else if (el.tagName === 'IFRAME') {
9+
const document = (el as HTMLIFrameElement).contentDocument;
10+
return document ? document.body : null;
11+
} else if (!(el as HTMLElement).offsetParent) {
12+
return null;
13+
}
514

6-
const lock = () => {
7-
originalOverflow = window.getComputedStyle(document.body).overflow;
8-
document.body.style.overflow = 'hidden';
9-
};
15+
return getClosestBody((el as HTMLElement).offsetParent!);
16+
}
1017

11-
const unlock = () => {
12-
document.body.style.overflow = originalOverflow;
13-
originalOverflow = null;
14-
};
18+
export interface BodyInfoItem {
19+
counter: number;
20+
initialOverflow: string | null;
21+
}
1522

16-
const increment = () => {
17-
counter++;
18-
if (counter === 1) {
19-
lock();
20-
}
21-
};
23+
const bodies: Map<HTMLElement, BodyInfoItem> = new Map();
2224

23-
const decrement = () => {
24-
counter--;
25-
if (counter === 0) {
26-
unlock();
27-
}
28-
};
25+
const doc: Document | undefined = typeof document === 'object' ? document : undefined;
26+
27+
export default !doc
28+
? function useLockBodyMock(_locked: boolean = true, _elementRef?: RefObject<HTMLElement>) {}
29+
: function useLockBody(locked: boolean = true, elementRef?: RefObject<HTMLElement>) {
30+
elementRef = elementRef || useRef(doc!.body);
31+
32+
useEffect(() => {
33+
const body = getClosestBody(elementRef!.current);
34+
if (!body) {
35+
return;
36+
}
2937

30-
const useLockBodyScroll = (enabled: boolean = true) => {
31-
useEffect(() => (enabled ? (increment(), decrement) : undefined), [enabled]);
32-
};
38+
const bodyInfo = bodies.get(body);
3339

34-
export default useLockBodyScroll;
40+
if (locked) {
41+
if (!bodyInfo) {
42+
bodies.set(body, { counter: 1, initialOverflow: body.style.overflow });
43+
body.style.overflow = 'hidden';
44+
} else {
45+
bodies.set(body, { counter: bodyInfo.counter + 1, initialOverflow: bodyInfo.initialOverflow });
46+
}
47+
} else {
48+
if (bodyInfo) {
49+
if (bodyInfo.counter === 1) {
50+
bodies.delete(body);
51+
body.style.overflow = bodyInfo.initialOverflow;
52+
} else {
53+
bodies.set(body, { counter: bodyInfo.counter - 1, initialOverflow: bodyInfo.initialOverflow });
54+
}
55+
}
56+
}
57+
}, [locked, elementRef.current]);
58+
};

Diff for: yarn.lock

+5
Original file line numberDiff line numberDiff line change
@@ -10140,6 +10140,11 @@ react-focus-lock@^1.18.3:
1014010140
prop-types "^15.6.2"
1014110141
react-clientside-effect "^1.2.0"
1014210142

10143+
react-frame-component@^4.1.1:
10144+
version "4.1.1"
10145+
resolved "https://registry.yarnpkg.com/react-frame-component/-/react-frame-component-4.1.1.tgz#ea8f7c518ef6b5ad72146dd1f648752369826894"
10146+
integrity sha512-NfJp90AvYA1R6+uSYafQ+n+UM2HjHqi4WGHeprVXa6quU9d8o6ZFRzQ36uemY82dlkZFzf2jigFx6E4UzNFajA==
10147+
1014310148
react-helmet-async@^1.0.2:
1014410149
version "1.0.2"
1014510150
resolved "https://registry.yarnpkg.com/react-helmet-async/-/react-helmet-async-1.0.2.tgz#bb55dd8268f7b15aac69c6b22e2f950abda8cc44"

0 commit comments

Comments
 (0)