Skip to content
This repository has been archived by the owner on Oct 19, 2021. It is now read-only.

fix(Modal): add keyboard trap #1115

Merged
merged 9 commits into from
Aug 9, 2018
44 changes: 43 additions & 1 deletion src/components/Modal/Modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ import { iconClose } from 'carbon-icons';
import Icon from '../Icon';
import Button from '../Button';

const matchesFuncName =
typeof Element !== 'undefined' &&
['matches', 'webkitMatchesSelector', 'msMatchesSelector'].filter(
name => typeof Element.prototype[name] === 'function'
)[0];

export default class Modal extends Component {
static propTypes = {
children: PropTypes.node,
Expand All @@ -25,6 +31,7 @@ export default class Modal extends Component {
onSecondarySubmit: PropTypes.func,
danger: PropTypes.bool,
shouldSubmitOnEnter: PropTypes.bool,
selectorsFloatingMenus: PropTypes.arrayOf(PropTypes.string),
};

static defaultProps = {
Expand All @@ -36,10 +43,21 @@ export default class Modal extends Component {
iconDescription: 'close the modal',
modalHeading: '',
modalLabel: '',
selectorsFloatingMenus: ['.bx--overflow-menu-options__btn'],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

};

button = React.createRef();

isFloatingMenu = target => {
return this.props.selectorsFloatingMenus.some(selector => {
if (target && typeof target[matchesFuncName] === 'function') {
if (target[matchesFuncName](selector)) {
return true;
}
}
});
};

handleKeyDown = evt => {
if (evt.which === 27) {
this.props.onRequestClose();
Expand All @@ -50,11 +68,27 @@ export default class Modal extends Component {
};

handleClick = evt => {
if (this.innerModal && !this.innerModal.contains(evt.target)) {
if (
this.innerModal &&
!this.isFloatingMenu(evt.target) &&
!this.innerModal.contains(evt.target)
) {
this.props.onRequestClose();
}
};

handleBlur = evt => {
// Keyboard trap
if (
this.innerModal &&
this.props.open &&
!this.isFloatingMenu(evt.relatedTarget) &&
(!evt.relatedTarget || !this.innerModal.contains(evt.relatedTarget))
) {
this.focusModal();
}
};

componentDidUpdate(prevProps) {
if (!prevProps.open && this.props.open) {
this.beingOpen = true;
Expand All @@ -63,6 +97,12 @@ export default class Modal extends Component {
}
}

focusModal = () => {
if (this.outerModal) {
this.outerModal.focus();
}
};

focusButton = () => {
if (this.button) {
this.button.current.focus();
Expand Down Expand Up @@ -95,6 +135,7 @@ export default class Modal extends Component {
iconDescription,
primaryButtonDisabled,
danger,
selectorsFloatingMenus, // eslint-disable-line
...other
} = this.props;

Expand Down Expand Up @@ -167,6 +208,7 @@ export default class Modal extends Component {
{...other}
onKeyDown={this.handleKeyDown}
onClick={this.handleClick}
onBlur={this.handleBlur}
className={modalClasses}
role="presentation"
tabIndex={-1}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,16 @@ exports[`ModalWrapper should render 1`] = `
primaryButtonDisabled={false}
primaryButtonText="Save"
secondaryButtonText="Cancel"
selectorsFloatingMenus={
Array [
".bx--overflow-menu-options__btn",
]
}
>
<div
className="bx--modal bx--modal-tall"
id="modal"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
role="presentation"
Expand Down
10 changes: 10 additions & 0 deletions src/components/OverflowMenu/OverflowMenu.js
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,11 @@ export default class OverflowMenu extends Component {
};

closeMenu = () => {
let wasOpen = this.state.open;
this.setState({ open: false }, () => {
if (wasOpen) {
this.focusMenuEl();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}
this.props.onClose();
});
};
Expand All @@ -332,6 +336,12 @@ export default class OverflowMenu extends Component {
this.menuEl = menuEl;
};

focusMenuEl = () => {
if (this.menuEl) {
this.menuEl.focus();
}
};

/**
* Handles the floating menu being unmounted.
* @param {Element} menuBody The DOM element of the menu body.
Expand Down