diff --git a/package.json b/package.json index fb2567188..5c8a0a176 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "babel-core": "^5.4.7", "babel-eslint": "^3.1.10", "babel-loader": "^5.1.3", - "bootstrap-sass": "3.3.1", + "bootstrap-sass": "3.3.5", + "classlist-polyfill": "^1.0.1", "classnames": "^2.1.2", "conventional-changelog": "0.1.0-alpha.1", "conventional-recommended-bump": "0.0.1", diff --git a/spec/pivotal-ui-react/modals/modals_spec.js b/spec/pivotal-ui-react/modals/modals_spec.js index 6a85d4021..959b4819c 100644 --- a/spec/pivotal-ui-react/modals/modals_spec.js +++ b/spec/pivotal-ui-react/modals/modals_spec.js @@ -61,7 +61,7 @@ describe('Modals', function() { }); describe('when mounting', function() { - it('removes the key up event listener', function() { + it('adds the key up event listener', function() { expect(document.body.addEventListener).toHaveBeenCalledWith('keyup', subject.refs.modal.onKeyUp, false); }); }); @@ -74,34 +74,61 @@ describe('Modals', function() { }); }); - describe('clicking on the modal trigger', function() { - beforeEach(function() { - $('#openButton').simulate('click'); - }); - + function itOpensTheModal() { it('renders a modal', function() { expect('.modal').toExist(); - expect('.modal').toHaveClass('modal-basic'); - expect('.modal .modal-backdrop').toHaveClass('fade'); - expect('.modal .modal-backdrop').toHaveClass('in'); expect('.modal .modal-header').toContainText('What a Header!'); expect('.modal .modal-body').toContainText('Text in a body'); expect('.modal .modal-footer').toContainText('Text in a footer'); }); + it('adds the modal-open class to the body', () => { + expect('body').toHaveClass('modal-open'); + }); + + it('renders a modal-backdrop', function() { + expect('.modal-backdrop').toExist(); + }); + } + + function itKeepsTheModalOpen() { itOpensTheModal(); } + + function itClosesTheModal() { + it('closes the modal', function() { + expect('.modal').not.toExist(); + }); + + it('removes the modal-backdrop', function() { + expect('.modal-backdrop').not.toExist(); + }); + + it('removes the modal-open class from the body', function() { + expect('body').not.toHaveClass('modal-open'); + }); + } + + describe('clicking on the modal trigger', function() { + beforeEach(function() { + $('#openButton').simulate('click'); + }); + + itOpensTheModal(); + describe('pressing any key', function() { describe('for the escape key', function() { - it('closes the modal', function() { + beforeEach(function() { document.body.addEventListener.calls.mostRecent().args[1]({keyCode: 27}); - expect(subject.refs.modal.state.isVisible).toBe(false); }); + + itClosesTheModal(); }); describe('any other key', function() { - it('does not close the modal', function() { + beforeEach(function() { document.body.addEventListener.calls.mostRecent().args[1]({keyCode: 13}); - expect(subject.refs.modal.state.isVisible).toBe(true); }); + + itKeepsTheModalOpen(); }); }); @@ -110,29 +137,31 @@ describe('Modals', function() { $('.modal button.close').simulate('click'); }); - it('closes the modal', function() { - expect(subject.refs.modal.state.isVisible).toBe(false); - }); + itClosesTheModal(); describe('opening the modal again', function() { beforeEach(function() { $('#openButton').simulate('click'); }); - it('opens the modal', function() { - expect(subject.refs.modal.state.isVisible).toBe(true); - }); + itOpensTheModal(); }); }); - describe('clicking on the modal backdrop', function() { + describe('clicking inside the modal-dialog', function() { beforeEach(function() { - $('.modal .modal-backdrop').simulate('click'); + $('.modal-dialog').simulate('click'); }); - it('closes the modal', function() { - expect(subject.refs.modal.state.isVisible).toBe(false); + itKeepsTheModalOpen(); + }); + + describe('clicking outside the modal-dialog', function() { + beforeEach(function() { + $('.modal').simulate('click'); }); + + itClosesTheModal(); }); describe('clicking the close button', function() { @@ -140,9 +169,7 @@ describe('Modals', function() { $('#closeButton').simulate('click'); }); - it('closes the modal', function() { - expect(subject.refs.modal.state.isVisible).toBe(false); - }); + itClosesTheModal(); }); }); }); diff --git a/src/pivotal-ui-react/modals/modals.js b/src/pivotal-ui-react/modals/modals.js index 9d80c5b54..dc26ae08c 100644 --- a/src/pivotal-ui-react/modals/modals.js +++ b/src/pivotal-ui-react/modals/modals.js @@ -1,5 +1,6 @@ var React = require('react/addons'); var {DefaultH4} = require('pui-react-typography'); +require('classlist-polyfill'); var ReactCSSTransitionGroup = React.addons.CSSTransitionGroup; /** @@ -56,7 +57,16 @@ var Modal = React.createClass({ }, getInitialState() { - return ({isVisible: false}); + return {isVisible: false}; + }, + + componentWillUpdate(nextProps, nextState) { + if (nextState.isVisible) { + document.body.classList.add('modal-open'); + } + else { + document.body.classList.remove('modal-open'); + } }, open() { @@ -67,6 +77,12 @@ var Modal = React.createClass({ this.setState({isVisible: false}); }, + childrenClick(e) { + if (e.target === this.refs.modal.getDOMNode()) { + this.close(); + } + }, + onKeyUp(e) { if (e.keyCode === 27) { this.close(); @@ -74,10 +90,11 @@ var Modal = React.createClass({ }, render() { - var modalInnards = this.state.isVisible ? - ( -