Skip to content

Commit

Permalink
fix: #550
Browse files Browse the repository at this point in the history
  • Loading branch information
xobotyi committed Aug 25, 2019
1 parent a851e90 commit 2617d74
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 31 deletions.
12 changes: 9 additions & 3 deletions docs/useLockBodyScroll.md
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -25,7 +30,8 @@ const Demo = () => {
## Reference

```ts
useLockBodyScroll(enabled?: boolean = true);
useLockBodyScroll(locked: boolean = true, elementRef?: RefObject<HTMLElement>);
```

- `enabled` &mdash; Hook will lock scrolling on the body element if `true`, defaults to `true`
- `locked` &mdash; Hook will lock scrolling on the body element if `true`, defaults to `true`
- `elementRef` &mdash; The element ref object to find the body element. Can be either a ref to body or iframe element.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
30 changes: 29 additions & 1 deletion src/__stories__/useLockBodyScroll.story.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -27,12 +29,38 @@ const AnotherComponent = () => {
);
};

const IframeComponent = () => {
const [mainLocked, toggleMainLocked] = useToggle(false);
const [iframeLocked, toggleIframeLocked] = useToggle(false);
const iframeElementRef = useRef<HTMLIFrameElement>(null);

useLockBodyScroll(mainLocked);
useLockBodyScroll(iframeLocked, iframeElementRef);

return (
<div style={{ height: '200vh' }}>
<Frame style={{ height: '50vh', width: '50vw' }}>
<div style={{ height: '200vh' }} ref={iframeElementRef}>
<button onClick={() => toggleMainLocked()} style={{ position: 'fixed', left: 0, top: 0 }}>
{mainLocked ? 'Unlock' : 'Lock'} main window scroll
</button>
<button onClick={() => toggleIframeLocked()} style={{ position: 'fixed', left: 0, top: 64 }}>
{iframeLocked ? 'Unlock' : 'Lock'} iframe window scroll
</button>
</div>
</Frame>
</div>
);
};

storiesOf('Side effects|useLockBodyScroll', module)
.add('Docs', () => <ShowDocs md={require('../../docs/useLockBodyScroll.md')} />)
.add('Demo', () => <Demo />)
.add('Two hooks on page', () => (
<>
<AnotherComponent />
<Demo />
<IframeComponent />
</>
));
))
.add('Iframe', () => <IframeComponent />);
78 changes: 51 additions & 27 deletions src/useLockBodyScroll.ts
Original file line number Diff line number Diff line change
@@ -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<HTMLElement, BodyInfoItem> = 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<HTMLElement>) {}
: function useLockBody(locked: boolean = true, elementRef?: RefObject<HTMLElement>) {
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]);
};
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down

0 comments on commit 2617d74

Please sign in to comment.