Skip to content

Commit

Permalink
[change] improve reliability on focus management.
Browse files Browse the repository at this point in the history
this PR allow a stack of modals to give back focus
to parent modal.
  • Loading branch information
diasbruno authored and claydiffrient committed Feb 20, 2017
1 parent 4232477 commit f6768b7
Show file tree
Hide file tree
Showing 5 changed files with 83 additions and 29 deletions.
32 changes: 26 additions & 6 deletions examples/basic/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,34 @@ Modal.setAppElement('#example');
var App = React.createClass({

getInitialState: function() {
return { modalIsOpen: false };
return { modalIsOpen: false, modal2: false };
},

openModal: function() {
this.setState({modalIsOpen: true});
this.setState({ ...this.state, modalIsOpen: true });
},

closeModal: function() {
this.setState({modalIsOpen: false});
this.setState({ ...this.state, modalIsOpen: false });
},

openSecondModal: function(event) {
event.preventDefault();
this.setState({ ...this.state, modal2:true });
},

closeSecondModal: function() {
this.setState({ ...this.state, modal2:false });
},

handleModalCloseRequest: function() {
// opportunity to validate something and keep the modal open even if it
// requested to be closed
this.setState({modalIsOpen: false});
this.setState({ ...this.state, modalIsOpen: false });
},

handleInputChange: function() {
this.setState({foo: 'bar'});
this.setState({ foo: 'bar' });
},

handleOnAfterOpenModal: function() {
Expand All @@ -38,9 +47,11 @@ var App = React.createClass({
render: function() {
return (
<div>
<button onClick={this.openModal}>Open Modal</button>
<button onClick={this.openModal}>Open Modal A</button>
<button onClick={this.openSecondModal}>Open Modal B</button>
<Modal
ref="mymodal"
id="test"
closeTimeoutMS={150}
isOpen={this.state.modalIsOpen}
onAfterOpen={this.handleOnAfterOpenModal}
Expand All @@ -59,8 +70,17 @@ var App = React.createClass({
<button>hi</button>
<button>hi</button>
<button>hi</button>
<button onClick={this.openSecondModal}>Open Modal B</button>
</form>
</Modal>
<Modal ref="mymodal2"
id="test2"
closeTimeoutMS={150}
isOpen={this.state.modal2}
onAfterOpen={() => {}}
onRequestClose={this.closeSecondModal}>
<p>test</p>
</Modal>
</div>
);
}
Expand Down
14 changes: 7 additions & 7 deletions lib/components/ModalPortal.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,12 @@ var ModalPortal = module.exports = React.createClass({
this.focusAfterRender = focus;
},

open: function() {
afterClose: function () {
focusManager.returnFocus();
focusManager.teardownScopedFocus();
},

open () {
if (this.state.afterOpen && this.state.beforeClose) {
clearTimeout(this.closeTimer);
this.setState({ beforeClose: false });
Expand Down Expand Up @@ -114,12 +119,7 @@ var ModalPortal = module.exports = React.createClass({
beforeClose: false,
isOpen: false,
afterOpen: false,
}, this.afterClose);
},

afterClose: function() {
focusManager.returnFocus();
focusManager.teardownScopedFocus();
}, function () { this.afterClose() }.bind(this))
},

handleKeyDown: function(event) {
Expand Down
18 changes: 9 additions & 9 deletions lib/helpers/focusManager.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
var findTabbable = require('../helpers/tabbable');
var focusLaterElements = [];
var modalElement = null;
var focusLaterElement = null;
var needToFocus = false;

function handleBlur(event) {
Expand All @@ -15,8 +15,8 @@ function handleFocus(event) {
}
// need to see how jQuery shims document.on('focusin') so we don't need the
// setTimeout, firefox doesn't support focusin, if it did, we could focus
// the element outside of a setTimeout. Side-effect of this implementation
// is that the document.body gets focus, and then we focus our element right
// the element outside of a setTimeout. Side-effect of this implementation
// is that the document.body gets focus, and then we focus our element right
// after, seems fine.
setTimeout(function() {
if (modalElement.contains(document.activeElement))
Expand All @@ -28,17 +28,19 @@ function handleFocus(event) {
}

exports.markForFocusLater = function() {
focusLaterElement = document.activeElement;
focusLaterElements.push(document.activeElement);
};

exports.returnFocus = function() {
var toFocus = null;
try {
focusLaterElement.focus();
toFocus = focusLaterElements.pop();
toFocus.focus();
return;
}
catch (e) {
console.warn('You tried to return focus to '+focusLaterElement+' but it is not in the DOM anymore');
console.warn('You tried to return focus to '+toFocus+' but it is not in the DOM anymore');
}
focusLaterElement = null;
};

exports.setupScopedFocus = function(element) {
Expand All @@ -64,5 +66,3 @@ exports.teardownScopedFocus = function() {
document.detachEvent('onFocus', handleFocus);
}
};


32 changes: 32 additions & 0 deletions specs/Modal.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,38 @@ describe('Modal', function () {
});
});

it('give back focus to previous element or modal.', function (done) {
var modal = renderModal({
isOpen: true,
onRequestClose: function () {
unmountModal();
done();
}
}, null, function () {});

renderModal({
isOpen: true,
onRequestClose: function () {
Simulate.keyDown(modal.portal.refs.content, {
// The keyCode is all that matters, so this works
key: 'FakeKeyToTestLater',
keyCode: 27,
which: 27
});
expect(document.activeElement).toEqual(modal.portal.refs.content);
}
}, null, function checkPortalFocus () {
expect(document.activeElement).toEqual(this.portal.refs.content);
Simulate.keyDown(this.portal.refs.content, {
// The keyCode is all that matters, so this works
key: 'FakeKeyToTestLater',
keyCode: 27,
which: 27
});
});
});


it('does not focus the modal content when a descendent is already focused', function() {
var input = (
<input
Expand Down
16 changes: 9 additions & 7 deletions specs/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,21 @@ import React from 'react';
import ReactDOM from 'react-dom';
import Modal from '../lib/components/Modal';

let _currentDiv = null;

const divStack = [];

export const renderModal = function(props, children, callback) {
props.ariaHideApp = false;
_currentDiv = document.createElement('div');
document.body.appendChild(_currentDiv);
const currentDiv = document.createElement('div');
divStack.push(currentDiv);
document.body.appendChild(currentDiv);
return ReactDOM.render(
<Modal {...props}>{children}</Modal>
, _currentDiv, callback);
, currentDiv, callback);
};

export const unmountModal = function() {
ReactDOM.unmountComponentAtNode(_currentDiv);
document.body.removeChild(_currentDiv);
_currentDiv = null;
const currentDiv = divStack.pop();
ReactDOM.unmountComponentAtNode(currentDiv);
document.body.removeChild(currentDiv);
};

0 comments on commit f6768b7

Please sign in to comment.