Skip to content

Commit

Permalink
[fixed] management of aria-hidden attribute decoupled from the manage…
Browse files Browse the repository at this point in the history
…ment of the body open class
  • Loading branch information
kloots authored and diasbruno committed Feb 2, 2018
1 parent 93b2c05 commit 6c4d4ad
Show file tree
Hide file tree
Showing 5 changed files with 137 additions and 45 deletions.
100 changes: 99 additions & 1 deletion specs/Modal.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
} from "./helper";

export default () => {
afterEach("check if test cleaned up rendered modals", emptyDOM);
afterEach("cleaned up all rendered modals", emptyDOM);

it("scopes tab navigation to the modal");
it("focuses the last focused element when tabbing in from browser chrome");
Expand Down Expand Up @@ -352,6 +352,104 @@ export default () => {
should(el.getAttribute("aria-hidden")).not.be.ok();
});

// eslint-disable-next-line max-len
it("removes aria-hidden when closed and another modal with ariaHideApp set to false is open", () => {
const rootNode = document.createElement("div");
document.body.appendChild(rootNode);

const appElement = document.createElement("div");
document.body.appendChild(appElement);

Modal.setAppElement(appElement);

const initialState = (
<div>
<Modal isOpen={true} ariaHideApp={false} id="test-1-modal-1" />
<Modal isOpen={true} ariaHideApp={true} id="test-1-modal-2" />
</div>
);

ReactDOM.render(initialState, rootNode);
appElement.getAttribute("aria-hidden").should.be.eql("true");

const updatedState = (
<div>
<Modal isOpen={true} ariaHideApp={false} id="test-1-modal-1" />
<Modal isOpen={false} ariaHideApp={true} id="test-1-modal-2" />
</div>
);

ReactDOM.render(updatedState, rootNode);
should(appElement.getAttribute("aria-hidden")).not.be.ok();

ReactDOM.unmountComponentAtNode(rootNode);
});

// eslint-disable-next-line max-len
it("maintains aria-hidden when closed and another modal with ariaHideApp set to true is open", () => {
const rootNode = document.createElement("div");
document.body.appendChild(rootNode);

const appElement = document.createElement("div");
document.body.appendChild(appElement);

Modal.setAppElement(appElement);

const initialState = (
<div>
<Modal isOpen={true} ariaHideApp={true} id="test-1-modal-1" />
<Modal isOpen={true} ariaHideApp={true} id="test-1-modal-2" />
</div>
);

ReactDOM.render(initialState, rootNode);
appElement.getAttribute("aria-hidden").should.be.eql("true");

const updatedState = (
<div>
<Modal isOpen={true} ariaHideApp={true} id="test-1-modal-1" />
<Modal isOpen={false} ariaHideApp={true} id="test-1-modal-2" />
</div>
);

ReactDOM.render(updatedState, rootNode);
appElement.getAttribute("aria-hidden").should.be.eql("true");

ReactDOM.unmountComponentAtNode(rootNode);
});

// eslint-disable-next-line max-len
it("removes aria-hidden when unmounted without close and second modal with ariaHideApp=false is open", () => {
const appElement = document.createElement("div");
document.body.appendChild(appElement);
Modal.setAppElement(appElement);

renderModal({ isOpen: true, ariaHideApp: false, id: "test-2-modal-1" });
should(appElement.getAttribute("aria-hidden")).not.be.ok();

renderModal({ isOpen: true, ariaHideApp: true, id: "test-2-modal-2" });
appElement.getAttribute("aria-hidden").should.be.eql("true");

unmountModal();
should(appElement.getAttribute("aria-hidden")).not.be.ok();
});

// eslint-disable-next-line max-len
it("maintains aria-hidden when unmounted without close and second modal with ariaHideApp=true is open", () => {
const appElement = document.createElement("div");
document.body.appendChild(appElement);
Modal.setAppElement(appElement);

renderModal({ isOpen: true, ariaHideApp: true, id: "test-3-modal-1" });
appElement.getAttribute("aria-hidden").should.be.eql("true");

renderModal({ isOpen: true, ariaHideApp: true, id: "test-3-modal-2" });
appElement.getAttribute("aria-hidden").should.be.eql("true");

unmountModal();
appElement.getAttribute("aria-hidden").should.be.eql("true");
});

