Skip to content

Commit

Permalink
[added] support Array, HTMLCollection and NodeList values for appElement
Browse files Browse the repository at this point in the history
  • Loading branch information
eemeli authored and diasbruno committed Apr 2, 2021
1 parent c9d8e2d commit e1807ce
Show file tree
Hide file tree
Showing 6 changed files with 82 additions and 13 deletions.
3 changes: 3 additions & 0 deletions docs/accessibility/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
40 changes: 40 additions & 0 deletions specs/Modal.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(<Modal isOpen={true} appElement={[el1, el2]} />, 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 {
Expand Down Expand Up @@ -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 (
<div>
<Modal isOpen>
<span>hello</span>
</Modal>
</div>
);
}
}
const appElement = "#id1, #id2";
Modal.setAppElement(appElement);
ReactDOM.render(<App />, 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);
Expand Down
13 changes: 11 additions & 2 deletions src/components/Modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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,
Expand Down
12 changes: 10 additions & 2 deletions src/components/ModalPortal.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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,
Expand Down
23 changes: 14 additions & 9 deletions src/helpers/ariaAppHider.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
[
Expand All @@ -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");
}
}

Expand Down
4 changes: 4 additions & 0 deletions src/helpers/safeHTMLElement.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

0 comments on commit e1807ce

Please sign in to comment.