Skip to content

Commit

Permalink
Add disableScrollLock prop
Browse files Browse the repository at this point in the history
  • Loading branch information
oliviertassinari committed Aug 2, 2019
1 parent 43de851 commit d6ea306
Show file tree
Hide file tree
Showing 11 changed files with 166 additions and 57 deletions.
1 change: 1 addition & 0 deletions docs/pages/api/modal.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ This component shares many concepts with [react-overlays](https://react-bootstra
| <span class="prop-name">disableEscapeKeyDown</span> | <span class="prop-type">bool</span> | <span class="prop-default">false</span> | If `true`, hitting escape will not fire any callback. |
| <span class="prop-name">disablePortal</span> | <span class="prop-type">bool</span> | <span class="prop-default">false</span> | Disable the portal behavior. The children stay within it's parent DOM hierarchy. |
| <span class="prop-name">disableRestoreFocus</span> | <span class="prop-type">bool</span> | <span class="prop-default">false</span> | If `true`, the modal will not restore focus to previously focused element once modal is hidden. |
| <span class="prop-name">disableScrollLock</span> | <span class="prop-type">bool</span> | <span class="prop-default">false</span> | Disable the scroll lock behavior. |
| <span class="prop-name">hideBackdrop</span> | <span class="prop-type">bool</span> | <span class="prop-default">false</span> | If `true`, the backdrop is not rendered. |
| <span class="prop-name">keepMounted</span> | <span class="prop-type">bool</span> | <span class="prop-default">false</span> | Always keep the children in the DOM. This prop can be useful in SEO situation or when you want to maximize the responsiveness of the Modal. |
| <span class="prop-name">onBackdropClick</span> | <span class="prop-type">func</span> | | Callback fired when the backdrop is clicked. |
Expand Down
51 changes: 51 additions & 0 deletions docs/src/pages/components/modal/ServerModal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React from 'react';
import { makeStyles } from '@material-ui/core/styles';
import Modal from '@material-ui/core/Modal';

const useStyles = makeStyles(theme => ({
root: {
transform: 'translateZ(0)',
height: 300,
flexGrow: 1,
},
modal: {
display: 'flex',
padding: theme.spacing(1),
alignItems: 'center',
justifyContent: 'center',
},
paper: {
width: 400,
backgroundColor: theme.palette.background.paper,
border: '2px solid #000',
boxShadow: theme.shadows[5],
padding: theme.spacing(2, 4, 4),
},
}));

export default function ServerModal() {
const classes = useStyles();
const rootRef = React.useRef(null);

return (
<div className={classes.root} ref={rootRef}>
<Modal
disablePortal
disableEnforceFocus
disableAutoFocus
open
aria-labelledby="server-modal-title"
aria-describedby="server-modal-description"
className={classes.modal}
container={() => rootRef.current}
>
<div className={classes.paper}>
<h2 id="server-modal-title">Server-side modal</h2>
<p id="server-modal-description">
You can disable the JavaScript, you will still see me.
</p>
</div>
</Modal>
</div>
);
}
53 changes: 53 additions & 0 deletions docs/src/pages/components/modal/ServerModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React from 'react';
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles';
import Modal from '@material-ui/core/Modal';

const useStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
transform: 'translateZ(0)',
height: 300,
flexGrow: 1,
},
modal: {
display: 'flex',
padding: theme.spacing(1),
alignItems: 'center',
justifyContent: 'center',
},
paper: {
width: 400,
backgroundColor: theme.palette.background.paper,
border: '2px solid #000',
boxShadow: theme.shadows[5],
padding: theme.spacing(2, 4, 4),
},
}),
);

export default function ServerModal() {
const classes = useStyles();
const rootRef = React.useRef<HTMLDivElement>(null);

return (
<div className={classes.root} ref={rootRef}>
<Modal
disablePortal
disableEnforceFocus
disableAutoFocus
open
aria-labelledby="server-modal-title"
aria-describedby="server-modal-description"
className={classes.modal}
container={() => rootRef.current}
>
<div className={classes.paper}>
<h2 id="server-modal-title">Server-side modal</h2>
<p id="server-modal-description">
You can disable the JavaScript, you will still see me.
</p>
</div>
</Modal>
</div>
);
}
3 changes: 1 addition & 2 deletions docs/src/pages/components/modal/SimpleModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ const useStyles = makeStyles(theme => ({
border: '2px solid #000',
boxShadow: theme.shadows[5],
padding: theme.spacing(2, 4, 4),
outline: 'none',
},
}));

