Skip to content

Commit 9076eb7

Browse files
Austin Wooddiasbruno
Austin Wood
authored andcommitted
[added] Support using multiple document.body classes
* update `bodyOpenClassName` prop to handle adding and removing multiple class names * update String#includes polyfill to work properly * ensure shared classes on `document.body` persist on one modal close if multiple modals are open * create new helper for adding/removing class names from body element * remove unmaintained and obsolete `element-class` library * rename refCount private variable `modals` to `classListMap` * create `get` method on refCount helper for public access to the class list count
1 parent 581be77 commit 9076eb7

File tree

7 files changed

+93
-27
lines changed

7 files changed

+93
-27
lines changed

docs/styles/classes.md

+17-5
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,22 @@
11
### CSS Classes
22

3-
Sometimes it may be preferable to use CSS classes rather than inline styles. You can use the `className` and `overlayClassName` props to specify a given CSS class for each of those.
4-
You can override the default class that is added to `document.body` when the modal is open by defining a property `bodyOpenClassName`.
3+
Sometimes it may be preferable to use CSS classes rather than inline styles.
54

6-
It's required that `bodyOpenClassName` must be `constant string`, otherwise we would end up with a complex system to manage which class name
7-
should appear or be removed from `document.body` from which modal (if using multiple modals simultaneously).
5+
You can use the `className` and `overlayClassName` props to specify a given CSS
6+
class for each of those.
7+
8+
You can override the default class that is added to `document.body` when the
9+
modal is open by defining a property `bodyOpenClassName`.
10+
11+
It's required that `bodyOpenClassName` must be `constant string`, otherwise we
12+
would end up with a complex system to manage which class name should appear or
13+
be removed from `document.body` from which modal (if using multiple modals
14+
simultaneously).
15+
16+
`bodyOpenClassName` can support adding multiple classes to `document.body` when
17+
the modal is open. Add as many class names as you desire, delineated by spaces.
18+
19+
Note: If you provide those props all default styles will not be applied, leaving
20+
all styles under control of the CSS class.
821

9-
Note: If you provide those props all default styles will not be applied, leaving all styles under control of the CSS class.
1022
The `portalClassName` can also be used however there are no styles by default applied

package.json

