diff --git a/docs/README.md b/docs/README.md index e63ac121..36f58725 100644 --- a/docs/README.md +++ b/docs/README.md @@ -84,6 +84,11 @@ import ReactModal from 'react-modal'; Note: By disabling the esc key from closing the modal you may introduce an accessibility issue. */ shouldCloseOnEsc={true} + /* + Boolean indicating if the modal should restore focus to the element that + had focus prior to its display. + */ + shouldReturnFocusAfterClose={true} /* String indicating the role of the modal, allowing the 'dialog' role to be applied if desired. */ diff --git a/specs/Modal.spec.js b/specs/Modal.spec.js index 5da1371a..6aed4586 100644 --- a/specs/Modal.spec.js +++ b/specs/Modal.spec.js @@ -139,7 +139,7 @@ export default () => { document.activeElement.should.be.eql(mcontent(modal)); }); - it("does not focus the modal content when shouldFocusAfterRender is false", () => { + it("does not focus modal content if shouldFocusAfterRender is false", () => { const modal = renderModal( { isOpen: true, shouldFocusAfterRender: false }, null @@ -317,7 +317,7 @@ export default () => { isBodyWithReactModalOpenClass("testBodyClass").should.not.be.ok(); }); - it("should not remove classes from document.body when rendering unopened modal", () => { + it("should not remove classes from document.body if modal is closed", () => { renderModal({ isOpen: true }); isBodyWithReactModalOpenClass().should.be.ok(); renderModal({ isOpen: false, bodyOpenClassName: "testBodyClass" }); @@ -340,7 +340,7 @@ export default () => { unmountModal(); }); - it("adding/removing aria-hidden without an appElement will try to fallback to document.body", () => { + it("uses document.body for aria-hidden if no appElement", () => { ariaAppHider.documentNotReadyOrSSRTesting(); const node = document.createElement("div"); ReactDOM.render(, node); @@ -349,7 +349,7 @@ export default () => { should(document.body.getAttribute("aria-hidden")).not.be.ok(); }); - it("raise an exception if appElement is a selector and no elements were found.", () => { + it("raises an exception if the appElement selector does not match", () => { should(() => ariaAppHider.setElement(".test")).throw(); }); diff --git a/specs/Modal.style.spec.js b/specs/Modal.style.spec.js index 2b17355f..c74d8eea 100644 --- a/specs/Modal.style.spec.js +++ b/specs/Modal.style.spec.js @@ -11,7 +11,7 @@ export default () => { mcontent(modal).style.top.should.be.eql(""); }); - it("overrides the default styles when a custom overlayClassName is used", () => { + it("overrides the default styles when using custom overlayClassName", () => { const modal = renderModal({ isOpen: true, overlayClassName: "myOverlayClass" diff --git a/src/components/Modal.js b/src/components/Modal.js index d67d4b49..f3e037c3 100644 --- a/src/components/Modal.js +++ b/src/components/Modal.js @@ -52,8 +52,9 @@ export default class Modal extends Component { onRequestClose: PropTypes.func, closeTimeoutMS: PropTypes.number, ariaHideApp: PropTypes.bool, - shouldFocusAfter: PropTypes.bool, + shouldFocusAfterRender: PropTypes.bool, shouldCloseOnOverlayClick: PropTypes.bool, + shouldReturnFocusAfterClose: PropTypes.bool, parentSelector: PropTypes.func, aria: PropTypes.object, role: PropTypes.string, @@ -71,6 +72,7 @@ export default class Modal extends Component { shouldFocusAfterRender: true, shouldCloseOnEsc: true, shouldCloseOnOverlayClick: true, + shouldReturnFocusAfterClose: true, parentSelector() { return document.body; } diff --git a/src/components/ModalPortal.js b/src/components/ModalPortal.js index 0a4354f6..434a9990 100644 --- a/src/components/ModalPortal.js +++ b/src/components/ModalPortal.js @@ -44,6 +44,7 @@ export default class ModalPortal extends Component { closeTimeoutMS: PropTypes.number, shouldFocusAfterRender: PropTypes.bool, shouldCloseOnOverlayClick: PropTypes.bool, + shouldReturnFocusAfterClose: PropTypes.bool, role: PropTypes.string, contentLabel: PropTypes.string, aria: PropTypes.object, @@ -137,8 +138,23 @@ export default class ModalPortal extends Component { afterClose = () => { // Remove body class bodyClassList.remove(this.props.bodyOpenClassName); - focusManager.returnFocus(); - focusManager.teardownScopedFocus(); + + if (this.shouldReturnFocus()) { + focusManager.returnFocus(); + focusManager.teardownScopedFocus(); + } + }; + + shouldReturnFocus = () => { + // Don't restore focus to the element that had focus prior to + // the modal's display if: + // 1. Focus was never shifted to the modal in the first place + // (shouldFocusAfterRender = false) + // 2. Explicit direction to not restore focus + return ( + this.props.shouldFocusAfterRender || + this.props.shouldReturnFocusAfterClose + ); }; open = () => { @@ -147,8 +163,11 @@ export default class ModalPortal extends Component { clearTimeout(this.closeTimer); this.setState({ beforeClose: false }); } else { - focusManager.setupScopedFocus(this.node); - focusManager.markForFocusLater(); + if (this.shouldReturnFocus()) { + focusManager.setupScopedFocus(this.node); + focusManager.markForFocusLater(); + } + this.setState({ isOpen: true }, () => { this.setState({ afterOpen: true });