diff --git a/docs/assets/style.css b/docs/assets/style.css index 8821e2ba6e..d03afccb44 100644 --- a/docs/assets/style.css +++ b/docs/assets/style.css @@ -165,3 +165,10 @@ body { .prop-table { background-color: white; } + +.bs-example.tooltip-static .tooltip { + position: relative; + display: inline-block; + margin: 5px 10px; + +} 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/ModalCustomSizing.js b/docs/examples/ModalCustomSizing.js index 5ddeab19de..4122e352d4 100644 --- a/docs/examples/ModalCustomSizing.js +++ b/docs/examples/ModalCustomSizing.js @@ -1,8 +1,11 @@ const MyModal = React.createClass({ render() { return ( - -
+ + + Modal heading + +

Wrapped Text

Ipsum molestiae natus adipisci modi eligendi? Debitis amet quae unde commodi aspernatur enim, consectetur. Cumque deleniti temporibus ipsam atque a dolores quisquam quisquam adipisci possimus laboriosam. Quibusdam facilis doloribus debitis! Sit quasi quod accusamus eos quod. Ab quos consequuntur eaque quo rem! Mollitia reiciendis porro quo magni incidunt dolore amet atque facilis ipsum deleniti rem! Dolores debitis voluptatibus ipsum dicta. Dolor quod amet ab sint esse distinctio tenetur. Veritatis laudantium quibusdam quidem corporis architecto veritatis. Ex facilis minima beatae sunt perspiciatis placeat. Quasi corporis @@ -19,10 +22,10 @@ const MyModal = React.createClass({ magni delectus maxime. Sit odit provident vel magnam quod. Possimus eligendi non corrupti tenetur culpa accusantium quod quis. Voluptatum quaerat animi dolore maiores molestias voluptate? Necessitatibus illo omnis laborum hic enim minima! Similique. Dolor voluptatum reprehenderit nihil adipisci aperiam voluptatem soluta magnam accusamus iste incidunt tempore consequatur illo illo odit. Asperiores nesciunt iusto nemo animi ratione. Sunt odit similique doloribus temporibus reiciendis! Ullam. Dolor dolores veniam animi sequi dolores molestias voluptatem iure velit. Elit dolore quaerat incidunt enim aut distinctio. Ratione molestiae laboriosam similique laboriosam eum et nemo expedita. Consequuntur perspiciatis cumque dolorem.

-
-
- -
+ + + +
); } 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..138c55f050 100644 --- a/docs/examples/ModalStatic.js +++ b/docs/examples/ModalStatic.js @@ -1,18 +1,25 @@ const modalInstance = (
- -
+ onHide={function(){}}> + + + Modal title + + + One fine body... -
-
+ + + -
+
); diff --git a/docs/examples/ModalTrigger.js b/docs/examples/ModalTrigger.js index 36b859d0df..4a25c06f86 100644 --- a/docs/examples/ModalTrigger.js +++ b/docs/examples/ModalTrigger.js @@ -1,42 +1,67 @@ -const MyModal = React.createClass({ +const Example = React.createClass({ + + getInitialState(){ + return { showModal: false }; + }, + + close(){ + this.setState({ showModal: false }); + }, + + open(){ + this.setState({ showModal: true }); + }, + render() { + 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/examples/Overlay.js b/docs/examples/Overlay.js new file mode 100644 index 0000000000..4f5f1b9fde --- /dev/null +++ b/docs/examples/Overlay.js @@ -0,0 +1,43 @@ + +const Example = React.createClass({ + getInitialState(){ + return { show: true }; + }, + + toggle(){ + this.setState({ show: !this.state.show }); + }, + + render(){ + const tooltip = Tooltip overload!; + + const sharedProps = { + show: this.state.show, + container: this, + target: props => React.findDOMNode(this.refs.target) + }; + + return ( +
+ + + + { tooltip } + + + { tooltip } + + + { tooltip } + + + { tooltip } + +
+ ); + } +}); + +React.render(, mountNode); diff --git a/docs/examples/OverlayCustom.js b/docs/examples/OverlayCustom.js new file mode 100644 index 0000000000..20230b56c6 --- /dev/null +++ b/docs/examples/OverlayCustom.js @@ -0,0 +1,45 @@ + +const Example = React.createClass({ + getInitialState(){ + return { show: true }; + }, + + toggle(){ + this.setState({ show: !this.state.show }); + }, + + render(){ + const style = { + position: 'absolute', + backgroundColor: '#EEE', + boxShadow: '0 5px 10px rgba(0, 0, 0, 0.2)', + border: '1px solid #CCC', + borderRadius: 3, + marginLeft: -5, + marginTop: 5, + padding: 10 + }; + + return ( +
+ + + this.setState({ show: false })} + placement="right" + container={this} + target={ props => React.findDOMNode(this.refs.target)} + > +
+ Holy guacamole! Check this info. +
+
+
+ ); + } +}); + +React.render(, mountNode); diff --git a/docs/examples/PopoverContained.js b/docs/examples/PopoverContained.js index afd3019e3e..3e0c036cee 100644 --- a/docs/examples/PopoverContained.js +++ b/docs/examples/PopoverContained.js @@ -1,13 +1,33 @@ -const positionerInstance = ( - - Holy guacamole! Check this info.} - > - - - -); +class Example extends React.Component { + constructor(props, context){ + super(props, context); + this.state = { show: false }; + } + render(){ -React.render(positionerInstance, mountNode); + return ( + + + + React.findDOMNode(this.state.target)} + placement='bottom' + container={mountNode} + containerPadding={20} + > + + Holy guacamole! Check this info. + + + + ); + } +} + +React.render(, mountNode); diff --git a/docs/examples/TooltipBasic.js b/docs/examples/TooltipBasic.js index f2df4f4a66..ff21989d5e 100644 --- a/docs/examples/TooltipBasic.js +++ b/docs/examples/TooltipBasic.js @@ -1,7 +1,18 @@ const tooltipInstance = ( -
- - Holy guacamole! Check this info. +
+ + Tooltip right + + + Tooltip top + + + + Tooltip left + + + + Tooltip bottom
); diff --git a/docs/examples/TooltipInCopy.js b/docs/examples/TooltipInCopy.js index 8842122aee..40c8d09ab0 100644 --- a/docs/examples/TooltipInCopy.js +++ b/docs/examples/TooltipInCopy.js @@ -1,9 +1,9 @@ const LinkWithTooltip = React.createClass({ render() { - let tooltip = {this.props.tooltip}; + let tooltip = {this.props.tooltip}; return ( - + {this.props.children} ); @@ -12,7 +12,13 @@ const LinkWithTooltip = React.createClass({ const copyInstance = (

- Tight pants next level keffiyeh you probably haven't heard of them. Photo booth beard raw denim letterpress vegan messenger bag stumptown. Farm-to-table seitan, mcsweeney's fixie sustainable quinoa 8-bit american apparel Another tooltip} href='#'>have a terry richardson vinyl chambray. Beard stumptown, cardigans banh mi lomo thundercats. Tofu biodiesel williamsburg marfa, four loko mcsweeney's cleanse vegan chambray. A really ironic artisan whatever keytar, scenester farm-to-table banksy Austin twitter handle freegan cred raw denim single-origin coffee viral. + Tight pants next level keffiyeh you probably haven't + heard of them. Photo booth beard raw denim letterpress vegan messenger bag stumptown. Farm-to-table seitan, mcsweeney's + fixie sustainable quinoa 8-bit american apparel Another tooltip} href='#'>have a + terry richardson vinyl chambray. Beard stumptown, cardigans banh mi lomo thundercats. Tofu biodiesel williamsburg marfa, four + loko mcsweeney's cleanse vegan chambray. A really ironic artisan whatever keytar, + scenester farm-to-table banksy Austin twitter handle freegan + cred raw denim single-origin coffee viral.

); diff --git a/docs/examples/TooltipPositioned.js b/docs/examples/TooltipPositioned.js index cedb961fa9..5f94bca114 100644 --- a/docs/examples/TooltipPositioned.js +++ b/docs/examples/TooltipPositioned.js @@ -1,15 +1,23 @@ + +const tooltip = ( + Holy guacamole! Check this info. +); + const positionerInstance = ( - Holy guacamole! Check this info.
}> + - Holy guacamole! Check this info.}> + + - Holy guacamole! Check this info.}> + + - Holy guacamole! Check this info.}> + + diff --git a/docs/generate-metadata.js b/docs/generate-metadata.js index 4cbef95619..bf30c07d0e 100644 --- a/docs/generate-metadata.js +++ b/docs/generate-metadata.js @@ -16,7 +16,8 @@ let cleanDoclets = desc => { return (idx === -1 ? desc : desc.substr(0, idx )).trim(); }; -let cleanDocletValue = str => str.replace(/^\{|\}$/g, ''); +let cleanDocletValue = str => str.trim().replace(/^\{/, '').replace(/\}$/, ''); + let isLiteral = str => (/^('|")/).test(str.trim()); @@ -26,7 +27,7 @@ let isLiteral = str => (/^('|")/).test(str.trim()); * @param {ComponentMetadata|PropMetadata} obj */ function parseDoclets(obj){ - obj.doclets = metadata.parseDoclets(obj.desc || ''); + obj.doclets = metadata.parseDoclets(obj.desc || '') || {}; obj.desc = cleanDoclets(obj.desc || ''); obj.descHtml = marked(obj.desc || ''); } diff --git a/docs/src/ComponentsPage.js b/docs/src/ComponentsPage.js index 65bd2e50b5..c3166eca4d 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,40 +307,57 @@ 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

+ {/* Tooltip */}
-

Tooltips Tooltip

-

Example tooltips

- -

Tooltip component.

- +

Tooltip

+

+ Tooltip component for a more stylish alternative to that anchor tag title attribute. +

+ -

Positioned tooltip component.

+

Attach and position tooltips with OverlayTrigger.

-

Positioned tooltip in copy.

+

Positioned tooltip in text copy.

-

Props

+

Props

- +

Overlay Trigger

+ +

Tooltip

+
{/* Popover */}
-

Popovers Popover

-

Example popovers

+

Popovers

-

Popover component.

- +

+ The Popover, offers a more robust alternative to the Tooltip for displaying overlays of content. +

+ -

Positioned popover component.

+

The Popover component, like the Tooltip can be used with an OverlayTrigger Component, and positioned around it.

Trigger behaviors. It's inadvisable to use "hover" or "focus" triggers for popovers, because they have poor accessibility from keyboard and on mobile devices.

@@ -351,6 +374,28 @@ const ComponentsPage = React.createClass({
+ {/* Overlay */} +
+

Overlay

+ +

+ The OverlayTrigger component is great for most use cases, but as a higher level abstraction it can lack the flexibility needed + to build more nuanced or custom behaviors into your Overlay components. For these cases it can be helpful to forgo the trigger and use + the Overlay component directly. +

+ + +

+ You don't need to use the provided Tooltip or Popover components. Creating custom overlays + is as easy as wrapping some markup in an Overlay component +

+ + +

Props

+ + +
+ {/* Progress Bar */}

Progress bars ProgressBar

@@ -788,8 +833,33 @@ const ComponentsPage = React.createClass({
+ {/* Utilities */} +
+

Utilities Portal, Position

+ +

Portal

+

+ A Component that renders its children into a new React "subtree" or container. The Portal component kind of like the React + equivalent to jQuery's .appendTo(), which is helpful for components that need to be appended to a DOM node other than + the component's direct parent. The Modal, and Overlay components use the Portal component internally. +

+

Props

+ + + +

Position

+

+ A Component that absolutely positions its child to a target component or DOM node. Useful for creating custom + popups or tooltips. Used by the Overlay Components. +

+

Props

+ + +
+ +
Modals Tooltips Popovers + Overlays Progress bars Navs Navbars @@ -829,6 +900,7 @@ const ComponentsPage = React.createClass({ Glyphicons Tables Input + Utilities Back to top diff --git a/docs/src/PropTable.js b/docs/src/PropTable.js index 9d585145c0..250a70a8f6 100644 --- a/docs/src/PropTable.js +++ b/docs/src/PropTable.js @@ -5,7 +5,29 @@ import Label from '../../src/Label'; import Table from '../../src/Table'; -let cleanDocletValue = str => str.replace(/^\{|\}$/g, ''); +let cleanDocletValue = str => str.trim().replace(/^\{/, '').replace(/\}$/, ''); + +function getPropsData(componentData, metadata){ + + let props = componentData.props || {}; + + if (componentData.composes) { + componentData.composes.forEach( other => { + props = merge({}, getPropsData(metadata[other] || {}, metadata), props); + + }); + } + + if (componentData.mixins) { + componentData.mixins.forEach( other => { + if ( componentData.composes.indexOf(other) === -1) { + props = merge({}, getPropsData(metadata[other] || {}, metadata), props); + } + }); + } + + return props; +} const PropTable = React.createClass({ @@ -13,10 +35,16 @@ const PropTable = React.createClass({ metadata: React.PropTypes.object }, + componentWillMount(){ + let componentData = this.context.metadata[this.props.component] || {}; + + this.propsData = getPropsData(componentData, this.context.metadata); + }, + render(){ - let metadata = this.context.metadata[this.props.component] || {}; + let propsData = this.propsData; - if ( !Object.keys(metadata.props || {}).length){ + if ( !Object.keys(propsData).length){ return ; } @@ -31,46 +59,36 @@ const PropTable = React.createClass({ - { this._renderRows() } + { this._renderRows(propsData) } ); }, - _renderRows(){ - let metadata = this.context.metadata[this.props.component] || {}; - let props = metadata.props || {}; - - if (metadata.composes) { - metadata.composes.forEach( other => { - props = merge(props, (this.context.metadata[other] || {}).props); - }); - } - - if (metadata.mixins) { - metadata.mixins.forEach( other => { - if ( metadata.composes.indexOf(other) === -1) { - props = merge(props, (this.context.metadata[other] || {}).props); - } - }); - } + _renderRows(propsData){ - return Object.keys(props) + return Object.keys(propsData) .sort() - .filter(propName => props[propName].type && !props[propName].doclets.private ) + .filter(propName => propsData[propName].type && !propsData[propName].doclets.private ) .map(propName => { - let prop = props[propName]; + let propData = propsData[propName]; return ( - {propName} {this.renderRequiredLabel(prop)} + {propName} {this.renderRequiredLabel(propData)} + + +
{this.getType(propData)}
+ {propData.defaultValue} + -
{this.getType(prop)}
+ { propData.doclets.deprecated + &&
{'Deprecated: ' + propData.doclets.deprecated + ' '}
+ } +
- {prop.defaultValue} - ); }); @@ -87,7 +105,7 @@ const PropTable = React.createClass({ }, getType(prop) { - let type = prop.type; + let type = prop.type || {}; let name = this.getDisplayTypeName(type.name); let doclets = prop.doclets || {}; @@ -95,9 +113,19 @@ const PropTable = React.createClass({ case 'object': return name; case 'union': - return type.value.map(val => this.getType({ type: val })).join(' | '); + return type.value.reduce((current, val, i, list) => { + let item = this.getType({ type: val }); + if (React.isValidElement(item)) { + item = React.cloneElement(item, {key: i}); + } + current = current.concat(item); + + return i === (list.length - 1) ? current : current.concat(' | '); + }, []); case 'array': - return `array<${this.getDisplayTypeName(type.value.name)}>`; + let child = this.getType({ type: type.value }); + + return {'array<'}{ child }{'>'}; case 'enum': return this.renderEnum(type); case 'custom': diff --git a/docs/src/ReactPlayground.js b/docs/src/ReactPlayground.js index 0b33a304a3..16c7c14d96 100644 --- a/docs/src/ReactPlayground.js +++ b/docs/src/ReactPlayground.js @@ -36,6 +36,7 @@ import * as modPagination from '../../src/Pagination'; import * as modPanel from '../../src/Panel'; import * as modPanelGroup from '../../src/PanelGroup'; import * as modPopover from '../../src/Popover'; +//import * as modPopoverTrigger from '../../src/PopoverTrigger'; import * as modProgressBar from '../../src/ProgressBar'; import * as modRow from '../../src/Row'; import * as modSplitButton from '../../src/SplitButton'; @@ -44,14 +45,22 @@ import * as modTable from '../../src/Table'; import * as modTabPane from '../../src/TabPane'; import * as modThumbnail from '../../src/Thumbnail'; import * as modTooltip from '../../src/Tooltip'; +//import * as modTooltipTrigger from '../../src/TooltipTrigger'; import * as modWell from '../../src/Well'; +import * as modPortal from '../../src/Portal'; +import * as modOverlay from '../../src/Overlay'; + import babel from 'babel-core/browser'; import CodeExample from './CodeExample'; + + const classNames = modClassNames.default; /* eslint-disable */ +const Portal = modPortal.default; + const React = modReact.default; const Accordion = modAccordion.default; const Alert = modAlert.default; @@ -89,6 +98,7 @@ const Pager = modPager.default; const Panel = modPanel.default; const PanelGroup = modPanelGroup.default; const Popover = modPopover.default; +//const PopoverTrigger = modPopoverTrigger.default; const ProgressBar = modProgressBar.default; const Row = modRow.default; const SplitButton = modSplitButton.default; @@ -97,7 +107,10 @@ const Table = modTable.default; const TabPane = modTabPane.default; const Thumbnail = modThumbnail.default; const Tooltip = modTooltip.default; +//const TooltipTrigger = modTooltipTrigger.default; const Well = modWell.default; +const Overlay = modOverlay.default; + /* eslint-enable */ const IS_MOBILE = typeof navigator !== 'undefined' && ( diff --git a/docs/src/Samples.js b/docs/src/Samples.js index c9b0aee582..47e6720802 100644 --- a/docs/src/Samples.js +++ b/docs/src/Samples.js @@ -32,7 +32,7 @@ export default { CollapsibleParagraph: require('fs').readFileSync(__dirname + '/../examples/CollapsibleParagraph.js', 'utf8'), ModalStatic: require('fs').readFileSync(__dirname + '/../examples/ModalStatic.js', 'utf8'), ModalTrigger: require('fs').readFileSync(__dirname + '/../examples/ModalTrigger.js', 'utf8'), - ModalOverlayMixin: require('fs').readFileSync(__dirname + '/../examples/ModalOverlayMixin.js', 'utf8'), + ModalContained: require('fs').readFileSync(__dirname + '/../examples/ModalContained.js', 'utf8'), ModalDefaultSizing: require('fs').readFileSync(__dirname + '/../examples/ModalDefaultSizing.js', 'utf8'), ModalCustomSizing: require('fs').readFileSync(__dirname + '/../examples/ModalCustomSizing.js', 'utf8'), @@ -99,5 +99,8 @@ export default { InputValidation: require('fs').readFileSync(__dirname + '/../examples/InputValidation.js', 'utf8'), InputHorizontal: require('fs').readFileSync(__dirname + '/../examples/InputHorizontal.js', 'utf8'), InputWrapper: require('fs').readFileSync(__dirname + '/../examples/InputWrapper.js', 'utf8'), - MenuItem: require('fs').readFileSync(__dirname + '/../examples/MenuItem.js', 'utf8') + MenuItem: require('fs').readFileSync(__dirname + '/../examples/MenuItem.js', 'utf8'), + + Overlay: require('fs').readFileSync(__dirname + '/../examples/Overlay.js', 'utf8'), + OverlayCustom: require('fs').readFileSync(__dirname + '/../examples/OverlayCustom.js', 'utf8') }; diff --git a/package.json b/package.json index bf7b11898e..a9c9dc9d62 100644 --- a/package.json +++ b/package.json @@ -95,7 +95,7 @@ "phantomjs": "^1.9.17", "portfinder": "^0.4.0", "react": "^0.13.1", - "react-component-metadata": "^1.1.1", + "react-component-metadata": "^1.2.1", "react-hot-loader": "^1.2.7", "react-router": "^0.13.1", "rimraf": "^2.3.2", diff --git a/src/Modal.js b/src/Modal.js index 39ff1b63ed..feaaa8ca31 100644 --- a/src/Modal.js +++ b/src/Modal.js @@ -1,15 +1,21 @@ -import React from 'react'; +/*eslint-disable react/prop-types */ +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 +37,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 +111,64 @@ 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. + * @type {function} + * @required + */ + onHide: requiredIfNot('onRequestHide', React.PropTypes.func), + + /** + * A Callback fired when the header closeButton or non-static backdrop is clicked. + * @deprecated Replaced by `onHide`. + */ + 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 +214,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 +225,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 +272,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 +360,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 +432,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/ModalBody.js b/src/ModalBody.js new file mode 100644 index 0000000000..40c667c001 --- /dev/null +++ b/src/ModalBody.js @@ -0,0 +1,26 @@ +import React from 'react'; +import classnames from 'classnames'; + +class ModalBody extends React.Component { + render() { + return ( +
+ {this.props.children} +
+ ); + } +} + +ModalBody.propTypes = { + /** + * A css class applied to the Component + */ + modalClassName: React.PropTypes.string +}; + +ModalBody.defaultProps = { + modalClassName: 'modal-body' +}; + + +export default ModalBody; diff --git a/src/ModalFooter.js b/src/ModalFooter.js new file mode 100644 index 0000000000..39f8759fe5 --- /dev/null +++ b/src/ModalFooter.js @@ -0,0 +1,27 @@ +import React from 'react'; +import classnames from 'classnames'; + + +class ModalFooter extends React.Component { + + render() { + return ( +
+ {this.props.children} +
+ ); + } +} + +ModalFooter.propTypes = { + /** + * A css class applied to the Component + */ + modalClassName: React.PropTypes.string +}; + +ModalFooter.defaultProps = { + modalClassName: 'modal-footer' +}; + +export default ModalFooter; diff --git a/src/ModalHeader.js b/src/ModalHeader.js new file mode 100644 index 0000000000..706546c64f --- /dev/null +++ b/src/ModalHeader.js @@ -0,0 +1,56 @@ + +import React from 'react'; +import classnames from 'classnames'; + +class ModalHeader extends React.Component { + + render() { + return ( +
+ { this.props.closeButton && + + } + { this.props.children } +
+ ); + } +} + +//used in liue of parent contexts right now to auto wire the close button +ModalHeader.__isModalHeader = true; + +ModalHeader.propTypes = { + /** + * A css class applied to the Component + */ + modalClassName: React.PropTypes.string, + /** + * Specify whether the Component should contain a close button + */ + closeButton: React.PropTypes.bool, + /** + * A Callback fired when the close button is clicked. If used directly inside a Modal component, the onHide will automatically + * be propagated up to the parent Modal `onHide`. + */ + onHide: React.PropTypes.func +}; + +ModalHeader.defaultProps = { + modalClassName: 'modal-header', + closeButton: false +}; + + +export default ModalHeader; diff --git a/src/ModalTitle.js b/src/ModalTitle.js new file mode 100644 index 0000000000..0c44bbaee5 --- /dev/null +++ b/src/ModalTitle.js @@ -0,0 +1,26 @@ +import React from 'react'; +import classnames from 'classnames'; + +class ModalTitle extends React.Component { + + render() { + return ( +

+ { this.props.children } +

+ ); + } +} + +ModalTitle.propTypes = { + /** + * A css class applied to the Component + */ + modalClassName: React.PropTypes.string +}; + +ModalTitle.defaultProps = { + modalClassName: 'modal-title' +}; + +export default ModalTitle; diff --git a/src/ModalTrigger.js b/src/ModalTrigger.js index 7b1eb73255..1bfde9325b 100644 --- a/src/ModalTrigger.js +++ b/src/ModalTrigger.js @@ -1,14 +1,27 @@ import React, { cloneElement } from 'react'; -import OverlayMixin from './OverlayMixin'; +import CustomPropTypes from './utils/CustomPropTypes'; +import deprecationWarning from './utils/deprecationWarning'; import createChainedFunction from './utils/createChainedFunction'; import createContextWrapper from './utils/createContextWrapper'; +function createHideDepreciationWrapper(hide){ + return function(...args){ + deprecationWarning( + 'The Modal prop `onRequestHide`', 'the `onHide` prop'); + + return hide(...args); + }; +} + const ModalTrigger = React.createClass({ - mixins: [OverlayMixin], propTypes: { modal: React.PropTypes.node.isRequired, + /** + * The DOM Node that the Component will render it's children into + */ + container: CustomPropTypes.mountable, onBlur: React.PropTypes.func, onFocus: React.PropTypes.func, onMouseOut: React.PropTypes.func, @@ -39,15 +52,31 @@ const ModalTrigger = React.createClass({ }); }, - renderOverlay() { - if (!this.state.isOverlayShown) { - return ; - } + componentDidMount(){ + this._overlay = document.createElement('div'); + React.render(this.getOverlay(), this._overlay); + }, + + componentWillUnmount() { + React.unmountComponentAtNode(this._overlay); + this._overlay = null; + clearTimeout(this._hoverDelay); + }, + + componentDidUpdate(){ + React.render(this.getOverlay(), this._overlay); + }, + + getOverlay() { + let modal = this.props.modal; return cloneElement( - this.props.modal, + modal, { - onRequestHide: this.hide + show: this.state.isOverlayShown, + onHide: this.hide, + onRequestHide: createHideDepreciationWrapper(this.hide), + container: modal.props.container || this.props.container } ); }, @@ -82,4 +111,19 @@ const ModalTrigger = React.createClass({ */ ModalTrigger.withContext = createContextWrapper(ModalTrigger, 'modal'); -export default ModalTrigger; +let DepreciatedModalTrigger = React.createClass({ + componentWillMount(){ + deprecationWarning( + 'The `ModalTrigger` component', 'the `Modal` component directly' + , 'http://react-bootstrap.github.io/components.html#modals'); + }, + + render(){ + return (); + } +}); + +DepreciatedModalTrigger.withContext = ModalTrigger.withContext; +DepreciatedModalTrigger.ModalTrigger = ModalTrigger; + +export default DepreciatedModalTrigger; diff --git a/src/Overlay.js b/src/Overlay.js new file mode 100644 index 0000000000..d425fe91a4 --- /dev/null +++ b/src/Overlay.js @@ -0,0 +1,63 @@ +/*eslint-disable object-shorthand, react/prop-types */ +import React from 'react'; +import Portal from './Portal'; +import Position from './Position'; +import RootCloseWrapper from './RootCloseWrapper'; + +class Overlay extends React.Component { + + constructor(props, context){ + super(props, context); + } + + render(){ + let { + container + , containerPadding + , target + , placement + , rootClose + , ...props } = this.props; + + let positionedChild = ( + + { this.props.children } + + ); + + if (rootClose) { + positionedChild = ( + + { positionedChild } + + ); + } + + return ( + + { props.show && + positionedChild + } + + ); + } +} + +Overlay.propTypes = { + ...Portal.propTypes, + ...Position.propTypes, + /** + * Set the visibility of the Overlay + */ + show: React.PropTypes.bool, + /** + * Specify whether the overlay should trigger onHide when the user clicks outside the overlay + */ + rootClose: React.PropTypes.bool, + /** + * A Callback fired by the Overlay when it wishes to be hidden. + */ + onHide: React.PropTypes.func +}; + +export default Overlay; diff --git a/src/OverlayMixin.js b/src/OverlayMixin.js index 76524ed61c..e1d56b09df 100644 --- a/src/OverlayMixin.js +++ b/src/OverlayMixin.js @@ -1,54 +1,65 @@ +/*eslint-disable react/prop-types */ import React from 'react'; import CustomPropTypes from './utils/CustomPropTypes'; import domUtils from './utils/domUtils'; +import deprecationWarning from './utils/deprecationWarning'; -export default { +export const OverlayMixin = { propTypes: { + container: CustomPropTypes.mountable }, - componentWillUnmount() { - this._unrenderOverlay(); - if (this._overlayTarget) { - this.getContainerDOMNode() - .removeChild(this._overlayTarget); - this._overlayTarget = null; - } + + componentDidMount() { + this._renderOverlay(); }, componentDidUpdate() { this._renderOverlay(); }, - componentDidMount() { - this._renderOverlay(); + componentWillUnmount() { + this._unrenderOverlay(); + this._mountOverlayTarget(); }, _mountOverlayTarget() { - this._overlayTarget = document.createElement('div'); - this.getContainerDOMNode() - .appendChild(this._overlayTarget); + if (!this._overlayTarget) { + this._overlayTarget = document.createElement('div'); + this.getContainerDOMNode() + .appendChild(this._overlayTarget); + } }, - _renderOverlay() { - if (!this._overlayTarget) { - this._mountOverlayTarget(); + _unmountOverlayTarget() { + if (this._overlayTarget) { + this.getContainerDOMNode() + .removeChild(this._overlayTarget); + this._overlayTarget = null; } + }, + + _renderOverlay() { let overlay = this.renderOverlay(); // Save reference to help testing if (overlay !== null) { + this._mountOverlayTarget(); this._overlayInstance = React.render(overlay, this._overlayTarget); } else { // Unrender if the component is null for transitions to null this._unrenderOverlay(); + this._unmountOverlayTarget(); } }, _unrenderOverlay() { - React.unmountComponentAtNode(this._overlayTarget); - this._overlayInstance = null; + if (this._overlayTarget) { + React.unmountComponentAtNode(this._overlayTarget); + this._overlayInstance = null; + } }, getOverlayDOMNode() { @@ -67,3 +78,14 @@ export default { return React.findDOMNode(this.props.container) || domUtils.ownerDocument(this).body; } }; + +export default { + + ...OverlayMixin, + + componentWillMount() { + deprecationWarning( + 'Overlay mixin', 'the `` Component' + , 'http://react-bootstrap.github.io/components.html#utilities-portal'); + } +}; diff --git a/src/OverlayTrigger.js b/src/OverlayTrigger.js index b708faf0bd..2be3bfd9d0 100644 --- a/src/OverlayTrigger.js +++ b/src/OverlayTrigger.js @@ -1,11 +1,13 @@ +/*eslint-disable react/prop-types */ import React, { cloneElement } from 'react'; -import OverlayMixin from './OverlayMixin'; -import RootCloseWrapper from './RootCloseWrapper'; - import createChainedFunction from './utils/createChainedFunction'; import createContextWrapper from './utils/createContextWrapper'; -import domUtils from './utils/domUtils'; +import Overlay from './Overlay'; +import position from './utils/overlayPositionUtils'; + +import deprecationWarning from './utils/deprecationWarning'; +import warning from 'react/lib/warning'; /** * Check if value one is inside or equal to the of value @@ -22,52 +24,95 @@ function isOneOf(one, of) { } const OverlayTrigger = React.createClass({ - mixins: [OverlayMixin], propTypes: { + + ...Overlay.propTypes, + + /** + * Specify which action or actions trigger Overlay visibility + */ trigger: React.PropTypes.oneOfType([ React.PropTypes.oneOf(['manual', 'click', 'hover', 'focus']), React.PropTypes.arrayOf(React.PropTypes.oneOf(['click', 'hover', 'focus'])) ]), - placement: React.PropTypes.oneOf(['top', 'right', 'bottom', 'left']), + + /** + * A millisecond delay amount to show and hide the Overlay once triggered + */ delay: React.PropTypes.number, + /** + * A millisecond delay amount before showing the Overlay once triggered. + */ delayShow: React.PropTypes.number, + /** + * A millisecond delay amount before hiding the Overlay once triggered. + */ delayHide: React.PropTypes.number, + + /** + * The initial visibility state of the Overlay, for more nuanced visibility controll consider + * using the Overlay component directly. + */ defaultOverlayShown: React.PropTypes.bool, + + /** + * An element or text to overlay next to the target. + */ overlay: React.PropTypes.node.isRequired, + + /** + * @private + */ onBlur: React.PropTypes.func, + /** + * @private + */ onClick: React.PropTypes.func, + /** + * @private + */ onFocus: React.PropTypes.func, + /** + * @private + */ onMouseEnter: React.PropTypes.func, + /** + * @private + */ onMouseLeave: React.PropTypes.func, - containerPadding: React.PropTypes.number, - rootClose: React.PropTypes.bool + + //override specific overlay props + /** + * @private + */ + target(){}, + /** + * @private + */ + onHide(){}, + /** + * @private + */ + show(){} }, getDefaultProps() { return { - placement: 'right', - trigger: ['hover', 'focus'], - containerPadding: 0 + trigger: ['hover', 'focus'] }; }, getInitialState() { return { isOverlayShown: this.props.defaultOverlayShown == null ? - false : this.props.defaultOverlayShown, - overlayLeft: null, - overlayTop: null, - arrowOffsetLeft: null, - arrowOffsetTop: null + false : this.props.defaultOverlayShown }; }, show() { this.setState({ isOverlayShown: true - }, function() { - this.updateOverlayPosition(); }); }, @@ -81,77 +126,90 @@ const OverlayTrigger = React.createClass({ if (this.state.isOverlayShown) { this.hide(); } else { - this.show(); + this.show(); } }, - renderOverlay() { - if (!this.state.isOverlayShown) { - return ; - } + componentDidMount(){ + this._mountNode = document.createElement('div'); + React.render(this._overlay, this._mountNode); + }, - const overlay = cloneElement( - this.props.overlay, - { - onRequestHide: this.hide, - placement: this.props.placement, - positionLeft: this.state.overlayLeft, - positionTop: this.state.overlayTop, - arrowOffsetLeft: this.state.arrowOffsetLeft, - arrowOffsetTop: this.state.arrowOffsetTop - } - ); + componentWillUnmount() { + React.unmountComponentAtNode(this._mountNode); + this._mountNode = null; + clearTimeout(this._hoverDelay); + }, - if (this.props.rootClose) { - return ( - - {overlay} - - ); - } else { - return overlay; - } + componentDidUpdate(){ + React.render(this._overlay, this._mountNode); + }, + + getOverlay(){ + let props = { + show: this.state.isOverlayShown, + onHide: this.hide, + rootClose: this.props.rootClose, + target: ()=> React.findDOMNode(this), + placement: this.props.placement, + container: this.props.container, + containerPadding: this.props.containerPadding + }; + + let overlay = cloneElement(this.props.overlay, { + placement: props.placement, + container: props.container + }); + + return ( + + { overlay } + + ); }, render() { - const child = React.Children.only(this.props.children); - if (this.props.trigger === 'manual') { - return child; - } + const trigger = React.Children.only(this.props.children); - const props = {}; + const props = { + 'aria-describedby': this.props.overlay.props.id + }; - props.onClick = createChainedFunction(child.props.onClick, this.props.onClick); - if (isOneOf('click', this.props.trigger)) { - props.onClick = createChainedFunction(this.toggle, props.onClick); - } + // create in render otherwise owner is lost... + this._overlay = this.getOverlay(); - if (isOneOf('hover', this.props.trigger)) { - props.onMouseEnter = createChainedFunction(this.handleDelayedShow, this.props.onMouseEnter); - props.onMouseLeave = createChainedFunction(this.handleDelayedHide, this.props.onMouseLeave); - } + if (this.props.trigger !== 'manual') { + + props.onClick = createChainedFunction(trigger.props.onClick, this.props.onClick); - if (isOneOf('focus', this.props.trigger)) { - props.onFocus = createChainedFunction(this.handleDelayedShow, this.props.onFocus); - props.onBlur = createChainedFunction(this.handleDelayedHide, this.props.onBlur); + if (isOneOf('click', this.props.trigger)) { + props.onClick = createChainedFunction(this.toggle, props.onClick); + } + + if (isOneOf('hover', this.props.trigger)) { + warning(!(this.props.trigger === 'hover'), + '[react-bootstrap] Specifying only the `"hover"` trigger limits the visibilty of the overlay to just mouse users. ' + + 'Consider also including the `"focus"` trigger so that touch and keyboard only users can see the overlay as well.'); + + props.onMouseOver = createChainedFunction(this.handleDelayedShow, this.props.onMouseOver); + props.onMouseOut = createChainedFunction(this.handleDelayedHide, this.props.onMouseOut); + } + + if (isOneOf('focus', this.props.trigger)) { + props.onFocus = createChainedFunction(this.handleDelayedShow, this.props.onFocus); + props.onBlur = createChainedFunction(this.handleDelayedHide, this.props.onBlur); + } + } + else { + deprecationWarning('"manual" trigger type', ' the Overlay component'); } return cloneElement( - child, + trigger, props ); }, - componentWillUnmount() { - clearTimeout(this._hoverDelay); - }, - - componentDidMount() { - if (this.props.defaultOverlayShown) { - this.updateOverlayPosition(); - } - }, - handleDelayedShow() { if (this._hoverDelay != null) { clearTimeout(this._hoverDelay); @@ -167,10 +225,10 @@ const OverlayTrigger = React.createClass({ return; } - this._hoverDelay = setTimeout(function() { + this._hoverDelay = setTimeout(() => { this._hoverDelay = null; this.show(); - }.bind(this), delay); + }, delay); }, handleDelayedHide() { @@ -188,133 +246,38 @@ const OverlayTrigger = React.createClass({ return; } - this._hoverDelay = setTimeout(function() { + this._hoverDelay = setTimeout(() => { this._hoverDelay = null; this.hide(); - }.bind(this), delay); - }, - - updateOverlayPosition() { - if (!this.isMounted()) { - return; - } - - this.setState(this.calcOverlayPosition()); + }, delay); }, + // deprecated Methods calcOverlayPosition() { - const childOffset = this.getPosition(); - - const overlayNode = this.getOverlayDOMNode(); - const overlayHeight = overlayNode.offsetHeight; - const overlayWidth = overlayNode.offsetWidth; - - const placement = this.props.placement; - let overlayLeft, overlayTop, arrowOffsetLeft, arrowOffsetTop; - - if (placement === 'left' || placement === 'right') { - overlayTop = childOffset.top + (childOffset.height - overlayHeight) / 2; - - if (placement === 'left') { - overlayLeft = childOffset.left - overlayWidth; - } else { - overlayLeft = childOffset.left + childOffset.width; - } - - const topDelta = this._getTopDelta(overlayTop, overlayHeight); - overlayTop += topDelta; - arrowOffsetTop = 50 * (1 - 2 * topDelta / overlayHeight) + '%'; - arrowOffsetLeft = null; - } else if (placement === 'top' || placement === 'bottom') { - overlayLeft = childOffset.left + (childOffset.width - overlayWidth) / 2; - - if (placement === 'top') { - overlayTop = childOffset.top - overlayHeight; - } else { - overlayTop = childOffset.top + childOffset.height; - } - - const leftDelta = this._getLeftDelta(overlayLeft, overlayWidth); - overlayLeft += leftDelta; - arrowOffsetLeft = 50 * (1 - 2 * leftDelta / overlayWidth) + '%'; - arrowOffsetTop = null; - } else { - throw new Error( - 'calcOverlayPosition(): No such placement of "' + - this.props.placement + '" found.' - ); - } - - return {overlayLeft, overlayTop, arrowOffsetLeft, arrowOffsetTop}; - }, + let overlay = this.props.overlay; - _getTopDelta(top, overlayHeight) { - const containerDimensions = this._getContainerDimensions(); - const containerScroll = containerDimensions.scroll; - const containerHeight = containerDimensions.height; + deprecationWarning('OverlayTrigger.calcOverlayPosition()', 'utils/overlayPositionUtils'); - const padding = this.props.containerPadding; - const topEdgeOffset = top - padding - containerScroll; - const bottomEdgeOffset = top + padding - containerScroll + overlayHeight; - - if (topEdgeOffset < 0) { - return -topEdgeOffset; - } else if (bottomEdgeOffset > containerHeight) { - return containerHeight - bottomEdgeOffset; - } else { - return 0; - } - }, - - _getLeftDelta(left, overlayWidth) { - const containerDimensions = this._getContainerDimensions(); - const containerWidth = containerDimensions.width; - - const padding = this.props.containerPadding; - const leftEdgeOffset = left - padding; - const rightEdgeOffset = left + padding + overlayWidth; - - if (leftEdgeOffset < 0) { - return -leftEdgeOffset; - } else if (rightEdgeOffset > containerWidth) { - return containerWidth - rightEdgeOffset; - } else { - return 0; - } - }, - - _getContainerDimensions() { - const containerNode = this.getContainerDOMNode(); - let width, height, scroll; - - if (containerNode.tagName === 'BODY') { - width = window.innerWidth; - height = window.innerHeight; - scroll = - domUtils.ownerDocument(containerNode).documentElement.scrollTop || - containerNode.scrollTop; - } else { - width = containerNode.offsetWidth; - height = containerNode.offsetHeight; - scroll = containerNode.scrollTop; - } - - return {width, height, scroll}; + return position.calcOverlayPosition( + overlay.props.placement || this.props.placement + , React.findDOMNode(overlay) + , React.findDOMNode(this) + , React.findDOMNode(overlay.props.container || this.props.container) + , overlay.props.containerPadding || this.props.containerPadding + ); }, getPosition() { - const node = React.findDOMNode(this); - const container = this.getContainerDOMNode(); + deprecationWarning('OverlayTrigger.getPosition()', 'utils/overlayPositionUtils'); - const offset = container.tagName === 'BODY' ? - domUtils.getOffset(node) : domUtils.getPosition(node, container); + let overlay = this.props.overlay; - return { - ...offset, - height: node.offsetHeight, - width: node.offsetWidth - }; + return position.getPosition( + React.findDOMNode(this) + , React.findDOMNode(overlay.props.container || this.props.container) + ); } + }); /** diff --git a/src/Popover.js b/src/Popover.js index c0ea27a6eb..e24b0c51a6 100644 --- a/src/Popover.js +++ b/src/Popover.js @@ -1,23 +1,57 @@ +/* eslint-disable react/no-multi-comp */ import React from 'react'; import classNames from 'classnames'; import BootstrapMixin from './BootstrapMixin'; import FadeMixin from './FadeMixin'; +import CustomPropTypes from './utils/CustomPropTypes'; const Popover = React.createClass({ + mixins: [BootstrapMixin, FadeMixin], propTypes: { + /** + * An html id attribute, necessary for accessibility + * @type {string} + * @required + */ + id: CustomPropTypes.isRequiredForA11y(React.PropTypes.string), + + /** + * Sets the direction the Popover is positioned towards. + */ placement: React.PropTypes.oneOf(['top', 'right', 'bottom', 'left']), + + /** + * The "left" position value for the Popover. + */ positionLeft: React.PropTypes.number, + /** + * The "top" position value for the Popover. + */ positionTop: React.PropTypes.number, + /** + * The "left" position value for the Popover arrow. + */ arrowOffsetLeft: React.PropTypes.oneOfType([ React.PropTypes.number, React.PropTypes.string ]), + /** + * The "top" position value for the Popover arrow. + */ arrowOffsetTop: React.PropTypes.oneOfType([ React.PropTypes.number, React.PropTypes.string ]), + /** + * Title text + */ title: React.PropTypes.node, + /** + * Specify whether the Popover should be use show and hide animations. + */ animation: React.PropTypes.bool + + }, getDefaultProps() { @@ -48,7 +82,7 @@ const Popover = React.createClass({ }; return ( -
+
{this.props.title ? this.renderTitle() : null}
diff --git a/src/Portal.js b/src/Portal.js new file mode 100644 index 0000000000..cc8f350701 --- /dev/null +++ b/src/Portal.js @@ -0,0 +1,34 @@ +import React from 'react'; +import CustomPropTypes from './utils/CustomPropTypes'; +import { OverlayMixin } from './OverlayMixin'; + +let Portal = React.createClass({ + + displayName: 'Portal', + + propTypes: { + /** + * The DOM Node that the Component will render it's children into + */ + container: CustomPropTypes.mountable + }, + + // we use the mixin for now, to avoid duplicating a bunch of code. + // when the deprecation is removed we need to move the logic here from OverlayMixin + mixins: [ OverlayMixin ], + + renderOverlay() { + if (!this.props.children) { + return null; + } + + return React.Children.only(this.props.children); + }, + + render() { + return null; + } +}); + + +export default Portal; diff --git a/src/Position.js b/src/Position.js new file mode 100644 index 0000000000..b92c256965 --- /dev/null +++ b/src/Position.js @@ -0,0 +1,102 @@ +import React, { cloneElement } from 'react'; +import domUtils from './utils/domUtils'; +import { calcOverlayPosition } from './utils/overlayPositionUtils'; +import CustomPropTypes from './utils/CustomPropTypes'; + +class Position extends React.Component { + + constructor(props, context){ + super(props, context); + this.state = { + positionLeft: null, + positionTop: null, + arrowOffsetLeft: null, + arrowOffsetTop: null + }; + } + + componentWillMount(){ + this._needsFlush = true; + } + + componentWillRecieveProps(){ + this._needsFlush = true; + } + + componentDidMount(){ + this._maybeUpdatePosition(); + } + componentDidUpate(){ + this._maybeUpdatePosition(); + } + + render() { + let { placement, children } = this.props; + let { positionLeft, positionTop, ...arrows } = this.props.target ? this.state : {}; + + return cloneElement( + React.Children.only(children), { + ...arrows, + placement, + positionTop, + positionLeft, + style: { + ...children.props.style, + left: positionLeft, + top: positionTop + } + } + ); + } + + _maybeUpdatePosition(){ + if ( this._needsFlush ) { + this._needsFlush = false; + this._updatePosition(); + } + } + + _updatePosition() { + if ( this.props.target == null ){ + return; + } + + let target = React.findDOMNode(this.props.target(this.props)); + let container = React.findDOMNode(this.props.container) || domUtils.ownerDocument(this).body; + + this.setState( + calcOverlayPosition( + this.props.placement + , React.findDOMNode(this) + , target + , container + , this.props.containerPadding)); + } +} + +Position.propTypes = { + /** + * The target DOM node the Component is positioned next too. + */ + target: React.PropTypes.func, + /** + * The "offsetParent" of the Component + */ + container: CustomPropTypes.mountable, + /** + * Distance in pixels the Component should be positioned to the edge of the Container. + */ + containerPadding: React.PropTypes.number, + /** + * The location that the overlay should be positioned to its target. + */ + placement: React.PropTypes.oneOf(['top', 'right', 'bottom', 'left']) +}; + +Position.defaultProps = { + containerPadding: 0, + placement: 'right' +}; + + +export default Position; diff --git a/src/Tooltip.js b/src/Tooltip.js index 1c7dafeb80..640267829e 100644 --- a/src/Tooltip.js +++ b/src/Tooltip.js @@ -1,21 +1,53 @@ +/* eslint-disable react/no-multi-comp */ import React from 'react'; import classNames from 'classnames'; import BootstrapMixin from './BootstrapMixin'; import FadeMixin from './FadeMixin'; +import CustomPropTypes from './utils/CustomPropTypes'; const Tooltip = React.createClass({ mixins: [BootstrapMixin, FadeMixin], propTypes: { + /** + * An html id attribute, necessary for accessibility + * @type {string} + * @required + */ + id: CustomPropTypes.isRequiredForA11y(React.PropTypes.string), + + /** + * Sets the direction the Tooltip is positioned towards. + */ placement: React.PropTypes.oneOf(['top', 'right', 'bottom', 'left']), + + /** + * The "left" position value for the Tooltip. + */ positionLeft: React.PropTypes.number, + /** + * The "top" position value for the Tooltip. + */ positionTop: React.PropTypes.number, + /** + * The "left" position value for the Tooltip arrow. + */ arrowOffsetLeft: React.PropTypes.oneOfType([ React.PropTypes.number, React.PropTypes.string ]), + /** + * The "top" position value for the Tooltip arrow. + */ arrowOffsetTop: React.PropTypes.oneOfType([ React.PropTypes.number, React.PropTypes.string ]), + /** + * Title text + */ + title: React.PropTypes.node, + /** + * Specify whether the Tooltip should be use show and hide animations. + */ animation: React.PropTypes.bool }, @@ -46,7 +78,7 @@ const Tooltip = React.createClass({ }; return ( -
+
{this.props.children} diff --git a/src/index.js b/src/index.js index 7d2d8e8e85..327c383768 100644 --- a/src/index.js +++ b/src/index.js @@ -28,12 +28,18 @@ 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'; import ModalTrigger from './ModalTrigger'; import OverlayTrigger from './OverlayTrigger'; import OverlayMixin from './OverlayMixin'; +import Overlay from './Overlay'; import PageHeader from './PageHeader'; import Pagination from './Pagination'; import Panel from './Panel'; @@ -53,6 +59,8 @@ import Tooltip from './Tooltip'; import utils from './utils'; import Well from './Well'; import styleMaps from './styleMaps'; +import Portal from './Portal'; +import Position from './Position'; export default { Accordion, @@ -85,10 +93,15 @@ export default { ListGroupItem, MenuItem, Modal, + ModalHeader, + ModalTitle, + ModalBody, + ModalFooter, Nav, Navbar, NavItem, ModalTrigger, + Overlay, OverlayTrigger, OverlayMixin, PageHeader, @@ -98,6 +111,8 @@ export default { Pager, Pagination, Popover, + Portal, + Position, ProgressBar, Row, SplitButton, diff --git a/src/utils/CustomPropTypes.js b/src/utils/CustomPropTypes.js index 53c96e29c1..cb57a653cb 100644 --- a/src/utils/CustomPropTypes.js +++ b/src/utils/CustomPropTypes.js @@ -3,6 +3,20 @@ import React from 'react'; const ANONYMOUS = '<>'; const CustomPropTypes = { + + isRequiredForA11y(propType){ + return function(props, propName, componentName){ + if (props[propName] === null) { + return new Error( + 'The prop `' + propName + '` is required to make ' + componentName + ' accessible ' + + 'for users using assistive technologies such as screen readers `' + ); + } + + return propType(props, propName, componentName); + }; + }, + /** * Checks whether a prop provides a DOM element * diff --git a/src/utils/overlayPositionUtils.js b/src/utils/overlayPositionUtils.js new file mode 100644 index 0000000000..eb4a0459ec --- /dev/null +++ b/src/utils/overlayPositionUtils.js @@ -0,0 +1,113 @@ +import domUtils from './domUtils'; + +const utils = { + + getContainerDimensions(containerNode) { + let width, height, scroll; + + if (containerNode.tagName === 'BODY') { + width = window.innerWidth; + height = window.innerHeight; + scroll = + domUtils.ownerDocument(containerNode).documentElement.scrollTop || + containerNode.scrollTop; + } else { + width = containerNode.offsetWidth; + height = containerNode.offsetHeight; + scroll = containerNode.scrollTop; + } + + return {width, height, scroll}; + }, + + getPosition(target, container) { + const offset = container.tagName === 'BODY' ? + domUtils.getOffset(target) : domUtils.getPosition(target, container); + + return { + ...offset, // eslint-disable-line object-shorthand + height: target.offsetHeight, + width: target.offsetWidth + }; + }, + + calcOverlayPosition(placement, overlayNode, target, container, padding) { + const childOffset = utils.getPosition(target, container); + + const overlayHeight = overlayNode.offsetHeight; + const overlayWidth = overlayNode.offsetWidth; + + let positionLeft, positionTop, arrowOffsetLeft, arrowOffsetTop; + + if (placement === 'left' || placement === 'right') { + positionTop = childOffset.top + (childOffset.height - overlayHeight) / 2; + + if (placement === 'left') { + positionLeft = childOffset.left - overlayWidth; + } else { + positionLeft = childOffset.left + childOffset.width; + } + + const topDelta = getTopDelta(positionTop, overlayHeight, container, padding); + + positionTop += topDelta; + arrowOffsetTop = 50 * (1 - 2 * topDelta / overlayHeight) + '%'; + arrowOffsetLeft = null; + + } else if (placement === 'top' || placement === 'bottom') { + positionLeft = childOffset.left + (childOffset.width - overlayWidth) / 2; + + if (placement === 'top') { + positionTop = childOffset.top - overlayHeight; + } else { + positionTop = childOffset.top + childOffset.height; + } + + const leftDelta = getLeftDelta(positionLeft, overlayWidth, container, padding); + positionLeft += leftDelta; + arrowOffsetLeft = 50 * (1 - 2 * leftDelta / overlayWidth) + '%'; + arrowOffsetTop = null; + } else { + throw new Error( + `calcOverlayPosition(): No such placement of "${placement }" found.` + ); + } + + return { positionLeft, positionTop, arrowOffsetLeft, arrowOffsetTop }; + } +}; + + +function getTopDelta(top, overlayHeight, container, padding) { + const containerDimensions = utils.getContainerDimensions(container); + const containerScroll = containerDimensions.scroll; + const containerHeight = containerDimensions.height; + + const topEdgeOffset = top - padding - containerScroll; + const bottomEdgeOffset = top + padding - containerScroll + overlayHeight; + + if (topEdgeOffset < 0) { + return -topEdgeOffset; + } else if (bottomEdgeOffset > containerHeight) { + return containerHeight - bottomEdgeOffset; + } else { + return 0; + } +} + +function getLeftDelta(left, overlayWidth, container, padding) { + const containerDimensions = utils.getContainerDimensions(container); + const containerWidth = containerDimensions.width; + + const leftEdgeOffset = left - padding; + const rightEdgeOffset = left + padding + overlayWidth; + + if (leftEdgeOffset < 0) { + return -leftEdgeOffset; + } else if (rightEdgeOffset > containerWidth) { + return containerWidth - rightEdgeOffset; + } else { + return 0; + } +} +export default utils; 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'); + }); + }); }); diff --git a/test/ModalTriggerSpec.js b/test/ModalTriggerSpec.js index dbdcc8af46..9485f402b8 100644 --- a/test/ModalTriggerSpec.js +++ b/test/ModalTriggerSpec.js @@ -1,8 +1,27 @@ import React from 'react'; import ReactTestUtils from 'react/lib/ReactTestUtils'; import ModalTrigger from '../src/ModalTrigger'; +import { shouldWarn } from './helpers'; + describe('ModalTrigger', function() { + + afterEach(()=> { + if ( console.warn.called ) { + shouldWarn('The `ModalTrigger` component is deprecated'); + } + }); + + it('Should warn about deprecated Component', function() { + ReactTestUtils.renderIntoDocument( + test
}> + + + ); + + shouldWarn('The `ModalTrigger` component is deprecated'); + }); + it('Should create ModalTrigger element', function() { const instance = ReactTestUtils.renderIntoDocument( test
}> diff --git a/test/OverlayMixinSpec.js b/test/OverlayMixinSpec.js index d1a52fd282..66818d48f5 100644 --- a/test/OverlayMixinSpec.js +++ b/test/OverlayMixinSpec.js @@ -1,6 +1,7 @@ import React from 'react'; import ReactTestUtils from 'react/lib/ReactTestUtils'; import OverlayMixin from '../src/OverlayMixin'; +import { shouldWarn } from './helpers'; describe('OverlayMixin', function () { let instance; @@ -17,10 +18,15 @@ describe('OverlayMixin', function () { } }); + afterEach(function() { if (instance && ReactTestUtils.isCompositeComponent(instance) && instance.isMounted()) { React.unmountComponentAtNode(React.findDOMNode(instance)); } + + if ( console.warn.called ) { + shouldWarn('Overlay mixin is deprecated'); + } }); it('Should render overlay into container (DOMNode)', function() { diff --git a/test/OverlayTriggerSpec.js b/test/OverlayTriggerSpec.js index 756cb4a46d..1d871151d8 100644 --- a/test/OverlayTriggerSpec.js +++ b/test/OverlayTriggerSpec.js @@ -79,103 +79,6 @@ describe('OverlayTrigger', function() { contextSpy.calledWith('value').should.be.true; }); - describe('#calcOverlayPosition()', function() { - [ - { - placement: 'left', - noOffset: [50, 300, null, '50%'], - offsetBefore: [-200, 150, null, '0%'], - offsetAfter: [300, 450, null, '100%'] - }, - { - placement: 'top', - noOffset: [200, 150, '50%', null], - offsetBefore: [50, -100, '0%', null], - offsetAfter: [350, 400, '100%', null] - }, - { - placement: 'bottom', - noOffset: [200, 450, '50%', null], - offsetBefore: [50, 200, '0%', null], - offsetAfter: [350, 700, '100%', null] - }, - { - placement: 'right', - noOffset: [350, 300, null, '50%'], - offsetBefore: [100, 150, null, '0%'], - offsetAfter: [600, 450, null, '100%'] - } - ].forEach(function(testCase) { - describe(`placement = ${testCase.placement}`, function() { - let instance; - - beforeEach(function() { - instance = ReactTestUtils.renderIntoDocument( - test
} - > - - - ); - - instance.getOverlayDOMNode = sinon.stub().returns({ - offsetHeight: 200, offsetWidth: 200 - }); - instance._getContainerDimensions = sinon.stub().returns({ - width: 600, height: 600, scroll: 100 - }); - }); - - function checkPosition(expected) { - const [ - overlayLeft, - overlayTop, - arrowOffsetLeft, - arrowOffsetTop - ] = expected; - - it('Should calculate the correct position', function() { - instance.calcOverlayPosition().should.eql( - {overlayLeft, overlayTop, arrowOffsetLeft, arrowOffsetTop} - ); - }); - } - - describe('no viewport offset', function() { - beforeEach(function() { - instance.getPosition = sinon.stub().returns({ - left: 250, top: 350, width: 100, height: 100 - }); - }); - - checkPosition(testCase.noOffset); - }); - - describe('viewport offset before', function() { - beforeEach(function() { - instance.getPosition = sinon.stub().returns({ - left: 0, top: 100, width: 100, height: 100 - }); - }); - - checkPosition(testCase.offsetBefore); - }); - - describe('viewport offset after', function() { - beforeEach(function() { - instance.getPosition = sinon.stub().returns({ - left: 500, top: 600, width: 100, height: 100 - }); - }); - - checkPosition(testCase.offsetAfter); - }); - }); - }); - }); - describe('overlay types', function() { [ { @@ -227,8 +130,8 @@ describe('OverlayTrigger', function() { test
} trigger='click' rootClose={testCase.rootClose} - > - + > + ); const overlayTrigger = React.findDOMNode(instance); diff --git a/test/PortalSpec.js b/test/PortalSpec.js new file mode 100644 index 0000000000..66a1b49fc0 --- /dev/null +++ b/test/PortalSpec.js @@ -0,0 +1,78 @@ +import React from 'react'; +import ReactTestUtils from 'react/lib/ReactTestUtils'; +import Portal from '../src/Portal'; + +describe('Portal', function () { + let instance; + + let Overlay = React.createClass({ + render() { + return ( +
+ {this.props.overlay} +
+ ); + }, + getOverlayDOMNode(){ + return this.refs.p.getOverlayDOMNode(); + } + }); + + afterEach(function() { + if (instance && ReactTestUtils.isCompositeComponent(instance) && instance.isMounted()) { + React.unmountComponentAtNode(React.findDOMNode(instance)); + } + }); + + it('Should render overlay into container (DOMNode)', function() { + let container = document.createElement('div'); + + instance = ReactTestUtils.renderIntoDocument( + } /> + ); + + assert.equal(container.querySelectorAll('#test1').length, 1); + }); + + it('Should render overlay into container (ReactComponent)', function() { + let Container = React.createClass({ + render() { + return } />; + } + }); + + instance = ReactTestUtils.renderIntoDocument( + + ); + + assert.equal(React.findDOMNode(instance).querySelectorAll('#test1').length, 1); + }); + + it('Should not render a null overlay', function() { + let Container = React.createClass({ + render() { + return ; + } + }); + + instance = ReactTestUtils.renderIntoDocument( + + ); + + assert.equal(instance.refs.overlay.getOverlayDOMNode(), null); + }); + + it('Should render only an overlay', function() { + let OnlyOverlay = React.createClass({ + render() { + return {this.props.overlay}; + } + }); + + let overlayInstance = ReactTestUtils.renderIntoDocument( + } /> + ); + + assert.equal(overlayInstance.refs.p.getOverlayDOMNode().nodeName, 'DIV'); + }); +}); diff --git a/test/utils/overlayPositionUtilsSpec.js b/test/utils/overlayPositionUtilsSpec.js new file mode 100644 index 0000000000..50703e4b4c --- /dev/null +++ b/test/utils/overlayPositionUtilsSpec.js @@ -0,0 +1,92 @@ +import position from '../../src/utils/overlayPositionUtils'; + +describe('calcOverlayPosition()', function() { + [ + { + placement: 'left', + noOffset: [50, 300, null, '50%'], + offsetBefore: [-200, 150, null, '0%'], + offsetAfter: [300, 450, null, '100%'] + }, + { + placement: 'top', + noOffset: [200, 150, '50%', null], + offsetBefore: [50, -100, '0%', null], + offsetAfter: [350, 400, '100%', null] + }, + { + placement: 'bottom', + noOffset: [200, 450, '50%', null], + offsetBefore: [50, 200, '0%', null], + offsetAfter: [350, 700, '100%', null] + }, + { + placement: 'right', + noOffset: [350, 300, null, '50%'], + offsetBefore: [100, 150, null, '0%'], + offsetAfter: [600, 450, null, '100%'] + } + ].forEach(function(testCase) { + + describe(`placement = ${testCase.placement}`, function() { + let overlayStub, padding, placement; + + beforeEach(function() { + placement = testCase.placement; + padding = 50; + overlayStub = { + offsetHeight: 200, offsetWidth: 200 + }; + + position.getContainerDimensions = sinon.stub().returns({ + width: 600, height: 600, scroll: 100 + }); + }); + + function checkPosition(expected) { + const [ + positionLeft, + positionTop, + arrowOffsetLeft, + arrowOffsetTop + ] = expected; + + it('Should calculate the correct position', function() { + position.calcOverlayPosition(placement, overlayStub, {}, {}, padding).should.eql( + { positionLeft, positionTop, arrowOffsetLeft, arrowOffsetTop } + ); + }); + } + + describe('no viewport offset', function() { + beforeEach(function() { + position.getPosition = sinon.stub().returns({ + left: 250, top: 350, width: 100, height: 100 + }); + }); + + checkPosition(testCase.noOffset); + }); + + describe('viewport offset before', function() { + beforeEach(function() { + position.getPosition = sinon.stub().returns({ + left: 0, top: 100, width: 100, height: 100 + }); + }); + + checkPosition(testCase.offsetBefore); + }); + + describe('viewport offset after', function() { + beforeEach(function() { + position.getPosition = sinon.stub().returns({ + left: 500, top: 600, width: 100, height: 100 + }); + }); + + checkPosition(testCase.offsetAfter); + }); + }); + }); + });