Skip to content

Commit eb20444

Browse files
Matthew Hollowaydiasbruno
Matthew Holloway
authored andcommittedDec 9, 2019
[fixed] Focus trap when reentering document (#742) (#791)
* [added] Focus trap when reentering document (#742)
1 parent 98dd5be commit eb20444

File tree

3 files changed

+113
-0
lines changed

3 files changed

+113
-0
lines changed
 

Diff for: ‎src/components/ModalPortal.js

+6
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import scopeTab from "../helpers/scopeTab";
55
import * as ariaAppHider from "../helpers/ariaAppHider";
66
import * as classList from "../helpers/classList";
77
import SafeHTMLElement from "../helpers/safeHTMLElement";
8+
import portalOpenInstances from "../helpers/portalOpenInstances";
9+
import "../helpers/bodyTrap";
810

911
// so that our CSS is statically analyzable
1012
const CLASS_NAMES = {
@@ -151,6 +153,8 @@ export default class ModalPortal extends Component {
151153
ariaHiddenInstances += 1;
152154
ariaAppHider.hide(appElement);
153155
}
156+
157+
portalOpenInstances.register(this);
154158
}
155159

156160
afterClose = () => {
@@ -191,6 +195,8 @@ export default class ModalPortal extends Component {
191195
if (this.props.onAfterClose) {
192196
this.props.onAfterClose();
193197
}
198+
199+
portalOpenInstances.deregister(this);
194200
};
195201

196202
open = () => {

Diff for: ‎src/helpers/bodyTrap.js

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import portalOpenInstances from "./portalOpenInstances";
2+
// Body focus trap see Issue #742
3+
4+
let before,
5+
after,
6+
instances = [];
7+
8+
function focusContent() {
9+
if (instances.length === 0) {
10+
if (process.env.NODE_ENV !== "production") {
11+
// eslint-disable-next-line no-console
12+
console.warn(`React-Modal: Open instances > 0 expected`);
13+
}
14+
return;
15+
}
16+
instances[instances.length - 1].focusContent();
17+
}
18+
19+
function bodyTrap(eventType, openInstances) {
20+
if (!before || !after) {
21+
before = document.createElement("div");
22+
before.setAttribute("data-react-modal-body-trap", "");
23+
before.style.position = "absolute";
24+
before.style.opacity = "0";
25+
before.setAttribute("tabindex", "0");
26+
before.addEventListener("focus", focusContent);
27+
after = before.cloneNode();
28+
after.addEventListener("focus", focusContent);
29+
}
30+
31+
instances = openInstances;
32+
33+
if (instances.length > 0) {
34+
// Add focus trap
35+
if (document.body.firstChild !== before) {
36+
document.body.insertBefore(before, document.body.firstChild);
37+
}
38+
if (document.body.lastChild !== after) {
39+
document.body.appendChild(after);
40+
}
41+
} else {
42+
// Remove focus trap
43+
if (before.parentElement) {
44+
before.parentElement.removeChild(before);
45+
}
46+
if (after.parentElement) {
47+
after.parentElement.removeChild(after);
48+
}
49+
}
50+
}
51+
52+
portalOpenInstances.subscribe(bodyTrap);

Diff for: ‎src/helpers/portalOpenInstances.js

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Tracks portals that are open and emits events to subscribers
2+
3+
class PortalOpenInstances {
4+
constructor() {
5+
this.openInstances = [];
6+
this.subscribers = [];
7+
}
8+
9+
register = openInstance => {
10+
if (this.openInstances.indexOf(openInstance) !== -1) {
11+
if (process.env.NODE_ENV !== "production") {
12+
// eslint-disable-next-line no-console
13+
console.warn(
14+
`React-Modal: Cannot register modal instance that's already open`
15+
);
16+
}
17+
return;
18+
}
19+
this.openInstances.push(openInstance);
20+
this.emit("register");
21+
};
22+
23+
deregister = openInstance => {
24+
const index = this.openInstances.indexOf(openInstance);
25+
if (index === -1) {
26+
if (process.env.NODE_ENV !== "production") {
27+
// eslint-disable-next-line no-console
28+
console.warn(
29+
`React-Modal: Unable to deregister ${openInstance} as it was never registered`
30+
);
31+
}
32+
return;
33+
}
34+
this.openInstances.splice(index, 1);
35+
this.emit("deregister");
36+
};
37+
38+
subscribe = callback => {
39+
this.subscribers.push(callback);
40+
};
41+
42+
emit = eventType => {
43+
this.subscribers.forEach(subscriber =>
44+
subscriber(
45+
eventType,
46+
// shallow copy to avoid accidental mutation
47+
this.openInstances.slice()
48+
)
49+
);
50+
};
51+
}
52+
53+
const portalOpenInstances = new PortalOpenInstances();
54+
55+
export default portalOpenInstances;

0 commit comments

Comments
 (0)
Please sign in to comment.