diff --git a/specs/Modal.spec.js b/specs/Modal.spec.js index 47c43410..7765d967 100644 --- a/specs/Modal.spec.js +++ b/specs/Modal.spec.js @@ -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"); @@ -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 = ( +
+ + +
+ ); + + ReactDOM.render(initialState, rootNode); + appElement.getAttribute("aria-hidden").should.be.eql("true"); + + const updatedState = ( +
+ + +
+ ); + + 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 = ( +
+ + +
+ ); + + ReactDOM.render(initialState, rootNode); + appElement.getAttribute("aria-hidden").should.be.eql("true"); + + const updatedState = ( +
+ + +
+ ); + + 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; diff --git a/specs/helper.js b/specs/helper.js index e840ae97..1cf57516 100644 --- a/specs/helper.js +++ b/specs/helper.js @@ -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( - {children}, + {children}, currentDiv, callback ); diff --git a/src/components/ModalPortal.js b/src/components/ModalPortal.js index 6f48c513..ea46d945 100644 --- a/src/components/ModalPortal.js +++ b/src/components/ModalPortal.js @@ -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"; @@ -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: { @@ -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); } } @@ -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) { diff --git a/src/helpers/bodyClassList.js b/src/helpers/bodyClassList.js index 19b98628..dab78df6 100644 --- a/src/helpers/bodyClassList.js +++ b/src/helpers/bodyClassList.js @@ -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 }; diff --git a/src/helpers/refCount.js b/src/helpers/refCount.js deleted file mode 100644 index 033c3192..00000000 --- a/src/helpers/refCount.js +++ /dev/null @@ -1,28 +0,0 @@ -const classListMap = {}; - -export function get() { - return classListMap; -} - -export function add(bodyClass) { - // Set variable and default if none - if (!classListMap[bodyClass]) { - classListMap[bodyClass] = 0; - } - classListMap[bodyClass] += 1; - return bodyClass; -} - -export function remove(bodyClass) { - if (classListMap[bodyClass]) { - classListMap[bodyClass] -= 1; - } - return bodyClass; -} - -export function totalCount() { - return Object.keys(classListMap).reduce( - (acc, curr) => acc + classListMap[curr], - 0 - ); -}