Skip to content

Commit 088e68e

Browse files
bannierdiasbruno
authored andcommitted
[added] add class to html when modal is open
1 parent e6159b6 commit 088e68e

File tree

7 files changed

+121
-8
lines changed

7 files changed

+121
-8
lines changed

Diff for: docs/README.md

+5
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@ import ReactModal from 'react-modal';
6969
See the `Styles` section for more details.
7070
*/
7171
bodyOpenClassName="ReactModal__Body--open"
72+
/*
73+
String className to be applied to the document.html (must be a constant string).
74+
See the `Styles` section for more details.
75+
*/
76+
htmlOpenClassName="ReactModal__Html--open"
7277
/*
7378
Boolean indicating if the appElement should be hidden
7479
*/

Diff for: examples/basic/app.css

+5
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,8 @@
2929
transform: scale(0.5) rotateX(30deg);
3030
transition: all 150ms ease-in;
3131
}
32+
33+
.ReactModal__Body--open,
34+
.ReactModal__Html--open {
35+
overflow: hidden;
36+
}

Diff for: specs/Modal.spec.js

+75-4
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import Modal from "react-modal";
66
import * as ariaAppHider from "react-modal/helpers/ariaAppHider";
77
import {
88
isBodyWithReactModalOpenClass,
9+
isHtmlWithReactModalOpenClass,
910
contentAttribute,
1011
mcontent,
1112
moverlay,
@@ -253,32 +254,49 @@ export default () => {
253254
(document.body.className.indexOf("custom-modal-open") > -1).should.be.ok();
254255
});
255256

