Skip to content

Commit

Permalink
[added] Support using multiple document.body classes
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
Austin Wood authored and diasbruno committed Jul 7, 2017
1 parent 581be77 commit 9076eb7
Show file tree
Hide file tree
Showing 7 changed files with 93 additions and 27 deletions.
22 changes: 17 additions & 5 deletions docs/styles/classes.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
### CSS Classes

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.
You can override the default class that is added to `document.body` when the modal is open by defining a property `bodyOpenClassName`.
Sometimes it may be preferable to use CSS classes rather than inline styles.

It's required that `bodyOpenClassName` must be `constant string`, otherwise we would end up with a complex system to manage which class name
should appear or be removed from `document.body` from which modal (if using multiple modals simultaneously).
You can use the `className` and `overlayClassName` props to specify a given CSS
class for each of those.

You can override the default class that is added to `document.body` when the
modal is open by defining a property `bodyOpenClassName`.

It's required that `bodyOpenClassName` must be `constant string`, otherwise we
would end up with a complex system to manage which class name should appear or
be removed from `document.body` from which modal (if using multiple modals
simultaneously).

`bodyOpenClassName` can support adding multiple classes to `document.body` when
the modal is open. Add as many class names as you desire, delineated by spaces.

Note: If you provide those props all default styles will not be applied, leaving
all styles under control of the CSS class.

Note: If you provide those props all default styles will not be applied, leaving all styles under control of the CSS class.
The `portalClassName` can also be used however there are no styles by default applied
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@
"webpack-dev-server": "1.11.0"
},
"dependencies": {
"element-class": "^0.2.0",
"exenv": "1.2.0",
"prop-types": "^15.5.10",
"react-dom-factories": "^1.0.0"
Expand Down
28 changes: 28 additions & 0 deletions specs/Modal.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,34 @@ describe('State', () => {
expect(!isBodyWithReactModalOpenClass()).toBeTruthy();
});

it('supports adding/removing multiple document.body classes', () => {
renderModal({
isOpen: true,
bodyOpenClassName: 'A B C'
});
expect(document.body.classList.contains('A', 'B', 'C')).toBeTruthy();
unmountModal();
expect(!document.body.classList.contains('A', 'B', 'C')).toBeTruthy();
});

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

expect(isBodyWithReactModalOpenClass('A B')).toBeTruthy();
unmountModal();
expect(!isBodyWithReactModalOpenClass('A B')).toBeTruthy();
expect(isBodyWithReactModalOpenClass('A')).toBeTruthy();
unmountModal();
expect(!isBodyWithReactModalOpenClass('A')).toBeTruthy();
});

it('should not add classes to document.body for unopened modals', () => {
renderModal({ isOpen: true });
expect(isBodyWithReactModalOpenClass()).toBeTruthy();
Expand Down
14 changes: 11 additions & 3 deletions specs/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,17 @@ const divStack = [];
/**
* Polyfill for String.includes on some node versions.
*/
if (!(String.prototype.hasOwnProperty('includes'))) {
String.prototype.includes = function(item) {
return this.length > 0 && this.split(" ").indexOf(item) !== -1;
if (!String.prototype.includes) {
String.prototype.includes = function(search, start) {
if (typeof start !== 'number') {
start = 0;
}

if (start + search.length > this.length) {
return false;
}

return this.indexOf(search, start) !== -1;
};
}

Expand Down
10 changes: 3 additions & 7 deletions src/components/ModalPortal.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import React, { Component } from 'react';
import { PropTypes } from 'prop-types';
import elementClass from 'element-class';
import * as focusManager from '../helpers/focusManager';
import scopeTab from '../helpers/scopeTab';
import * as ariaAppHider from '../helpers/ariaAppHider';
import * as refCount from '../helpers/refCount';
import * as bodyClassList from '../helpers/bodyClassList';
import SafeHTMLElement from '../helpers/safeHTMLElement';

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

beforeOpen() {
const { appElement, ariaHideApp, bodyOpenClassName } = this.props;
refCount.add(bodyOpenClassName);
// Add body class
elementClass(document.body).add(bodyOpenClassName);
bodyClassList.add(bodyOpenClassName);
// Add aria-hidden to appElement
if (ariaHideApp) {
ariaAppHider.hide(appElement);
Expand All @@ -130,11 +129,8 @@ export default class ModalPortal extends Component {

beforeClose() {
const { appElement, ariaHideApp, bodyOpenClassName } = this.props;
refCount.remove(bodyOpenClassName);
// Remove class if no more modals are open
if (refCount.count(bodyOpenClassName) === 0) {
elementClass(document.body).remove(bodyOpenClassName);
}
bodyClassList.remove(bodyOpenClassName);
// Reset aria-hidden attribute if all modals have been removed
if (ariaHideApp && refCount.totalCount() < 1) {
ariaAppHider.show(appElement);
Expand Down
20 changes: 20 additions & 0 deletions src/helpers/bodyClassList.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import * as refCount from './refCount';

export function add (bodyClass) {
// Increment class(es) on refCount tracker and add class(es) to body
bodyClass
.split(' ')
.map(refCount.add)
.forEach(className => document.body.classList.add(className));
}

export function remove (bodyClass) {
const classListMap = refCount.get();
// Decrement class(es) from the refCount tracker
// and remove unused class(es) from body
bodyClass
.split(' ')
.map(refCount.remove)
.filter(className => classListMap[className] === 0)
.forEach(className => document.body.classList.remove(className));
}
25 changes: 14 additions & 11 deletions src/helpers/refCount.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,26 @@
const modals = {};
const classListMap = {};

export function get() {
return classListMap;
}

export function add(bodyClass) {
// Set variable and default if none
if (!modals[bodyClass]) {
modals[bodyClass] = 0;
if (!classListMap[bodyClass]) {
classListMap[bodyClass] = 0;
}
modals[bodyClass] += 1;
classListMap[bodyClass] += 1;
return bodyClass;
}

export function remove(bodyClass) {
if (modals[bodyClass]) {
modals[bodyClass] -= 1;
if (classListMap[bodyClass]) {
classListMap[bodyClass] -= 1;
}
}

export function count(bodyClass) {
return modals[bodyClass];
return bodyClass;
}

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

0 comments on commit 9076eb7

Please sign in to comment.