Expand Down Expand Up @@ -56,7 +55,7 @@ export default function SimpleModal() {
onClose={handleClose}
>
<div style={modalStyle} className={classes.paper}>
<h2 id="modal-title">Text in a modal</h2>
<h2 id="simple-modal-title">Text in a modal</h2>
<p id="simple-modal-description">
Duis mollis, est non commodo luctus, nisi erat porttitor ligula.
</p>
Expand Down
3 changes: 1 addition & 2 deletions docs/src/pages/components/modal/SimpleModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ const useStyles = makeStyles((theme: Theme) =>
border: '2px solid #000',
boxShadow: theme.shadows[5],
padding: theme.spacing(2, 4, 4),
outline: 'none',
},
}),
);
Expand Down Expand Up @@ -58,7 +57,7 @@ export default function SimpleModal() {
onClose={handleClose}
>
<div style={modalStyle} className={classes.paper}>
<h2 id="modal-title">Text in a modal</h2>
<h2 id="simple-modal-title">Text in a modal</h2>
<p id="simple-modal-description">
Duis mollis, est non commodo luctus, nisi erat porttitor ligula.
</p>
Expand Down
15 changes: 12 additions & 3 deletions docs/src/pages/components/modal/modal.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ Modal is a lower-level construct that is leveraged by the following components:

{{"demo": "pages/components/modal/SimpleModal.js"}}

Notice that you can disable the blue outline with the `outline: 0` CSS property.

## Performance

The content of the modal is **lazily mounted** into the DOM.
Expand Down Expand Up @@ -85,16 +87,23 @@ Additionally, you may give a description of your modal with the `aria-describedb

```jsx
<Modal
aria-labelledby="simple-modal-title"
aria-describedby="simple-modal-description"
aria-labelledby="modal-title"
aria-describedby="modal-description"
>
<h2 id="modal-title">
My Title
</h2>
<p id="simple-modal-description">
<p id="modal-description">
My Description
</p>
</Modal>
```

- The [WAI-ARIA Authoring Practices 1.1](https://www.w3.org/TR/wai-aria-practices/examples/dialog-modal/dialog.html) can help you set the initial focus on the most relevant element, based on your modal content.

## Server-side modal

React [doesn't support](https://github.com/facebook/react/issues/13097) the [`createPortal()`](https://reactjs.org/docs/portals.html) API on the server.
In order to make it work, you need to disable this feature with the `disablePortal` prop:

{{"demo": "pages/components/modal/ServerModal.js"}}
1 change: 1 addition & 0 deletions packages/material-ui/src/Modal/Modal.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export interface ModalProps
disableEscapeKeyDown?: boolean;
disablePortal?: PortalProps['disablePortal'];
disableRestoreFocus?: boolean;
disableScrollLock?: boolean;
hideBackdrop?: boolean;
keepMounted?: boolean;
manager?: ModalManager;
Expand Down
30 changes: 15 additions & 15 deletions packages/material-ui/src/Modal/Modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,10 @@ function getHasTransition(props) {
return props.children ? props.children.props.hasOwnProperty('in') : false;
}

// A modal manager used to track and manage the state of open Modals.
// Modals don't open on the server so this won't conflict with concurrent requests.
const defaultManager = new ModalManager();

function getModal(modal, mountNodeRef, modalRef) {
modal.current.modalRef = modalRef.current;
modal.current.mountNode = mountNodeRef.current;
return modal.current;
}

export const styles = theme => ({
/* Styles applied to the root element. */
root: {
Expand Down Expand Up @@ -73,6 +68,7 @@ const Modal = React.forwardRef(function Modal(props, ref) {
disableEscapeKeyDown = false,
disablePortal = false,
disableRestoreFocus = false,
disableScrollLock = false,
hideBackdrop = false,
keepMounted = false,
manager = defaultManager,
Expand All @@ -93,9 +89,14 @@ const Modal = React.forwardRef(function Modal(props, ref) {
const hasTransition = getHasTransition(props);

const getDoc = () => ownerDocument(mountNodeRef.current);
const getModal = () => {
modal.current.modalRef = modalRef.current;
modal.current.mountNode = mountNodeRef.current;
return modal.current;
};

const handleMounted = () => {
manager.mount(getModal(modal, mountNodeRef, modalRef));
manager.mount(getModal(), { disableScrollLock });

// Fix a bug on Chrome where the scroll isn't initially 0.
modalRef.current.scrollTop = 0;
Expand All @@ -104,18 +105,15 @@ const Modal = React.forwardRef(function Modal(props, ref) {
const handleOpen = useEventCallback(() => {
const resolvedContainer = getContainer(container) || getDoc().body;

manager.add(getModal(modal, mountNodeRef, modalRef), resolvedContainer);
manager.add(getModal(), resolvedContainer);

// The element was already mounted.
if (modalRef.current) {
handleMounted();
}
});

const isTopModal = React.useCallback(
() => manager.isTopModal(getModal(modal, mountNodeRef, modalRef)),
[manager],
);
const isTopModal = React.useCallback(() => manager.isTopModal(getModal()), [manager]);

const handlePortalRef = useEventCallback(node => {
mountNodeRef.current = node;
Expand All @@ -136,7 +134,7 @@ const Modal = React.forwardRef(function Modal(props, ref) {
});

const handleClose = React.useCallback(() => {
manager.remove(getModal(modal, mountNodeRef, modalRef));
manager.remove(getModal());
}, [manager]);

React.useEffect(() => {
Expand Down Expand Up @@ -316,6 +314,10 @@ Modal.propTypes = {
* modal is hidden.
*/
disableRestoreFocus: PropTypes.bool,
/**
* Disable the scroll lock behavior.
*/
disableScrollLock: PropTypes.bool,
/**
* If `true`, the backdrop is not rendered.
*/
Expand All @@ -328,8 +330,6 @@ Modal.propTypes = {
keepMounted: PropTypes.bool,
/**
* @ignore
*
* A modal manager used to track and manage the state of open Modals.
*/
manager: PropTypes.object,
/**
Expand Down
46 changes: 21 additions & 25 deletions packages/material-ui/src/Modal/ModalManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,32 +58,30 @@ function findIndexOf(containerInfo, callback) {
return idx;
}

function handleNewContainer(containerInfo) {
// We are only interested in the actual `style` here because we will override it.
const restoreStyle = {
overflow: containerInfo.container.style.overflow,
'padding-right': containerInfo.container.style.paddingRight,
};

const style = {
overflow: 'hidden',
};

function handleContainer(containerInfo, props) {
const restoreStyle = {};
const style = {};
const restorePaddings = [];
let fixedNodes;

if (containerInfo.overflowing) {
const scrollbarSize = getScrollbarSize();
if (!props.disableScrollLock) {
restoreStyle.overflow = containerInfo.container.style.overflow;
restoreStyle['padding-right'] = containerInfo.container.style.paddingRight;
style.overflow = 'hidden';

// Use computed style, here to get the real padding to add our scrollbar width.
style['padding-right'] = `${getPaddingRight(containerInfo.container) + scrollbarSize}px`;
if (isOverflowing(containerInfo.container)) {
const scrollbarSize = getScrollbarSize();

// .mui-fixed is a global helper.
fixedNodes = ownerDocument(containerInfo.container).querySelectorAll('.mui-fixed');
[].forEach.call(fixedNodes, node => {
restorePaddings.push(node.style.paddingRight);
node.style.paddingRight = `${getPaddingRight(node) + scrollbarSize}px`;
});
// Use computed style, here to get the real padding to add our scrollbar width.
style['padding-right'] = `${getPaddingRight(containerInfo.container) + scrollbarSize}px`;

// .mui-fixed is a global helper.
fixedNodes = ownerDocument(containerInfo.container).querySelectorAll('.mui-fixed');
[].forEach.call(fixedNodes, node => {
restorePaddings.push(node.style.paddingRight);
node.style.paddingRight = `${getPaddingRight(node) + scrollbarSize}px`;
});
}
}

Object.keys(style).forEach(key => {
Expand Down Expand Up @@ -137,7 +135,6 @@ export default class ModalManager {
// this.contaniners[containerIndex] = {
// modals: [],
// container,
// overflowing,
// restore: null,
// }
this.contaniners = [];
Expand Down Expand Up @@ -169,20 +166,19 @@ export default class ModalManager {
this.contaniners.push({
modals: [modal],
container,
overflowing: isOverflowing(container),
restore: null,
hiddenSiblingNodes,
});

return modalIndex;
}

mount(modal) {
mount(modal, props) {
const containerIndex = findIndexOf(this.contaniners, item => item.modals.indexOf(modal) !== -1);
const containerInfo = this.contaniners[containerIndex];

if (!containerInfo.restore) {
containerInfo.restore = handleNewContainer(containerInfo);
containerInfo.restore = handleContainer(containerInfo, props);
}
}

Expand Down
Loading

0 comments on commit d6ea306

Please sign in to comment.