Skip to content

Commit

Permalink
[added] add class to html when modal is open
Browse files Browse the repository at this point in the history
  • Loading branch information
bannier authored and diasbruno committed Feb 21, 2018
1 parent e6159b6 commit 088e68e
Show file tree
Hide file tree
Showing 7 changed files with 121 additions and 8 deletions.
5 changes: 5 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ import ReactModal from 'react-modal';
See the `Styles` section for more details.
*/
bodyOpenClassName="ReactModal__Body--open"
/*
String className to be applied to the document.html (must be a constant string).
See the `Styles` section for more details.
*/
htmlOpenClassName="ReactModal__Html--open"
/*
Boolean indicating if the appElement should be hidden
*/
Expand Down
5 changes: 5 additions & 0 deletions examples/basic/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,8 @@
transform: scale(0.5) rotateX(30deg);
transition: all 150ms ease-in;
}

.ReactModal__Body--open,
.ReactModal__Html--open {
overflow: hidden;
}
79 changes: 75 additions & 4 deletions specs/Modal.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import Modal from "react-modal";
import * as ariaAppHider from "react-modal/helpers/ariaAppHider";
import {
isBodyWithReactModalOpenClass,
isHtmlWithReactModalOpenClass,
contentAttribute,
mcontent,
moverlay,
Expand Down Expand Up @@ -253,32 +254,49 @@ export default () => {
(document.body.className.indexOf("custom-modal-open") > -1).should.be.ok();
});