256-
it("don't append class to document.body if modal is not open", () => {
257+
it("supports overriding react modal open class in html.", () => {
258+
renderModal({ isOpen: true, htmlOpenClassName: "custom-modal-open" });
259+
(
260+
document
261+
.getElementsByTagName("html")[0]
262+
.className.indexOf("custom-modal-open") > -1
263+
).should.be.ok();
264+
});
265+
266+
// eslint-disable-next-line max-len
267+
it("don't append class to document.body and html if modal is not open", () => {
257268
renderModal({ isOpen: false });
258269
isBodyWithReactModalOpenClass().should.not.be.ok();
270+
isHtmlWithReactModalOpenClass().should.not.be.ok();
259271
unmountModal();
260272
});
261273

262-
it("append class to document.body if modal is open", () => {
274+
it("append class to document.body and html if modal is open", () => {
263275
renderModal({ isOpen: true });
264276
isBodyWithReactModalOpenClass().should.be.ok();
277+
isHtmlWithReactModalOpenClass().should.be.ok();
265278
unmountModal();
266279
});
267280

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

274-
it("remove class from document.body when no modals opened", () => {
289+
it("remove class from document.body and html when no modals opened", () => {
275290
renderModal({ isOpen: true });
276291
renderModal({ isOpen: true });
277292
isBodyWithReactModalOpenClass().should.be.ok();
293+
isHtmlWithReactModalOpenClass().should.be.ok();
278294
unmountModal();
279295
isBodyWithReactModalOpenClass().should.be.ok();
296+
isHtmlWithReactModalOpenClass().should.be.ok();
280297
unmountModal();
281298
isBodyWithReactModalOpenClass().should.not.be.ok();
299+
isHtmlWithReactModalOpenClass().should.not.be.ok();
282300
});
283301

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

349+
it("supports adding/removing multiple html classes", () => {
350+
renderModal({
351+
isOpen: true,
352+
htmlOpenClassName: "A B C"
353+
});
354+
document
355+
.getElementsByTagName("html")[0]
356+
.classList.contains("A", "B", "C")
357+
.should.be.ok();
358+
unmountModal();
359+
document
360+
.getElementsByTagName("html")[0]
361+
.classList.contains("A", "B", "C")
362+
.should.not.be.ok();
363+
});
364+
365+
it("does not remove shared classes if more than one modal is open", () => {
366+
renderModal({
367+
isOpen: true,
368+
htmlOpenClassName: "A"
369+
});
370+
renderModal({
371+
isOpen: true,
372+
htmlOpenClassName: "A B"
373+
});
374+
375+
isHtmlWithReactModalOpenClass("A B").should.be.ok();
376+
unmountModal();
377+
isHtmlWithReactModalOpenClass("A B").should.not.be.ok();
378+
isHtmlWithReactModalOpenClass("A").should.be.ok();
379+
unmountModal();
380+
isHtmlWithReactModalOpenClass("A").should.not.be.ok();
381+
});
382+
383+
it("should not add classes to html for unopened modals", () => {
384+
renderModal({ isOpen: true });
385+
isHtmlWithReactModalOpenClass().should.be.ok();
386+
renderModal({ isOpen: false, htmlOpenClassName: "testHtmlClass" });
387+
isHtmlWithReactModalOpenClass("testHtmlClass").should.not.be.ok();
388+
});
389+
390+
it("should not remove classes from html if modal is closed", () => {
391+
renderModal({ isOpen: true });
392+
isHtmlWithReactModalOpenClass().should.be.ok();
393+
renderModal({ isOpen: false, htmlOpenClassName: "testHtmlClass" });
394+
renderModal({ isOpen: false });
395+
isHtmlWithReactModalOpenClass("testHtmlClass").should.not.be.ok();
396+
isHtmlWithReactModalOpenClass().should.be.ok();
397+
renderModal({ isOpen: false });
398+
renderModal({ isOpen: false });
399+
isHtmlWithReactModalOpenClass().should.be.ok();
400+
});
401+
331402
it("additional aria attributes", () => {
332403
const modal = renderModal(
333404
{ isOpen: true, aria: { labelledby: "a" } },

Diff for: specs/helper.js

+12-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import React from "react";
22
import ReactDOM from "react-dom";
3-
import Modal, { bodyOpenClassName } from "../src/components/Modal";
3+
import Modal, {
4+
bodyOpenClassName,
5+
htmlOpenClassName
6+
} from "../src/components/Modal";
47
import TestUtils from "react-dom/test-utils";
58

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

36+
/**
37+
* Check if the html contains the react modal
38+
* open class.
39+
* @return {Boolean}
40+
*/
41+
export const isHtmlWithReactModalOpenClass = (htmlClass = htmlOpenClassName) =>
42+
document.getElementsByTagName("html")[0].className.includes(htmlClass);
43+
3344
/**
3445
* Returns a rendered dom element by class.
3546
* @param {React} element A react instance.

Diff for: src/components/Modal.js

+3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import SafeHTMLElement, { canUseDOM } from "../helpers/safeHTMLElement";
77

88
export const portalClassName = "ReactModalPortal";
99
export const bodyOpenClassName = "ReactModal__Body--open";
10+
export const htmlOpenClassName = "ReactModal__Html--open";
1011

1112
const isReact16 = ReactDOM.createPortal !== undefined;
1213
const createPortal = isReact16
@@ -31,6 +32,7 @@ export default class Modal extends Component {
3132
}),
3233
portalClassName: PropTypes.string,
3334
bodyOpenClassName: PropTypes.string,
35+
htmlOpenClassName: PropTypes.string,
3436
className: PropTypes.oneOfType([
3537
PropTypes.string,
3638
PropTypes.shape({
@@ -69,6 +71,7 @@ export default class Modal extends Component {
6971
isOpen: false,
7072
portalClassName,
7173
bodyOpenClassName,
74+
htmlOpenClassName,
7275
ariaHideApp: true,
7376
closeTimeoutMS: 0,
7477
shouldFocusAfterRender: true,

Diff for: src/components/ModalPortal.js

+21-3
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export default class ModalPortal extends Component {
3838
className: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
3939
overlayClassName: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
4040
bodyOpenClassName: PropTypes.string,
41+
htmlOpenClassName: PropTypes.string,
4142
ariaHideApp: PropTypes.bool,
4243
appElement: PropTypes.instanceOf(SafeHTMLElement),
4344
onAfterOpen: PropTypes.func,
@@ -84,6 +85,13 @@ export default class ModalPortal extends Component {
8485
"This may cause unexpected behavior when multiple modals are open."
8586
);
8687
}
88+
if (newProps.htmlOpenClassName !== this.props.htmlOpenClassName) {
89+
// eslint-disable-next-line no-console
90+
console.warn(
91+
'React-Modal: "htmlOpenClassName" prop has been modified. ' +
92+
"This may cause unexpected behavior when multiple modals are open."
93+
);
94+
}
8795
}
8896
// Focus only needs to be set once when the modal is being opened
8997
if (!this.props.isOpen && newProps.isOpen) {
@@ -121,9 +129,15 @@ export default class ModalPortal extends Component {
121129
};
122130

123131
beforeOpen() {
124-
const { appElement, ariaHideApp, bodyOpenClassName } = this.props;
125-
// Add body class
126-
bodyClassList.add(bodyOpenClassName);
132+
const {
133+
appElement,
134+
ariaHideApp,
135+
bodyOpenClassName,
136+
htmlOpenClassName
137+
} = this.props;
138+
// Add body and html class
139+
bodyClassList.add(document.body, bodyOpenClassName);
140+
classList.add(document.getElementsByTagName("html")[0], htmlOpenClassName);
127141
// Add aria-hidden to appElement
128142
if (ariaHideApp) {
129143
ariaHiddenInstances += 1;
@@ -136,6 +150,10 @@ export default class ModalPortal extends Component {
136150

137151
// Remove body class
138152
bodyClassList.remove(this.props.bodyOpenClassName);
153+
classList.remove(
154+
document.getElementsByTagName("html")[0],
155+
this.props.htmlOpenClassName
156+
);
139157

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

0 commit comments

Comments
 (0)