Skip to content

Commit ebec638

Browse files
diasbrunoclaydiffrient
authored andcommitted
[change] improve reliability on focus management.
this PR allow a stack of modals to give back focus to parent modal.
1 parent 6ba1c8d commit ebec638

File tree

5 files changed

+74
-20
lines changed

5 files changed

+74
-20
lines changed

examples/basic/app.js

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,25 +9,34 @@ Modal.setAppElement('#example');
99
var App = React.createClass({
1010

1111
getInitialState: function() {
12-
return { modalIsOpen: false };
12+
return { modalIsOpen: false, modal2: false };
1313
},
1414

1515
openModal: function() {
16-
this.setState({modalIsOpen: true});
16+
this.setState({ ...this.state, modalIsOpen: true });
1717
},
1818

1919
closeModal: function() {
20-
this.setState({modalIsOpen: false});
20+
this.setState({ ...this.state, modalIsOpen: false });
21+
},
22+
23+
openSecondModal: function(event) {
24+
event.preventDefault();
25+
this.setState({ ...this.state, modal2:true });
26+
},
27+
28+
closeSecondModal: function() {
29+
this.setState({ ...this.state, modal2:false });
2130
},
2231

2332
handleModalCloseRequest: function() {
2433
// opportunity to validate something and keep the modal open even if it
2534
// requested to be closed
26-
this.setState({modalIsOpen: false});
35+
this.setState({ ...this.state, modalIsOpen: false });
2736
},
2837

2938
handleInputChange: function() {
30-
this.setState({foo: 'bar'});
39+
this.setState({ foo: 'bar' });
3140
},
3241

3342
handleOnAfterOpenModal: function() {
@@ -38,9 +47,11 @@ var App = React.createClass({
3847
render: function() {
3948
return (
4049
<div>
41-
<button onClick={this.openModal}>Open Modal</button>
50+
<button onClick={this.openModal}>Open Modal A</button>
51+
<button onClick={this.openSecondModal}>Open Modal B</button>
4252
<Modal
4353
ref="mymodal"
54+
id="test"
4455
closeTimeoutMS={150}
4556
isOpen={this.state.modalIsOpen}
4657
onAfterOpen={this.handleOnAfterOpenModal}
@@ -59,8 +70,17 @@ var App = React.createClass({
5970
<button>hi</button>
6071
<button>hi</button>
6172
<button>hi</button>
73+
<button onClick={this.openSecondModal}>Open Modal B</button>
6274
</form>
6375
</Modal>
76+
<Modal ref="mymodal2"
77+
id="test2"
78+
closeTimeoutMS={150}
79+
isOpen={this.state.modal2}
80+
onAfterOpen={() => {}}
81+
onRequestClose={this.closeSecondModal}>
82+
<p>test</p>
83+
</Modal>
6484
</div>
6585
);
6686
}

lib/components/ModalPortal.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,6 @@ export default class ModalPortal extends Component {
5252
}
5353
};
5454

55-
static afterClose () {
56-
returnFocus();
57-
teardownScopedFocus();
58-
}
59-
6055
constructor () {
6156
super();
6257
this.state = {
@@ -99,6 +94,11 @@ export default class ModalPortal extends Component {
9994
this.focusAfterRender = focus;
10095
}
10196

97+
afterClose = () => {
98+
returnFocus();
99+
teardownScopedFocus();
100+
}
101+
102102
open () {
103103
if (this.state.afterOpen && this.state.beforeClose) {
104104
clearTimeout(this.closeTimer);
@@ -142,7 +142,7 @@ export default class ModalPortal extends Component {
142142
beforeClose: false,
143143
isOpen: false,
144144
afterOpen: false,
145-
}, this.afterClose);
145+
}, () => this.afterClose());
146146
}
147147

148148
handleKeyDown = (event) => {

lib/helpers/focusManager.js

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import findTabbable from './tabbable';
22

3+
const focusLaterElements = [];
34
let modalElement = null;
4-
let focusLaterElement = null;
55
let needToFocus = false;
66

77
function handleBlur () {
@@ -30,16 +30,18 @@ function handleFocus () {
3030
}
3131

3232
export function markForFocusLater () {
33-
focusLaterElement = document.activeElement;
33+
focusLaterElements.push(document.activeElement);
3434
}
3535

3636
export function returnFocus () {
37+
let toFocus = null;
3738
try {
38-
focusLaterElement.focus();
39+
toFocus = focusLaterElements.pop();
40+
toFocus.focus();
41+
return;
3942
} catch (e) {
40-
console.warn(`You tried to return focus to ${focusLaterElement} but it is not in the DOM anymore`);
43+
console.warn(`You tried to return focus to ${toFocus} but it is not in the DOM anymore`);
4144
}
42-
focusLaterElement = null;
4345
}
4446

4547
export function setupScopedFocus (element) {

specs/Modal.spec.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,37 @@ describe('Modal', () => {
111111
});
112112
});
113113

114+
it('give back focus to previous element or modal.', (done) => {
115+
const modal = renderModal({
116+
isOpen: true,
117+
onRequestClose () {
118+
unmountModal();
119+
done();
120+
}
121+
}, null, () => {});
122+
123+
renderModal({
124+
isOpen: true,
125+
onRequestClose () {
126+
Simulate.keyDown(modal.portal.content, {
127+
// The keyCode is all that matters, so this works
128+
key: 'FakeKeyToTestLater',
129+
keyCode: 27,
130+
which: 27
131+
});
132+
expect(document.activeElement).toEqual(modal.portal.content);
133+
}
134+
}, null, function checkPortalFocus () {
135+
expect(document.activeElement).toEqual(this.portal.content);
136+
Simulate.keyDown(this.portal.content, {
137+
// The keyCode is all that matters, so this works
138+
key: 'FakeKeyToTestLater',
139+
keyCode: 27,
140+
which: 27
141+
});
142+
});
143+
});
144+
114145
it('does not focus the modal content when a descendent is already focused', () => {
115146
const input = (
116147
<input

specs/helper.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,23 @@ import React from 'react';
55
import ReactDOM from 'react-dom';
66
import Modal from '../lib/components/Modal';
77

8-
let currentDiv = null;
8+
const divStack = [];
99

1010
export function renderModal (props, children, callback) {
1111
const myProps = {
1212
ariaHideApp: false,
1313
...props
1414
};
15-
currentDiv = document.createElement('div');
15+
const currentDiv = document.createElement('div');
16+
divStack.push(currentDiv);
1617
document.body.appendChild(currentDiv);
1718
return ReactDOM.render(
1819
<Modal {...myProps}>{children}</Modal>
1920
, currentDiv, callback);
2021
}
2122

2223
export const unmountModal = () => {
24+
const currentDiv = divStack.pop();
2325
ReactDOM.unmountComponentAtNode(currentDiv);
2426
document.body.removeChild(currentDiv);
25-
currentDiv = null;
2627
};

0 commit comments

Comments
 (0)