Skip to content

Commit

Permalink
fixup! Prevent page from scrolling when Modal is open (#397)
Browse files Browse the repository at this point in the history
  • Loading branch information
hubacekj committed Apr 3, 2023
1 parent 4431eb9 commit 2b0d60b
Show file tree
Hide file tree
Showing 2 changed files with 196 additions and 5 deletions.
51 changes: 47 additions & 4 deletions src/lib/components/Modal/Modal.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import PropTypes from 'prop-types';
import React, {
useEffect,
useLayoutEffect,
useRef,
} from 'react';
import { createPortal } from 'react-dom';
Expand Down Expand Up @@ -80,6 +81,7 @@ export const Modal = ({
closeButtonRef,
portalId,
position,
preventScrollBellow,
primaryButtonRef,
size,
...restProps
Expand All @@ -97,14 +99,10 @@ export const Modal = ({
};

useEffect(() => {
const bodyElement = document.getElementsByTagName('body')[0];
bodyElement.style = 'overflow-y: hidden';

window.document.addEventListener('keydown', keyPressHandler, false);

const removeKeyPressHandlerAndScrollPrevention = () => {
window.document.removeEventListener('keydown', keyPressHandler, false);
bodyElement.style = 'overflow-y: auto';
};

// If `autoFocus` is set to `true`, following code finds first form field element
Expand Down Expand Up @@ -132,6 +130,34 @@ export const Modal = ({
return removeKeyPressHandlerAndScrollPrevention;
}, []); // eslint-disable-line react-hooks/exhaustive-deps

useLayoutEffect(() => {
if (preventScrollBellow === 'off') {
return () => {};
}

if (preventScrollBellow === 'default') {
const scrollbarWidth = Math.abs(window.innerWidth - window.document.documentElement.clientWidth);
const prevOverflow = window.document.body.style.overflow;
const prevPaddingRight = window.document.body.style.paddingRight;

window.document.body.style.overflow = 'hidden';
if (Number.isNaN(parseInt(prevPaddingRight, 10))) {
window.document.body.style.paddingRight = `${scrollbarWidth}px`;
} else {
window.document.body.style.paddingRight = `calc(${prevPaddingRight} + ${scrollbarWidth}px)`;
}

return () => {
window.document.body.style.overflow = prevOverflow;
window.document.body.style.paddingRight = prevPaddingRight;
};
}

preventScrollBellow?.start();

return preventScrollBellow?.reset;
}, [preventScrollBellow]);

if (portalId === null) {
return preRender(
children,
Expand Down Expand Up @@ -162,6 +188,7 @@ Modal.defaultProps = {
closeButtonRef: null,
portalId: null,
position: 'center',
preventScrollBellow: 'default',
primaryButtonRef: null,
size: 'medium',
};
Expand Down Expand Up @@ -197,6 +224,22 @@ Modal.propTypes = {
* Vertical position of the modal inside browser window.
*/
position: PropTypes.oneOf(['top', 'center']),
/**
* Mode in which Modal prevents scroll of elements bellow:
* * `default` - Modal prevents scroll on the `body` element
* * `off` - Modal does not prevent any scroll
* * object
* * * `reset` - method called on Modal's unmount to reset scroll prevention
* * * `start` - method called on Modal's mount to custom scroll prevention
*/
preventScrollBellow: PropTypes.oneOf([
'default',
'off',
PropTypes.shape({
reset: PropTypes.func,
start: PropTypes.func,
}),
]),
/**
* Reference to primary button element. It is used to submit modal when Enter key is pressed and as fallback
* when `autoFocus` functionality does not find any input element to be focused.
Expand Down
150 changes: 149 additions & 1 deletion src/lib/components/Modal/README.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,7 @@ below.

### ModalFooter

ModalFooter is an optional part of the Modal which allows you to display
ModalFooter is an noptional part of the Modal which allows you to display
user actions.

There are two ways to position buttons within the ModalFooter:
Expand Down Expand Up @@ -1007,6 +1007,154 @@ independent of the page itself. This can be done in three ways using the
off to prevent the modal from scrolling to the end immediately after being
opened.

## Prevent scrolling bellow the Modal

You can choose the mode in which Modal prevents scroll of the page bellow. Default
mode prevents scroll on `<body>` element. If you choose `off`, there will be no scroll
prevention. If you need more flexibility, define your own methods `start` (called
on Modal's mount) and `reset` (called on Modal's unmount) wrapped by an object
and handle scroll prevention yourself.

<Playground>
{() => {
const [modalOpen, setModalOpen] = React.useState(null);
const modalPrimaryButtonRef = React.useRef();
const modalCloseButtonRef = React.useRef();
const customScrollPreventionObject = {
start: () => {
// YOUR CUSTOM SCROLL PREVENTING LOGIC GOES HERE
window.document.body.style.overflowY = 'hidden'
},
reset: () => {
// YOUR CUSTOM SCROLL RE-ENABLING LOGIC GOES HERE
window.document.body.style.overflowY = 'auto'
},
};
return (
<>
<Button
label="Launch modal with default scroll prevention"
onClick={() => setModalOpen(1)}
/>
<Button
label="Launch modal with no scroll prevention"
onClick={() => setModalOpen(2)}
/>
<Button
label="Launch modal with custom scroll prevention"
onClick={() => setModalOpen(3)}
/>
<div>
{modalOpen === 1 && (
<Modal
closeButtonRef={modalOpen === 1 ? modalCloseButtonRef : null}
primaryButtonRef={modalOpen === 1 ? modalPrimaryButtonRef : null}
>
<ModalHeader>
<ModalTitle>Delete the user?</ModalTitle>
<ModalCloseButton onClick={() => setModalOpen(false)} />
</ModalHeader>
<ModalBody>
<ModalContent>
<p>
Do you really want to delete the user <code>admin</code>?
This cannot be undone.
</p>
</ModalContent>
</ModalBody>
<ModalFooter>
<Button
color="danger"
label="Delete"
onClick={() => setModalOpen(false)}
ref={modalOpen === 1 ? modalPrimaryButtonRef : null}
/>
<Button
color="secondary"
label="Close"
onClick={() => setModalOpen(false)}
priority="outline"
ref={modalOpen === 1 ? modalCloseButtonRef : null}
/>
</ModalFooter>
</Modal>
)}
{modalOpen === 2 && (
<Modal
preventScrollBellow="off"
closeButtonRef={modalOpen === 2 ? modalCloseButtonRef : null}
primaryButtonRef={modalOpen === 2 ? modalPrimaryButtonRef : null}
>
<ModalHeader>
<ModalTitle>Delete the user?</ModalTitle>
<ModalCloseButton onClick={() => setModalOpen(false)} />
</ModalHeader>
<ModalBody>
<ModalContent>
<p>
Do you really want to delete the user <code>admin</code>?
This cannot be undone.
</p>
</ModalContent>
</ModalBody>
<ModalFooter>
<Button
color="danger"
label="Delete"
onClick={() => setModalOpen(false)}
ref={modalOpen === 2 ? modalPrimaryButtonRef : null}
/>
<Button
color="secondary"
label="Close"
onClick={() => setModalOpen(false)}
priority="outline"
ref={modalOpen === 2 ? modalCloseButtonRef : null}
/>
</ModalFooter>
</Modal>
)}
{modalOpen === 3 && (
<Modal
closeButtonRef={modalCloseButtonRef}
primaryButtonRef={modalPrimaryButtonRef}
preventScrollBellow={customScrollPreventionObject}
>
<ModalHeader>
<ModalTitle>Delete the user?</ModalTitle>
<ModalCloseButton onClick={() => setModalOpen(false)} />
</ModalHeader>
<ModalBody>
<ModalContent>
<p>
Do you really want to delete the user <code>admin</code>?
This cannot be undone.
</p>
</ModalContent>
</ModalBody>
<ModalFooter>
<Button
color="danger"
label="Delete"
onClick={() => setModalOpen(false)}
ref={modalOpen === 3 ? modalPrimaryButtonRef : null}
/>
<Button
color="secondary"
label="Close"
onClick={() => setModalOpen(false)}
priority="outline"
ref={modalOpen === 3 ? modalCloseButtonRef : null}
/>
</ModalFooter>
</Modal>
)}
</div>
</>
);
}}
</Playground>

<!-- markdownlint-disable MD024 -->

## Forwarding HTML Attributes
Expand Down

0 comments on commit 2b0d60b

Please sign in to comment.