Skip to content

Commit

Permalink
Add class name manager utility for document.body element
Browse files Browse the repository at this point in the history
  • Loading branch information
Richard Palmer committed Mar 15, 2017
1 parent c8dbd6a commit 9df40eb
Show file tree
Hide file tree
Showing 5 changed files with 186 additions and 20 deletions.
4 changes: 0 additions & 4 deletions components/Modal/ModalAnimator.css
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
.documentBody {
overflow: hidden;
}

.root {
position: absolute;
height: 100%;
Expand Down
21 changes: 5 additions & 16 deletions components/Modal/ModalAnimator.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,15 @@ import cx from 'classnames';
import { TransitionMotion, spring } from 'react-motion';
import uniqueId from 'lodash/fp/uniqueId';
import Portal from 'react-portal';
import { canUseDOM } from 'exenv';
import { ESC } from '../../constants/keycodes';
import noop from '../../utils/noop';
import BodyClassNameConductor from '../../utils/BodyClassNameConductor/BodyClassNameConductor';

import css from './ModalAnimator.css';

const DEFAULT_WINDOW_SPRING_CONFIG = { stiffness: 200, damping: 22 };
const DEFAULT_OVERLAY_SPRING_CONFIG = { stiffness: 500, damping: 18 };

const toggleBodyClass = (add, className) => {
if (!canUseDOM) return;

const body = document.querySelector('body');

if (add && !body.classList.contains(className)) {
body.classList.add(className);
} else if (!add && body.classList.contains(className)) {
body.classList.remove(className);
}
};

export default class ModalAnimator extends Component {
static propTypes = {
id: PropTypes.string,
Expand Down Expand Up @@ -56,11 +44,12 @@ export default class ModalAnimator extends Component {
constructor(props) {
super(props);
this.id = props.id || uniqueId('modal');
this.bodyClassName = new BodyClassNameConductor(this.id);
}

componentDidMount() {
const { active } = this.props;
toggleBodyClass(active, css.documentBody);
if (active) this.bodyClassName.add('noScroll');
this.keyupEvent = window.addEventListener('keyup', this.handleKeyUp);
}

Expand All @@ -69,12 +58,12 @@ export default class ModalAnimator extends Component {
const { active: oldActive } = this.props;

if (newActive !== oldActive) {
toggleBodyClass(newActive, css.documentBody);
this.bodyClassName.toggle('noScroll');
}
}

componentWillUnmount() {
toggleBodyClass(false, css.documentBody);
this.bodyClassName.remove('noScroll');
window.removeEventListener('keyup', this.keyupEvent);
}

Expand Down
3 changes: 3 additions & 0 deletions utils/BodyClassNameConductor/BodyClassNameConductor.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.noScroll {
overflow: hidden;
}
76 changes: 76 additions & 0 deletions utils/BodyClassNameConductor/BodyClassNameConductor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { canUseDOM } from 'exenv';

import css from './BodyClassNameConductor.css';

export default class BodyClassNameConductor {
static classNameOriginMap = {};

constructor(origin) {
this.origin = origin;
}

getClassNameOriginMap = className =>
BodyClassNameConductor.classNameOriginMap[className] || new Set();

getBody = () => {
if (!this.body) this.body = document.querySelector('body');
return this.body;
}

addClassNameOrigin = (className) => {
const originList = this.getClassNameOriginMap(className);
originList.add(this.origin);

BodyClassNameConductor.classNameOriginMap = Object.assign({},
BodyClassNameConductor.classNameOriginMap, {
[className]: originList,
}
);
};

removeClassNameOrigin = (className) => {
const originList = this.getClassNameOriginMap(className);
originList.delete(this.origin);

BodyClassNameConductor.classNameOriginMap = Object.assign({},
BodyClassNameConductor.classNameOriginMap, {
[className]: originList,
}
);
};

add = (className) => {
if (!canUseDOM) return;

this.addClassNameOrigin(className);

if (!this.getBody().classList.contains(css[className])) {
this.getBody().classList.add(css[className]);
}
};

remove = (className) => {
if (!canUseDOM) return;

this.removeClassNameOrigin(className);

if (
this.getClassNameOriginMap(className).size <= 0 &&
this.getBody().classList.contains(css[className])
) {
this.getBody().classList.remove(css[className]);
}
};

toggle = (className) => {
if (!canUseDOM) return;

const originMap = this.getClassNameOriginMap(className);

if (originMap.has(this.origin)) {
this.remove(className);
} else {
this.add(className);
}
};
}
102 changes: 102 additions & 0 deletions utils/BodyClassNameConductor/BodyClassNameConductor.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import BodyClassNameConductor from './BodyClassNameConductor';

const mocked = {
foo: 'bar',
baz: 'baz',
};

jest.mock('./BodyClassNameConductor.css', () => ({
foo: 'bar',
baz: 'baz',
}));

it('adds a classname to the body', () => {
const conductor = new BodyClassNameConductor('foo');
const body = document.querySelector('body');
conductor.add('foo');

expect(body.classList.contains(mocked.foo)).toBe(true);
});

it('removes a classname from the body', () => {
const conductor = new BodyClassNameConductor('foo');
const body = document.querySelector('body');

conductor.add('foo');
conductor.remove('foo');

expect(body.classList.contains(mocked.foo)).toBe(false);
});

it('toggles a classname on the body', () => {
const conductor = new BodyClassNameConductor('foo');
const body = document.querySelector('body');
conductor.toggle('foo');

expect(body.classList.contains(mocked.foo)).toBe(true);

conductor.toggle('foo');

expect(body.classList.contains(mocked.foo)).toBe(false);
});

it('add multiple classnames to the body', () => {
const conductor = new BodyClassNameConductor('foo');
const body = document.querySelector('body');
conductor.add('foo');
conductor.add('baz');

expect(body.classList.contains(mocked.foo)).toBe(true);
expect(body.classList.contains(mocked.baz)).toBe(true);
});

it('remove multiple classnames from the body', () => {
const conductor = new BodyClassNameConductor('foo');
const body = document.querySelector('body');
conductor.add('foo');
conductor.add('baz');

conductor.remove('foo');
conductor.remove('baz');

expect(body.classList.contains(mocked.foo)).toBe(false);
expect(body.classList.contains(mocked.baz)).toBe(false);
});

test('that a classname is only removed when no origins references it', () => {
const conductorA = new BodyClassNameConductor('a');
const conductorB = new BodyClassNameConductor('b');
const body = document.querySelector('body');

conductorA.add('foo');
conductorB.add('foo');

expect(body.classList.contains(mocked.foo)).toBe(true);

conductorA.remove('foo');

expect(body.classList.contains(mocked.foo)).toBe(true);

conductorB.remove('foo');

expect(body.classList.contains(mocked.foo)).toBe(false);
});

test('that a classname is only toggled when no origins references it', () => {
const conductorA = new BodyClassNameConductor('a');
const conductorB = new BodyClassNameConductor('b');
const body = document.querySelector('body');

conductorA.add('foo');
conductorB.add('foo');

expect(body.classList.contains(mocked.foo)).toBe(true);

conductorA.toggle('foo');

expect(body.classList.contains(mocked.foo)).toBe(true);

conductorB.toggle('foo');

expect(body.classList.contains(mocked.foo)).toBe(false);
});

0 comments on commit 9df40eb

Please sign in to comment.