diff --git a/docs/examples/.eslintrc b/docs/examples/.eslintrc
index 6f2fa1bdf5..9dbc49c623 100644
--- a/docs/examples/.eslintrc
+++ b/docs/examples/.eslintrc
@@ -36,6 +36,7 @@
"ModalTrigger",
"OverlayTrigger",
"OverlayMixin",
+ "Overlay",
"PageHeader",
"PageItem",
"Pager",
diff --git a/docs/examples/ModalContained.js b/docs/examples/ModalContained.js
index 626f17db42..aeab199ef7 100644
--- a/docs/examples/ModalContained.js
+++ b/docs/examples/ModalContained.js
@@ -9,28 +9,40 @@
* }
*/
-const ContainedModal = React.createClass({
- render() {
- return (
-
Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros.
Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor.
@@ -13,10 +16,10 @@ const MySmallModal = React.createClass({Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros.
Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor.
Aenean lacinia bibendum nulla sed consectetur. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Donec sed odio dui. Donec ullamcorper nulla non metus auctor fringilla.
-Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros.
Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor.
@@ -37,24 +43,37 @@ const MyLargeModal = React.createClass({Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros.
Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor.
Aenean lacinia bibendum nulla sed consectetur. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Donec sed odio dui. Donec ullamcorper nulla non metus auctor fringilla.
-Duis mollis, est non commodo luctus, nisi erat porttitor ligula.
- -TODO
- -TODO
- -Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros.
-Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor.
-Aenean lacinia bibendum nulla sed consectetur. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Donec sed odio dui. Donec ullamcorper nulla non metus auctor fringilla.
-Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros.
-Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor.
-Aenean lacinia bibendum nulla sed consectetur. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Donec sed odio dui. Donec ullamcorper nulla non metus auctor fringilla.
-Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros.
-Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor.
-Aenean lacinia bibendum nulla sed consectetur. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Donec sed odio dui. Donec ullamcorper nulla non metus auctor fringilla.
-Click to get the full Modal experience!
+ + + +Duis mollis, est non commodo luctus, nisi erat porttitor ligula.
+ +there is a
there is a
Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros.
+Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor.
+Aenean lacinia bibendum nulla sed consectetur. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Donec sed odio dui. Donec ullamcorper nulla non metus auctor fringilla.
+Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros.
+Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor.
+Aenean lacinia bibendum nulla sed consectetur. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Donec sed odio dui. Donec ullamcorper nulla non metus auctor fringilla.
+Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros.
+Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor.
+Aenean lacinia bibendum nulla sed consectetur. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Donec sed odio dui. Donec ullamcorper nulla non metus auctor fringilla.
+A rendered modal with header, body, and set of actions in the footer.
-The header is added automatically if you pass in a title
prop.
+ A rendered modal with header, body, and set of actions in the footer. The {'
Component comes with
+ a few convenient "sub components": {'
, {'
, {'
,
+ and {'
, which you can use to build the Modal content.
+
+ The Modal Header, Title, Body, and Footer components are available as static properties the {'
component, but you can also,
+ import them directly from the /lib
directory like: {"require('react-bootstrap/lib/ModalHeader')"}
.
+
Use <ModalTrigger />
to create a real modal that's added to the document body when opened.
Use {'
in combination with other components to show or hide your Modal.
Use OverlayMixin
in a custom component to manage the modal's state yourself.
You will need to add the following css to your project and ensure that your container has the modal-container
class.
{React.DOM.code(null, @@ -301,7 +307,19 @@ const ComponentsPage = React.createClass({Modal
- ModalTrigger
+Modal.Header
++ + Modal.Title
++ + Modal.Body
++ + Modal.Footer
++ + ModalTrigger Deprecated: use the Modal directly to manage it's visibility
diff --git a/src/Modal.js b/src/Modal.js index 39ff1b63ed..f862eacd89 100644 --- a/src/Modal.js +++ b/src/Modal.js @@ -1,15 +1,20 @@ -import React from 'react'; +import React, { cloneElement } from 'react'; + import classNames from 'classnames'; +import createChainedFunction from './utils/createChainedFunction'; import BootstrapMixin from './BootstrapMixin'; import FadeMixin from './FadeMixin'; import domUtils from './utils/domUtils'; import EventListener from './utils/EventListener'; +import deprecationWarning from './utils/deprecationWarning'; + +import Portal from './Portal'; +import Body from './ModalBody'; +import Header from './ModalHeader'; +import Title from './ModalTitle'; +import Footer from './ModalFooter'; -// TODO: -// - aria-labelledby -// - Add `modal-body` div if only one child passed in that doesn't already have it -// - Tests /** * Gets the correct clientHeight of the modal container @@ -31,6 +36,22 @@ function getContainer(context){ domUtils.ownerDocument(context).body; } +function requiredIfNot(key, type){ + return function(props, propName, componentName){ + let propType = type; + + if ( props[ key] === undefined ){ + propType = propType.isRequired; + } + return propType(props, propName, componentName); + }; +} + +function toChildArray(children){ + let result = []; + React.Children.forEach(children, c => result.push(c)); + return result; +} let currentFocusListener; @@ -89,20 +110,55 @@ function getScrollbarSize(){ } -const Modal = React.createClass({ +const ModalMarkup = React.createClass({ mixins: [BootstrapMixin, FadeMixin], propTypes: { + /** + * The Modal title text + * @deprecated Use the "Modal.Header" component instead + */ title: React.PropTypes.node, + /** + * Include a backdrop component. Specify 'static' for a backdrop that doesn't trigger an "onHide" when clicked. + */ backdrop: React.PropTypes.oneOf(['static', true, false]), + /** + * Include a backdrop component. Specify 'static' for a backdrop that doesn't trigger an "onHide" when clicked. + */ keyboard: React.PropTypes.bool, + + /** + * Specify whether the Modal heading should contain a close button + * @deprecated Use the "Modal.Header" Component instead + */ closeButton: React.PropTypes.bool, - container: React.PropTypes.object, + + /** + * Open and close the Modal with a slide and fade animation. + */ animation: React.PropTypes.bool, - onRequestHide: React.PropTypes.func.isRequired, + /** + * A Callback fired when the header closeButton or non-static backdrop is clicked. + */ + onHide: requiredIfNot('onRequestHide', React.PropTypes.func), + /** + * A css class to apply to the Modal dialog DOM node. + */ dialogClassName: React.PropTypes.string, + + /** + * When `true` The modal will automatically shift focus to itself when it opens, and replace it to the last focused element when it closes. + * Generally this should never be set to false as it makes the Modal less accessible to assistive technologies, like screen-readers. + */ autoFocus: React.PropTypes.bool, + + /** + * When `true` The modal will prevent focus from leaving the Modal while open. + * Consider leaving the default value here, as it is necessary to make the Modal work well with assistive technologies, + * such as screen readers. + */ enforceFocus: React.PropTypes.bool }, @@ -148,9 +204,8 @@ const Modal = React.createClass({ onClick={this.props.backdrop === true ? this.handleBackdropClick : null} ref="modal"> -@@ -160,6 +215,35 @@ const Modal = React.createClass({ this.renderBackdrop(modal, state.backdropStyles) : modal; }, + renderContent() { + let children = toChildArray(this.props.children); // b/c createFragment is in addons and children can be a key'd object + let hasNewHeader = children.some( c => c.type.__isModalHeader); + + if (!hasNewHeader && this.props.title != null){ + deprecationWarning( + 'Specifying `closeButton` or `title` Modal props', + 'the new Modal.Header, and Modal.Title components'); + + children.unshift( +- {this.props.title ? this.renderHeader() : null} - {this.props.children} ++ { this.renderContent() }+ { this.props.title && + + ); + } + + return React.Children.map(children, child => { + // TODO: use context in 0.14 + if (child.type.__isModalHeader) { + return cloneElement(child, { + onHide: createChainedFunction(this._getHide(), child.props.onHide) + }); + } + return child; + }); + }, + renderBackdrop(modal) { let classes = { 'modal-backdrop': true, @@ -178,27 +262,12 @@ const Modal = React.createClass({ ); }, - renderHeader() { - let closeButton; - if (this.props.closeButton) { - closeButton = ( - - ); + _getHide(){ + if ( !this.props.onHide && this.props.onRequestHide){ + deprecationWarning('The Modal prop `onRequestHide`', 'the `onHide` prop'); } - return ( -{this.props.title} + } +- {closeButton} - {this.renderTitle()} -- ); - }, - - renderTitle() { - return ( - React.isValidElement(this.props.title) ? - this.props.title :{this.props.title}
- ); + return this.props.onHide || this.props.onRequestHide; }, iosClickHack() { @@ -281,12 +350,12 @@ const Modal = React.createClass({ return; } - this.props.onRequestHide(); + this._getHide()(); }, handleDocumentKeyUp(e) { if (this.props.keyboard && e.keyCode === 27) { - this.props.onRequestHide(); + this._getHide()(); } }, @@ -353,4 +422,38 @@ const Modal = React.createClass({ } }); +const Modal = React.createClass({ + propTypes: { + ...Portal.propTypes, + ...ModalMarkup.propTypes + }, + + defaultProps: { + show: null + }, + + render() { + let { show, ...props } = this.props; + + let modal = ( +{this.props.children} + ); + // I can't think of another way to not break back compat while defaulting container + if ( show != null ){ + return ( ++ { show && modal } + + ); + } else { + return modal; + } + } +}); + +Modal.Body = Body; +Modal.Header = Header; +Modal.Title = Title; +Modal.Footer = Footer; + export default Modal; diff --git a/src/index.js b/src/index.js index a16f1649c1..a15ce04b7b 100644 --- a/src/index.js +++ b/src/index.js @@ -28,6 +28,11 @@ import ListGroup from './ListGroup'; import ListGroupItem from './ListGroupItem'; import MenuItem from './MenuItem'; import Modal from './Modal'; +import ModalHeader from './ModalHeader'; +import ModalTitle from './ModalTitle'; +import ModalBody from './ModalBody'; +import ModalFooter from './ModalFooter'; + import Nav from './Nav'; import Navbar from './Navbar'; import NavItem from './NavItem'; @@ -87,6 +92,10 @@ export default { ListGroupItem, MenuItem, Modal, + ModalHeader, + ModalTitle, + ModalBody, + ModalFooter, Nav, Navbar, NavItem, diff --git a/test/ModalSpec.js b/test/ModalSpec.js index 3a1e315526..3d425c14cd 100644 --- a/test/ModalSpec.js +++ b/test/ModalSpec.js @@ -1,13 +1,14 @@ import React from 'react'; import ReactTestUtils from 'react/lib/ReactTestUtils'; import Modal from '../src/Modal'; +import { shouldWarn } from './helpers'; describe('Modal', function () { it('Should render the modal content', function() { let noOp = function () {}; let instance = ReactTestUtils.renderIntoDocument( -+ Message ); @@ -18,21 +19,19 @@ describe('Modal', function () { let Container = React.createClass({ getInitialState() { - return {modalOpen: true}; + return { modalOpen: true }; }, handleCloseModal() { - this.setState({modalOpen: false}); + this.setState({ modalOpen: false }); }, render() { - if (this.state.modalOpen) { - return ( -+ return ( + ++ ); } }); let instance = ReactTestUtils.renderIntoDocument( @@ -41,6 +40,7 @@ describe('Modal', function () { assert.ok(React.findDOMNode(instance).className.match(/\modal-open\b/)); let backdrop = React.findDOMNode(instance).getElementsByClassName('modal-backdrop')[0]; + ReactTestUtils.Simulate.click(backdrop); setTimeout(function(){ assert.equal(React.findDOMNode(instance).className.length, 0); @@ -52,7 +52,7 @@ describe('Modal', function () { it('Should close the modal when the backdrop is clicked', function (done) { let doneOp = function () { done(); }; let instance = ReactTestUtils.renderIntoDocument( -Message - ); - } else { - return ; - } ++ Message ); @@ -64,7 +64,7 @@ describe('Modal', function () { it('Should close the modal when the modal background is clicked', function (done) { let doneOp = function () { done(); }; let instance = ReactTestUtils.renderIntoDocument( -+ Message ); @@ -76,7 +76,7 @@ describe('Modal', function () { it('Should pass bsSize to the dialog', function () { let noOp = function () {}; let instance = ReactTestUtils.renderIntoDocument( -+ Message ); @@ -88,7 +88,7 @@ describe('Modal', function () { it('Should pass dialogClassName to the dialog', function () { let noOp = function () {}; let instance = ReactTestUtils.renderIntoDocument( -+ Message ); @@ -133,7 +133,7 @@ describe('Modal', function () { render() { if (this.state.modalOpen) { return ( -+ Message ); @@ -167,7 +167,8 @@ describe('Modal', function () { render() { if (this.state.modalOpen) { return ( -{}} container={this}> + + {}} container={this}> Message ); @@ -220,4 +221,32 @@ describe('Modal', function () { }); + describe('deprecations', function(){ + it('Should render the modal header and title', function() { + let instance = ReactTestUtils.renderIntoDocument( +{}}> + Message + + ); + + (()=> { + ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'button'); + ReactTestUtils.findRenderedComponentWithType(instance, Modal.Header); + ReactTestUtils.findRenderedComponentWithType(instance, Modal.Title); + }).should.not.throw(); + + shouldWarn( + 'Specifying `closeButton` or `title` Modal props is deprecated'); + }); + + it('Should warn about onRequestHide', function() { + ReactTestUtils.renderIntoDocument( +{}}> + + ); + + shouldWarn('The Modal prop `onRequestHide` is deprecated'); + }); + }); });+