Skip to content

Commit

Permalink
feat(PF4-Modal): Adds keyboard and screen reader focus trapping (patt…
Browse files Browse the repository at this point in the history
…ernfly#1011)

* feat(PF4-Modal): Adds keyboard and screen reader focus trapping

BREAKING CHANGE: The reactRoot prop is required

Fixes patternfly#561

* Clicking outside of modal closes it
  • Loading branch information
michaelkro authored and tlabaj committed Dec 12, 2018
1 parent a3f72d8 commit 7fa93b3
Show file tree
Hide file tree
Showing 7 changed files with 66 additions and 37 deletions.
11 changes: 9 additions & 2 deletions packages/patternfly-4/react-core/src/components/Modal/Modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import ModalContent from './ModalContent';
import safeHTMLElement from '../../internal/safeHTMLElement';
import { canUseDOM } from 'exenv';
import { KEY_CODES } from '../../internal/constants';
import { css } from '@patternfly/react-styles';
Expand All @@ -23,7 +24,9 @@ const propTypes = {
/** A callback for when the close button is clicked */
onClose: PropTypes.func,
/** Creates a large version of the Modal */
isLarge: PropTypes.bool
isLarge: PropTypes.bool,
/** React application root element */
reactRoot: PropTypes.instanceOf(safeHTMLElement).isRequired
};

const defaultProps = {
Expand Down Expand Up @@ -62,8 +65,10 @@ class Modal extends React.Component {
componentDidUpdate() {
if (this.props.isOpen) {
document.body.classList.add(css(styles.backdropOpen));
this.props.reactRoot.setAttribute('aria-hidden', true);
} else {
document.body.classList.remove(css(styles.backdropOpen));
this.props.reactRoot.removeAttribute('aria-hidden');
}
}

Expand All @@ -73,6 +78,8 @@ class Modal extends React.Component {
}

render() {
const { reactRoot, ...props } = this.props;

if (!canUseDOM) {
return null;
}
Expand All @@ -81,7 +88,7 @@ class Modal extends React.Component {
this.container = document.createElement('div');
}

return ReactDOM.createPortal(<ModalContent {...this.props} id={this.id} />, this.container);
return ReactDOM.createPortal(<ModalContent {...props} id={this.id} />, this.container);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ const props = {
title: 'Modal',
onClose: jest.fn(),
isOpen: false,
children: 'modal content'
children: 'modal content',
reactRoot: document.createElement('div')
};

test('Modal creates a container element once for div', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import FocusTrap from 'focus-trap-react';
import ModalBoxBody from './ModalBoxBody';
import ModalBoxHeader from './ModalBoxHeader';
import ModalBoxHCloseButton from './ModalBoxCloseButton';
Expand Down Expand Up @@ -47,14 +48,16 @@ const ModalContent = ({ children, className, isOpen, title, hideTitle, actions,
return (
<Backdrop>
<Bullseye>
<ModalBox className={className} isLarge={isLarge} title={title} id={id}>
<ModalBoxHCloseButton onClose={onClose} />
{modalBoxHeader}
<ModalBoxBody {...props} id={id}>
{children}
</ModalBoxBody>
{modalBoxFooter}
</ModalBox>
<FocusTrap focusTrapOptions={{ clickOutsideDeactivates: true }}>
<ModalBox className={className} isLarge={isLarge} title={title} id={id}>
<ModalBoxHCloseButton onClose={onClose} />
{modalBoxHeader}
<ModalBoxBody {...props} id={id}>
{children}
</ModalBoxBody>
{modalBoxFooter}
</ModalBox>
</FocusTrap>
</Bullseye>
</Backdrop>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,36 +8,48 @@ exports[`Modal Content Test isOpen 1`] = `
className=""
component="div"
>
<ModalBox
className=""
id="id"
isLarge={false}
title="Test Modal Content title"
<FocusTrap
_createFocusTrap={[Function]}
active={true}
focusTrapOptions={
Object {
"clickOutsideDeactivates": true,
}
}
paused={false}
tag="div"
>
<ModalBoxCloseButton
className=""
onClose={[Function]}
/>
<ModalBoxHeader
className=""
>
Test Modal Content title
</ModalBoxHeader>
<ModalBoxBody
<ModalBox
className=""
id="id"
isLarge={false}
title="Test Modal Content title"
>
This is a ModalBox header
</ModalBoxBody>
<ModalBoxFooter
className=""
>
</ModalBoxFooter>
</ModalBox>
<ModalBoxCloseButton
className=""
onClose={[Function]}
/>
<ModalBoxHeader
className=""
>
Test Modal Content title
</ModalBoxHeader>
<ModalBoxBody
className=""
id="id"
>
This is a ModalBox header
</ModalBoxBody>
<ModalBoxFooter
className=""
>
</ModalBoxFooter>
</ModalBox>
</FocusTrap>
</Bullseye>
</Backdrop>
`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ class LargeModal extends React.Component {
Confirm
</Button>
]}
reactRoot={document.querySelector('#___gatsby')}
>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore
magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ class SimpleModal extends React.Component {
Confirm
</Button>
]}
reactRoot={document.querySelector('#___gatsby')}
>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore
magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// https://github.com/reactjs/react-modal/blob/master/src/helpers/safeHTMLElement.js
import { canUseDOM } from 'exenv';

export default (canUseDOM ? window.HTMLElement : {});

0 comments on commit 7fa93b3

Please sign in to comment.