Skip to content

Commit

Permalink
[added] htmlOpenClassName will follow the same rules like...
Browse files Browse the repository at this point in the history
bodyOpenClassName.
  • Loading branch information
diasbruno committed Feb 21, 2018
1 parent 088e68e commit 0c6d966
Show file tree
Hide file tree
Showing 7 changed files with 183 additions and 109 deletions.
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ import ReactModal from 'react-modal';
bodyOpenClassName="ReactModal__Body--open"
/*
String className to be applied to the document.html (must be a constant string).
This attribute is `null` by default.
See the `Styles` section for more details.
*/
htmlOpenClassName="ReactModal__Html--open"
Expand Down
17 changes: 16 additions & 1 deletion docs/styles/classes.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ any styles applied using these default classes will not override the default
styles as they would if specified using the `className` or `overlayClassName`
props.

#### For the document body
#### For the document.body and html tag

You can override the default class that is added to `document.body` when the
modal is open by defining a property `bodyOpenClassName`.
Expand All @@ -61,6 +61,21 @@ non-default `bodyOpenClassName`), you could use the following CSS:
}
```

You can define a class to be added to the html tag, using the `htmlOpenClassName`
attribute, which can be helpeful to stop the page to scroll to the top when open
a modal.

This attribute follows the same rules as `bodyOpenClassName`, it must be a *constant string*;

Here is an example that can help preventing this behavior:

```CSS
.ReactModal__Body--open,
.ReactModal__Html--open {
overflow: hidden;
}
```

#### For the entire portal

To specify a class to be applied to the entire portal, you may use the
Expand Down
89 changes: 25 additions & 64 deletions specs/Modal.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import * as ariaAppHider from "react-modal/helpers/ariaAppHider";
import {
isBodyWithReactModalOpenClass,
isHtmlWithReactModalOpenClass,
htmlClassList,
contentAttribute,
mcontent,
moverlay,
Expand Down Expand Up @@ -254,46 +255,45 @@ export default () => {
(document.body.className.indexOf("custom-modal-open") > -1).should.be.ok();
});

it("supports overriding react modal open class in html.", () => {
it("supports setting 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();
isHtmlWithReactModalOpenClass("custom-modal-open").should.be.ok();
});

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

it("don't append class to <html /> if modal is closed.", () => {
renderModal({ isOpen: false, htmlOpenClassName: "custom-modal-open" });
isHtmlWithReactModalOpenClass().should.not.be.ok();
unmountModal();
});

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

it("don't append class to <html /> if not defined.", () => {
renderModal({ isOpen: true });
htmlClassList().should.be.empty();
});

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

it("remove class from document.body and html when no modals opened", () => {
it("remove class from document.body 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();
Expand Down Expand Up @@ -346,57 +346,18 @@ 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({
it("should not remove classes from <html /> if modal is closed", () => {
const modalA = renderModal({ isOpen: false });
isHtmlWithReactModalOpenClass().should.not.be.ok();
const modalB = renderModal({
isOpen: true,
htmlOpenClassName: "A B"
htmlOpenClassName: "testHtmlClass"
});

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 });
modalA.portal.close();
isHtmlWithReactModalOpenClass("testHtmlClass").should.be.ok();
modalB.portal.close();
isHtmlWithReactModalOpenClass().should.not.be.ok();
renderModal({ isOpen: false });
isHtmlWithReactModalOpenClass().should.be.ok();
});

it("additional aria attributes", () => {
Expand Down
22 changes: 16 additions & 6 deletions specs/helper.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import React from "react";
import ReactDOM from "react-dom";
import Modal, {
bodyOpenClassName,
htmlOpenClassName
} from "../src/components/Modal";
import Modal, { bodyOpenClassName } from "../src/components/Modal";
import TestUtils from "react-dom/test-utils";

const divStack = [];
Expand All @@ -25,6 +22,12 @@ if (!String.prototype.includes) {
};
}

/**
* Return the class list object from `document.body`.
* @return {Array}
*/
export const documentBodyClassList = () => document.body.classList;

/**
* Check if the document.body contains the react modal
* open class.
Expand All @@ -33,13 +36,20 @@ if (!String.prototype.includes) {
export const isBodyWithReactModalOpenClass = (bodyClass = bodyOpenClassName) =>
document.body.className.includes(bodyClass);

/**
* Return the class list object from <html />.
* @return {Array}
*/
export const htmlClassList = () =>
document.getElementsByTagName("html")[0].classList;

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

/**
* Returns a rendered dom element by class.
Expand Down
2 changes: 0 additions & 2 deletions src/components/Modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ 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 Down Expand Up @@ -71,7 +70,6 @@ export default class Modal extends Component {
isOpen: false,
portalClassName,
bodyOpenClassName,
htmlOpenClassName,
ariaHideApp: true,
closeTimeoutMS: 0,
shouldFocusAfterRender: true,
Expand Down
41 changes: 27 additions & 14 deletions src/components/ModalPortal.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import PropTypes from "prop-types";
import * as focusManager from "../helpers/focusManager";
import scopeTab from "../helpers/scopeTab";
import * as ariaAppHider from "../helpers/ariaAppHider";
import * as bodyClassList from "../helpers/bodyClassList";
import * as classList from "../helpers/classList";
import SafeHTMLElement from "../helpers/safeHTMLElement";

// so that our CSS is statically analyzable
Expand Down Expand Up @@ -132,28 +132,41 @@ export default class ModalPortal extends Component {
const {
appElement,
ariaHideApp,
bodyOpenClassName,
htmlOpenClassName
htmlOpenClassName,
bodyOpenClassName
} = this.props;
// Add body and html class
bodyClassList.add(document.body, bodyOpenClassName);
classList.add(document.getElementsByTagName("html")[0], htmlOpenClassName);
// Add aria-hidden to appElement

// Add classes.
classList.add(document.body, bodyOpenClassName);

htmlOpenClassName &&
classList.add(
document.getElementsByTagName("html")[0],
htmlOpenClassName
);

if (ariaHideApp) {
ariaHiddenInstances += 1;
ariaAppHider.hide(appElement);
}
}

afterClose = () => {
const { appElement, ariaHideApp } = this.props;
const {
appElement,
ariaHideApp,
htmlOpenClassName,
bodyOpenClassName
} = this.props;

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

htmlOpenClassName &&
classList.remove(
document.getElementsByTagName("html")[0],
htmlOpenClassName
);

// Reset aria-hidden attribute if all modals have been removed
if (ariaHideApp && ariaHiddenInstances > 0) {
Expand Down
Loading

0 comments on commit 0c6d966

Please sign in to comment.