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 15, 2017
1 parent 6ba1c8d commit ebec638
Show file tree
Hide file tree
Showing 5 changed files with 74 additions and 20 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
12 changes: 6 additions & 6 deletions lib/components/ModalPortal.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,6 @@ export default class ModalPortal extends Component {
}
};

static afterClose () {
returnFocus();
teardownScopedFocus();
}

constructor () {
super();
this.state = {
Expand Down Expand Up @@ -99,6 +94,11 @@ export default class ModalPortal extends Component {
this.focusAfterRender = focus;
}

afterClose = () => {
returnFocus();
teardownScopedFocus();
}

open () {
if (this.state.afterOpen && this.state.beforeClose) {
clearTimeout(this.closeTimer);
Expand Down Expand Up @@ -142,7 +142,7 @@ export default class ModalPortal extends Component {
beforeClose: false,
isOpen: false,
afterOpen: false,
}, this.afterClose);
}, () => this.afterClose());
}

handleKeyDown = (event) => {
Expand Down
12 changes: 7 additions & 5 deletions lib/helpers/focusManager.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import findTabbable from './tabbable';

const focusLaterElements = [];
let modalElement = null;
let focusLaterElement = null;
let needToFocus = false;

function handleBlur () {
Expand Down Expand Up @@ -30,16 +30,18 @@ function handleFocus () {
}

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

export function returnFocus () {
let 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;
}

export function setupScopedFocus (element) {
Expand Down
31 changes: 31 additions & 0 deletions specs/Modal.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,37 @@ describe('Modal', () => {
});
});

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

renderModal({
isOpen: true,
onRequestClose () {
Simulate.keyDown(modal.portal.content, {
// The keyCode is all that matters, so this works
key: 'FakeKeyToTestLater',
keyCode: 27,
which: 27
});
expect(document.activeElement).toEqual(modal.portal.content);
}
}, null, function checkPortalFocus () {
expect(document.activeElement).toEqual(this.portal.content);
Simulate.keyDown(this.portal.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', () => {
const input = (
<input
Expand Down
7 changes: 4 additions & 3 deletions specs/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,23 @@ import React from 'react';
import ReactDOM from 'react-dom';
import Modal from '../lib/components/Modal';

let currentDiv = null;
const divStack = [];

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

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

0 comments on commit ebec638

Please sign in to comment.