From e1807cefe60a27e76a44a0fdcb0820023cf730ba Mon Sep 17 00:00:00 2001 From: Eemeli Aro Date: Wed, 31 Mar 2021 20:27:54 +0300 Subject: [PATCH] [added] support Array, HTMLCollection and NodeList values for appElement --- docs/accessibility/index.md | 3 +++ specs/Modal.spec.js | 40 ++++++++++++++++++++++++++++++++++ src/components/Modal.js | 13 +++++++++-- src/components/ModalPortal.js | 12 ++++++++-- src/helpers/ariaAppHider.js | 23 +++++++++++-------- src/helpers/safeHTMLElement.js | 4 ++++ 6 files changed, 82 insertions(+), 13 deletions(-) diff --git a/docs/accessibility/index.md b/docs/accessibility/index.md index 5ba13f18..505ae9f8 100644 --- a/docs/accessibility/index.md +++ b/docs/accessibility/index.md @@ -23,6 +23,9 @@ rewritten: Modal.setAppElement(document.getElementById('root')); ``` +Using a selector that matches multiple elements or passing a list of DOM +elements will hide all of the elements. + If you are already applying the `aria-hidden` attribute to your app content through other means, you can pass the `ariaHideApp={false}` prop to your modal to avoid getting a warning that your app element is not specified. diff --git a/specs/Modal.spec.js b/specs/Modal.spec.js index d13f1fe3..6131db84 100644 --- a/specs/Modal.spec.js +++ b/specs/Modal.spec.js @@ -65,6 +65,16 @@ export default () => { ReactDOM.unmountComponentAtNode(node); }); + it("accepts array of appElement as a prop", () => { + const el1 = document.createElement("div"); + const el2 = document.createElement("div"); + const node = document.createElement("div"); + ReactDOM.render(, node); + el1.getAttribute("aria-hidden").should.be.eql("true"); + el2.getAttribute("aria-hidden").should.be.eql("true"); + ReactDOM.unmountComponentAtNode(node); + }); + it("renders into the body, not in context", () => { const node = document.createElement("div"); class App extends Component { @@ -108,6 +118,36 @@ export default () => { ReactDOM.unmountComponentAtNode(node); }); + // eslint-disable-next-line max-len + it("allow setting appElement of type string matching multiple elements", () => { + const el1 = document.createElement("div"); + el1.id = "id1"; + document.body.appendChild(el1); + const el2 = document.createElement("div"); + el2.id = "id2"; + document.body.appendChild(el2); + const node = document.createElement("div"); + class App extends Component { + render() { + return ( +
+ + hello + +
+ ); + } + } + const appElement = "#id1, #id2"; + Modal.setAppElement(appElement); + ReactDOM.render(, node); + el1.getAttribute("aria-hidden").should.be.eql("true"); + el2.getAttribute("aria-hidden").should.be.eql("true"); + ReactDOM.unmountComponentAtNode(node); + document.body.removeChild(el1); + document.body.removeChild(el2); + }); + it("default parentSelector should be document.body.", () => { const modal = renderModal({ isOpen: true }); modal.props.parentSelector().should.be.eql(document.body); diff --git a/src/components/Modal.js b/src/components/Modal.js index aeaea8dd..cb6bc53d 100644 --- a/src/components/Modal.js +++ b/src/components/Modal.js @@ -3,7 +3,11 @@ import ReactDOM from "react-dom"; import PropTypes from "prop-types"; import ModalPortal from "./ModalPortal"; import * as ariaAppHider from "../helpers/ariaAppHider"; -import SafeHTMLElement, { canUseDOM } from "../helpers/safeHTMLElement"; +import SafeHTMLElement, { + SafeNodeList, + SafeHTMLCollection, + canUseDOM +} from "../helpers/safeHTMLElement"; import { polyfill } from "react-lifecycles-compat"; @@ -52,7 +56,12 @@ class Modal extends Component { beforeClose: PropTypes.string.isRequired }) ]), - appElement: PropTypes.instanceOf(SafeHTMLElement), + appElement: PropTypes.oneOfType([ + PropTypes.instanceOf(SafeHTMLElement), + PropTypes.instanceOf(SafeHTMLCollection), + PropTypes.instanceOf(SafeNodeList), + PropTypes.arrayOf(PropTypes.instanceOf(SafeHTMLElement)) + ]), onAfterOpen: PropTypes.func, onRequestClose: PropTypes.func, closeTimeoutMS: PropTypes.number, diff --git a/src/components/ModalPortal.js b/src/components/ModalPortal.js index ca05eb8d..b7ac6cea 100644 --- a/src/components/ModalPortal.js +++ b/src/components/ModalPortal.js @@ -4,7 +4,10 @@ import * as focusManager from "../helpers/focusManager"; import scopeTab from "../helpers/scopeTab"; import * as ariaAppHider from "../helpers/ariaAppHider"; import * as classList from "../helpers/classList"; -import SafeHTMLElement from "../helpers/safeHTMLElement"; +import SafeHTMLElement, { + SafeHTMLCollection, + SafeNodeList +} from "../helpers/safeHTMLElement"; import portalOpenInstances from "../helpers/portalOpenInstances"; import "../helpers/bodyTrap"; @@ -43,7 +46,12 @@ export default class ModalPortal extends Component { bodyOpenClassName: PropTypes.string, htmlOpenClassName: PropTypes.string, ariaHideApp: PropTypes.bool, - appElement: PropTypes.instanceOf(SafeHTMLElement), + appElement: PropTypes.oneOfType([ + PropTypes.instanceOf(SafeHTMLElement), + PropTypes.instanceOf(SafeHTMLCollection), + PropTypes.instanceOf(SafeNodeList), + PropTypes.arrayOf(PropTypes.instanceOf(SafeHTMLElement)) + ]), onAfterOpen: PropTypes.func, onAfterClose: PropTypes.func, onRequestClose: PropTypes.func, diff --git a/src/helpers/ariaAppHider.js b/src/helpers/ariaAppHider.js index 017b17db..2ee47710 100644 --- a/src/helpers/ariaAppHider.js +++ b/src/helpers/ariaAppHider.js @@ -16,14 +16,21 @@ export function setElement(element) { if (typeof useElement === "string" && canUseDOM) { const el = document.querySelectorAll(useElement); assertNodeList(el, useElement); - useElement = "length" in el ? el[0] : el; + useElement = el; } globalElement = useElement || globalElement; return globalElement; } export function validateElement(appElement) { - if (!appElement && !globalElement) { + const el = appElement || globalElement; + if (el) { + return Array.isArray(el) || + el instanceof HTMLCollection || + el instanceof NodeList + ? el + : [el]; + } else { warning( false, [ @@ -35,21 +42,19 @@ export function validateElement(appElement) { ].join(" ") ); - return false; + return []; } - - return true; } export function hide(appElement) { - if (validateElement(appElement)) { - (appElement || globalElement).setAttribute("aria-hidden", "true"); + for (let el of validateElement(appElement)) { + el.setAttribute("aria-hidden", "true"); } } export function show(appElement) { - if (validateElement(appElement)) { - (appElement || globalElement).removeAttribute("aria-hidden"); + for (let el of validateElement(appElement)) { + el.removeAttribute("aria-hidden"); } } diff --git a/src/helpers/safeHTMLElement.js b/src/helpers/safeHTMLElement.js index 5f650887..8ae67884 100644 --- a/src/helpers/safeHTMLElement.js +++ b/src/helpers/safeHTMLElement.js @@ -4,6 +4,10 @@ const EE = ExecutionEnvironment; const SafeHTMLElement = EE.canUseDOM ? window.HTMLElement : {}; +export const SafeHTMLCollection = EE.canUseDOM ? window.HTMLCollection : {}; + +export const SafeNodeList = EE.canUseDOM ? window.NodeList : {}; + export const canUseDOM = EE.canUseDOM; export default SafeHTMLElement;