diff --git a/specs/Modal.spec.js b/specs/Modal.spec.js
index 60836313..44dfbe2c 100644
--- a/specs/Modal.spec.js
+++ b/specs/Modal.spec.js
@@ -100,6 +100,92 @@ export default () => {
});
});
+ it("restores each app element's original aria-hidden value", () => {
+ withElementCollector(() => {
+ const visible = createHTMLElement("div");
+ const hidden = createHTMLElement("div");
+ const explicitFalse = createHTMLElement("div");
+ const node = createHTMLElement("div");
+ hidden.setAttribute("aria-hidden", "true");
+ explicitFalse.setAttribute("aria-hidden", "false");
+ const targets = [visible, hidden, explicitFalse];
+
+ ReactDOM.render(, node);
+ targets.forEach(el => el.getAttribute("aria-hidden").should.eql("true"));
+ ReactDOM.render(, node);
+
+ should(visible.getAttribute("aria-hidden")).equal(null);
+ hidden.getAttribute("aria-hidden").should.eql("true");
+ explicitFalse.getAttribute("aria-hidden").should.eql("false");
+ ReactDOM.unmountComponentAtNode(node);
+ });
+ });
+
+ it("restores overlapping app elements only after their last modal closes", () => {
+ withElementCollector(() => {
+ const first = createHTMLElement("div");
+ const shared = createHTMLElement("div");
+ const last = createHTMLElement("div");
+ const node = createHTMLElement("div");
+ shared.setAttribute("aria-hidden", "false");
+ last.setAttribute("aria-hidden", "true");
+ const render = (openFirst, openLast) => ReactDOM.render(
+
+
+
+
,
+ node
+ );
+
+ render(true, true);
+ render(false, true);
+ should(first.getAttribute("aria-hidden")).equal(null);
+ shared.getAttribute("aria-hidden").should.eql("true");
+ last.getAttribute("aria-hidden").should.eql("true");
+ render(false, false);
+ shared.getAttribute("aria-hidden").should.eql("false");
+ last.getAttribute("aria-hidden").should.eql("true");
+ ReactDOM.unmountComponentAtNode(node);
+ });
+ });
+
+ it("restores the original target when props change before unmount", () => {
+ withElementCollector(() => {
+ const first = createHTMLElement("div");
+ const second = createHTMLElement("div");
+ const node = createHTMLElement("div");
+ first.setAttribute("aria-hidden", "false");
+
+ ReactDOM.render(, node);
+ ReactDOM.render(, node);
+ ReactDOM.unmountComponentAtNode(node);
+
+ first.getAttribute("aria-hidden").should.eql("false");
+ should(second.getAttribute("aria-hidden")).equal(null);
+ });
+ });
+
+ it("keeps a shared target hidden when the last-opened modal closes first", () => {
+ withElementCollector(() => {
+ const target = createHTMLElement("div");
+ const node = createHTMLElement("div");
+ const render = (first, second) => ReactDOM.render(
+
+
+
+
,
+ node
+ );
+
+ render(true, true);
+ render(true, false);
+ target.getAttribute("aria-hidden").should.eql("true");
+ render(false, false);
+ should(target.getAttribute("aria-hidden")).equal(null);
+ ReactDOM.unmountComponentAtNode(node);
+ });
+ });
+
it("renders into the body, not in context", () => {
withElementCollector(() => {
const node = createHTMLElement("div");
diff --git a/src/components/ModalPortal.js b/src/components/ModalPortal.js
index 7f20df67..ec8ead47 100644
--- a/src/components/ModalPortal.js
+++ b/src/components/ModalPortal.js
@@ -25,8 +25,6 @@ const CLASS_NAMES = {
const isTabKey = event => event.code === "Tab" || event.keyCode === 9;
const isEscKey = event => event.code === "Escape" || event.keyCode === 27;
-let ariaHiddenInstances = 0;
-
export default class ModalPortal extends Component {
static defaultProps = {
style: {
@@ -90,6 +88,7 @@ export default class ModalPortal extends Component {
this.shouldClose = null;
this.moveFromContentToOverlay = null;
+ this.ariaHiddenElements = null;
}
componentDidMount() {
@@ -171,22 +170,15 @@ export default class ModalPortal extends Component {
htmlOpenClassName
);
- if (ariaHideApp) {
- ariaHiddenInstances += 1;
- ariaAppHider.hide(appElement);
+ if (ariaHideApp && !this.ariaHiddenElements) {
+ this.ariaHiddenElements = ariaAppHider.hide(appElement);
}
portalOpenInstances.register(this);
}
afterClose = () => {
- const {
- appElement,
- ariaHideApp,
- htmlOpenClassName,
- bodyOpenClassName,
- parentSelector
- } = this.props;
+ const { htmlOpenClassName, bodyOpenClassName, parentSelector } = this.props;
const parentDocument =
(parentSelector && parentSelector().ownerDocument) || document;
@@ -201,13 +193,9 @@ export default class ModalPortal extends Component {
htmlOpenClassName
);
- // Reset aria-hidden attribute if all modals have been removed
- if (ariaHideApp && ariaHiddenInstances > 0) {
- ariaHiddenInstances -= 1;
-
- if (ariaHiddenInstances === 0) {
- ariaAppHider.show(appElement);
- }
+ if (this.ariaHiddenElements) {
+ ariaAppHider.show(this.ariaHiddenElements);
+ this.ariaHiddenElements = null;
}
if (this.props.shouldFocusAfterRender) {
diff --git a/src/helpers/ariaAppHider.js b/src/helpers/ariaAppHider.js
index 2620bcad..658bebaa 100644
--- a/src/helpers/ariaAppHider.js
+++ b/src/helpers/ariaAppHider.js
@@ -2,10 +2,12 @@ import warning from "warning";
import { canUseDOM } from "./safeHTMLElement";
let globalElement = null;
+let hiddenElements = new Map();
/* eslint-disable no-console */
/* istanbul ignore next */
export function resetState() {
+ hiddenElements.clear();
if (globalElement) {
if (globalElement.removeAttribute) {
globalElement.removeAttribute("aria-hidden");
@@ -75,14 +77,35 @@ export function validateElement(appElement) {
}
export function hide(appElement) {
- for (let el of validateElement(appElement)) {
+ const elements = Array.prototype.slice.call(validateElement(appElement));
+ for (let el of elements) {
+ const previous = hiddenElements.get(el);
+ if (previous) {
+ previous.count += 1;
+ } else {
+ hiddenElements.set(el, {
+ count: 1,
+ value: el.getAttribute("aria-hidden")
+ });
+ }
el.setAttribute("aria-hidden", "true");
}
+ return elements;
}
export function show(appElement) {
for (let el of validateElement(appElement)) {
- el.removeAttribute("aria-hidden");
+ const previous = hiddenElements.get(el);
+ if (!previous) continue;
+ previous.count -= 1;
+ if (previous.count === 0) {
+ if (previous.value === null) {
+ el.removeAttribute("aria-hidden");
+ } else {
+ el.setAttribute("aria-hidden", previous.value);
+ }
+ hiddenElements.delete(el);
+ }
}
}