Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[added] support Array, HTMLCollection and NodeList values for appElement #861

Merged
merged 2 commits into from
Apr 2, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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.

diasbruno marked this conversation as resolved.
Show resolved Hide resolved
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;