-1
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,6 @@
6161
"webpack-dev-server": "1.11.0"
6262
},
6363
"dependencies": {
64-
"element-class": "^0.2.0",
6564
"exenv": "1.2.0",
6665
"prop-types": "^15.5.10",
6766
"react-dom-factories": "^1.0.0"

specs/Modal.spec.js

+28
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,34 @@ describe('State', () => {
271271
expect(!isBodyWithReactModalOpenClass()).toBeTruthy();
272272
});
273273

274+
it('supports adding/removing multiple document.body classes', () => {
275+
renderModal({
276+
isOpen: true,
277+
bodyOpenClassName: 'A B C'
278+
});
279+
expect(document.body.classList.contains('A', 'B', 'C')).toBeTruthy();
280+
unmountModal();
281+
expect(!document.body.classList.contains('A', 'B', 'C')).toBeTruthy();
282+
});
283+
284+
it('does not remove shared classes if more than one modal is open', () => {
285+
renderModal({
286+
isOpen: true,
287+
bodyOpenClassName: 'A'
288+
});
289+
renderModal({
290+
isOpen: true,
291+
bodyOpenClassName: 'A B'
292+
});
293+
294+
expect(isBodyWithReactModalOpenClass('A B')).toBeTruthy();
295+
unmountModal();
296+
expect(!isBodyWithReactModalOpenClass('A B')).toBeTruthy();
297+
expect(isBodyWithReactModalOpenClass('A')).toBeTruthy();
298+
unmountModal();
299+
expect(!isBodyWithReactModalOpenClass('A')).toBeTruthy();
300+
});
301+
274302
it('should not add classes to document.body for unopened modals', () => {
275303
renderModal({ isOpen: true });
276304
expect(isBodyWithReactModalOpenClass()).toBeTruthy();

specs/helper.js

+11-3
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,17 @@ const divStack = [];
88
/**
99
* Polyfill for String.includes on some node versions.
1010
*/
11-
if (!(String.prototype.hasOwnProperty('includes'))) {
12-
String.prototype.includes = function(item) {
13-
return this.length > 0 && this.split(" ").indexOf(item) !== -1;
11+
if (!String.prototype.includes) {
12+
String.prototype.includes = function(search, start) {
13+
if (typeof start !== 'number') {
14+
start = 0;
15+
}
16+
17+
if (start + search.length > this.length) {
18+
return false;
19+
}
20+
21+
return this.indexOf(search, start) !== -1;
1422
};
1523
}
1624

src/components/ModalPortal.js

+3-7
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import React, { Component } from 'react';
22
import { PropTypes } from 'prop-types';
3-
import elementClass from 'element-class';
43
import * as focusManager from '../helpers/focusManager';
54
import scopeTab from '../helpers/scopeTab';
65
import * as ariaAppHider from '../helpers/ariaAppHider';
76
import * as refCount from '../helpers/refCount';
7+
import * as bodyClassList from '../helpers/bodyClassList';
88
import SafeHTMLElement from '../helpers/safeHTMLElement';
99

1010
// so that our CSS is statically analyzable
@@ -119,9 +119,8 @@ export default class ModalPortal extends Component {
119119

120120
beforeOpen() {
121121
const { appElement, ariaHideApp, bodyOpenClassName } = this.props;
122-
refCount.add(bodyOpenClassName);
123122
// Add body class
124-
elementClass(document.body).add(bodyOpenClassName);
123+
bodyClassList.add(bodyOpenClassName);
125124
// Add aria-hidden to appElement
126125
if (ariaHideApp) {
127126
ariaAppHider.hide(appElement);
@@ -130,11 +129,8 @@ export default class ModalPortal extends Component {
130129

131130
beforeClose() {
132131
const { appElement, ariaHideApp, bodyOpenClassName } = this.props;
133-
refCount.remove(bodyOpenClassName);
134132
// Remove class if no more modals are open
135-
if (refCount.count(bodyOpenClassName) === 0) {
136-
elementClass(document.body).remove(bodyOpenClassName);
137-
}
133+
bodyClassList.remove(bodyOpenClassName);
138134
// Reset aria-hidden attribute if all modals have been removed
139135
if (ariaHideApp && refCount.totalCount() < 1) {
140136
ariaAppHider.show(appElement);

src/helpers/bodyClassList.js

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import * as refCount from './refCount';
2+
3+
export function add (bodyClass) {
4+
// Increment class(es) on refCount tracker and add class(es) to body
5+
bodyClass
6+
.split(' ')
7+
.map(refCount.add)
8+
.forEach(className => document.body.classList.add(className));
9+
}
10+
11+
export function remove (bodyClass) {
12+
const classListMap = refCount.get();
13+
// Decrement class(es) from the refCount tracker
14+
// and remove unused class(es) from body
15+
bodyClass
16+
.split(' ')
17+
.map(refCount.remove)
18+
.filter(className => classListMap[className] === 0)
19+
.forEach(className => document.body.classList.remove(className));
20+
}

src/helpers/refCount.js

+14-11
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,26 @@
1-
const modals = {};
1+
const classListMap = {};
2+
3+
export function get() {
4+
return classListMap;
5+
}
26

37
export function add(bodyClass) {
48
// Set variable and default if none
5-
if (!modals[bodyClass]) {
6-
modals[bodyClass] = 0;
9+
if (!classListMap[bodyClass]) {
10+
classListMap[bodyClass] = 0;
711
}
8-
modals[bodyClass] += 1;
12+
classListMap[bodyClass] += 1;
13+
return bodyClass;
914
}
1015

1116
export function remove(bodyClass) {
12-
if (modals[bodyClass]) {
13-
modals[bodyClass] -= 1;
17+
if (classListMap[bodyClass]) {
18+
classListMap[bodyClass] -= 1;
1419
}
15-
}
16-
17-
export function count(bodyClass) {
18-
return modals[bodyClass];
20+
return bodyClass;
1921
}
2022

2123
export function totalCount() {
22-
return Object.keys(modals).reduce((acc, curr) => acc + modals[curr], 0);
24+
return Object.keys(classListMap)
25+
.reduce((acc, curr) => acc + classListMap[curr], 0);
2326
}

0 commit comments

Comments
 (0)