it("adds --after-open for animations", () => {
const modal = renderModal({ isOpen: true });
const rg = /--after-open/i;
Expand Down
5 changes: 3 additions & 2 deletions specs/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,14 +121,15 @@ export const mouseUpAt = Simulate.mouseUp;
export const mouseDownAt = Simulate.mouseDown;

export const renderModal = function(props, children, callback) {
props.ariaHideApp = false;
const modalProps = { ariaHideApp: false, ...props };

const currentDiv = document.createElement("div");
divStack.push(currentDiv);
document.body.appendChild(currentDiv);

// eslint-disable-next-line react/no-render-return-value
return ReactDOM.render(
<Modal {...props}>{children}</Modal>,
<Modal {...modalProps}>{children}</Modal>,
currentDiv,
callback
);
Expand Down
12 changes: 9 additions & 3 deletions src/components/ModalPortal.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import PropTypes from "prop-types";
import * as focusManager from "../helpers/focusManager";
import scopeTab from "../helpers/scopeTab";
import * as ariaAppHider from "../helpers/ariaAppHider";
import * as refCount from "../helpers/refCount";
import * as bodyClassList from "../helpers/bodyClassList";
import SafeHTMLElement from "../helpers/safeHTMLElement";

Expand All @@ -16,6 +15,8 @@ const CLASS_NAMES = {
const TAB_KEY = 9;
const ESC_KEY = 27;

let ariaHiddenInstances = 0;

export default class ModalPortal extends Component {
static defaultProps = {
style: {
Expand Down Expand Up @@ -121,6 +122,7 @@ export default class ModalPortal extends Component {
bodyClassList.add(bodyOpenClassName);
// Add aria-hidden to appElement
if (ariaHideApp) {
ariaHiddenInstances += 1;
ariaAppHider.hide(appElement);
}
}
Expand All @@ -132,8 +134,12 @@ export default class ModalPortal extends Component {
bodyClassList.remove(this.props.bodyOpenClassName);

// Reset aria-hidden attribute if all modals have been removed
if (ariaHideApp && refCount.totalCount() < 1) {
ariaAppHider.show(appElement);
if (ariaHideApp && ariaHiddenInstances > 0) {
ariaHiddenInstances -= 1;

if (ariaHiddenInstances === 0) {
ariaAppHider.show(appElement);
}
}

if (this.props.shouldFocusAfterRender) {
Expand Down
37 changes: 26 additions & 11 deletions src/helpers/bodyClassList.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,35 @@
import * as refCount from "./refCount";
const classListMap = {};

export function add(bodyClass) {
// Increment class(es) on refCount tracker and add class(es) to body
const addClassToMap = className => {
// Set variable and default if none
if (!classListMap[className]) {
classListMap[className] = 0;
}
classListMap[className] += 1;
return className;
};

const removeClassFromMap = className => {
if (classListMap[className]) {
classListMap[className] -= 1;
}
return className;
};

const add = bodyClass => {
bodyClass
.split(" ")
.map(refCount.add)
.map(addClassToMap)
.forEach(className => document.body.classList.add(className));
}
};

export function remove(bodyClass) {
const classListMap = refCount.get();
// Decrement class(es) from the refCount tracker
// and remove unused class(es) from body
const remove = bodyClass => {
// Remove unused class(es) from body
bodyClass
.split(" ")
.map(refCount.remove)
.map(removeClassFromMap)
.filter(className => classListMap[className] === 0)
.forEach(className => document.body.classList.remove(className));
}
};

export { add, remove };
28 changes: 0 additions & 28 deletions src/helpers/refCount.js

This file was deleted.

0 comments on commit 6c4d4ad

Please sign in to comment.