it("don't append class to document.body if modal is not open", () => {
it("supports overriding react modal open class in html.", () => {
renderModal({ isOpen: true, htmlOpenClassName: "custom-modal-open" });
(
document
.getElementsByTagName("html")[0]
.className.indexOf("custom-modal-open") > -1
).should.be.ok();
});

// eslint-disable-next-line max-len
it("don't append class to document.body and html if modal is not open", () => {
renderModal({ isOpen: false });
isBodyWithReactModalOpenClass().should.not.be.ok();
isHtmlWithReactModalOpenClass().should.not.be.ok();
unmountModal();
});

it("append class to document.body if modal is open", () => {
it("append class to document.body and html if modal is open", () => {
renderModal({ isOpen: true });
isBodyWithReactModalOpenClass().should.be.ok();
isHtmlWithReactModalOpenClass().should.be.ok();
unmountModal();
});

it("removes class from document.body when unmounted without closing", () => {
// eslint-disable-next-line max-len
it("removes class from document.body and html when unmounted without closing", () => {
renderModal({ isOpen: true });
unmountModal();
isBodyWithReactModalOpenClass().should.not.be.ok();
isHtmlWithReactModalOpenClass().should.not.be.ok();
});

it("remove class from document.body when no modals opened", () => {
it("remove class from document.body and html when no modals opened", () => {
renderModal({ isOpen: true });
renderModal({ isOpen: true });
isBodyWithReactModalOpenClass().should.be.ok();
isHtmlWithReactModalOpenClass().should.be.ok();
unmountModal();
isBodyWithReactModalOpenClass().should.be.ok();
isHtmlWithReactModalOpenClass().should.be.ok();
unmountModal();
isBodyWithReactModalOpenClass().should.not.be.ok();
isHtmlWithReactModalOpenClass().should.not.be.ok();
});

it("supports adding/removing multiple document.body classes", () => {
Expand Down Expand Up @@ -328,6 +346,59 @@ export default () => {
isBodyWithReactModalOpenClass().should.be.ok();
});

it("supports adding/removing multiple html classes", () => {
renderModal({
isOpen: true,
htmlOpenClassName: "A B C"
});
document
.getElementsByTagName("html")[0]
.classList.contains("A", "B", "C")
.should.be.ok();
unmountModal();
document
.getElementsByTagName("html")[0]
.classList.contains("A", "B", "C")
.should.not.be.ok();
});

it("does not remove shared classes if more than one modal is open", () => {
renderModal({
isOpen: true,
htmlOpenClassName: "A"
});
renderModal({
isOpen: true,
htmlOpenClassName: "A B"
});

isHtmlWithReactModalOpenClass("A B").should.be.ok();
unmountModal();
isHtmlWithReactModalOpenClass("A B").should.not.be.ok();
isHtmlWithReactModalOpenClass("A").should.be.ok();
unmountModal();
isHtmlWithReactModalOpenClass("A").should.not.be.ok();
});

it("should not add classes to html for unopened modals", () => {
renderModal({ isOpen: true });
isHtmlWithReactModalOpenClass().should.be.ok();
renderModal({ isOpen: false, htmlOpenClassName: "testHtmlClass" });
isHtmlWithReactModalOpenClass("testHtmlClass").should.not.be.ok();
});

it("should not remove classes from html if modal is closed", () => {
renderModal({ isOpen: true });
isHtmlWithReactModalOpenClass().should.be.ok();
renderModal({ isOpen: false, htmlOpenClassName: "testHtmlClass" });
renderModal({ isOpen: false });
isHtmlWithReactModalOpenClass("testHtmlClass").should.not.be.ok();
isHtmlWithReactModalOpenClass().should.be.ok();
renderModal({ isOpen: false });
renderModal({ isOpen: false });
isHtmlWithReactModalOpenClass().should.be.ok();
});

it("additional aria attributes", () => {
const modal = renderModal(
{ isOpen: true, aria: { labelledby: "a" } },
Expand Down
13 changes: 12 additions & 1 deletion specs/helper.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import React from "react";
import ReactDOM from "react-dom";
import Modal, { bodyOpenClassName } from "../src/components/Modal";
import Modal, {
bodyOpenClassName,
htmlOpenClassName
} from "../src/components/Modal";
import TestUtils from "react-dom/test-utils";

const divStack = [];
Expand Down Expand Up @@ -30,6 +33,14 @@ if (!String.prototype.includes) {
export const isBodyWithReactModalOpenClass = (bodyClass = bodyOpenClassName) =>
document.body.className.includes(bodyClass);

/**
* Check if the html contains the react modal
* open class.
* @return {Boolean}
*/
export const isHtmlWithReactModalOpenClass = (htmlClass = htmlOpenClassName) =>
document.getElementsByTagName("html")[0].className.includes(htmlClass);

/**
* Returns a rendered dom element by class.
* @param {React} element A react instance.
Expand Down
3 changes: 3 additions & 0 deletions src/components/Modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import SafeHTMLElement, { canUseDOM } from "../helpers/safeHTMLElement";

export const portalClassName = "ReactModalPortal";
export const bodyOpenClassName = "ReactModal__Body--open";
export const htmlOpenClassName = "ReactModal__Html--open";

const isReact16 = ReactDOM.createPortal !== undefined;
const createPortal = isReact16
Expand All @@ -31,6 +32,7 @@ export default class Modal extends Component {
}),
portalClassName: PropTypes.string,
bodyOpenClassName: PropTypes.string,
htmlOpenClassName: PropTypes.string,
className: PropTypes.oneOfType([
PropTypes.string,
PropTypes.shape({
Expand Down Expand Up @@ -69,6 +71,7 @@ export default class Modal extends Component {
isOpen: false,
portalClassName,
bodyOpenClassName,
htmlOpenClassName,
ariaHideApp: true,
closeTimeoutMS: 0,
shouldFocusAfterRender: true,
Expand Down
24 changes: 21 additions & 3 deletions src/components/ModalPortal.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export default class ModalPortal extends Component {
className: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
overlayClassName: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
bodyOpenClassName: PropTypes.string,
htmlOpenClassName: PropTypes.string,
ariaHideApp: PropTypes.bool,
appElement: PropTypes.instanceOf(SafeHTMLElement),
onAfterOpen: PropTypes.func,
Expand Down Expand Up @@ -84,6 +85,13 @@ export default class ModalPortal extends Component {
"This may cause unexpected behavior when multiple modals are open."
);
}
if (newProps.htmlOpenClassName !== this.props.htmlOpenClassName) {
// eslint-disable-next-line no-console
console.warn(
'React-Modal: "htmlOpenClassName" prop has been modified. ' +
"This may cause unexpected behavior when multiple modals are open."
);
}
}
// Focus only needs to be set once when the modal is being opened
if (!this.props.isOpen && newProps.isOpen) {
Expand Down Expand Up @@ -121,9 +129,15 @@ export default class ModalPortal extends Component {
};

beforeOpen() {
const { appElement, ariaHideApp, bodyOpenClassName } = this.props;
// Add body class
bodyClassList.add(bodyOpenClassName);
const {
appElement,
ariaHideApp,
bodyOpenClassName,
htmlOpenClassName
} = this.props;
// Add body and html class
bodyClassList.add(document.body, bodyOpenClassName);
classList.add(document.getElementsByTagName("html")[0], htmlOpenClassName);
// Add aria-hidden to appElement
if (ariaHideApp) {
ariaHiddenInstances += 1;
Expand All @@ -136,6 +150,10 @@ export default class ModalPortal extends Component {

// Remove body class
bodyClassList.remove(this.props.bodyOpenClassName);
classList.remove(
document.getElementsByTagName("html")[0],
this.props.htmlOpenClassName
);

// Reset aria-hidden attribute if all modals have been removed
if (ariaHideApp && ariaHiddenInstances > 0) {
Expand Down
File renamed without changes.

0 comments on commit 088e68e

Please sign in to comment.