From 83b4cbcfaacf935f4dde45210d97442a1889a655 Mon Sep 17 00:00:00 2001 From: jquense Date: Sun, 28 Jun 2015 20:08:28 -0400 Subject: [PATCH] [changed] Modal doesn't require ModalTrigger --- docs/examples/.eslintrc | 1 + docs/examples/ModalContained.js | 48 +++++--- docs/examples/ModalDefaultSizing.js | 65 +++++++---- docs/examples/ModalOverlayMixin.js | 43 ------- docs/examples/ModalStatic.js | 18 ++- docs/examples/ModalTrigger.js | 91 +++++++++------ docs/src/ComponentsPage.js | 36 ++++-- src/Modal.js | 167 ++++++++++++++++++++++------ src/index.js | 9 ++ test/ModalSpec.js | 61 +++++++--- 10 files changed, 356 insertions(+), 183 deletions(-) delete mode 100644 docs/examples/ModalOverlayMixin.js 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 ( - -
- Elit est explicabo ipsum eaque dolorem blanditiis doloribus sed id ipsam, beatae, rem fuga id earum? Inventore et facilis obcaecati. -
-
- -
-
- ); - } -}); - const Trigger = React.createClass({ + getInitialState(){ + return { show: false }; + }, + render() { + let close = e => this.setState({ show: false}); + return (
- } container={this}> - - + + + + + Contained Modal + + + Elit est explicabo ipsum eaque dolorem blanditiis doloribus sed id ipsam, beatae, rem fuga id earum? Inventore et facilis obcaecati. + + + + +
); } diff --git a/docs/examples/ModalDefaultSizing.js b/docs/examples/ModalDefaultSizing.js index aabd5dfc1c..19c9ffe0d3 100644 --- a/docs/examples/ModalDefaultSizing.js +++ b/docs/examples/ModalDefaultSizing.js @@ -1,8 +1,11 @@ const MySmallModal = React.createClass({ render() { return ( - -
+ + + Modal heading + +

Wrapped Text

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.

-
-
- -
+ + + +
); } @@ -25,8 +28,11 @@ const MySmallModal = React.createClass({ const MyLargeModal = React.createClass({ render() { return ( - -
+ + + Modal heading + +

Wrapped Text

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.

-
-
- -
+ + + +
); } }); -const overlayTriggerInstance = ( - - }> - - - }> - - - -); +const App = React.createClass({ + getInitialState(){ + return { smShow: false, lgShow: false }; + }, + render(){ + let smClose = e => this.setState({ smShow: false }); + let lgClose = e => this.setState({ lgShow: false }); -React.render(overlayTriggerInstance, mountNode); + return ( + + + + + + + + ); + } +}); + +React.render(, mountNode); diff --git a/docs/examples/ModalOverlayMixin.js b/docs/examples/ModalOverlayMixin.js deleted file mode 100644 index 3a47c75b33..0000000000 --- a/docs/examples/ModalOverlayMixin.js +++ /dev/null @@ -1,43 +0,0 @@ -// Our custom component is managing whether the Modal is visible -const CustomModalTrigger = React.createClass({ - mixins: [OverlayMixin], - - getInitialState() { - return { - isModalOpen: false - }; - }, - - handleToggle() { - this.setState({ - isModalOpen: !this.state.isModalOpen - }); - }, - - render() { - return ( - - ); - }, - - // This is called by the `OverlayMixin` when this component - // is mounted or updated and the return value is appended to the body. - renderOverlay() { - if (!this.state.isModalOpen) { - return ; - } - - return ( - -
- This modal is controlled by our custom trigger component. -
-
- -
-
- ); - } -}); - -React.render(, mountNode); diff --git a/docs/examples/ModalStatic.js b/docs/examples/ModalStatic.js index f01a4d058e..a266659b7a 100644 --- a/docs/examples/ModalStatic.js +++ b/docs/examples/ModalStatic.js @@ -1,18 +1,24 @@ const modalInstance = (
- -
+ onHide={function(){}}> + + + Modal title + + + One fine body... -
-
+ + + -
+
); diff --git a/docs/examples/ModalTrigger.js b/docs/examples/ModalTrigger.js index 36b859d0df..c919ac852e 100644 --- a/docs/examples/ModalTrigger.js +++ b/docs/examples/ModalTrigger.js @@ -1,42 +1,61 @@ -const MyModal = React.createClass({ +const Example = React.createClass({ + + getInitialState(){ + return { showModal: false }; + }, + render() { + let closeModal = e => this.setState({ showModal: false }); + + let popover = very popover. such engagement; + let tooltip = wow.; + return ( - -
-

Text in a modal

-

Duis mollis, est non commodo luctus, nisi erat porttitor ligula.

- -

Popover in a modal

-

TODO

- -

Tooltips in a modal

-

TODO

- -
- -

Overflowing text to show scroll behavior

-

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!

+ + + + + + Modal heading + + +

Text in a modal

+

Duis mollis, est non commodo luctus, nisi erat porttitor ligula.

+ +

Popover in a modal

+

there is a popover here

+ +

Tooltips in a modal

+

there is a tooltip here

+ +
+ +

Overflowing text to show scroll behavior

+

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.

+
+ + + +
+
); } }); -const overlayTriggerInstance = ( - }> - - -); - -React.render(overlayTriggerInstance, mountNode); +React.render(, mountNode); diff --git a/docs/src/ComponentsPage.js b/docs/src/ComponentsPage.js index 3ac37f435e..9eeca587c9 100644 --- a/docs/src/ComponentsPage.js +++ b/docs/src/ComponentsPage.js @@ -262,19 +262,25 @@ const ComponentsPage = React.createClass({

Modals Modal

A static example

-

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. +

+
+

Additional Import Options

+

+ 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')"}. +

+

Live demo

-

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.

-

Custom trigger

-

Use OverlayMixin in a custom component to manage the modal's state yourself.

- - -

Contained Modal

+

Contained Modal

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">
-
- {this.props.title ? this.renderHeader() : null} - {this.props.children} +
+ { this.renderContent() }
@@ -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.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 ( -
- {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 ( +
+ Message - ); - } else { - 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 ); @@ -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'); + }); + }); });