diff --git a/.babelrc b/.babelrc
index 8f85e598f6..4d5093a16f 100644
--- a/.babelrc
+++ b/.babelrc
@@ -1,5 +1,5 @@
{
- "optional": [
- "es7.objectRestSpread"
- ]
+ "stage": 1,
+ "optional": ["runtime"],
+ "loose": ["all"]
}
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index c6e9b1752a..1378edcc7a 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -124,6 +124,16 @@ Also Bootstrap mentions http://getbootstrap.com/getting-started/#examples
as examples of things you can do, but they are not part of the core library,
therefore this project is the wrong place to implement them.
+## Breaking changes
+
+Breaking changes should be accompanied with deprecations of removed
+functionality. Prior to the 1.0.0 release, we aim to follow React's example of
+taking two Minor releases to break old functionality. As such, changes that
+intend to remove or change public APIs should be be submitted against the
+`vX-rc` branch, and should be accompanied with deprecation warnings on the old
+APIs. The deprecated APIs themselves should not be removed until the Minor
+release after that.
+
## Notes for lodash functions usage in the code
You can use `lodash` but keep it to things where it actually needs it, i.e. don't use `lodash`'s `forEach` when `Array.prototype.forEach` is fine.
diff --git a/MAINTAINING.md b/MAINTAINING.md
index e6c5f29b14..402e8379ae 100644
--- a/MAINTAINING.md
+++ b/MAINTAINING.md
@@ -95,13 +95,13 @@ then be re-applied and released with the proper version bump.
### Release Candidates
In an effort to reduce the frequency with which we introduce breaking changes we
-should do our best to first push deprecation warnings in a Minor or Patch
-release. Also, Pull Requests with breaking changes should be submitted against
-the `vX-rc` branch, where X is the next Major version. Which we will in turn
-release as an `alpha` release of the next Major version. When we are ready to
-release the next Major version bump we will merge the `vX-rc` branch into the
-`master` branch and cut a `beta` release. Once bugs have been addressed with
-the `beta` release then we will release the Major version bump.
+should do our best to first push deprecation warnings in a Minor release. Also,
+Pull Requests with breaking changes should be submitted against the `vX-rc`
+branch, where X is the next Major version. Which we will in turn release as an
+`alpha` release of the next Major version. When we are ready to release the next
+Major version bump we will merge the `vX-rc` branch into the `master` branch and
+cut a `beta` release. Once bugs have been addressed with the `beta` release
+then we will release the Major version bump.
### Live releasing the documentation
diff --git a/README.md b/README.md
index 03372003b2..3364b9ece7 100644
--- a/README.md
+++ b/README.md
@@ -16,7 +16,7 @@
[![devDependency Status][dev-deps-badge]][dev-deps]
[![peerDependency Status][peer-deps-badge]][peer-deps]
-__Under active development - APIs will change.__ Check out the [1.0.0 Roadmap](https://github.com/react-bootstrap/react-bootstrap/wiki#100-roadmap) and [Contributing Guidelines][contributing] to see where you can help out. Prior to the 1.0.0 release, breaking changes should result in a Minor version bump.
+__Under active development - APIs will change.__ Check out the [1.0.0 Roadmap](https://github.com/react-bootstrap/react-bootstrap/wiki#100-roadmap) and [Contributing Guidelines][contributing] to see where you can help out. Prior to the 1.0.0 release, deprecations or breaking changes should result in a Minor version bump.
## Docs
diff --git a/docs/build.js b/docs/build.js
index feccdc030e..98a01c94e1 100644
--- a/docs/build.js
+++ b/docs/build.js
@@ -35,9 +35,11 @@ function generateHTML(fileName, propData) {
});
}
-export default function BuildDocs({ dev }) {
+export default function BuildDocs({dev}) {
console.log('Building: '.cyan + 'docs'.green + (dev ? ' [DEV]'.grey : ''));
+ const devOption = dev ? '' : '-p';
+
return exec(`rimraf ${docsBuilt}`)
.then(() => fsp.mkdir(docsBuilt))
.then(metadata)
@@ -46,7 +48,7 @@ export default function BuildDocs({ dev }) {
let pagesGenerators = Root.getPages().map( page => generateHTML(page, propData));
return Promise.all(pagesGenerators.concat([
- exec(`webpack --config webpack.docs.js ${dev ? '' : '-p '}--bail`),
+ exec(`webpack --config webpack.docs.js --bail ${devOption}`),
copy(license, docsBuilt),
copy(readmeSrc, readmeDest)
]));
diff --git a/docs/examples/.eslintrc b/docs/examples/.eslintrc
index 9dbc49c623..00c22821a7 100644
--- a/docs/examples/.eslintrc
+++ b/docs/examples/.eslintrc
@@ -52,6 +52,8 @@
"TabPane",
"Tooltip",
"Well",
- "Thumbnail"
+ "Thumbnail",
+ "Collapse",
+ "Fade"
}
}
diff --git a/docs/examples/Collapse.js b/docs/examples/Collapse.js
new file mode 100644
index 0000000000..c30069d8d0
--- /dev/null
+++ b/docs/examples/Collapse.js
@@ -0,0 +1,28 @@
+class Example extends React.Component {
+ constructor(...args){
+ super(...args);
+
+ this.state = {};
+ }
+
+ render(){
+
+ return (
+
+
this.setState({ open: !this.state.open })}>
+ click
+
+
+
+
+ Anim pariatur cliche reprehenderit, enim eiusmod high life accusamus terry richardson ad squid.
+ Nihil anim keffiyeh helvetica, craft beer labore wes anderson cred nesciunt sapiente ea proident.
+
+
+
+
+ );
+ }
+}
+
+React.render( , mountNode);
diff --git a/docs/examples/Fade.js b/docs/examples/Fade.js
new file mode 100644
index 0000000000..2583412829
--- /dev/null
+++ b/docs/examples/Fade.js
@@ -0,0 +1,29 @@
+
+class Example extends React.Component {
+
+ constructor(...args){
+ super(...args);
+ this.state = {};
+ }
+
+ render(){
+
+ return (
+
+
this.setState({ open: !this.state.open })}>
+ click
+
+
+
+
+ Anim pariatur cliche reprehenderit, enim eiusmod high life accusamus terry richardson ad squid.
+ Nihil anim keffiyeh helvetica, craft beer labore wes anderson cred nesciunt sapiente ea proident.
+
+
+
+
+ );
+ }
+}
+
+React.render( , mountNode);
diff --git a/docs/examples/ModalTrigger.js b/docs/examples/Modal.js
similarity index 100%
rename from docs/examples/ModalTrigger.js
rename to docs/examples/Modal.js
diff --git a/docs/examples/ModalCustomSizing.js b/docs/examples/ModalCustomSizing.js
index 4122e352d4..701d67338a 100644
--- a/docs/examples/ModalCustomSizing.js
+++ b/docs/examples/ModalCustomSizing.js
@@ -1,40 +1,52 @@
-const MyModal = React.createClass({
- render() {
+const Example = React.createClass({
+
+ getInitialState(){
+ return { show: false };
+ },
+
+ render(){
+ let close = e => this.setState({ show: false });
+
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
- odio eaque voluptatibus ratione magnam nulla? Amet cum maiores consequuntur totam dicta! Inventore adipisicing vel vero odio modi doloremque? Vitae porro impedit ea minima laboriosam quisquam neque. Perspiciatis omnis obcaecati consequatur sunt deleniti similique facilis sequi. Ipsum harum vitae modi reiciendis officiis.
- Quas laudantium laudantium modi corporis nihil provident consectetur omnis, natus nulla distinctio illum corporis. Sit ex earum odio ratione consequatur odit minus laborum? Eos? Sit ipsum illum architecto aspernatur perspiciatis error fuga illum, tempora harum earum, a dolores. Animi facilis inventore harum dolore accusamus
- fuga provident molestiae eum! Odit dicta error dolorem sunt reprehenderit. Sit similique iure quae obcaecati harum. Eum saepe fugit magnam dicta aliquam? Sapiente possimus aliquam fugiat officia culpa sint! Beatae voluptates voluptatem excepturi molestiae alias in tenetur beatae placeat architecto. Sit possimus rerum
- fugiat sapiente aspernatur. Necessitatibus tempora animi dicta perspiciatis tempora a velit in! Doloribus perspiciatis doloribus suscipit nam earum. Deleniti veritatis eaque totam assumenda fuga sapiente! Id recusandae. Consectetur necessitatibus eaque velit nobis aliquid? Fugit illum qui suscipit aspernatur alias ipsum
- repudiandae! Quia omnis quisquam dignissimos a mollitia. Suscipit aspernatur eum maiores repellendus ipsum doloribus alias voluptatum consequatur. Consectetur quibusdam veniam quas tenetur necessitatibus repudiandae? Rem optio vel alias neque optio sapiente quidem similique reiciendis tempore. Illum accusamus officia
- cum enim minima eligendi consectetur nemo veritatis nam nisi! Adipisicing nobis perspiciatis dolorum adipisci soluta architecto doloremque voluptatibus omnis debitis quas repellendus. Consequuntur assumenda illum commodi mollitia asperiores? Quis aspernatur consequatur modi veritatis aliquid at? Atque vel iure quos.
- Amet provident voluptatem amet aliquam deserunt sint, elit dolorem ipsa, voluptas? Quos esse facilis neque nihil sequi non? Voluptates rem ab quae dicta culpa dolorum sed atque molestias debitis omnis! Sit sint repellendus deleniti officiis distinctio. Impedit vel quos harum doloribus corporis. Laborum ullam nemo quaerat
- reiciendis recusandae minima dicta molestias rerum. Voluptas et ut omnis est ipsum accusamus harum. Amet exercitationem quasi velit inventore neque doloremque! Consequatur neque dolorem vel impedit sunt voluptate. Amet quo amet magni exercitationem libero recusandae possimus pariatur. Cumque eum blanditiis vel vitae
- distinctio! Tempora! Consectetur sit eligendi neque sunt soluta laudantium natus qui aperiam quisquam consectetur consequatur sit sint a unde et. At voluptas ut officiis esse totam quasi dolorem! Hic deserunt doloribus repudiandae! Lorem quod ab nostrum asperiores aliquam ab id consequatur, expedita? Tempora quaerat
- ex ea temporibus in tempore voluptates cumque. Quidem nam dolor reiciendis qui dolor assumenda ipsam veritatis quasi. Esse! Sit consectetur hic et sunt iste! Accusantium atque elit voluptate asperiores corrupti temporibus mollitia! Placeat soluta odio ad blanditiis nisi. Eius reiciendis id quos dolorum eaque suscipit
- 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.
-
-
- Close
-
-
+
+ this.setState({ show: true })}>
+ Launch demo modal
+
+
+
+
+ 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
+ odio eaque voluptatibus ratione magnam nulla? Amet cum maiores consequuntur totam dicta! Inventore adipisicing vel vero odio modi doloremque? Vitae porro impedit ea minima laboriosam quisquam neque. Perspiciatis omnis obcaecati consequatur sunt deleniti similique facilis sequi. Ipsum harum vitae modi reiciendis officiis.
+ Quas laudantium laudantium modi corporis nihil provident consectetur omnis, natus nulla distinctio illum corporis. Sit ex earum odio ratione consequatur odit minus laborum? Eos? Sit ipsum illum architecto aspernatur perspiciatis error fuga illum, tempora harum earum, a dolores. Animi facilis inventore harum dolore accusamus
+ fuga provident molestiae eum! Odit dicta error dolorem sunt reprehenderit. Sit similique iure quae obcaecati harum. Eum saepe fugit magnam dicta aliquam? Sapiente possimus aliquam fugiat officia culpa sint! Beatae voluptates voluptatem excepturi molestiae alias in tenetur beatae placeat architecto. Sit possimus rerum
+ fugiat sapiente aspernatur. Necessitatibus tempora animi dicta perspiciatis tempora a velit in! Doloribus perspiciatis doloribus suscipit nam earum. Deleniti veritatis eaque totam assumenda fuga sapiente! Id recusandae. Consectetur necessitatibus eaque velit nobis aliquid? Fugit illum qui suscipit aspernatur alias ipsum
+ repudiandae! Quia omnis quisquam dignissimos a mollitia. Suscipit aspernatur eum maiores repellendus ipsum doloribus alias voluptatum consequatur. Consectetur quibusdam veniam quas tenetur necessitatibus repudiandae? Rem optio vel alias neque optio sapiente quidem similique reiciendis tempore. Illum accusamus officia
+ cum enim minima eligendi consectetur nemo veritatis nam nisi! Adipisicing nobis perspiciatis dolorum adipisci soluta architecto doloremque voluptatibus omnis debitis quas repellendus. Consequuntur assumenda illum commodi mollitia asperiores? Quis aspernatur consequatur modi veritatis aliquid at? Atque vel iure quos.
+ Amet provident voluptatem amet aliquam deserunt sint, elit dolorem ipsa, voluptas? Quos esse facilis neque nihil sequi non? Voluptates rem ab quae dicta culpa dolorum sed atque molestias debitis omnis! Sit sint repellendus deleniti officiis distinctio. Impedit vel quos harum doloribus corporis. Laborum ullam nemo quaerat
+ reiciendis recusandae minima dicta molestias rerum. Voluptas et ut omnis est ipsum accusamus harum. Amet exercitationem quasi velit inventore neque doloremque! Consequatur neque dolorem vel impedit sunt voluptate. Amet quo amet magni exercitationem libero recusandae possimus pariatur. Cumque eum blanditiis vel vitae
+ distinctio! Tempora! Consectetur sit eligendi neque sunt soluta laudantium natus qui aperiam quisquam consectetur consequatur sit sint a unde et. At voluptas ut officiis esse totam quasi dolorem! Hic deserunt doloribus repudiandae! Lorem quod ab nostrum asperiores aliquam ab id consequatur, expedita? Tempora quaerat
+ ex ea temporibus in tempore voluptates cumque. Quidem nam dolor reiciendis qui dolor assumenda ipsam veritatis quasi. Esse! Sit consectetur hic et sunt iste! Accusantium atque elit voluptate asperiores corrupti temporibus mollitia! Placeat soluta odio ad blanditiis nisi. Eius reiciendis id quos dolorum eaque suscipit
+ 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.
+
+
+ Close
+
+
+
);
}
});
-const overlayTriggerInstance = (
- }>
- Launch demo modal
-
-);
-React.render(overlayTriggerInstance, mountNode);
+React.render( , mountNode);
diff --git a/docs/src/ComponentsPage.js b/docs/src/ComponentsPage.js
index 72196adf90..9b34f99e72 100644
--- a/docs/src/ComponentsPage.js
+++ b/docs/src/ComponentsPage.js
@@ -57,6 +57,7 @@ const ComponentsPage = React.createClass({
+
{/* Buttons */}
Buttons Button
@@ -855,6 +856,22 @@ const ComponentsPage = React.createClass({
Props
+
+
Transitions
+
+
Collapse
+
Add a collapse toggle animation to an element or component.
+
+
+
Props
+
+
+
Fade
+
Add a fade animation to a child element or component.
+
+
+
Props
+
diff --git a/docs/src/GettingStartedPage.js b/docs/src/GettingStartedPage.js
index f19af4f741..883dff20e9 100644
--- a/docs/src/GettingStartedPage.js
+++ b/docs/src/GettingStartedPage.js
@@ -43,7 +43,12 @@ $ npm install react-bootstrap`
codeText={
`var Alert = require('react-bootstrap/lib/Alert');
// or
-var Alert = require('react-bootstrap').Alert;`
+var Alert = require('react-bootstrap').Alert;
+
+// with ES6 modules
+import Alert from 'react-bootstrap/lib/Alert';
+// or
+import {Alert} from 'react-bootstrap';`
}
/>
diff --git a/docs/src/ReactPlayground.js b/docs/src/ReactPlayground.js
index 16c7c14d96..8dcf69d7b0 100644
--- a/docs/src/ReactPlayground.js
+++ b/docs/src/ReactPlayground.js
@@ -3,16 +3,22 @@ import * as modClassNames from 'classnames';
import * as modAccordion from '../../src/Accordion';
import * as modAlert from '../../src/Alert';
import * as modBadge from '../../src/Badge';
-import * as modmodButton from '../../src/Button';
+import * as modButton from '../../src/Button';
import * as modButtonGroup from '../../src/ButtonGroup';
import * as modButtonInput from '../../src/ButtonInput';
-import * as modmodButtonToolbar from '../../src/ButtonToolbar';
+
+import * as modButtonToolbar from '../../src/ButtonToolbar';
+import * as modCollapse from '../../src/Collapse';
+
import * as modCollapsibleNav from '../../src/CollapsibleNav';
import * as modCollapsibleMixin from '../../src/CollapsibleMixin';
import * as modCarousel from '../../src/Carousel';
import * as modCarouselItem from '../../src/CarouselItem';
import * as modCol from '../../src/Col';
import * as modDropdownButton from '../../src/DropdownButton';
+
+import * as modFade from '../../src/Fade';
+
import * as modFormControls from '../../src/FormControls';
import * as modGlyphicon from '../../src/Glyphicon';
import * as modGrid from '../../src/Grid';
@@ -26,9 +32,7 @@ import * as modNavbar from '../../src/Navbar';
import * as modNavItem from '../../src/NavItem';
import * as modMenuItem from '../../src/MenuItem';
import * as modModal from '../../src/Modal';
-import * as modModalTrigger from '../../src/ModalTrigger';
import * as modOverlayTrigger from '../../src/OverlayTrigger';
-import * as modOverlayMixin from '../../src/OverlayMixin';
import * as modPageHeader from '../../src/PageHeader';
import * as modPageItem from '../../src/PageItem';
import * as modPager from '../../src/Pager';
@@ -58,24 +62,29 @@ import CodeExample from './CodeExample';
const classNames = modClassNames.default;
+
/* eslint-disable */
+
const Portal = modPortal.default;
+const Collapse = modCollapse.default;
+const Fade = modFade.default;
+
const React = modReact.default;
const Accordion = modAccordion.default;
const Alert = modAlert.default;
const Badge = modBadge.default;
-const Button = modmodButton.default;
+const Button = modButton.default;
const ButtonGroup = modButtonGroup.default;
const ButtonInput = modButtonInput.default;
-const ButtonToolbar = modmodButtonToolbar.default;
+const ButtonToolbar = modButtonToolbar.default;
const CollapsibleNav = modCollapsibleNav.default;
const CollapsibleMixin = modCollapsibleMixin.default;
const Carousel = modCarousel.default;
const CarouselItem = modCarouselItem.default;
const Col = modCol.default;
const DropdownButton = modDropdownButton.default;
-const FormControls = modFormControls.default;
+const FormControls = modFormControls;
const Glyphicon = modGlyphicon.default;
const Grid = modGrid.default;
const Input = modInput.default;
@@ -88,9 +97,9 @@ const Navbar = modNavbar.default;
const NavItem = modNavItem.default;
const MenuItem = modMenuItem.default;
const Modal = modModal.default;
-const ModalTrigger = modModalTrigger.default;
+
const OverlayTrigger = modOverlayTrigger.default;
-const OverlayMixin = modOverlayMixin.default;
+
const PageHeader = modPageHeader.default;
const PageItem = modPageItem.default;
const Pagination = modPagination.default;
diff --git a/docs/src/Samples.js b/docs/src/Samples.js
index 47e6720802..fffdbba1dc 100644
--- a/docs/src/Samples.js
+++ b/docs/src/Samples.js
@@ -1,6 +1,9 @@
/* eslint no-path-concat: 0, no-var: 0 */
export default {
+ Collapse: require('fs').readFileSync(__dirname + '/../examples/Collapse.js', 'utf8'),
+ Fade: require('fs').readFileSync(__dirname + '/../examples/Fade.js', 'utf8'),
+
ButtonTypes: require('fs').readFileSync(__dirname + '/../examples/ButtonTypes.js', 'utf8'),
ButtonSizes: require('fs').readFileSync(__dirname + '/../examples/ButtonSizes.js', 'utf8'),
ButtonBlock: require('fs').readFileSync(__dirname + '/../examples/ButtonBlock.js', 'utf8'),
@@ -31,8 +34,7 @@ export default {
PanelGroupAccordion: require('fs').readFileSync(__dirname + '/../examples/PanelGroupAccordion.js', 'utf8'),
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'),
-
+ Modal: require('fs').readFileSync(__dirname + '/../examples/Modal.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'),
diff --git a/package.json b/package.json
index 04f5eee3a8..c70f241ebb 100644
--- a/package.json
+++ b/package.json
@@ -107,6 +107,7 @@
"yargs": "^3.5.4"
},
"dependencies": {
+ "babel-runtime": "^5.1.10",
"classnames": "^2.0.0"
}
}
diff --git a/src/Collapse.js b/src/Collapse.js
new file mode 100644
index 0000000000..505c116085
--- /dev/null
+++ b/src/Collapse.js
@@ -0,0 +1,198 @@
+import React from 'react';
+import Transition from './Transition';
+import domUtils from './utils/domUtils';
+import createChainedFunction from './utils/createChainedFunction';
+
+let capitalize = str => str[0].toUpperCase() + str.substr(1);
+
+// reading a dimension prop will cause the browser to recalculate,
+// which will let our animations work
+let triggerBrowserReflow = node => node.offsetHeight; //eslint-disable-line no-unused-expressions
+
+const MARGINS = {
+ height: ['marginTop', 'marginBottom'],
+ width: ['marginLeft', 'marginRight']
+};
+
+function getDimensionValue(dimension, elem){
+ let value = elem[`offset${capitalize(dimension)}`];
+ let computedStyles = domUtils.getComputedStyles(elem);
+ let margins = MARGINS[dimension];
+
+ return (value +
+ parseInt(computedStyles[margins[0]], 10) +
+ parseInt(computedStyles[margins[1]], 10)
+ );
+}
+
+class Collapse extends React.Component {
+
+ constructor(props, context){
+ super(props, context);
+
+ this.onEnterListener = this.handleEnter.bind(this);
+ this.onEnteringListener = this.handleEntering.bind(this);
+ this.onEnteredListener = this.handleEntered.bind(this);
+ this.onExitListener = this.handleExit.bind(this);
+ this.onExitingListener = this.handleExiting.bind(this);
+ }
+
+ render() {
+ let enter = createChainedFunction(this.onEnterListener, this.props.onEnter);
+ let entering = createChainedFunction(this.onEnteringListener, this.props.onEntering);
+ let entered = createChainedFunction(this.onEnteredListener, this.props.onEntered);
+ let exit = createChainedFunction(this.onExitListener, this.props.onExit);
+ let exiting = createChainedFunction(this.onExitingListener, this.props.onExiting);
+
+ return (
+
+ { this.props.children }
+
+ );
+ }
+
+ /* -- Expanding -- */
+ handleEnter(elem){
+ let dimension = this._dimension();
+ elem.style[dimension] = '0';
+ }
+
+ handleEntering(elem){
+ let dimension = this._dimension();
+
+ elem.style[dimension] = this._getScrollDimensionValue(elem, dimension);
+ }
+
+ handleEntered(elem){
+ let dimension = this._dimension();
+ elem.style[dimension] = null;
+ }
+
+ /* -- Collapsing -- */
+ handleExit(elem){
+ let dimension = this._dimension();
+
+ elem.style[dimension] = this.props.getDimensionValue(dimension, elem) + 'px';
+ }
+
+ handleExiting(elem){
+ let dimension = this._dimension();
+
+ triggerBrowserReflow(elem);
+ elem.style[dimension] = '0';
+ }
+
+ _dimension(){
+ return typeof this.props.dimension === 'function'
+ ? this.props.dimension()
+ : this.props.dimension;
+ }
+
+ //for testing
+ _getTransitionInstance(){
+ return this.refs.transition;
+ }
+
+ _getScrollDimensionValue(elem, dimension){
+ return elem[`scroll${capitalize(dimension)}`] + 'px';
+ }
+}
+
+Collapse.propTypes = {
+ /**
+ * Collapse the Component in or out.
+ */
+ in: React.PropTypes.bool,
+
+ /**
+ * Provide the duration of the animation in milliseconds, used to ensure that finishing callbacks are fired even if the
+ * original browser transition end events are canceled.
+ */
+ duration: React.PropTypes.number,
+
+ /**
+ * Specifies the dimension used when collapsing.
+ *
+ * _Note: Bootstrap only partially supports 'width'!
+ * You will need to supply your own css animation for the `.width` css class._
+ */
+ dimension: React.PropTypes.oneOfType([
+ React.PropTypes.oneOf(['height', 'width']),
+ React.PropTypes.func
+ ]),
+
+ /**
+ * A function that returns the height or width of the animating DOM node. Allows for providing some custom logic how much
+ * Collapse component should animate in its specified dimension.
+ *
+ * `getDimensionValue` is called with the current dimension prop value and the DOM node.
+ */
+ getDimensionValue: React.PropTypes.func,
+
+ /**
+ * A Callback fired before the component starts to expand.
+ */
+ onEnter: React.PropTypes.func,
+
+ /**
+ * A Callback fired immediately after the component starts to expand.
+ */
+ onEntering: React.PropTypes.func,
+
+ /**
+ * A Callback fired after the component has expanded.
+ */
+ onEntered: React.PropTypes.func,
+
+ /**
+ * A Callback fired before the component starts to collapse.
+ */
+ onExit: React.PropTypes.func,
+
+ /**
+ * A Callback fired immediately after the component starts to collapse.
+ */
+ onExiting: React.PropTypes.func,
+
+ /**
+ * A Callback fired after the component has collapsed.
+ */
+ onExited: React.PropTypes.func,
+
+ /**
+ * Specify whether the transitioning component should be unmounted (removed from the DOM) once the exit animation finishes.
+ */
+ unmountOnExit: React.PropTypes.bool,
+
+ /**
+ * Specify whether the component should collapse or expand when it mounts.
+ */
+ transitionAppear: React.PropTypes.bool
+};
+
+Collapse.defaultProps = {
+ in: false,
+ duration: 300,
+ dimension: 'height',
+ transitionAppear: false,
+ unmountOnExit: false,
+ getDimensionValue
+};
+
+export default Collapse;
+
diff --git a/src/CollapsibleMixin.js b/src/CollapsibleMixin.js
index 31895e4041..c1abcbaada 100644
--- a/src/CollapsibleMixin.js
+++ b/src/CollapsibleMixin.js
@@ -1,5 +1,8 @@
import React from 'react';
import TransitionEvents from './utils/TransitionEvents';
+import deprecationWarning from './utils/deprecationWarning';
+
+let warned = false;
const CollapsibleMixin = {
@@ -21,6 +24,13 @@ const CollapsibleMixin = {
};
},
+ componentWillMount(){
+ if ( !warned ){
+ deprecationWarning('CollapsibleMixin', 'Collapse Component');
+ warned = true;
+ }
+ },
+
componentWillUpdate(nextProps, nextState){
let willExpanded = nextProps.expanded != null ? nextProps.expanded : nextState.expanded;
if (willExpanded === this.isExpanded()) {
diff --git a/src/CollapsibleNav.js b/src/CollapsibleNav.js
index 427d0b30f3..04a21db3ef 100644
--- a/src/CollapsibleNav.js
+++ b/src/CollapsibleNav.js
@@ -1,14 +1,13 @@
import React, { cloneElement } from 'react';
import BootstrapMixin from './BootstrapMixin';
-import CollapsibleMixin from './CollapsibleMixin';
+import Collapse from './Collapse';
import classNames from 'classnames';
-import domUtils from './utils/domUtils';
import ValidComponentChildren from './utils/ValidComponentChildren';
import createChainedFunction from './utils/createChainedFunction';
const CollapsibleNav = React.createClass({
- mixins: [BootstrapMixin, CollapsibleMixin],
+ mixins: [BootstrapMixin],
propTypes: {
onSelect: React.PropTypes.func,
@@ -19,41 +18,29 @@ const CollapsibleNav = React.createClass({
eventKey: React.PropTypes.any
},
- getCollapsibleDOMNode() {
- return React.findDOMNode(this);
- },
-
- getCollapsibleDimensionValue() {
- let height = 0;
- let nodes = this.refs;
- for (let key in nodes) {
- if (nodes.hasOwnProperty(key)) {
-
- let n = React.findDOMNode(nodes[key]);
- let h = n.offsetHeight;
- let computedStyles = domUtils.getComputedStyles(n);
-
- height += (h +
- parseInt(computedStyles.marginTop, 10) +
- parseInt(computedStyles.marginBottom, 10)
- );
- }
- }
- return height;
- },
render() {
/*
* this.props.collapsible is set in NavBar when an eventKey is supplied.
*/
- const classes = this.props.collapsible ? this.getCollapsibleClassSet('navbar-collapse') : null;
+ const classes = this.props.collapsible ? 'navbar-collapse' : null;
const renderChildren = this.props.collapsible ? this.renderCollapsibleNavChildren : this.renderChildren;
- return (
+ let nav = (
{ValidComponentChildren.map(this.props.children, renderChildren)}
);
+
+ if ( this.props.collapsible ){
+ return (
+
+ { nav }
+
+ );
+ } else {
+ return nav;
+ }
},
getChildActiveProp(child) {
diff --git a/src/Fade.js b/src/Fade.js
new file mode 100644
index 0000000000..523508fff5
--- /dev/null
+++ b/src/Fade.js
@@ -0,0 +1,89 @@
+import React from 'react';
+import Transition from './Transition';
+
+class Fade extends React.Component {
+
+ constructor(props, context){
+ super(props, context);
+ }
+
+ render() {
+ return (
+
+ { this.props.children }
+
+ );
+ }
+}
+
+Fade.propTypes = {
+ /**
+ * Fade the Component in or out.
+ */
+ in: React.PropTypes.bool,
+
+ /**
+ * Provide the duration of the animation in milliseconds, used to ensure that finishing callbacks are fired even if the
+ * original browser transition end events are canceled.
+ */
+ duration: React.PropTypes.number,
+
+ /**
+ * A Callback fired before the component starts to fade in.
+ */
+ onEnter: React.PropTypes.func,
+
+ /**
+ * A Callback fired immediately after the component has started to faded in.
+ */
+ onEntering: React.PropTypes.func,
+
+ /**
+ * A Callback fired after the component has faded in.
+ */
+ onEntered: React.PropTypes.func,
+
+ /**
+ * A Callback fired before the component starts to fade out.
+ */
+ onExit: React.PropTypes.func,
+
+ /**
+ * A Callback fired immediately after the component has started to faded out.
+ */
+ onExiting: React.PropTypes.func,
+
+ /**
+ * A Callback fired after the component has faded out.
+ */
+ onExited: React.PropTypes.func,
+
+
+ /**
+ * Specify whether the transitioning component should be unmounted (removed from the DOM) once the exit animation finishes.
+ */
+ unmountOnExit: React.PropTypes.bool,
+
+ /**
+ * Specify whether the component should fade in or out when it mounts.
+ */
+ transitionAppear: React.PropTypes.bool
+
+};
+
+Fade.defaultProps = {
+ in: false,
+ duration: 300,
+ dimension: 'height',
+ transitionAppear: false,
+ unmountOnExit: false
+};
+
+export default Fade;
+
diff --git a/src/FadeMixin.js b/src/FadeMixin.js
index b5597d013d..b4dfe666f2 100644
--- a/src/FadeMixin.js
+++ b/src/FadeMixin.js
@@ -1,5 +1,6 @@
import React from 'react';
import domUtils from './utils/domUtils';
+import deprecationWarning from './utils/deprecationWarning';
// TODO: listen for onTransitionEnd to remove el
function getElementsAndSelf (root, classes){
@@ -16,7 +17,16 @@ function getElementsAndSelf (root, classes){
return els;
}
+let warned = false;
+
export default {
+ componentWillMount(){
+ if ( !warned ){
+ deprecationWarning('FadeMixin', 'Fade Component');
+ warned = true;
+ }
+ },
+
_fadeIn() {
let els;
diff --git a/src/FormControls/index.js b/src/FormControls/index.js
index 5a7c16286e..ef0f49c381 100644
--- a/src/FormControls/index.js
+++ b/src/FormControls/index.js
@@ -1,5 +1 @@
-import Static from './Static';
-
-export default {
- Static
-};
+export Static from './Static';
diff --git a/src/Input.js b/src/Input.js
index 85b3a59a03..f4f5dfe2ab 100644
--- a/src/Input.js
+++ b/src/Input.js
@@ -1,15 +1,11 @@
import React from 'react';
import InputBase from './InputBase';
-import ButtonInput from './ButtonInput';
-import FormControls from './FormControls';
+import * as FormControls from './FormControls';
import deprecationWarning from './utils/deprecationWarning';
class Input extends InputBase {
render() {
- if (ButtonInput.types.indexOf(this.props.type) > -1) {
- deprecationWarning(`Input type=${this.props.type}`, 'ButtonInput');
- return ;
- } else if (this.props.type === 'static') {
+ if (this.props.type === 'static') { // eslint-disable-line react/prop-types
deprecationWarning('Input type=static', 'StaticText');
return ;
}
diff --git a/src/ListGroupItem.js b/src/ListGroupItem.js
index dae133d54f..ac79d8bd37 100644
--- a/src/ListGroupItem.js
+++ b/src/ListGroupItem.js
@@ -1,7 +1,7 @@
import React, { cloneElement } from 'react';
import BootstrapMixin from './BootstrapMixin';
import classNames from 'classnames';
-
+import SafeAnchor from './SafeAnchor';
const ListGroupItem = React.createClass({
mixins: [BootstrapMixin],
@@ -51,12 +51,12 @@ const ListGroupItem = React.createClass({
renderAnchor(classes) {
return (
-
{this.props.header ? this.renderStructuredContent() : this.props.children}
-
+
);
},
diff --git a/src/MenuItem.js b/src/MenuItem.js
index aa8b7c65b5..0f8f331724 100644
--- a/src/MenuItem.js
+++ b/src/MenuItem.js
@@ -1,5 +1,6 @@
import React from 'react';
import classNames from 'classnames';
+import SafeAnchor from './SafeAnchor';
const MenuItem = React.createClass({
propTypes: {
@@ -16,7 +17,6 @@ const MenuItem = React.createClass({
getDefaultProps() {
return {
- href: '#',
active: false
};
},
@@ -34,9 +34,9 @@ const MenuItem = React.createClass({
renderAnchor() {
return (
-
+
{this.props.children}
-
+
);
},
diff --git a/src/Modal.js b/src/Modal.js
index 70b5945bab..c42f7b2e0c 100644
--- a/src/Modal.js
+++ b/src/Modal.js
@@ -4,12 +4,11 @@ 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 Fade from './Fade';
import Body from './ModalBody';
import Header from './ModalHeader';
@@ -37,23 +36,6 @@ 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;
@@ -108,19 +90,15 @@ function getScrollbarSize(){
document.body.removeChild(scrollDiv);
scrollDiv = null;
+ return scrollbarSize;
}
const ModalMarkup = React.createClass({
- mixins: [BootstrapMixin, FadeMixin],
+ mixins: [ BootstrapMixin ],
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.
*/
@@ -130,12 +108,6 @@ const ModalMarkup = React.createClass({
*/
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,
-
/**
* Open and close the Modal with a slide and fade animation.
*/
@@ -145,13 +117,7 @@ const ModalMarkup = React.createClass({
* @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,
+ onHide: React.PropTypes.func.isRequired,
/**
* A css class to apply to the Modal dialog DOM node.
@@ -199,8 +165,7 @@ const ModalMarkup = React.createClass({
let classes = {
modal: true,
- fade: this.props.animation,
- 'in': !this.props.animation
+ in: this.props.show && !this.props.animation
};
let modal = (
@@ -226,28 +191,11 @@ const ModalMarkup = React.createClass({
},
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 => {
+ return React.Children.map(this.props.children, child => {
// TODO: use context in 0.14
if (child.type.__isModalHeader) {
return cloneElement(child, {
- onHide: createChainedFunction(this._getHide(), child.props.onHide)
+ onHide: createChainedFunction(this.props.onHide, child.props.onHide)
});
}
return child;
@@ -255,31 +203,27 @@ const ModalMarkup = React.createClass({
},
renderBackdrop(modal) {
- let classes = {
- 'modal-backdrop': true,
- fade: this.props.animation,
- 'in': !this.props.animation
- };
-
- let onClick = this.props.backdrop === true ?
- this.handleBackdropClick : null;
+ let { animation } = this.props;
+ let duration = Modal.BACKDROP_TRANSITION_DURATION; //eslint-disable-line no-use-before-define
+
+ let backdrop = (
+
+ );
return (
-
+ { animation
+ ?
{backdrop}
+ : backdrop
+ }
{modal}
);
},
- _getHide(){
- if ( !this.props.onHide && this.props.onRequestHide){
- deprecationWarning('The Modal prop `onRequestHide`', 'the `onHide` prop');
- }
-
- return this.props.onHide || this.props.onRequestHide;
- },
-
iosClickHack() {
// IOS only allows click events to be delegated to the document on elements
// it considers 'clickable' - anchors, buttons, etc. We fake a click handler on the
@@ -360,12 +304,12 @@ const ModalMarkup = React.createClass({
return;
}
- this._getHide()();
+ this.props.onHide();
},
handleDocumentKeyUp(e) {
if (this.props.keyboard && e.keyCode === 27) {
- this._getHide()();
+ this.props.onHide();
}
},
@@ -438,26 +382,42 @@ const Modal = React.createClass({
...ModalMarkup.propTypes
},
- defaultProps: {
- show: null
+ getDefaultProps(){
+ return {
+ show: false,
+ animation: true
+ };
},
render() {
- let { show, ...props } = this.props;
+ let { children, ...props } = this.props;
+
+ let show = !!props.show;
let modal = (
- {this.props.children}
+
+ { children }
+
+ );
+
+ return (
+
+ { props.animation
+ ? (
+
+ { modal }
+
+ )
+ : show && modal
+ }
+
+
);
- // I can't think of another way to not break back compat while defaulting container
- if ( !this.props.__isUsedInModalTrigger && show != null ){
- return (
-
- { show && modal }
-
- );
- } else {
- return modal;
- }
}
});
@@ -466,4 +426,7 @@ Modal.Header = Header;
Modal.Title = Title;
Modal.Footer = Footer;
+Modal.TRANSITION_DURATION = 300;
+Modal.BACKDROP_TRANSITION_DURATION = 150;
+
export default Modal;
diff --git a/src/ModalTrigger.js b/src/ModalTrigger.js
deleted file mode 100644
index e1087dc384..0000000000
--- a/src/ModalTrigger.js
+++ /dev/null
@@ -1,121 +0,0 @@
-import React, { cloneElement } from 'react';
-import CustomPropTypes from './utils/CustomPropTypes';
-import deprecationWarning from './utils/deprecationWarning';
-
-import createChainedFunction from './utils/createChainedFunction';
-import createContextWrapper from './utils/createContextWrapper';
-import { OverlayMixin } from './OverlayMixin';
-
-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,
- onMouseOver: React.PropTypes.func
- },
-
-
- getInitialState() {
- return {
- isOverlayShown: false
- };
- },
-
- show() {
- this.setState({
- isOverlayShown: true
- });
- },
-
- hide() {
- this.setState({
- isOverlayShown: false
- });
- },
-
- toggle() {
- this.setState({
- isOverlayShown: !this.state.isOverlayShown
- });
- },
-
- renderOverlay() {
- let modal = this.props.modal;
-
- if (!this.state.isOverlayShown) {
- return ;
- }
-
- return cloneElement(
- modal,
- {
- onHide: this.hide,
- onRequestHide: createHideDepreciationWrapper(this.hide),
- __isUsedInModalTrigger: true
- }
- );
- },
-
- render() {
- let child = React.Children.only(this.props.children);
- let props = {};
-
- props.onClick = createChainedFunction(child.props.onClick, this.toggle);
- props.onMouseOver = createChainedFunction(child.props.onMouseOver, this.props.onMouseOver);
- props.onMouseOut = createChainedFunction(child.props.onMouseOut, this.props.onMouseOut);
- props.onFocus = createChainedFunction(child.props.onFocus, this.props.onFocus);
- props.onBlur = createChainedFunction(child.props.onBlur, this.props.onBlur);
-
- return cloneElement(child, props);
- }
-});
-
-/**
- * Creates a new ModalTrigger class that forwards the relevant context
- *
- * This static method should only be called at the module level, instead of in
- * e.g. a render() method, because it's expensive to create new classes.
- *
- * For example, you would want to have:
- *
- * > export default ModalTrigger.withContext({
- * > myContextKey: React.PropTypes.object
- * > });
- *
- * and import this when needed.
- */
-ModalTrigger.withContext = createContextWrapper(ModalTrigger, 'modal');
-
-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/Nav.js b/src/Nav.js
index e1c6f6d293..72b98e1a5f 100644
--- a/src/Nav.js
+++ b/src/Nav.js
@@ -1,14 +1,13 @@
import React, { cloneElement } from 'react';
import BootstrapMixin from './BootstrapMixin';
-import CollapsibleMixin from './CollapsibleMixin';
+import Collapse from './Collapse';
import classNames from 'classnames';
-import domUtils from './utils/domUtils';
import ValidComponentChildren from './utils/ValidComponentChildren';
import createChainedFunction from './utils/createChainedFunction';
const Nav = React.createClass({
- mixins: [BootstrapMixin, CollapsibleMixin],
+ mixins: [BootstrapMixin],
propTypes: {
activeHref: React.PropTypes.string,
@@ -43,33 +42,24 @@ const Nav = React.createClass({
getDefaultProps() {
return {
- bsClass: 'nav'
+ bsClass: 'nav',
+ expanded: true
};
},
- getCollapsibleDOMNode() {
- return React.findDOMNode(this);
- },
-
- getCollapsibleDimensionValue() {
- let node = React.findDOMNode(this.refs.ul);
- let height = node.offsetHeight;
- let computedStyles = domUtils.getComputedStyles(node);
-
- return height + parseInt(computedStyles.marginTop, 10) + parseInt(computedStyles.marginBottom, 10);
- },
-
render() {
- const classes = this.props.collapsible ? this.getCollapsibleClassSet('navbar-collapse') : null;
+ const classes = this.props.collapsible ? 'navbar-collapse' : null;
if (this.props.navbar && !this.props.collapsible) {
return (this.renderUl());
}
return (
-
- { this.renderUl() }
-
+
+
+ {this.renderUl()}
+
+
);
},
diff --git a/src/NavItem.js b/src/NavItem.js
index e0ea7b3d46..2be7501eee 100644
--- a/src/NavItem.js
+++ b/src/NavItem.js
@@ -1,6 +1,7 @@
import React from 'react';
import classNames from 'classnames';
import BootstrapMixin from './BootstrapMixin';
+import SafeAnchor from './SafeAnchor';
const NavItem = React.createClass({
mixins: [BootstrapMixin],
@@ -18,12 +19,6 @@ const NavItem = React.createClass({
'aria-controls': React.PropTypes.string
},
- getDefaultProps() {
- return {
- href: '#'
- };
- },
-
render() {
let {
role,
@@ -46,8 +41,7 @@ const NavItem = React.createClass({
title,
target,
id: linkId,
- onClick: this.handleClick,
- ref: 'anchor'
+ onClick: this.handleClick
};
if (!role && href === '#') {
@@ -56,9 +50,9 @@ const NavItem = React.createClass({
return (
-
+
{ children }
-
+
);
},
diff --git a/src/Overlay.js b/src/Overlay.js
index d425fe91a4..a5b27d923f 100644
--- a/src/Overlay.js
+++ b/src/Overlay.js
@@ -1,13 +1,26 @@
/*eslint-disable object-shorthand, react/prop-types */
-import React from 'react';
+import React, { cloneElement } from 'react';
import Portal from './Portal';
import Position from './Position';
import RootCloseWrapper from './RootCloseWrapper';
+import CustomPropTypes from './utils/CustomPropTypes';
+import Fade from './Fade';
+import classNames from 'classnames';
+
class Overlay extends React.Component {
constructor(props, context){
super(props, context);
+
+ this.state = { exited: false };
+ this.onHiddenListener = this.handleHidden.bind(this);
+ }
+
+ componentWillReceiveProps(nextProps) {
+ if (this.props.show){
+ this.setState({ exited: false });
+ }
}
render(){
@@ -17,30 +30,60 @@ class Overlay extends React.Component {
, target
, placement
, rootClose
+ , children
+ , animation: Transition
, ...props } = this.props;
- let positionedChild = (
-
- { this.props.children }
-
- );
+ let child = null;
- if (rootClose) {
- positionedChild = (
-
- { positionedChild }
-
+ if ( Transition === true ){
+ Transition = Fade;
+ }
+
+ if (props.show || (Transition && !this.state.exited)) {
+
+ child = children;
+
+ // Position the child before the animation to avoid `null` DOM nodes
+ child = (
+
+ { child }
+
);
+
+ child = Transition
+ ? (
+
+ { child }
+
+ )
+ : cloneElement(child, { className: classNames('in', child.className) });
+
+
+ if (rootClose) {
+ child = (
+
+ { child }
+
+ );
+ }
}
return (
-
- { props.show &&
- positionedChild
- }
+
+ { child }
);
}
+
+ handleHidden(){
+ this.setState({ exited: true });
+ }
}
Overlay.propTypes = {
@@ -57,7 +100,19 @@ Overlay.propTypes = {
/**
* A Callback fired by the Overlay when it wishes to be hidden.
*/
- onHide: React.PropTypes.func
+ onHide: React.PropTypes.func,
+
+ /**
+ * Use animation
+ */
+ animation: React.PropTypes.oneOfType([
+ React.PropTypes.bool,
+ CustomPropTypes.elementType
+ ])
+};
+
+Overlay.defaultProps = {
+ animation: Fade
};
export default Overlay;
diff --git a/src/OverlayMixin.js b/src/OverlayMixin.js
deleted file mode 100644
index e1d56b09df..0000000000
--- a/src/OverlayMixin.js
+++ /dev/null
@@ -1,91 +0,0 @@
-/*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 const OverlayMixin = {
- propTypes: {
-
- container: CustomPropTypes.mountable
- },
-
-
- componentDidMount() {
- this._renderOverlay();
- },
-
- componentDidUpdate() {
- this._renderOverlay();
- },
-
- componentWillUnmount() {
- this._unrenderOverlay();
- this._mountOverlayTarget();
- },
-
- _mountOverlayTarget() {
- if (!this._overlayTarget) {
- this._overlayTarget = document.createElement('div');
- this.getContainerDOMNode()
- .appendChild(this._overlayTarget);
- }
- },
-
- _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() {
- if (this._overlayTarget) {
- React.unmountComponentAtNode(this._overlayTarget);
- this._overlayInstance = null;
- }
- },
-
- getOverlayDOMNode() {
- if (!this.isMounted()) {
- throw new Error('getOverlayDOMNode(): A component must be mounted to have a DOM node.');
- }
-
- if (this._overlayInstance) {
- return React.findDOMNode(this._overlayInstance);
- }
-
- return null;
- },
-
- getContainerDOMNode() {
- 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 2be3bfd9d0..b90215f353 100644
--- a/src/OverlayTrigger.js
+++ b/src/OverlayTrigger.js
@@ -4,9 +4,6 @@ import React, { cloneElement } from 'react';
import createChainedFunction from './utils/createChainedFunction';
import createContextWrapper from './utils/createContextWrapper';
import Overlay from './Overlay';
-import position from './utils/overlayPositionUtils';
-
-import deprecationWarning from './utils/deprecationWarning';
import warning from 'react/lib/warning';
/**
@@ -33,7 +30,7 @@ const OverlayTrigger = React.createClass({
* Specify which action or actions trigger Overlay visibility
*/
trigger: React.PropTypes.oneOfType([
- React.PropTypes.oneOf(['manual', 'click', 'hover', 'focus']),
+ React.PropTypes.oneOf(['click', 'hover', 'focus']),
React.PropTypes.arrayOf(React.PropTypes.oneOf(['click', 'hover', 'focus']))
]),
@@ -178,30 +175,24 @@ const OverlayTrigger = React.createClass({
// create in render otherwise owner is lost...
this._overlay = this.getOverlay();
- if (this.props.trigger !== 'manual') {
-
- props.onClick = createChainedFunction(trigger.props.onClick, this.props.onClick);
+ props.onClick = createChainedFunction(trigger.props.onClick, this.props.onClick);
- 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.');
+ if (isOneOf('click', this.props.trigger)) {
+ props.onClick = createChainedFunction(this.toggle, props.onClick);
+ }
- props.onMouseOver = createChainedFunction(this.handleDelayedShow, this.props.onMouseOver);
- props.onMouseOut = createChainedFunction(this.handleDelayedHide, this.props.onMouseOut);
- }
+ 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.');
- if (isOneOf('focus', this.props.trigger)) {
- props.onFocus = createChainedFunction(this.handleDelayedShow, this.props.onFocus);
- props.onBlur = createChainedFunction(this.handleDelayedHide, this.props.onBlur);
- }
+ props.onMouseOver = createChainedFunction(this.handleDelayedShow, this.props.onMouseOver);
+ props.onMouseOut = createChainedFunction(this.handleDelayedHide, this.props.onMouseOut);
}
- else {
- deprecationWarning('"manual" trigger type', ' the Overlay component');
+
+ if (isOneOf('focus', this.props.trigger)) {
+ props.onFocus = createChainedFunction(this.handleDelayedShow, this.props.onFocus);
+ props.onBlur = createChainedFunction(this.handleDelayedHide, this.props.onBlur);
}
return cloneElement(
@@ -250,32 +241,6 @@ const OverlayTrigger = React.createClass({
this._hoverDelay = null;
this.hide();
}, delay);
- },
-
- // deprecated Methods
- calcOverlayPosition() {
- let overlay = this.props.overlay;
-
- deprecationWarning('OverlayTrigger.calcOverlayPosition()', 'utils/overlayPositionUtils');
-
- 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() {
- deprecationWarning('OverlayTrigger.getPosition()', 'utils/overlayPositionUtils');
-
- let overlay = this.props.overlay;
-
- return position.getPosition(
- React.findDOMNode(this)
- , React.findDOMNode(overlay.props.container || this.props.container)
- );
}
});
diff --git a/src/PageItem.js b/src/PageItem.js
index 5a7c22d7f9..44ef5e419c 100644
--- a/src/PageItem.js
+++ b/src/PageItem.js
@@ -1,5 +1,6 @@
import React from 'react';
import classNames from 'classnames';
+import SafeAnchor from './SafeAnchor';
const PageItem = React.createClass({
@@ -14,12 +15,6 @@ const PageItem = React.createClass({
eventKey: React.PropTypes.any
},
- getDefaultProps() {
- return {
- href: '#'
- };
- },
-
render() {
let classes = {
'disabled': this.props.disabled,
@@ -31,14 +26,13 @@ const PageItem = React.createClass({
-
+ onClick={this.handleSelect}>
{this.props.children}
-
+
);
},
diff --git a/src/PaginationButton.js b/src/PaginationButton.js
index 0238b5b9b7..e49e1344ca 100644
--- a/src/PaginationButton.js
+++ b/src/PaginationButton.js
@@ -2,6 +2,7 @@ import React from 'react';
import classNames from 'classnames';
import BootstrapMixin from './BootstrapMixin';
import createSelectedEvent from './utils/createSelectedEvent';
+import SafeAnchor from './SafeAnchor';
const PaginationButton = React.createClass({
mixins: [BootstrapMixin],
@@ -25,9 +26,6 @@ const PaginationButton = React.createClass({
},
handleClick(event) {
- // This would go away once SafeAnchor is available
- event.preventDefault();
-
if (this.props.onSelect) {
let selectedEvent = createSelectedEvent(this.props.eventKey);
this.props.onSelect(event, selectedEvent);
@@ -35,14 +33,22 @@ const PaginationButton = React.createClass({
},
render() {
- let classes = this.getBsClassSet();
+ let classes = {
+ active: this.props.active,
+ disabled: this.props.disabled,
+ ...this.getBsClassSet()
+ };
- classes.active = this.props.active;
- classes.disabled = this.props.disabled;
+ let {
+ className,
+ ...anchorProps // eslint-disable-line object-shorthand
+ } = this.props;
return (
-
- {this.props.children}
+
+
);
}
diff --git a/src/Panel.js b/src/Panel.js
index d0bde0f778..d6dd1ae24a 100644
--- a/src/Panel.js
+++ b/src/Panel.js
@@ -2,10 +2,10 @@ import React, { cloneElement } from 'react';
import classNames from 'classnames';
import BootstrapMixin from './BootstrapMixin';
-import CollapsibleMixin from './CollapsibleMixin';
+import Collapse from './Collapse';
const Panel = React.createClass({
- mixins: [BootstrapMixin, CollapsibleMixin],
+ mixins: [BootstrapMixin],
propTypes: {
collapsible: React.PropTypes.bool,
@@ -13,6 +13,8 @@ const Panel = React.createClass({
header: React.PropTypes.node,
id: React.PropTypes.string,
footer: React.PropTypes.node,
+ defaultExpanded: React.PropTypes.bool,
+ expanded: React.PropTypes.bool,
eventKey: React.PropTypes.any
},
@@ -23,6 +25,18 @@ const Panel = React.createClass({
};
},
+ getInitialState(){
+ let defaultExpanded = this.props.defaultExpanded != null ?
+ this.props.defaultExpanded :
+ this.props.expanded != null ?
+ this.props.expanded :
+ false;
+
+ return {
+ expanded: defaultExpanded
+ };
+ },
+
handleSelect(e){
e.selected = true;
@@ -38,19 +52,11 @@ const Panel = React.createClass({
},
handleToggle(){
- this.setState({expanded:!this.state.expanded});
+ this.setState({ expanded: !this.state.expanded});
},
- getCollapsibleDimensionValue() {
- return React.findDOMNode(this.refs.panel).scrollHeight;
- },
-
- getCollapsibleDOMNode() {
- if (!this.isMounted() || !this.refs || !this.refs.panel) {
- return null;
- }
-
- return React.findDOMNode(this.refs.panel);
+ isExpanded(){
+ return this.props.expanded != null ? this.props.expanded : this.state.expanded;
},
render() {
@@ -69,13 +75,16 @@ const Panel = React.createClass({
let collapseClass = this.prefixClass('collapse');
return (
-
- {this.renderBody()}
-
+
+
+ {this.renderBody()}
+
+
+
);
},
diff --git a/src/Popover.js b/src/Popover.js
index e24b0c51a6..5b9f974d2e 100644
--- a/src/Popover.js
+++ b/src/Popover.js
@@ -2,12 +2,11 @@
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],
+ mixins: [ BootstrapMixin ],
propTypes: {
/**
@@ -45,29 +44,19 @@ const Popover = React.createClass({
/**
* Title text
*/
- title: React.PropTypes.node,
- /**
- * Specify whether the Popover should be use show and hide animations.
- */
- animation: React.PropTypes.bool
-
-
+ title: React.PropTypes.node
},
getDefaultProps() {
return {
- placement: 'right',
- animation: true
+ placement: 'right'
};
},
render() {
const classes = {
'popover': true,
- [this.props.placement]: true,
- // in class will be added by the FadeMixin when the animation property is true
- 'in': !this.props.animation && (this.props.positionLeft != null || this.props.positionTop != null),
- 'fade': this.props.animation
+ [this.props.placement]: true
};
const style = {
diff --git a/src/Portal.js b/src/Portal.js
index cc8f350701..575f93268b 100644
--- a/src/Portal.js
+++ b/src/Portal.js
@@ -1,6 +1,7 @@
+/*eslint-disable react/prop-types */
import React from 'react';
import CustomPropTypes from './utils/CustomPropTypes';
-import { OverlayMixin } from './OverlayMixin';
+import domUtils from './utils/domUtils';
let Portal = React.createClass({
@@ -13,22 +14,82 @@ let Portal = React.createClass({
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 ],
+ componentDidMount() {
+ this._renderOverlay();
+ },
+
+ componentDidUpdate() {
+ this._renderOverlay();
+ },
- renderOverlay() {
- if (!this.props.children) {
- return null;
+ componentWillUnmount() {
+ this._unrenderOverlay();
+ this._mountOverlayTarget();
+ },
+
+ _mountOverlayTarget() {
+ if (!this._overlayTarget) {
+ this._overlayTarget = document.createElement('div');
+ this.getContainerDOMNode()
+ .appendChild(this._overlayTarget);
}
+ },
- return React.Children.only(this.props.children);
+ _unmountOverlayTarget() {
+ if (this._overlayTarget) {
+ this.getContainerDOMNode()
+ .removeChild(this._overlayTarget);
+ this._overlayTarget = null;
+ }
+ },
+
+ _renderOverlay() {
+
+ let overlay = !this.props.children
+ ? null
+ : React.Children.only(this.props.children);
+
+ // Save reference for future access.
+ 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() {
+ if (this._overlayTarget) {
+ React.unmountComponentAtNode(this._overlayTarget);
+ this._overlayInstance = null;
+ }
},
render() {
return null;
+ },
+
+ getOverlayDOMNode() {
+ if (!this.isMounted()) {
+ throw new Error('getOverlayDOMNode(): A component must be mounted to have a DOM node.');
+ }
+
+ if (this._overlayInstance) {
+ if (this._overlayInstance.getWrappedDOMNode) {
+ return this._overlayInstance.getWrappedDOMNode();
+ } else {
+ return React.findDOMNode(this._overlayInstance);
+ }
+ }
+
+ return null;
+ },
+
+ getContainerDOMNode() {
+ return React.findDOMNode(this.props.container) || domUtils.ownerDocument(this).body;
}
});
-
export default Portal;
diff --git a/src/Position.js b/src/Position.js
index 13c7f09d31..6b3b8ff9a0 100644
--- a/src/Position.js
+++ b/src/Position.js
@@ -31,13 +31,13 @@ class Position extends React.Component {
}
render() {
- let { placement, children } = this.props;
+ let { children, ...props } = this.props;
let { positionLeft, positionTop, ...arrows } = this.props.target ? this.state : {};
return cloneElement(
React.Children.only(children), {
+ ...props,
...arrows,
- placement,
positionTop,
positionLeft,
style: {
@@ -61,13 +61,14 @@ class Position extends React.Component {
return;
}
+ let overlay = React.findDOMNode(this);
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)
+ , overlay
, target
, container
, this.props.containerPadding));
diff --git a/src/RootCloseWrapper.js b/src/RootCloseWrapper.js
index 146c688ab9..35fecab769 100644
--- a/src/RootCloseWrapper.js
+++ b/src/RootCloseWrapper.js
@@ -4,6 +4,15 @@ import EventListener from './utils/EventListener';
// TODO: Merge this logic with dropdown logic once #526 is done.
+// TODO: Consider using an ES6 symbol here, once we use babel-runtime.
+const CLICK_WAS_INSIDE = '__click_was_inside';
+
+function suppressRootClose(event) {
+ // Tag the native event to prevent the root close logic on document click.
+ // This seems safer than using event.nativeEvent.stopImmediatePropagation(),
+ // which is only supported in IE >= 9.
+ event.nativeEvent[CLICK_WAS_INSIDE] = true;
+}
export default class RootCloseWrapper extends React.Component {
constructor(props) {
@@ -23,10 +32,8 @@ export default class RootCloseWrapper extends React.Component {
}
handleDocumentClick(e) {
- // If the click originated from within this component, don't do anything.
- // e.srcElement is required for IE8 as e.target is undefined
- let target = e.target || e.srcElement;
- if (domUtils.contains(React.findDOMNode(this), target)) {
+ // This is now the native event.
+ if (e[CLICK_WAS_INSIDE]) {
return;
}
@@ -54,7 +61,21 @@ export default class RootCloseWrapper extends React.Component {
}
render() {
- return React.Children.only(this.props.children);
+ // Wrap the child in a new element, so the child won't have to handle
+ // potentially combining multiple onClick listeners.
+ return (
+
+ {React.Children.only(this.props.children)}
+
+ );
+ }
+
+ getWrappedDOMNode() {
+ // We can't use a ref to identify the wrapped child, since we might be
+ // stealing the ref from the owner, but we know exactly the DOM structure
+ // that will be rendered, so we can just do this to get the child's DOM
+ // node for doing size calculations in OverlayMixin.
+ return React.findDOMNode(this).children[0];
}
componentWillUnmount() {
diff --git a/src/SafeAnchor.js b/src/SafeAnchor.js
new file mode 100644
index 0000000000..ae45a9c261
--- /dev/null
+++ b/src/SafeAnchor.js
@@ -0,0 +1,35 @@
+import React from 'react';
+import createChainedFunction from './utils/createChainedFunction';
+
+/**
+ * Note: This is intended as a stop-gap for accessibility concerns that the
+ * Bootstrap CSS does not address as they have styled anchors and not buttons
+ * in many cases.
+ */
+export default class SafeAnchor extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleClick = this.handleClick.bind(this);
+ }
+
+ handleClick(event) {
+ if (this.props.href === undefined) {
+ event.preventDefault();
+ }
+ }
+
+ render() {
+ return (
+
+ );
+ }
+}
+
+SafeAnchor.propTypes = {
+ href: React.PropTypes.string,
+ onClick: React.PropTypes.func
+};
diff --git a/src/SubNav.js b/src/SubNav.js
index e9487c1ada..814604cc76 100644
--- a/src/SubNav.js
+++ b/src/SubNav.js
@@ -4,6 +4,7 @@ import classNames from 'classnames';
import ValidComponentChildren from './utils/ValidComponentChildren';
import createChainedFunction from './utils/createChainedFunction';
import BootstrapMixin from './BootstrapMixin';
+import SafeAnchor from './SafeAnchor';
const SubNav = React.createClass({
mixins: [BootstrapMixin],
@@ -99,14 +100,13 @@ const SubNav = React.createClass({
return (
-
+ onClick={this.handleClick}>
{this.props.text}
-
+
{ValidComponentChildren.map(this.props.children, this.renderNavItem)}
diff --git a/src/Thumbnail.js b/src/Thumbnail.js
index e93792146f..997d426b95 100644
--- a/src/Thumbnail.js
+++ b/src/Thumbnail.js
@@ -1,6 +1,7 @@
import React from 'react';
import classSet from 'classnames';
import BootstrapMixin from './BootstrapMixin';
+import SafeAnchor from './SafeAnchor';
const Thumbnail = React.createClass({
mixins: [BootstrapMixin],
@@ -22,9 +23,9 @@ const Thumbnail = React.createClass({
if(this.props.href) {
return (
-
+
-
+
);
}
else {
diff --git a/src/Tooltip.js b/src/Tooltip.js
index 640267829e..0cecc0dc9b 100644
--- a/src/Tooltip.js
+++ b/src/Tooltip.js
@@ -2,11 +2,10 @@
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],
+ mixins: [BootstrapMixin],
propTypes: {
/**
@@ -44,27 +43,19 @@ const Tooltip = React.createClass({
/**
* Title text
*/
- title: React.PropTypes.node,
- /**
- * Specify whether the Tooltip should be use show and hide animations.
- */
- animation: React.PropTypes.bool
+ title: React.PropTypes.node
},
getDefaultProps() {
return {
- placement: 'right',
- animation: true
+ placement: 'right'
};
},
render() {
const classes = {
'tooltip': true,
- [this.props.placement]: true,
- // in class will be added by the FadeMixin when the animation property is true
- 'in': !this.props.animation && (this.props.positionLeft != null || this.props.positionTop != null),
- 'fade': this.props.animation
+ [this.props.placement]: true
};
const style = {
diff --git a/src/Transition.js b/src/Transition.js
new file mode 100644
index 0000000000..a49124aac7
--- /dev/null
+++ b/src/Transition.js
@@ -0,0 +1,285 @@
+import React from 'react';
+import TransitionEvents from './utils/TransitionEvents';
+import classnames from 'classnames';
+
+function omit(obj, keys) {
+ let included = Object.keys(obj).filter( k => keys.indexOf(k) === -1);
+ let newObj = {};
+
+ included.forEach( key => newObj[key] = obj[key] );
+ return newObj;
+}
+
+function ensureTransitionEnd(node, handler, duration){
+ let fired = false;
+ let done = e => {
+ if (!fired) {
+ fired = true;
+ handler(e);
+ }
+ };
+
+ if ( node ) {
+ TransitionEvents.addEndEventListener(node, done);
+ setTimeout(done, duration);
+ } else {
+ setTimeout(done, 0);
+ }
+}
+
+// reading a dimension prop will cause the browser to recalculate,
+// which will let our animations work
+let triggerBrowserReflow = node => node.offsetHeight; //eslint-disable-line no-unused-expressions
+
+class Transition extends React.Component {
+
+ constructor(props, context){
+ super(props, context);
+
+ this.state = {
+ in: !props.in,
+ transitioning: false
+ };
+
+ this.needsTransition = true;
+ }
+
+ componentWillReceiveProps(nextProps) {
+ if (nextProps.in !== this.props.in) {
+ this.needsTransition = true;
+ }
+ }
+
+ componentDidUpdate() {
+ this.processChild();
+ }
+
+ componentWillMount() {
+ this._mounted = true;
+
+ if (!this.props.transitionAppear) {
+ this.needsTransition = false;
+ this.setState({ in: this.props.in });
+ }
+ }
+
+ componentWillUnmount(){
+ this._mounted = false;
+ }
+
+ componentDidMount() {
+ if (this.props.transitionAppear) {
+ this.processChild();
+ }
+ }
+
+ processChild(){
+ let needsTransition = this.needsTransition;
+ let enter = this.props.in;
+
+ if (needsTransition) {
+ this.needsTransition = false;
+ this[enter ? 'performEnter' : 'performLeave']();
+ }
+ }
+
+ performEnter() {
+ let maybeNode = React.findDOMNode(this);
+
+ let enter = node => {
+ node = this.props.transitioningNode(node) || node;
+
+ this.props.onEnter(node);
+
+ this.safeSetState({ in: true, transitioning: true, needInitialRender: false }, ()=> {
+
+ this.props.onEntering(node);
+
+ ensureTransitionEnd(node, () => {
+ if ( this.state.in ){
+ this.safeSetState({
+ transitioning: false
+ }, () => this.props.onEntered(node));
+ }
+
+ }, this.props.duration);
+ });
+ };
+
+ if (maybeNode) {
+ enter(maybeNode);
+ }
+ else if (this.props.unmountOnExit) {
+ this._ensureNode(enter);
+ }
+ }
+
+ performLeave() {
+ let node = React.findDOMNode(this);
+
+ node = this.props.transitioningNode(node) || node;
+
+ this.props.onExit(node);
+
+ this.setState({ in: false, transitioning: true }, () => {
+ this.props.onExiting(node);
+
+ ensureTransitionEnd(node, () => {
+ if ( !this.state.in ){
+ this.safeSetState({ transitioning: false }, ()=> this.props.onExited(node));
+ }
+ }, this.props.duration);
+ });
+ }
+
+ _ensureNode(callback) {
+
+ this.setState({ needInitialRender: true }, ()=> {
+ let node = React.findDOMNode(this);
+
+ triggerBrowserReflow(node);
+
+ callback(node);
+ });
+ }
+
+ safeSetState(newState, cb){
+ if (this._mounted) {
+ this.setState(newState, cb);
+ }
+ }
+
+ render() {
+ let childProps = omit(this.props, Object.keys(Transition.propTypes).concat('children'));
+
+ let child = this.props.children;
+ let starting = this.state.needInitialRender;
+ let out = !this.state.in && !this.state.transitioning;
+
+ if ( !child || (this.props.unmountOnExit && out && !starting) ){
+ return null;
+ }
+
+ let classes = '';
+
+ // using `classnames()` here causes a subtle bug,
+ // hence the verbose if/else if sequence.
+ if (this.state.in && !this.state.transitioning) {
+ classes = this.props.enteredClassName;
+ }
+
+ else if (this.state.in && this.state.transitioning) {
+ classes = this.props.enteringClassName;
+ }
+
+ else if (!this.state.in && !this.state.transitioning) {
+ classes = this.props.exitedClassName;
+ }
+
+ else if (!this.state.in && this.state.transitioning) {
+ classes = this.props.exitingClassName;
+ }
+
+ return React.cloneElement(child, {
+ ...childProps,
+ className: classnames(
+ child.props.className,
+ this.props.className,
+ classes)
+ });
+ }
+}
+
+Transition.propTypes = {
+ /**
+ * Triggers the Enter or Exit animation
+ */
+ in: React.PropTypes.bool,
+
+ /**
+ * Specify whether the transitioning component should be unmounted (removed from the DOM) once the exit animation finishes.
+ */
+ unmountOnExit: React.PropTypes.bool,
+
+ /**
+ * Specify whether transitions should run when the Transition component mounts.
+ */
+ transitionAppear: React.PropTypes.bool,
+
+ /**
+ * Provide the duration of the animation in milliseconds, used to ensure that finishing callbacks are fired even if the
+ * original browser transition end events are canceled.
+ */
+ duration: React.PropTypes.number,
+
+ /**
+ * A css class or classes applied once the Component has exited.
+ */
+ exitedClassName: React.PropTypes.string,
+ /**
+ * A css class or classes applied while the Component is exiting.
+ */
+ exitingClassName: React.PropTypes.string,
+ /**
+ * A css class or classes applied once the Component has entered.
+ */
+ enteredClassName: React.PropTypes.string,
+ /**
+ * A css class or classes applied while the Component is entering.
+ */
+ enteringClassName: React.PropTypes.string,
+
+ /**
+ * A function that returns the DOM node to animate. This Node will have the transition classes applied to it.
+ * When left out, the Component will use its immediate child.
+ *
+ * @private
+ */
+ transitioningNode: React.PropTypes.func,
+
+ /**
+ * A callback fired just before the "entering" classes are applied
+ */
+ onEnter: React.PropTypes.func,
+ /**
+ * A callback fired just after the "entering" classes are applied
+ */
+ onEntering: React.PropTypes.func,
+ /**
+ * A callback fired after "enter" classes are applied
+ */
+ onEntered: React.PropTypes.func,
+ /**
+ * A callback fired after "exiting" classes are applied
+ */
+ onExit: React.PropTypes.func,
+ /**
+ * A callback fired after "exiting" classes are applied
+ */
+ onExiting: React.PropTypes.func,
+ /**
+ * A callback fired after "exit" classes are applied
+ */
+ onExited: React.PropTypes.func
+};
+
+// name the function so it is clearer in the documentation
+const noop = ()=>{};
+
+Transition.defaultProps = {
+ in: false,
+ duration: 300,
+ unmountOnExit: false,
+ transitionAppear: false,
+ transitioningNode: noop,
+
+ onEnter: noop,
+ onEntering: noop,
+ onEntered: noop,
+
+ onExit: noop,
+ onExiting: noop,
+ onExited: noop
+};
+
+export default Transition;
diff --git a/src/index.js b/src/index.js
index 327c383768..ae6077a6f2 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,128 +1,70 @@
-import Accordion from './Accordion';
-import Affix from './Affix';
-import AffixMixin from './AffixMixin';
-import Alert from './Alert';
-import BootstrapMixin from './BootstrapMixin';
-import Badge from './Badge';
-import Button from './Button';
-import ButtonGroup from './ButtonGroup';
-import ButtonInput from './ButtonInput';
-import ButtonToolbar from './ButtonToolbar';
-import CollapsibleNav from './CollapsibleNav';
-import Carousel from './Carousel';
-import CarouselItem from './CarouselItem';
-import Col from './Col';
-import CollapsibleMixin from './CollapsibleMixin';
-import DropdownButton from './DropdownButton';
-import DropdownMenu from './DropdownMenu';
-import DropdownStateMixin from './DropdownStateMixin';
-import FadeMixin from './FadeMixin';
-import FormControls from './FormControls';
-import Glyphicon from './Glyphicon';
-import Grid from './Grid';
-import Input from './Input';
-import Interpolate from './Interpolate';
-import Jumbotron from './Jumbotron';
-import Label from './Label';
-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';
+export Accordion from './Accordion';
+export Affix from './Affix';
+export AffixMixin from './AffixMixin';
+export Alert from './Alert';
+export Badge from './Badge';
+export BootstrapMixin from './BootstrapMixin';
+export Button from './Button';
+export ButtonGroup from './ButtonGroup';
+export ButtonInput from './ButtonInput';
+export ButtonToolbar from './ButtonToolbar';
+export Carousel from './Carousel';
+export CarouselItem from './CarouselItem';
+export Col from './Col';
+export CollapsibleMixin from './CollapsibleMixin';
+export CollapsibleNav from './CollapsibleNav';
+export DropdownButton from './DropdownButton';
+export DropdownMenu from './DropdownMenu';
+export DropdownStateMixin from './DropdownStateMixin';
+export FadeMixin from './FadeMixin';
+export Glyphicon from './Glyphicon';
+export Grid from './Grid';
+export Input from './Input';
+export Interpolate from './Interpolate';
+export Jumbotron from './Jumbotron';
+export Label from './Label';
+export ListGroup from './ListGroup';
+export ListGroupItem from './ListGroupItem';
+export MenuItem from './MenuItem';
-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';
-import PanelGroup from './PanelGroup';
-import PageItem from './PageItem';
-import Pager from './Pager';
-import Popover from './Popover';
-import ProgressBar from './ProgressBar';
-import Row from './Row';
-import SplitButton from './SplitButton';
-import SubNav from './SubNav';
-import TabbedArea from './TabbedArea';
-import Table from './Table';
-import TabPane from './TabPane';
-import Thumbnail from './Thumbnail';
-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 Modal from './Modal';
+export ModalHeader from './ModalHeader';
+export ModalTitle from './ModalTitle';
+export ModalBody from './ModalBody';
+export ModalFooter from './ModalFooter';
-export default {
- Accordion,
- Affix,
- AffixMixin,
- Alert,
- BootstrapMixin,
- Badge,
- Button,
- ButtonGroup,
- ButtonInput,
- ButtonToolbar,
- CollapsibleNav,
- Carousel,
- CarouselItem,
- Col,
- CollapsibleMixin,
- DropdownButton,
- DropdownMenu,
- DropdownStateMixin,
- FadeMixin,
- FormControls,
- Glyphicon,
- Grid,
- Input,
- Interpolate,
- Jumbotron,
- Label,
- ListGroup,
- ListGroupItem,
- MenuItem,
- Modal,
- ModalHeader,
- ModalTitle,
- ModalBody,
- ModalFooter,
- Nav,
- Navbar,
- NavItem,
- ModalTrigger,
- Overlay,
- OverlayTrigger,
- OverlayMixin,
- PageHeader,
- Panel,
- PanelGroup,
- PageItem,
- Pager,
- Pagination,
- Popover,
- Portal,
- Position,
- ProgressBar,
- Row,
- SplitButton,
- SubNav,
- TabbedArea,
- Table,
- TabPane,
- Thumbnail,
- Tooltip,
- utils,
- Well,
- styleMaps
-};
+export Nav from './Nav';
+export Navbar from './Navbar';
+export NavItem from './NavItem';
+
+export Overlay from './Overlay';
+export OverlayTrigger from './OverlayTrigger';
+
+export PageHeader from './PageHeader';
+export PageItem from './PageItem';
+export Pager from './Pager';
+export Pagination from './Pagination';
+export Panel from './Panel';
+export PanelGroup from './PanelGroup';
+export Popover from './Popover';
+export ProgressBar from './ProgressBar';
+export Row from './Row';
+export SafeAnchor from './SafeAnchor';
+export SplitButton from './SplitButton';
+export styleMaps from './styleMaps';
+export SubNav from './SubNav';
+export TabbedArea from './TabbedArea';
+export Table from './Table';
+export TabPane from './TabPane';
+export Thumbnail from './Thumbnail';
+export Tooltip from './Tooltip';
+export Well from './Well';
+
+export Portal from './Portal';
+export Position from './Position';
+
+export Collapse from './Collapse';
+export Fade from './Collapse';
+
+export * as FormControls from './FormControls';
+export * as utils from './utils';
diff --git a/src/utils/index.js b/src/utils/index.js
index aad9066a10..f024432a7b 100644
--- a/src/utils/index.js
+++ b/src/utils/index.js
@@ -1,13 +1,5 @@
-import childrenValueInputValidation from './childrenValueInputValidation';
-import createChainedFunction from './createChainedFunction';
-import CustomPropTypes from './CustomPropTypes';
-import domUtils from './domUtils';
-import ValidComponentChildren from './ValidComponentChildren';
-
-export default {
- childrenValueInputValidation,
- createChainedFunction,
- CustomPropTypes,
- domUtils,
- ValidComponentChildren
-};
+export childrenValueInputValidation from './childrenValueInputValidation';
+export createChainedFunction from './createChainedFunction';
+export CustomPropTypes from './CustomPropTypes';
+export domUtils from './domUtils';
+export ValidComponentChildren from './ValidComponentChildren';
diff --git a/test/CollapseSpec.js b/test/CollapseSpec.js
new file mode 100644
index 0000000000..0ac893fcf0
--- /dev/null
+++ b/test/CollapseSpec.js
@@ -0,0 +1,216 @@
+import React from 'react';
+import ReactTestUtils from 'react/lib/ReactTestUtils';
+import Collapse from '../src/Collapse';
+
+describe('Collapse', function () {
+
+ let Component, instance;
+
+ beforeEach(function(){
+
+ Component = React.createClass({
+ render(){
+ let { children, ...props } = this.props;
+
+ return (
+ this.collapse = r}
+ getDimensionValue={()=> 15 }
+ {...props}
+ >
+
+
+ );
+ }
+ });
+ });
+
+ it('Should default to collapsed', function () {
+ instance = ReactTestUtils.renderIntoDocument(
+ Panel content
+ );
+
+ assert.ok(
+ instance.collapse.props.in === false);
+ });
+
+
+ describe('collapsed', function(){
+
+ it('Should have collapse class', function () {
+ instance = ReactTestUtils.renderIntoDocument(
+ Panel content
+ );
+
+ assert.ok(ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'collapse'));
+ });
+ });
+
+ describe('from collapsed to expanded', function(){
+ let scrollHeightStub;
+
+ beforeEach(function(){
+ instance = ReactTestUtils.renderIntoDocument(
+ Panel content
+ );
+
+ // since scrollHeight is gonna be 0 detached from the DOM
+ scrollHeightStub = sinon.stub(instance.collapse, '_getScrollDimensionValue');
+ scrollHeightStub.returns('15px');
+ });
+
+
+ it('Should have collapsing class', function () {
+ instance.setProps({ in: true });
+
+ let node = React.findDOMNode(instance);
+
+ assert.equal(node.className, 'collapsing');
+ });
+
+ it('Should set initial 0px height', function (done) {
+ let node = React.findDOMNode(instance);
+
+ function onEnter(){
+ assert.equal(node.style.height, '0px');
+ done();
+ }
+
+ assert.equal(node.style.height, '');
+
+ instance.setProps({ in: true, onEnter });
+ });
+
+ it('Should set node to height', function () {
+ let node = React.findDOMNode(instance);
+
+ assert.equal(node.styled, undefined);
+
+ instance.setProps({ in: true });
+ assert.equal(node.style.height, '15px');
+ });
+
+ it('Should transition from collapsing to not collapsing', function (done) {
+ let node = React.findDOMNode(instance);
+
+ function onEntered(){
+ assert.equal(node.className, 'collapse in');
+ done();
+ }
+
+ instance.setProps({ in: true, onEntered });
+
+ assert.equal(node.className, 'collapsing');
+ });
+
+ it('Should clear height after transition complete', function (done) {
+ let node = React.findDOMNode(instance);
+
+ function onEntered(){
+ assert.equal(node.style.height, '');
+ done();
+ }
+
+ assert.equal(node.style.height, '');
+
+ instance.setProps({ in: true, onEntered });
+ assert.equal(node.style.height, '15px');
+ });
+ });
+
+ describe('from expanded to collapsed', function(){
+ beforeEach(function(){
+ instance = ReactTestUtils.renderIntoDocument(
+ Panel content
+ );
+ });
+
+ it('Should have collapsing class', function () {
+ instance.setProps({ in: false });
+ let node = React.findDOMNode(instance);
+ assert.equal(node.className, 'collapsing');
+ });
+
+ it('Should set initial height', function () {
+ let node = React.findDOMNode(instance);
+
+ function onExit(){
+ assert.equal(node.style.height, '15px');
+ }
+
+ assert.equal(node.style.height, '');
+ instance.setProps({ in: false, onExit });
+ });
+
+ it('Should set node to height', function () {
+ let node = React.findDOMNode(instance);
+ assert.equal(node.style.height, '');
+
+ instance.setProps({ in: false });
+ assert.equal(node.style.height, '0px');
+ });
+
+ it('Should transition from collapsing to not collapsing', function (done) {
+ let node = React.findDOMNode(instance);
+
+ function onExited(){
+ assert.equal(node.className, 'collapse');
+ done();
+ }
+
+ instance.setProps({ in: false, onExited });
+
+ assert.equal(node.className, 'collapsing');
+ });
+
+ it('Should have 0px height after transition complete', function (done) {
+ let node = React.findDOMNode(instance);
+
+ function onExited(){
+ assert.ok(node.style.height === '0px');
+ done();
+ }
+
+ assert.equal(node.style.height, '');
+
+ instance.setProps({ in: false, onExited });
+ });
+ });
+
+ describe('expanded', function(){
+
+ it('Should have collapse and in class', function () {
+ instance = ReactTestUtils.renderIntoDocument(
+ Panel content
+ );
+ assert.ok(ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'collapse in'));
+ });
+ });
+
+ describe('dimension', function(){
+ beforeEach(function(){
+ instance = ReactTestUtils.renderIntoDocument(
+ Panel content
+ );
+ });
+
+ it('Defaults to height', function(){
+ assert.equal(instance.collapse._dimension(), 'height');
+ });
+
+ it('Uses getCollapsibleDimension if exists', function(){
+
+ function dimension(){
+ return 'whatevs';
+ }
+
+ instance.setProps({ dimension });
+
+ assert.equal(instance.collapse._dimension(), 'whatevs');
+ });
+ });
+});
diff --git a/test/CollapsibleMixinSpec.js b/test/CollapsibleMixinSpec.js
index 324b32ad44..c38527d2ae 100644
--- a/test/CollapsibleMixinSpec.js
+++ b/test/CollapsibleMixinSpec.js
@@ -32,6 +32,12 @@ describe('CollapsibleMixin', function () {
});
});
+ afterEach(()=> {
+ if (console.warn.calledWithMatch('CollapsibleMixin is deprecated')){
+ console.warn.reset();
+ }
+ });
+
describe('getInitialState', function(){
it('Should check defaultExpanded', function () {
instance = ReactTestUtils.renderIntoDocument(
diff --git a/test/FactoriesSpec.js b/test/FactoriesSpec.js
index f03810c277..040d636430 100644
--- a/test/FactoriesSpec.js
+++ b/test/FactoriesSpec.js
@@ -4,7 +4,7 @@ import components from '../tools/public-components';
let props = {
ButtonInput: {value: 'button'},
Glyphicon: {glyph: 'star'},
- Modal: {onRequestHide() {}},
+ Modal: {onHide() {}},
ModalTrigger: {modal: React.DOM.div(null)},
OverlayTrigger: {overlay: React.DOM.div(null)}
};
diff --git a/test/FadeMixinSpec.js b/test/FadeMixinSpec.js
index bcd055b5c6..7e6086dd0f 100644
--- a/test/FadeMixinSpec.js
+++ b/test/FadeMixinSpec.js
@@ -19,6 +19,13 @@ describe('FadeMixin', function () {
});
});
+ afterEach(()=> {
+ if (console.warn.calledWithMatch('FadeMixin is deprecated')){
+ console.warn.reset();
+ }
+ });
+
+
it('Should add the in class to all elements', function (done) {
let instance = ReactTestUtils.renderIntoDocument( );
diff --git a/test/FadeSpec.js b/test/FadeSpec.js
new file mode 100644
index 0000000000..0f85fdfd40
--- /dev/null
+++ b/test/FadeSpec.js
@@ -0,0 +1,81 @@
+import React from 'react';
+import ReactTestUtils from 'react/lib/ReactTestUtils';
+import Fade from '../src/Fade';
+
+describe('Fade', function () {
+
+ let Component, instance;
+
+ beforeEach(function(){
+
+ Component = React.createClass({
+ render(){
+ let { children, ...props } = this.props;
+
+ return (
+ this.fade = r}
+ {...props}
+ >
+
+ {children}
+
+
+ );
+ }
+ });
+ });
+
+ it('Should default to hidden', function () {
+ instance = ReactTestUtils.renderIntoDocument(
+ Panel content
+ );
+
+ assert.ok(
+ instance.fade.props.in === false);
+ });
+
+ it('Should always have the "fade" class', () => {
+ instance = ReactTestUtils.renderIntoDocument(
+ Panel content
+ );
+
+ assert.ok(
+ instance.fade.props.in === false);
+
+ assert.equal(
+ React.findDOMNode(instance).className, 'fade');
+
+ });
+
+ it('Should add "in" class when entering', done => {
+ instance = ReactTestUtils.renderIntoDocument(
+ Panel content
+ );
+
+ function onEntering(){
+ assert.equal(React.findDOMNode(instance).className, 'fade in');
+ done();
+ }
+
+ assert.ok(
+ instance.fade.props.in === false);
+
+ instance.setProps({ in: true, onEntering });
+ });
+
+ it('Should remove "in" class when exiting', done => {
+ instance = ReactTestUtils.renderIntoDocument(
+ Panel content
+ );
+
+ function onExiting(){
+ assert.equal(React.findDOMNode(instance).className, 'fade');
+ done();
+ }
+
+ assert.equal(
+ React.findDOMNode(instance).className, 'fade in');
+
+ instance.setProps({ in: false, onExiting });
+ });
+});
diff --git a/test/FormControlsSpec.js b/test/FormControlsSpec.js
index cc56ecd618..31c9941d30 100644
--- a/test/FormControlsSpec.js
+++ b/test/FormControlsSpec.js
@@ -1,6 +1,6 @@
import React from 'react';
import ReactTestUtils from 'react/lib/ReactTestUtils';
-import FormControls from '../src/FormControls';
+import * as FormControls from '../src/FormControls';
describe('Form Controls', function () {
describe('Static', function () {
diff --git a/test/InputSpec.js b/test/InputSpec.js
index 63a601f17e..f4bf29c622 100644
--- a/test/InputSpec.js
+++ b/test/InputSpec.js
@@ -41,30 +41,6 @@ describe('Input', function () {
assert.equal(instance.getValue(), 'v');
});
- it('throws a deprecation warning on type=button', function () {
- ReactTestUtils.renderIntoDocument(
-
- );
-
- shouldWarn('deprecated');
- });
-
- it('throws a deprecation warning on type=reset', function () {
- ReactTestUtils.renderIntoDocument(
-
- );
-
- shouldWarn('deprecated');
- });
-
- it('throws a deprecation warning on type=submit', function () {
- ReactTestUtils.renderIntoDocument(
-
- );
-
- shouldWarn('deprecated');
- });
-
it('throws a warning when type=static', function () {
ReactTestUtils.renderIntoDocument(
diff --git a/test/ModalSpec.js b/test/ModalSpec.js
index 3d425c14cd..5a9b78f520 100644
--- a/test/ModalSpec.js
+++ b/test/ModalSpec.js
@@ -1,18 +1,31 @@
import React from 'react';
import ReactTestUtils from 'react/lib/ReactTestUtils';
import Modal from '../src/Modal';
-import { shouldWarn } from './helpers';
+import { render } from './helpers';
describe('Modal', function () {
+ let mountPoint;
+
+ beforeEach(()=>{
+ mountPoint = document.createElement('div');
+ document.body.appendChild(mountPoint);
+ });
+
+ afterEach(function () {
+ React.unmountComponentAtNode(mountPoint);
+ document.body.removeChild(mountPoint);
+ });
it('Should render the modal content', function() {
let noOp = function () {};
- let instance = ReactTestUtils.renderIntoDocument(
-
+ let instance = render(
+
Message
- );
- assert.ok(ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'strong'));
+ , mountPoint);
+
+ assert.ok(
+ ReactTestUtils.findRenderedDOMComponentWithTag(instance.refs.modal, 'strong'));
});
it('Should add modal-open class to the modal container while open', function(done) {
@@ -27,21 +40,30 @@ describe('Modal', function () {
render() {
return (
-
+
Message
);
}
});
- let instance = ReactTestUtils.renderIntoDocument(
-
- );
+ let instance = render(
+
+ , mountPoint);
+
+ let modal = ReactTestUtils.findRenderedComponentWithType(instance, Modal);
+
assert.ok(React.findDOMNode(instance).className.match(/\modal-open\b/));
- let backdrop = React.findDOMNode(instance).getElementsByClassName('modal-backdrop')[0];
+ let backdrop = React.findDOMNode(modal.refs.modal).getElementsByClassName('modal-backdrop')[0];
ReactTestUtils.Simulate.click(backdrop);
+
setTimeout(function(){
assert.equal(React.findDOMNode(instance).className.length, 0);
done();
@@ -51,55 +73,77 @@ describe('Modal', function () {
it('Should close the modal when the backdrop is clicked', function (done) {
let doneOp = function () { done(); };
- let instance = ReactTestUtils.renderIntoDocument(
-
+ let instance = render(
+
Message
- );
+ , mountPoint);
+
+ let backdrop = React.findDOMNode(instance.refs.modal)
+ .getElementsByClassName('modal-backdrop')[0];
- let backdrop = React.findDOMNode(instance).getElementsByClassName('modal-backdrop')[0];
ReactTestUtils.Simulate.click(backdrop);
});
it('Should close the modal when the modal background is clicked', function (done) {
let doneOp = function () { done(); };
- let instance = ReactTestUtils.renderIntoDocument(
-
+
+ let instance = render(
+
Message
- );
+ , mountPoint);
+
+ let backdrop = React.findDOMNode(instance.refs.modal)
+ .getElementsByClassName('modal')[0];
- let backdrop = React.findDOMNode(instance).getElementsByClassName('modal')[0];
ReactTestUtils.Simulate.click(backdrop);
});
+ it('Should close the modal when the modal close button is clicked', function (done) {
+ let doneOp = function () { done(); };
+
+ let instance = render(
+
+
+ Message
+
+ , mountPoint);
+
+ let button = React.findDOMNode(instance.refs.modal)
+ .getElementsByClassName('close')[0];
+
+ ReactTestUtils.Simulate.click(button);
+ });
+
it('Should pass bsSize to the dialog', function () {
let noOp = function () {};
- let instance = ReactTestUtils.renderIntoDocument(
-
+ let instance = render(
+
Message
- );
+ , mountPoint);
- let dialog = React.findDOMNode(instance).getElementsByClassName('modal-dialog')[0];
+ let dialog = React.findDOMNode(instance.refs.modal).getElementsByClassName('modal-dialog')[0];
assert.ok(dialog.className.match(/\bmodal-sm\b/));
});
it('Should pass dialogClassName to the dialog', function () {
let noOp = function () {};
- let instance = ReactTestUtils.renderIntoDocument(
-
+ let instance = render(
+
Message
- );
+ , mountPoint);
- let dialog = ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'modal-dialog');
+ let dialog = ReactTestUtils.findRenderedDOMComponentWithClass(instance.refs.modal, 'modal-dialog');
assert.match(dialog.props.className, /\btestCss\b/);
});
describe('Focused state', function () {
let focusableContainer = null;
- beforeEach(function () {
+
+ beforeEach(()=>{
focusableContainer = document.createElement('div');
focusableContainer.tabIndex = 0;
document.body.appendChild(focusableContainer);
@@ -111,142 +155,48 @@ describe('Modal', function () {
document.body.removeChild(focusableContainer);
});
- it('Should focus on the Modal when it is opened', function (done) {
- document.activeElement.should.equal(focusableContainer);
-
- let doneOp = function () {
- // focus should be back on the previous element when modal closed
- setTimeout(function () {
- document.activeElement.should.equal(focusableContainer);
- done();
- }, 0);
- };
-
- let Container = React.createClass({
- getInitialState() {
- return {modalOpen: true};
- },
- handleCloseModal() {
- this.setState({modalOpen: false});
- doneOp();
- },
- render() {
- if (this.state.modalOpen) {
- return (
-
- Message
-
- );
- } else {
- return ;
- }
- }
- });
-
- let instance = React.render( , focusableContainer);
-
- setTimeout(function () {
- // modal should be focused when opened
- let modal = React.findDOMNode(instance).getElementsByClassName('modal')[0];
- document.activeElement.should.equal(modal);
-
- // close the modal
- let backdrop = React.findDOMNode(instance).getElementsByClassName('modal-backdrop')[0];
- ReactTestUtils.Simulate.click(backdrop);
- }, 0);
- });
-
- it('Should not focus on the Modal when autoFocus is false', function (done) {
+ it('Should focus on the Modal when it is opened', function () {
document.activeElement.should.equal(focusableContainer);
- let Container = React.createClass({
- getInitialState() {
- return {modalOpen: true};
- },
- render() {
- if (this.state.modalOpen) {
- return (
-
- {}} container={this}>
- Message
-
- );
- } else {
- return ;
- }
- }
- });
-
- React.render( , focusableContainer);
-
- setTimeout(function () {
- // modal should be focused when opened
- document.activeElement.should.equal(focusableContainer);
- done();
- }, 0);
- });
+ let instance = render(
+ {}} animation={false}>
+ Message
+
+ , focusableContainer);
- it('Should not focus Modal when child has focus', function (done) {
+ document.activeElement.className.should.contain('modal');
- document.activeElement.should.equal(focusableContainer);
+ instance.renderWithProps({ show: false });
- let Container = React.createClass({
- getInitialState() {
- return {modalOpen: true};
- },
- render() {
- if (this.state.modalOpen) {
- return (
- {}} container={this}>
-
-
- );
- } else {
- return ;
- }
- }
- });
-
- let instance = React.render( , focusableContainer);
-
- setTimeout(function () {
- let input = React.findDOMNode(
- ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'input'));
-
- document.activeElement.should.equal(input);
- done();
- }, 0);
+ document.activeElement.should.equal(focusableContainer);
});
- });
- describe('deprecations', function(){
- it('Should render the modal header and title', function() {
- let instance = ReactTestUtils.renderIntoDocument(
- {}}>
+ it('Should not focus on the Modal when autoFocus is false', function () {
+ render(
+ {}} animation={false}>
Message
- );
-
- (()=> {
- ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'button');
- ReactTestUtils.findRenderedComponentWithType(instance, Modal.Header);
- ReactTestUtils.findRenderedComponentWithType(instance, Modal.Title);
- }).should.not.throw();
+ , focusableContainer);
- shouldWarn(
- 'Specifying `closeButton` or `title` Modal props is deprecated');
+ document.activeElement.should.equal(focusableContainer);
});
- it('Should warn about onRequestHide', function() {
- ReactTestUtils.renderIntoDocument(
- {}}>
-
+ it('Should not focus Modal when child has focus', function () {
+
+ document.activeElement.should.equal(focusableContainer);
+
+ render(
+ {}} animation={false}>
+
- );
+ , focusableContainer);
- shouldWarn('The Modal prop `onRequestHide` is deprecated');
+ let input = document.getElementsByTagName('input')[0];
+
+ document.activeElement.should.equal(input);
});
});
+
});
diff --git a/test/ModalTriggerSpec.js b/test/ModalTriggerSpec.js
deleted file mode 100644
index 9485f402b8..0000000000
--- a/test/ModalTriggerSpec.js
+++ /dev/null
@@ -1,123 +0,0 @@
-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}>
- button
-
- );
-
- shouldWarn('The `ModalTrigger` component is deprecated');
- });
-
- it('Should create ModalTrigger element', function() {
- const instance = ReactTestUtils.renderIntoDocument(
- test}>
- button
-
- );
- const modalTrigger = React.findDOMNode(instance);
- assert.equal(modalTrigger.nodeName, 'BUTTON');
- });
-
- it('Should pass ModalTrigger onMouseOver prop to child', function() {
- const callback = sinon.spy();
- const instance = ReactTestUtils.renderIntoDocument(
- test} onMouseOver={callback}>
- button
-
- );
- const modalTrigger = React.findDOMNode(instance);
- ReactTestUtils.Simulate.mouseOver(modalTrigger);
- callback.called.should.be.true;
- });
-
- it('Should pass ModalTrigger onMouseOut prop to child', function() {
- const callback = sinon.spy();
- const instance = ReactTestUtils.renderIntoDocument(
- test} onMouseOut={callback}>
- button
-
- );
- const modalTrigger = React.findDOMNode(instance);
- ReactTestUtils.Simulate.mouseOut(modalTrigger);
- callback.called.should.be.true;
- });
-
- it('Should pass ModalTrigger onFocus prop to child', function() {
- const callback = sinon.spy();
- const instance = ReactTestUtils.renderIntoDocument(
- test} onFocus={callback}>
- button
-
- );
- const modalTrigger = React.findDOMNode(instance);
- ReactTestUtils.Simulate.focus(modalTrigger);
- callback.called.should.be.true;
- });
-
- it('Should pass ModalTrigger onBlur prop to child', function() {
- const callback = sinon.spy();
- const instance = ReactTestUtils.renderIntoDocument(
- test} onBlur={callback}>
- button
-
- );
- const modalTrigger = React.findDOMNode(instance);
- ReactTestUtils.Simulate.blur(modalTrigger);
- callback.called.should.be.true;
- });
-
- // This is just a copy of the test case for OverlayTrigger.
- it('Should forward requested context', function() {
- const contextTypes = {
- key: React.PropTypes.string
- };
-
- const contextSpy = sinon.spy();
- class ContextReader extends React.Component {
- render() {
- contextSpy(this.context.key);
- return
;
- }
- }
- ContextReader.contextTypes = contextTypes;
-
- const TriggerWithContext = ModalTrigger.withContext(contextTypes);
- class ContextHolder extends React.Component {
- getChildContext() {
- return {key: 'value'};
- }
-
- render() {
- return (
- }
- >
- button
-
- );
- }
- }
- ContextHolder.childContextTypes = contextTypes;
-
- const instance = ReactTestUtils.renderIntoDocument( );
- const modalTrigger = React.findDOMNode(instance);
- ReactTestUtils.Simulate.click(modalTrigger);
-
- contextSpy.calledWith('value').should.be.true;
- });
-});
diff --git a/test/NavSpec.js b/test/NavSpec.js
index 947666e573..a86f47e0dc 100644
--- a/test/NavSpec.js
+++ b/test/NavSpec.js
@@ -83,9 +83,9 @@ describe('Nav', function () {
);
- let items = ReactTestUtils.scryRenderedComponentsWithType(instance, NavItem);
+ let items = ReactTestUtils.scryRenderedDOMComponentsWithTag(instance, 'A');
- ReactTestUtils.Simulate.click(items[1].refs.anchor);
+ ReactTestUtils.Simulate.click(items[1]);
});
it('Should set the correct item active by href', function () {
diff --git a/test/OverlayMixinSpec.js b/test/OverlayMixinSpec.js
deleted file mode 100644
index 66818d48f5..0000000000
--- a/test/OverlayMixinSpec.js
+++ /dev/null
@@ -1,89 +0,0 @@
-import React from 'react';
-import ReactTestUtils from 'react/lib/ReactTestUtils';
-import OverlayMixin from '../src/OverlayMixin';
-import { shouldWarn } from './helpers';
-
-describe('OverlayMixin', function () {
- let instance;
-
- let Overlay = React.createClass({
- mixins: [OverlayMixin],
-
- render() {
- return
;
- },
-
- renderOverlay() {
- return this.props.overlay;
- }
- });
-
-
- 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() {
- 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({
- mixins: [OverlayMixin],
-
- render() {
- return null;
- },
-
- renderOverlay() {
- return this.props.overlay;
- }
- });
-
- let overlayInstance = ReactTestUtils.renderIntoDocument(
- } />
- );
-
- assert.equal(overlayInstance.getOverlayDOMNode().nodeName, 'DIV');
- });
-});
diff --git a/test/OverlayTriggerSpec.js b/test/OverlayTriggerSpec.js
index 1d871151d8..768ad20c7c 100644
--- a/test/OverlayTriggerSpec.js
+++ b/test/OverlayTriggerSpec.js
@@ -139,13 +139,66 @@ describe('OverlayTrigger', function() {
});
it('Should have correct isOverlayShown state', function () {
- const event = document.createEvent('HTMLEvents');
- event.initEvent('click', true, true);
- document.documentElement.dispatchEvent(event);
+ document.documentElement.click();
+ // Need to click this way for it to propagate to document element.
instance.state.isOverlayShown.should.equal(testCase.shownAfterClick);
});
});
});
+
+ describe('replaced overlay', function () {
+ let instance;
+
+ beforeEach(function () {
+ class ReplacedOverlay extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleClick = this.handleClick.bind(this);
+ this.state = {replaced: false};
+ }
+
+ handleClick() {
+ this.setState({replaced: true});
+ }
+
+ render() {
+ if (this.state.replaced) {
+ return (
+ replaced
+ );
+ } else {
+ return (
+
+ );
+ }
+ }
+ }
+
+ instance = ReactTestUtils.renderIntoDocument(
+ }
+ trigger='click' rootClose={true}
+ >
+ button
+
+ );
+ const overlayTrigger = React.findDOMNode(instance);
+ ReactTestUtils.Simulate.click(overlayTrigger);
+ });
+
+ it('Should still be shown', function () {
+ // Need to click this way for it to propagate to document element.
+ const replaceOverlay = document.getElementById('replace-overlay');
+ replaceOverlay.click();
+
+ instance.state.isOverlayShown.should.be.true;
+ });
+ });
});
});
diff --git a/test/PageItemSpec.js b/test/PageItemSpec.js
index f88efcc4df..56f3d5e879 100644
--- a/test/PageItemSpec.js
+++ b/test/PageItemSpec.js
@@ -38,7 +38,7 @@ describe('PageItem', function () {
it('Should call "onSelect" when item is clicked', function (done) {
function handleSelect(key, href) {
assert.equal(key, 1);
- assert.equal(href, '#');
+ assert.equal(href, undefined);
done();
}
let instance = ReactTestUtils.renderIntoDocument(
diff --git a/test/PopoverSpec.js b/test/PopoverSpec.js
index 45b6f00757..cca9014442 100644
--- a/test/PopoverSpec.js
+++ b/test/PopoverSpec.js
@@ -11,16 +11,8 @@ describe('Popover', function () {
);
assert.ok(ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'popover-title'));
assert.ok(ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'popover-content'));
- assert.ok(ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'fade'));
+
assert.ok(ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'strong'));
});
- it('Should not have the fade class if animation is false', function () {
- let instance = ReactTestUtils.renderIntoDocument(
-
- Popover Content
-
- );
- assert.equal(React.findDOMNode(instance).className.match(/\bfade\b/), null, 'The fade class should not be present');
- });
});
diff --git a/test/SafeAnchorSpec.js b/test/SafeAnchorSpec.js
new file mode 100644
index 0000000000..2271c2fbbe
--- /dev/null
+++ b/test/SafeAnchorSpec.js
@@ -0,0 +1,101 @@
+import React from 'react';
+import ReactTestUtils from 'react/lib/ReactTestUtils';
+import SafeAnchor from '../src/SafeAnchor';
+
+describe('SafeAnchor', function() {
+ it('renders an anchor tag', function() {
+ const instance = ReactTestUtils.renderIntoDocument( );
+ const node = React.findDOMNode(instance);
+
+ node.tagName.should.equal('A');
+ });
+
+ it('forwards arbitrary props to the anchor', function() {
+ const instance = ReactTestUtils.renderIntoDocument( );
+ const anchor = ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'A');
+
+ anchor.props.herpa.should.equal('derpa');
+ });
+
+ it('forwards provided href', function() {
+ const instance = ReactTestUtils.renderIntoDocument( );
+ const anchor = ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'A');
+
+ anchor.props.href.should.equal('http://google.com');
+ });
+
+ it('ensures that an href is provided', function() {
+ const instance = ReactTestUtils.renderIntoDocument( );
+ const anchor = ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'A');
+
+ anchor.props.href.should.equal('');
+ });
+
+ it('forwards onClick handler', function(done) {
+ const handleClick = (event) => {
+ done();
+ };
+ const instance = ReactTestUtils.renderIntoDocument( );
+ const anchor = ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'A');
+
+ ReactTestUtils.Simulate.click(anchor);
+ });
+
+ it('prevents default when no href is provided', function(done) {
+ const handleClick = (event) => {
+ expect(event.isDefaultPrevented()).to.not.be.ok;
+
+ setTimeout(() => {
+ event.isDefaultPrevented().should.be.true;
+ done();
+ }, 100);
+ };
+ const instance = ReactTestUtils.renderIntoDocument( );
+ const anchor = ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'A');
+
+ ReactTestUtils.Simulate.click(anchor);
+ });
+
+ it('does not prevent default when href is provided', function(done) {
+ const handleClick = (event) => {
+ expect(event.isDefaultPrevented()).to.not.be.ok;
+
+ setTimeout(() => {
+ expect(event.isDefaultPrevented()).to.not.be.ok;
+ done();
+ });
+ };
+ const instance = ReactTestUtils.renderIntoDocument( );
+ const anchor = ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'A');
+
+ ReactTestUtils.Simulate.click(anchor);
+ });
+
+ it('forwards provided role', function () {
+ const instance = ReactTestUtils.renderIntoDocument( );
+ const anchor = ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'A');
+
+ anchor.props.role.should.equal('test');
+ });
+
+ it('forwards provided role with href', function () {
+ const instance = ReactTestUtils.renderIntoDocument( );
+ const anchor = ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'A');
+
+ anchor.props.role.should.equal('test');
+ });
+
+ it('set role=button with no provided href', function () {
+ const instance = ReactTestUtils.renderIntoDocument( );
+ const anchor = ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'A');
+
+ anchor.props.role.should.equal('button');
+ });
+
+ it('sets no role with provided href', function () {
+ const instance = ReactTestUtils.renderIntoDocument( );
+ const anchor = ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'A');
+
+ expect(anchor.props.role).to.be.undefined;
+ });
+});
diff --git a/test/TooltipSpec.js b/test/TooltipSpec.js
index a07bb06073..01b93bc42c 100644
--- a/test/TooltipSpec.js
+++ b/test/TooltipSpec.js
@@ -10,15 +10,7 @@ describe('Tooltip', function () {
);
assert.ok(ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'strong'));
- assert.ok(ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'fade'));
- });
- it('Should not have the fade class if animation is false', function () {
- let instance = ReactTestUtils.renderIntoDocument(
-
- Tooltip Content
-
- );
- assert.equal(React.findDOMNode(instance).className.match(/\bfade\b/), null, 'The fade class should not be present');
});
+
});
diff --git a/test/TransitionSpec.js b/test/TransitionSpec.js
new file mode 100644
index 0000000000..7945ba8ceb
--- /dev/null
+++ b/test/TransitionSpec.js
@@ -0,0 +1,231 @@
+import React from 'react';
+import ReactTestUtils from 'react/lib/ReactTestUtils';
+import { render } from './helpers';
+import Transition from '../src/Transition';
+//import classNames from 'classnames';
+
+describe('Transition', function () {
+
+
+ it('should not transition on mount', function(){
+ let instance = render(
+ { throw new Error('should not Enter'); }}>
+
+
+ );
+
+ instance.state.in.should.equal(true);
+ assert.ok(!instance.state.transitioning);
+ });
+
+ it('should transition on mount with transitionAppear', done =>{
+ let instance = ReactTestUtils.renderIntoDocument(
+ done()}
+ >
+
+
+ );
+
+ instance.state.in.should.equal(true);
+ instance.state.transitioning.should.equal(true);
+ });
+
+ describe('entering', ()=> {
+ let instance;
+
+ beforeEach(function(){
+ instance = render(
+
+
+
+ );
+ });
+
+ it('should fire callbacks', done => {
+ let onEnter = sinon.spy();
+ let onEntering = sinon.spy();
+
+ instance.state.in.should.equal(false);
+
+ instance = instance.renderWithProps({
+
+ in: true,
+
+ onEnter,
+
+ onEntering,
+
+ onEntered(){
+ assert.ok(onEnter.calledOnce);
+ assert.ok(onEntering.calledOnce);
+ assert.ok(onEnter.calledBefore(onEntering));
+ done();
+ }
+ });
+ });
+
+ it('should move to each transition state', done => {
+ let count = 0;
+
+ instance.state.in.should.equal(false);
+
+ instance = instance.renderWithProps({
+
+ in: true,
+
+ onEnter(){
+ count++;
+ instance.state.in.should.equal(false);
+ instance.state.transitioning.should.equal(false);
+ },
+
+ onEntering(){
+ count++;
+ instance.state.in.should.equal(true);
+ instance.state.transitioning.should.equal(true);
+ },
+
+ onEntered(){
+ instance.state.in.should.equal(true);
+ instance.state.transitioning.should.equal(false);
+ assert.ok(count === 2);
+ done();
+ }
+ });
+ });
+
+ it('should apply classes at each transition state', done => {
+ let count = 0;
+
+ instance.state.in.should.equal(false);
+
+ instance = instance.renderWithProps({
+
+ in: true,
+
+ onEnter(node){
+ count++;
+ assert.equal(node.className, '');
+ },
+
+ onEntering(node){
+ count++;
+ assert.equal(node.className, 'test-entering');
+ },
+
+ onEntered(node){
+ assert.equal(node.className, 'test-enter');
+ assert.ok(count === 2);
+ done();
+ }
+ });
+ });
+
+ });
+
+
+ describe('exiting', ()=> {
+ let instance;
+
+ beforeEach(function(){
+ instance = render(
+
+
+
+ );
+ });
+
+ it('should fire callbacks', done => {
+ let onExit = sinon.spy();
+ let onExiting = sinon.spy();
+
+ instance.state.in.should.equal(true);
+
+ instance = instance.renderWithProps({
+
+ in: false,
+
+ onExit,
+
+ onExiting,
+
+ onExited(){
+ assert.ok(onExit.calledOnce);
+ assert.ok(onExiting.calledOnce);
+ assert.ok(onExit.calledBefore(onExiting));
+ done();
+ }
+ });
+ });
+
+ it('should move to each transition state', done => {
+ let count = 0;
+
+ instance.state.in.should.equal(true);
+
+ instance = instance.renderWithProps({
+
+ in: false,
+
+ onExit(){
+ count++;
+ instance.state.in.should.equal(true);
+ instance.state.transitioning.should.equal(false);
+ },
+
+ onExiting(){
+ count++;
+ instance.state.in.should.equal(false);
+ instance.state.transitioning.should.equal(true);
+ },
+
+ onExited(){
+ instance.state.in.should.equal(false);
+ instance.state.transitioning.should.equal(false);
+ //assert.ok(count === 2);
+ done();
+ }
+ });
+ });
+
+ it('should apply classes at each transition state', done => {
+ let count = 0;
+
+ instance.state.in.should.equal(true);
+
+ instance = instance.renderWithProps({
+
+ in: false,
+
+ onExit(node){
+ count++;
+ assert.equal(node.className, '');
+ },
+
+ onExiting(node){
+ count++;
+ assert.equal(node.className, 'test-exiting');
+ },
+
+ onExited(node){
+ assert.equal(node.className, 'test-exit');
+ assert.ok(count === 2);
+ done();
+ }
+ });
+ });
+
+ });
+
+});
diff --git a/test/helpers.js b/test/helpers.js
index 74960b4db4..9df659ecae 100644
--- a/test/helpers.js
+++ b/test/helpers.js
@@ -1,5 +1,30 @@
+import React from 'react';
+import { cloneElement } from 'react';
+
export function shouldWarn(about) {
console.warn.called.should.be.true;
console.warn.calledWithMatch(about).should.be.true;
console.warn.reset();
}
+
+/**
+ * Helper for rendering and updating props for plain class Components
+ * since `setProps` is deprecated.
+ * @param {ReactElement} element Root element to render
+ * @param {HTMLElement?} mountPoint Optional mount node, when empty it uses an unattached div like `renderIntoDocument()`
+ * @return {ComponentInstance} The instance, with a new method `renderWithProps` which will return a new instance with updated props
+ */
+export function render(element, mountPoint){
+ let mount = mountPoint || document.createElement('div');
+ let instance = React.render(element, mount);
+
+ if (!instance.renderWithProps) {
+ instance.renderWithProps = function(newProps) {
+
+ return render(
+ cloneElement(element, newProps), mount);
+ };
+ }
+
+ return instance;
+}
diff --git a/test/server/ModalSpec.js b/test/server/ModalSpec.js
index e78832c956..fae565c4c8 100644
--- a/test/server/ModalSpec.js
+++ b/test/server/ModalSpec.js
@@ -8,7 +8,7 @@ describe('Modal', () => {
assert.doesNotThrow(function renderOnServerSide() {
return React.renderToString(
-
+
Message
);
diff --git a/tools/amd/bower.json b/tools/amd/bower.json
index d1bf07a5bb..5d22b3626e 100644
--- a/tools/amd/bower.json
+++ b/tools/amd/bower.json
@@ -14,7 +14,6 @@
"**/.*"
],
"dependencies": {
- "classnames": "<%= pkg.dependencies.classnames %>",
"react": "<%= pkg.peerDependencies.react %>"
}
}
diff --git a/tools/amd/build.js b/tools/amd/build.js
index a610f3cdb2..3f67d2c57f 100644
--- a/tools/amd/build.js
+++ b/tools/amd/build.js
@@ -3,9 +3,7 @@ import path from 'path';
import fsp from 'fs-promise';
import { copy } from '../fs-utils';
import { exec } from '../exec';
-import generateFactories from '../generateFactories';
-import { repoRoot, srcRoot, bowerRoot } from '../constants';
-import { buildFolder } from '../buildBabel';
+import { repoRoot, bowerRoot } from '../constants';
const packagePath = path.join(repoRoot, 'package.json');
const bowerTemplate = path.join(__dirname, 'bower.json');
@@ -14,11 +12,6 @@ const bowerJson = path.join(bowerRoot, 'bower.json');
const readme = path.join(__dirname, 'README.md');
const license = path.join(repoRoot, 'LICENSE');
-const babelOptions = {
- __reactBootstrapDeprecationWarning: true,
- modules: 'amd'
-};
-
const libDestination = path.join(bowerRoot, 'lib');
function bowerConfig() {
@@ -39,8 +32,6 @@ export default function BuildBower() {
.then(() => fsp.mkdirs(libDestination))
.then(() => Promise.all([
bowerConfig(),
- generateFactories(libDestination, babelOptions),
- buildFolder(srcRoot, libDestination, babelOptions),
copy(readme, bowerRoot),
copy(license, bowerRoot)
]))
diff --git a/tools/build-cli.js b/tools/build-cli.js
index 64d764da2c..d3b69fed77 100644
--- a/tools/build-cli.js
+++ b/tools/build-cli.js
@@ -9,6 +9,7 @@ import { setExecOptions } from './exec';
import yargs from 'yargs';
const argv = yargs
+ .help('h')
.option('docs-only', {
demand: false,
default: false
@@ -35,11 +36,11 @@ setExecOptions(argv);
let buildProcess;
if (argv.libOnly) {
- buildProcess = lib();
+ buildProcess = lib(argv);
} else if (argv.docsOnly) {
buildProcess = docs(argv);
} else {
- buildProcess = build();
+ buildProcess = build(argv);
}
buildProcess
diff --git a/tools/build.js b/tools/build.js
index d0e79ae53e..401b123a75 100644
--- a/tools/build.js
+++ b/tools/build.js
@@ -6,21 +6,21 @@ import { copy } from './fs-utils';
import { distRoot, bowerRoot } from './constants';
import { exec } from './exec';
-function forkAndBuildDocs(verbose) {
+function forkAndBuildDocs({verbose}) {
console.log('Building: '.cyan + 'docs'.green);
- let options = verbose ? ' -- --verbose' : '';
+ const verboseOption = verbose ? '--verbose' : '';
- return exec(`npm run docs-build${options}`)
+ return exec(`npm run docs-build -- ${verboseOption}`)
.then(() => console.log('Built: '.cyan + 'docs'.green));
}
-export default function Build(verbose) {
+export default function Build(options) {
return Promise.all([
lib(),
bower(),
dist(),
- forkAndBuildDocs(verbose)
+ forkAndBuildDocs(options)
])
.then(() => copy(distRoot, bowerRoot));
}
diff --git a/tools/buildBabel.js b/tools/buildBabel.js
index 87a79f8b85..aae1d57738 100644
--- a/tools/buildBabel.js
+++ b/tools/buildBabel.js
@@ -11,13 +11,7 @@ export function buildContent(content, filename, destination, babelOptions={}) {
}
export function buildFile(filename, destination, babelOptions={}) {
- let content = fs.readFileSync(filename, {encoding: 'utf8'});
- if (babelOptions.__reactBootstrapDeprecationWarning) {
- content = `console.warn('This file is deprecated, and will be removed in v0.24.0. Use react-bootstrap.js or react-bootstrap.min.js instead.');
-console.warn('You can read more about it at https://github.com/react-bootstrap/react-bootstrap/issues/693');
-${content}`;
- }
-
+ const content = fs.readFileSync(filename, {encoding: 'utf8'});
// We only have .js files that we need to build
if(path.extname(filename) === '.js') {
const outputPath = path.join(destination, path.basename(filename));
diff --git a/tools/dist/build.js b/tools/dist/build.js
index 27047db903..624327e7a8 100644
--- a/tools/dist/build.js
+++ b/tools/dist/build.js
@@ -6,8 +6,8 @@ export default function BuildDistributable() {
return exec(`rimraf ${distRoot}`)
.then(() => Promise.all([
- exec('webpack --bail'),
- exec('webpack --bail -p')
+ exec(`webpack --bail`),
+ exec(`webpack --bail -p`)
]))
.then(() => console.log('Built: '.cyan + 'distributable'.green));
}
diff --git a/tools/public-components.js b/tools/public-components.js
index a030df7253..67fc67f0c7 100644
--- a/tools/public-components.js
+++ b/tools/public-components.js
@@ -1,5 +1,5 @@
import React from 'react';
-import index from '../src/index';
+import * as index from '../src/index';
let components = [];
Object.keys(index).forEach(function (item) {
diff --git a/tools/release-scripts/changelog.js b/tools/release-scripts/changelog.js
index a97b72b786..c31f53841a 100644
--- a/tools/release-scripts/changelog.js
+++ b/tools/release-scripts/changelog.js
@@ -29,11 +29,9 @@ export default (version) => {
.then(() => exec(`node_modules/.bin/changelog --title v${version} --out ${output}${additionalArgs}`))
.then(() => safeExec(`git add ${changelog}`))
.then(() => {
- if (!removedAlphaChangelog) {
- return null;
+ if (removedAlphaChangelog || isPrerelease) {
+ return safeExec(`git add -A ${alphaChangelog}`);
}
-
- return safeExec(`git add -A ${alphaChangelog}`);
})
.then(() => console.log('Generated Changelog'.cyan));
};
diff --git a/tools/release-scripts/release.js b/tools/release-scripts/release.js
index 5273a4810d..1216299696 100644
--- a/tools/release-scripts/release.js
+++ b/tools/release-scripts/release.js
@@ -16,6 +16,7 @@ import { bowerRepo, bowerRoot, tmpBowerRepo, docsRoot, docsRepo, tmpDocsRepo } f
const yargsConf = yargs
.usage('Usage: $0 [--preid ]\nor\nUsage: $0 --docs')
+ .help('h')
.example('$0 --docs', 'Release only docs')
.example('$0 minor --preid beta', 'Release with minor version bump with pre-release tag')
.example('$0 major', 'Release with major version bump')
@@ -38,7 +39,7 @@ const yargsConf = yargs
alias: 'n',
demand: false,
default: false,
- describe: 'Execute command in dry run mode. Will not commit, tag, push, or publish anything. Userful for testing.'
+ describe: 'Execute command in dry run mode. Will not commit, tag, push, or publish anything. Useful for testing.'
})
.option('verbose', {
demand: false,
diff --git a/webpack/base.config.js b/webpack/base.config.js
index bbcefeaa53..102dde14a7 100644
--- a/webpack/base.config.js
+++ b/webpack/base.config.js
@@ -1,20 +1,6 @@
-import fs from 'fs';
-import path from 'path';
import webpack from 'webpack';
import yargs from 'yargs';
-const babelCache = path.resolve(path.join(__dirname, '../.babel-cache'));
-
-if (!fs.existsSync(babelCache)) {
- try {
- fs.mkdirSync(babelCache);
- } catch (err) {
- if (err.code !== 'EEXIST') {
- console.error(err.stack);
- }
- }
-}
-
export const options = yargs
.alias('p', 'optimize-minimize')
.alias('d', 'debug')
@@ -24,7 +10,7 @@ export const options = yargs
})
.argv;
-export const jsLoader = `babel?cacheDirectory=${babelCache}`;
+export const jsLoader = 'babel?cacheDirectory';
const baseConfig = {
entry: undefined,