diff --git a/.eslintrc b/.eslintrc
index 06e1f98f2a..d52de5d000 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -12,10 +12,12 @@
"babel"
],
"rules": {
+ "constructor-super": 2,
"comma-spacing": 2,
"comma-style": [2, "last"],
"one-var": [2, { "initialized": "never" }],
"key-spacing": 0,
+ "no-this-before-super": 2,
"no-underscore-dangle": 0,
"no-unused-vars": [2, { "vars": "all", "args": "none" }],
"no-var": 2,
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5e91ee2f30..59cae1dbfa 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,32 @@
+v0.23.7 - Wed, 01 Jul 2015 15:18:30 GMT
+---------------------------------------
+
+- [35ea201](../../commit/35ea201) [fixed] Accidental breaking change in Modal trigger
+
+
+
+v0.23.6 - Wed, 01 Jul 2015 00:48:02 GMT
+---------------------------------------
+
+- [1b1af04](../../commit/1b1af04) [changed] deprecate ModalTrigger
+- [83b4cbc](../../commit/83b4cbc) [changed] Modal doesn't require ModalTrigger
+- [d70f617](../../commit/d70f617) [changed] tooltips and popovers required id's for a11y
+- [389cf3f](../../commit/389cf3f) [changed] Deprecate OverlayTrigger positioning api and "manual" trigger
+- [5eb8666](../../commit/5eb8666) [added] Overlay component
+- [1638f69](../../commit/1638f69) [added] Position component for custom Overlays
+- [f799110](../../commit/f799110) [added] Portal component; replaces OverlayMixin
+- [97ef415](../../commit/97ef415) [fixed] Modal won't steal focus from children
+- [a8b177a](../../commit/a8b177a) [fixed] Stack overflow with nested Modals
+- [3caa866](../../commit/3caa866) [changed] Update babel-loader
+- [6ffa325](../../commit/6ffa325) [fixed] 'componentClass' property type is 'elementType' now
+- [0e5980f](../../commit/0e5980f) [added] 'elementType' custom prop type validator
+- [8f582d2](../../commit/8f582d2) [changed] Update karma-chrome-launcher. Dev dependency
+- [d4089d0](../../commit/d4089d0) [changed] Update eslint-plugin-mocha. Dev dependency
+- [fd547f4](../../commit/fd547f4) [changed] Update karma-mocha. Dev dependency.
+- [c5797e8](../../commit/c5797e8) [added] componentClass prop to Jumbotron
+
+
+
v0.23.5 - Tue, 23 Jun 2015 01:31:35 GMT
---------------------------------------
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 0f939d1f8d..dad46214b5 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -17,9 +17,10 @@ and submitting pull requests, but please respect the following restrictions:
- Please do not use the issue tracker for personal support requests. Stack
Overflow ([react-bootstrap](http://stackoverflow.com/questions/tagged/react-bootstrap)
- tag), [Slack](http://www.reactiflux.com/) or
- [gitter](https://gitter.im/react-bootstrap/react-bootstrap) are better places
- to get help.
+ tag), [Slack](http://www.reactiflux.com/),
+ [gitter](https://gitter.im/react-bootstrap/react-bootstrap), or
+ [Thinkful](http://start.thinkful.com/react/?utm_source=github&utm_medium=badge&utm_campaign=react-bootstrap)
+ are better places to get help.
- Please do not open issues or pull requests regarding the code in React or
Bootstrap (open them in their respective repositories).
@@ -68,11 +69,52 @@ doesn't make sense to do this, then it doesn't make sense to use `[changed]` or
`[removed]` :). For further reading on writing a well formed commit message,
check out these [5 useful tips for a better commit message][commit-message]
+### Using `[changed]` with development dependencies updates
+
+Use `[changed]` if dev-dependency has impact on the resulting code or API.
+`babel` is a good example of such dev-dependency.
+`chai`, `colors`, `express` or `eslint` are good examples when there is
+no need to add `[changed]`.
+
+## Visual Changes
+
+When making a visual change, if at all feasible please provide screenshots
+and/or screencasts of the proposed change. This will help us to understand the
+desired change easier.
+
## Docs
Please update the docs with any API changes, the code and docs should always be
in sync.
+Component prop documentation is generated automatically from the React components
+and their leading comments. Please make sure to provide comments for any `propTypes` you add
+or change in a Component.
+
+```js
+propTypes: {
+ /**
+ * Sets the visibility of the Component
+ */
+ show: React.PropTypes.bool,
+
+ /**
+ * A callback fired when the visibility changes
+ * @type {func}
+ * @required
+ */
+ onHide: myCustomPropType
+}
+```
+
+There are a few caveats to this format that differ from conventional JSDoc comments.
+
+- Only specific doclets (the @ things) should be used, and only when the data cannot be parsed from the component itself
+ - `@type`: Override the "type", use the same names as the default React PropTypes: string, func, bool, number, object. You can express enum and oneOfType types, Like `{("optionA"|"optionB")}`.
+ - `@required`: to mark a prop as required (use the normal React isRequired if possible)
+ - `@private`: Will hide the prop in the documentation
+- All description text should be above the doclets.
+
## Implement additional components and features
This project is seeking parity with the core Bootstrap library.
diff --git a/README.md b/README.md
index 3b8833288e..3364b9ece7 100644
--- a/README.md
+++ b/README.md
@@ -7,6 +7,7 @@
[![HuBoard][huboard-badge]][huboard]
[![Gitter][gitter-badge]][gitter]
+[![Thinkful][thinkful-badge]][thinkful]
[![NPM version][npm-badge]][npm]
[![Bower version][bower-badge]][bower]
@@ -75,5 +76,8 @@ Yes please! See the [contributing guidelines][contributing] for details.
[huboard-badge]: https://img.shields.io/badge/Hu-Board-7965cc.svg
[huboard]: https://huboard.com/react-bootstrap/react-bootstrap
+[thinkful-badge]: https://tf-assets-staging.s3.amazonaws.com/badges/thinkful_repo_badge.svg
+[thinkful]: http://start.thinkful.com/react/?utm_source=github&utm_medium=badge&utm_campaign=react-bootstrap
+
[appveyor-badge]: https://ci.appveyor.com/api/projects/status/ylitpyo6n5yq1s6i/branch/master?svg=true
[appveyor]: https://ci.appveyor.com/project/react-bootstrap/react-bootstrap/branch/master
diff --git a/appveyor.yml b/appveyor.yml
index cac1fa5920..430ba4a1af 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -4,7 +4,7 @@ version: '{build}'
install:
# Get the latest stable version of io.js
- ps: Install-Product node ''
- - npm -g install npm@latest
+ - npm -g install npm@2.11.x
- set PATH=%APPDATA%\npm;%PATH%
- node --version
- npm --version
diff --git a/docs/assets/style.css b/docs/assets/style.css
index 645481e0bf..d03afccb44 100644
--- a/docs/assets/style.css
+++ b/docs/assets/style.css
@@ -162,3 +162,13 @@ body {
position: absolute;
}
+.prop-table {
+ background-color: white;
+}
+
+.bs-example.tooltip-static .tooltip {
+ position: relative;
+ display: inline-block;
+ margin: 5px 10px;
+
+}
diff --git a/docs/build.js b/docs/build.js
index 1ad8005952..98a01c94e1 100644
--- a/docs/build.js
+++ b/docs/build.js
@@ -6,6 +6,7 @@ import Root from './src/Root';
import fsp from 'fs-promise';
import { copy } from '../tools/fs-utils';
import { exec } from '../tools/exec';
+import metadata from './generate-metadata';
const repoRoot = path.resolve(__dirname, '../');
const docsBuilt = path.join(repoRoot, 'docs-built');
@@ -21,12 +22,12 @@ const readmeDest = path.join(docsBuilt, 'README.md');
* @return {Promise} promise
* @internal
*/
-function generateHTML(fileName) {
+function generateHTML(fileName, propData) {
return new Promise((resolve, reject) => {
const urlSlug = fileName === 'index.html' ? '/' : `/${fileName}`;
Router.run(routes, urlSlug, Handler => {
- let html = React.renderToString(React.createElement(Handler));
+ let html = React.renderToString(React.createElement(Handler, { propData }));
html = '' + html;
let write = fsp.writeFile(path.join(docsBuilt, fileName), html);
resolve(write);
@@ -41,8 +42,10 @@ export default function BuildDocs({dev}) {
return exec(`rimraf ${docsBuilt}`)
.then(() => fsp.mkdir(docsBuilt))
- .then(() => {
- let pagesGenerators = Root.getPages().map(generateHTML);
+ .then(metadata)
+ .then(propData => {
+
+ let pagesGenerators = Root.getPages().map( page => generateHTML(page, propData));
return Promise.all(pagesGenerators.concat([
exec(`webpack --config webpack.docs.js --bail ${devOption}`),
diff --git a/docs/examples/.eslintrc b/docs/examples/.eslintrc
index 6f2fa1bdf5..9dbc49c623 100644
--- a/docs/examples/.eslintrc
+++ b/docs/examples/.eslintrc
@@ -36,6 +36,7 @@
"ModalTrigger",
"OverlayTrigger",
"OverlayMixin",
+ "Overlay",
"PageHeader",
"PageItem",
"Pager",
diff --git a/docs/examples/ModalContained.js b/docs/examples/ModalContained.js
index 626f17db42..aeab199ef7 100644
--- a/docs/examples/ModalContained.js
+++ b/docs/examples/ModalContained.js
@@ -9,28 +9,40 @@
* }
*/
-const ContainedModal = React.createClass({
- render() {
- return (
-
-
- Elit est explicabo ipsum eaque dolorem blanditiis doloribus sed id ipsam, beatae, rem fuga id earum? Inventore et facilis obcaecati.
-
Ipsum molestiae natus adipisci modi eligendi? Debitis amet quae unde commodi aspernatur enim, consectetur. Cumque deleniti temporibus ipsam atque a dolores quisquam quisquam adipisci possimus laboriosam. Quibusdam facilis doloribus debitis! Sit quasi quod accusamus eos quod. Ab quos consequuntur eaque quo rem!
Mollitia reiciendis porro quo magni incidunt dolore amet atque facilis ipsum deleniti rem! Dolores debitis voluptatibus ipsum dicta. Dolor quod amet ab sint esse distinctio tenetur. Veritatis laudantium quibusdam quidem corporis architecto veritatis. Ex facilis minima beatae sunt perspiciatis placeat. Quasi corporis
@@ -19,10 +22,10 @@ const MyModal = React.createClass({
magni delectus maxime. Sit odit provident vel magnam quod. Possimus eligendi non corrupti tenetur culpa accusantium quod quis. Voluptatum quaerat animi dolore maiores molestias voluptate? Necessitatibus illo omnis laborum hic enim minima! Similique. Dolor voluptatum reprehenderit nihil adipisci aperiam voluptatem soluta
magnam accusamus iste incidunt tempore consequatur illo illo odit. Asperiores nesciunt iusto nemo animi ratione. Sunt odit similique doloribus temporibus reiciendis! Ullam. Dolor dolores veniam animi sequi dolores molestias voluptatem iure velit. Elit dolore quaerat incidunt enim aut distinctio. Ratione molestiae laboriosam
similique laboriosam eum et nemo expedita. Consequuntur perspiciatis cumque dolorem.
Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros.
Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor.
Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros.
Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor.
Aenean lacinia bibendum nulla sed consectetur. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Donec sed odio dui. Donec ullamcorper nulla non metus auctor fringilla.
Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros.
Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor.
Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros.
Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor.
Aenean lacinia bibendum nulla sed consectetur. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Donec sed odio dui. Donec ullamcorper nulla non metus auctor fringilla.
-
-
-
-
+
+
+
+
);
}
});
-const overlayTriggerInstance = (
-
- }>
-
-
- }>
-
-
-
-);
+const App = React.createClass({
+ getInitialState(){
+ return { smShow: false, lgShow: false };
+ },
+ render(){
+ let smClose = e => this.setState({ smShow: false });
+ let lgClose = e => this.setState({ lgShow: false });
-React.render(overlayTriggerInstance, mountNode);
+ return (
+
+
+
+
+
+
+
+ );
+ }
+});
+
+React.render(, mountNode);
diff --git a/docs/examples/ModalOverlayMixin.js b/docs/examples/ModalOverlayMixin.js
deleted file mode 100644
index 3a47c75b33..0000000000
--- a/docs/examples/ModalOverlayMixin.js
+++ /dev/null
@@ -1,43 +0,0 @@
-// Our custom component is managing whether the Modal is visible
-const CustomModalTrigger = React.createClass({
- mixins: [OverlayMixin],
-
- getInitialState() {
- return {
- isModalOpen: false
- };
- },
-
- handleToggle() {
- this.setState({
- isModalOpen: !this.state.isModalOpen
- });
- },
-
- render() {
- return (
-
- );
- },
-
- // This is called by the `OverlayMixin` when this component
- // is mounted or updated and the return value is appended to the body.
- renderOverlay() {
- if (!this.state.isModalOpen) {
- return ;
- }
-
- return (
-
-
- This modal is controlled by our custom trigger component.
-
Duis mollis, est non commodo luctus, nisi erat porttitor ligula.
-
-
Popover in a modal
-
TODO
-
-
Tooltips in a modal
-
TODO
-
-
-
-
Overflowing text to show scroll behavior
-
Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros.
-
Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor.
-
Aenean lacinia bibendum nulla sed consectetur. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Donec sed odio dui. Donec ullamcorper nulla non metus auctor fringilla.
-
Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros.
-
Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor.
-
Aenean lacinia bibendum nulla sed consectetur. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Donec sed odio dui. Donec ullamcorper nulla non metus auctor fringilla.
-
Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros.
-
Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor.
-
Aenean lacinia bibendum nulla sed consectetur. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Donec sed odio dui. Donec ullamcorper nulla non metus auctor fringilla.
-
-
-
-
-
+
+
Click to get the full Modal experience!
+
+
+
+
+
+ Modal heading
+
+
+
Text in a modal
+
Duis mollis, est non commodo luctus, nisi erat porttitor ligula.
Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros.
+
Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor.
+
Aenean lacinia bibendum nulla sed consectetur. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Donec sed odio dui. Donec ullamcorper nulla non metus auctor fringilla.
+
Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros.
+
Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor.
+
Aenean lacinia bibendum nulla sed consectetur. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Donec sed odio dui. Donec ullamcorper nulla non metus auctor fringilla.
+
Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros.
+
Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor.
+
Aenean lacinia bibendum nulla sed consectetur. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Donec sed odio dui. Donec ullamcorper nulla non metus auctor fringilla.
- Tight pants next level keffiyeh you probably haven't heard of them. Photo booth beard raw denim letterpress vegan messenger bag stumptown. Farm-to-table seitan, mcsweeney's fixie sustainable quinoa 8-bit american apparel Another tooltip} href='#'>have a terry richardson vinyl chambray. Beard stumptown, cardigans banh mi lomo thundercats. Tofu biodiesel williamsburg marfa, four loko mcsweeney's cleanse vegan chambray. A really ironic artisan whatever keytar, scenester farm-to-table banksy Austin twitter handle freegan cred raw denim single-origin coffee viral.
+ Tight pants next level keffiyeh you probably haven't
+ heard of them. Photo booth beard raw denim letterpress vegan messenger bag stumptown. Farm-to-table seitan, mcsweeney's
+ fixie sustainable quinoa 8-bit american apparel Another tooltip} href='#'>have a
+ terry richardson vinyl chambray. Beard stumptown, cardigans banh mi lomo thundercats. Tofu biodiesel williamsburg marfa, four
+ loko mcsweeney's cleanse vegan chambray. A really ironic artisan whatever keytar,
+ scenester farm-to-table banksy Austin twitter handle freegan
+ cred raw denim single-origin coffee viral.
);
diff --git a/docs/examples/TooltipPositioned.js b/docs/examples/TooltipPositioned.js
index cedb961fa9..5f94bca114 100644
--- a/docs/examples/TooltipPositioned.js
+++ b/docs/examples/TooltipPositioned.js
@@ -1,15 +1,23 @@
+
+const tooltip = (
+ Holy guacamole! Check this info.
+);
+
const positionerInstance = (
- Holy guacamole! Check this info.}>
+
- Holy guacamole! Check this info.}>
+
+
- Holy guacamole! Check this info.}>
+
+
- Holy guacamole! Check this info.}>
+
+
diff --git a/docs/generate-metadata.js b/docs/generate-metadata.js
new file mode 100644
index 0000000000..bf30c07d0e
--- /dev/null
+++ b/docs/generate-metadata.js
@@ -0,0 +1,104 @@
+import metadata from 'react-component-metadata';
+import glob from 'glob';
+import fsp from 'fs-promise';
+import promisify from '../tools/promisify';
+import marked from 'marked';
+
+marked.setOptions({
+ xhtml: true
+});
+
+let globp = promisify(glob);
+
+// removes doclet syntax from comments
+let cleanDoclets = desc => {
+ let idx = desc.indexOf('@');
+ return (idx === -1 ? desc : desc.substr(0, idx )).trim();
+};
+
+let cleanDocletValue = str => str.trim().replace(/^\{/, '').replace(/\}$/, '');
+
+
+let isLiteral = str => (/^('|")/).test(str.trim());
+
+/**
+ * parse out description doclets to an object and remove the comment
+ *
+ * @param {ComponentMetadata|PropMetadata} obj
+ */
+function parseDoclets(obj){
+ obj.doclets = metadata.parseDoclets(obj.desc || '') || {};
+ obj.desc = cleanDoclets(obj.desc || '');
+ obj.descHtml = marked(obj.desc || '');
+}
+
+/**
+ * Reads the JSDoc "doclets" and applies certain ones to the prop type data
+ * This allows us to "fix" parsing errors, or unparsable data with JSDoc style comments
+ *
+ * @param {Object} props Object Hash of the prop metadata
+ * @param {String} propName
+ */
+function applyPropDoclets(props, propName){
+ let prop = props[propName];
+ let doclets = prop.doclets;
+ let value;
+
+ // the @type doclet to provide a prop type
+ // Also allows enums (oneOf) if string literals are provided
+ // ex: @type {("optionA"|"optionB")}
+ if (doclets.type) {
+ value = cleanDocletValue(doclets.type);
+ prop.type.name = value;
+
+ if ( value[0] === '(' ) {
+ value = value.substring(1, value.length - 1).split('|');
+
+ prop.type.value = value;
+ prop.type.name = value.every(isLiteral) ? 'enum' : 'union';
+ }
+ }
+
+ // Use @required to mark a prop as required
+ // useful for custom propTypes where there isn't a `.isRequired` addon
+ if ( doclets.required) {
+ prop.required = true;
+ }
+}
+
+
+export default function generate(destination, options = { mixins: true }){
+
+ return globp(__dirname + '/../src/**/*.js') //eslint-disable-line no-path-concat
+ .then( files => {
+
+ let results = files.map(
+ filename => fsp.readFile(filename).then(content => metadata(content, options)) );
+
+ return Promise.all(results)
+ .then( data => {
+ let result = {};
+
+ data.forEach(components => {
+ Object.keys(components).forEach(key => {
+ const component = components[key];
+
+ parseDoclets(component);
+
+ Object.keys(component.props).forEach( propName => {
+ const prop = component.props[propName];
+
+ parseDoclets(prop);
+ applyPropDoclets(component.props, propName);
+ });
+ });
+
+ //combine all the component metadata into one large object
+ result = { ...result, ...components };
+ });
+
+ return result;
+ })
+ .catch( e => setTimeout(()=> { throw e; }));
+ });
+}
diff --git a/docs/server.js b/docs/server.js
index 5d0157dfca..66b49144c8 100644
--- a/docs/server.js
+++ b/docs/server.js
@@ -5,6 +5,8 @@ import path from 'path';
import Router from 'react-router';
import routes from './src/Routes';
import httpProxy from 'http-proxy';
+
+import metadata from './generate-metadata';
import ip from 'ip';
const development = process.env.NODE_ENV !== 'production';
@@ -21,20 +23,27 @@ if (development) {
proxy.web(req, res, { target });
});
- app.use(function renderApp(req, res) {
- res.header('Access-Control-Allow-Origin', target);
- res.header('Access-Control-Allow-Headers', 'X-Requested-With');
-
- Router.run(routes, req.url, Handler => {
- let html = React.renderToString();
- res.send('' + html);
- });
- });
-
proxy.on('error', function(e) {
console.log('Could not connect to webpack proxy'.red);
console.log(e.toString().red);
});
+
+ console.log('Prop data generation started:'.green);
+
+ metadata().then( props => {
+ console.log('Prop data generation finished:'.green);
+
+ app.use(function renderApp(req, res) {
+ res.header('Access-Control-Allow-Origin', target);
+ res.header('Access-Control-Allow-Headers', 'X-Requested-With');
+
+ Router.run(routes, req.url, Handler => {
+ let html = React.renderToString();
+ res.send('' + html);
+ });
+ });
+ });
+
} else {
app.use(express.static(path.join(__dirname, '../docs-built')));
}
diff --git a/docs/src/ComponentsPage.js b/docs/src/ComponentsPage.js
index 93aa06266e..c3166eca4d 100644
--- a/docs/src/ComponentsPage.js
+++ b/docs/src/ComponentsPage.js
@@ -1,4 +1,4 @@
-/* eslint no-path-concat: 0, react/no-did-mount-set-state: 0 */
+/* eslint react/no-did-mount-set-state: 0 */
import React from 'react';
@@ -9,6 +9,7 @@ import NavItem from '../../src/NavItem';
import NavMain from './NavMain';
import PageHeader from './PageHeader';
+import PropTable from './PropTable';
import PageFooter from './PageFooter';
import ReactPlayground from './ReactPlayground';
import Samples from './Samples';
@@ -67,6 +68,7 @@ const ComponentsPage = React.createClass({
flush against each other. To preserve the spacing between multiple inline buttons, wrap your
button group in {''}.
+
Sizes
Fancy larger or smaller buttons? Add bsSize="large", bsSize="small", or bsSize="xsmall" for additional sizes.
@@ -99,6 +101,10 @@ const ComponentsPage = React.createClass({
feedback as to the loading state, this can easily be done by updating
your {''}’s props from a state change like below.
+
+
CollapsibleMixin can be used to create your own components with collapse functionality.
+
+
Props
+
+
Panels, Accordion
+
+
+
PanelGroup
+
+
Modals Modal
A static example
-
A rendered modal with header, body, and set of actions in the footer.
-
The header is added automatically if you pass in a title prop.
+
+ A rendered modal with header, body, and set of actions in the footer. The {''} Component comes with
+ a few convenient "sub components": {''}, {''}, {''},
+ and {''}, which you can use to build the Modal content.
+
+
+
Additional Import Options
+
+ The Modal Header, Title, Body, and Footer components are available as static properties the {''} component, but you can also,
+ import them directly from the /lib directory like: {"require('react-bootstrap/lib/ModalHeader')"}.
+
+
Live demo
-
Use <ModalTrigger /> to create a real modal that's added to the document body when opened.
+
Use {''} in combination with other components to show or hide your Modal.
-
Custom trigger
-
Use OverlayMixin in a custom component to manage the modal's state yourself.
-
-
-
Contained Modal
+
Contained Modal
You will need to add the following css to your project and ensure that your container has the modal-container class.
You can apply custom css to the modal dialog div using the "dialogClassName" prop. Example is using a custom css class with width set to 90%.
+
+
Props
+
+
Modal
+
+
+
Modal.Header
+
+
+
Modal.Title
+
+
+
Modal.Body
+
+
+
Modal.Footer
+
+
+
ModalTrigger Deprecated: use the Modal directly to manage it's visibility
+
+
+
{/* Tooltip */}
-
Tooltips Tooltip
-
Example tooltips
-
-
Tooltip component.
-
+
Tooltip
+
+ Tooltip component for a more stylish alternative to that anchor tag title attribute.
+
+
-
Positioned tooltip component.
+
Attach and position tooltips with OverlayTrigger.
-
Positioned tooltip in copy.
+
Positioned tooltip in text copy.
+
+
Props
+
+
Overlay Trigger
+
+
+
Tooltip
+
{/* Popover */}
-
Popovers Popover
-
Example popovers
+
Popovers
-
Popover component.
-
+
+ The Popover, offers a more robust alternative to the Tooltip for displaying overlays of content.
+
+
-
Positioned popover component.
+
The Popover component, like the Tooltip can be used with an OverlayTrigger Component, and positioned around it.
Trigger behaviors. It's inadvisable to use "hover" or "focus" triggers for popovers, because they have poor accessibility from keyboard and on mobile devices.
Positioned popover components in scrolling container.
+
+
Props
+
+
+
+
+ {/* Overlay */}
+
+
Overlay
+
+
+ The OverlayTrigger component is great for most use cases, but as a higher level abstraction it can lack the flexibility needed
+ to build more nuanced or custom behaviors into your Overlay components. For these cases it can be helpful to forgo the trigger and use
+ the Overlay component directly.
+
+
+
+
+ You don't need to use the provided Tooltip or Popover components. Creating custom overlays
+ is as easy as wrapping some markup in an Overlay component
+
Add responsive prop to make them scroll horizontally up to small devices (under 768px). When viewing on anything larger than 768px wide, you will not see any difference in these tables.
If type is not set, child element(s) will be rendered instead of an input element.
getValue() will not work when used this way.
+
+
Props
+
+
+ {/* Utilities */}
+
+
Utilities Portal, Position
+
+
Portal
+
+ A Component that renders its children into a new React "subtree" or container. The Portal component kind of like the React
+ equivalent to jQuery's .appendTo(), which is helpful for components that need to be appended to a DOM node other than
+ the component's direct parent. The Modal, and Overlay components use the Portal component internally.
+
+
Props
+
+
+
+
Position
+
+ A Component that absolutely positions its child to a target component or DOM node. Useful for creating custom
+ popups or tooltips. Used by the Overlay Components.
+
+ );
+ });
+ },
+
+ renderRequiredLabel(prop) {
+ if (!prop.required) {
+ return null;
+ }
+
+ return (
+
+ );
+ },
+
+ getType(prop) {
+ let type = prop.type || {};
+ let name = this.getDisplayTypeName(type.name);
+ let doclets = prop.doclets || {};
+
+ switch (name) {
+ case 'object':
+ return name;
+ case 'union':
+ return type.value.reduce((current, val, i, list) => {
+ let item = this.getType({ type: val });
+ if (React.isValidElement(item)) {
+ item = React.cloneElement(item, {key: i});
+ }
+ current = current.concat(item);
+
+ return i === (list.length - 1) ? current : current.concat(' | ');
+ }, []);
+ case 'array':
+ let child = this.getType({ type: type.value });
+
+ return {'array<'}{ child }{'>'};
+ case 'enum':
+ return this.renderEnum(type);
+ case 'custom':
+ return cleanDocletValue(doclets.type || name);
+ default:
+ return name;
+ }
+ },
+
+ getDisplayTypeName(typeName) {
+ if (typeName === 'func') {
+ return 'function';
+ } else if (typeName === 'bool') {
+ return 'boolean';
+ } else {
+ return typeName;
+ }
+ },
+
+ renderEnum(enumType) {
+ const enumValues = enumType.value || [];
+
+ const renderedEnumValues = [];
+ enumValues.forEach(function renderEnumValue(enumValue, i) {
+ if (i > 0) {
+ renderedEnumValues.push(
+ ,
+ );
+ }
+
+ renderedEnumValues.push(
+ {enumValue}
+ );
+ });
+
+ return (
+ one of: {renderedEnumValues}
+ );
+ }
+});
+
+
+
+export default PropTable;
diff --git a/docs/src/ReactPlayground.js b/docs/src/ReactPlayground.js
index 743bb65979..02e170c48f 100644
--- a/docs/src/ReactPlayground.js
+++ b/docs/src/ReactPlayground.js
@@ -36,6 +36,7 @@ import * as modPagination from '../../src/Pagination';
import * as modPanel from '../../src/Panel';
import * as modPanelGroup from '../../src/PanelGroup';
import * as modPopover from '../../src/Popover';
+//import * as modPopoverTrigger from '../../src/PopoverTrigger';
import * as modProgressBar from '../../src/ProgressBar';
import * as modRow from '../../src/Row';
import * as modSplitButton from '../../src/SplitButton';
@@ -44,14 +45,22 @@ import * as modTable from '../../src/Table';
import * as modTabPane from '../../src/TabPane';
import * as modThumbnail from '../../src/Thumbnail';
import * as modTooltip from '../../src/Tooltip';
+//import * as modTooltipTrigger from '../../src/TooltipTrigger';
import * as modWell from '../../src/Well';
+import * as modPortal from '../../src/Portal';
+import * as modOverlay from '../../src/Overlay';
+
import babel from 'babel-core/browser';
import CodeExample from './CodeExample';
+
+
const classNames = modClassNames.default;
/* eslint-disable */
+const Portal = modPortal.default;
+
const React = modReact.default;
const Accordion = modAccordion.default;
const Alert = modAlert.default;
@@ -89,6 +98,7 @@ const Pager = modPager.default;
const Panel = modPanel.default;
const PanelGroup = modPanelGroup.default;
const Popover = modPopover.default;
+//const PopoverTrigger = modPopoverTrigger.default;
const ProgressBar = modProgressBar.default;
const Row = modRow.default;
const SplitButton = modSplitButton.default;
@@ -97,7 +107,10 @@ const Table = modTable.default;
const TabPane = modTabPane.default;
const Thumbnail = modThumbnail.default;
const Tooltip = modTooltip.default;
+//const TooltipTrigger = modTooltipTrigger.default;
const Well = modWell.default;
+const Overlay = modOverlay.default;
+
/* eslint-enable */
const IS_MOBILE = typeof navigator !== 'undefined' && (
diff --git a/docs/src/Root.js b/docs/src/Root.js
index f3ce4b2b76..449db4528e 100644
--- a/docs/src/Root.js
+++ b/docs/src/Root.js
@@ -14,7 +14,8 @@ const Root = React.createClass({
'index.html',
'introduction.html',
'getting-started.html',
- 'components.html'
+ 'components.html',
+ 'support.html'
];
}
},
@@ -25,6 +26,14 @@ const Root = React.createClass({
};
},
+ childContextTypes: {
+ metadata: React.PropTypes.object
+ },
+
+ getChildContext(){
+ return { metadata: this.props.propData };
+ },
+
render() {
// Dump out our current props to a global object via a script tag so
// when initialising the browser environment we can bootstrap from the
@@ -64,7 +73,7 @@ const Root = React.createClass({
-
+
diff --git a/docs/src/Routes.js b/docs/src/Routes.js
index 790880e163..ae95030507 100644
--- a/docs/src/Routes.js
+++ b/docs/src/Routes.js
@@ -5,6 +5,7 @@ import HomePage from './HomePage';
import IntroductionPage from './IntroductionPage';
import GettingStartedPage from './GettingStartedPage';
import ComponentsPage from './ComponentsPage';
+import SupportPage from './SupportPage';
import NotFoundPage from './NotFoundPage';
import {Route, DefaultRoute, NotFoundRoute} from 'react-router';
@@ -17,5 +18,6 @@ export default (
+
);
diff --git a/docs/src/Samples.js b/docs/src/Samples.js
index c9b0aee582..47e6720802 100644
--- a/docs/src/Samples.js
+++ b/docs/src/Samples.js
@@ -32,7 +32,7 @@ export default {
CollapsibleParagraph: require('fs').readFileSync(__dirname + '/../examples/CollapsibleParagraph.js', 'utf8'),
ModalStatic: require('fs').readFileSync(__dirname + '/../examples/ModalStatic.js', 'utf8'),
ModalTrigger: require('fs').readFileSync(__dirname + '/../examples/ModalTrigger.js', 'utf8'),
- ModalOverlayMixin: require('fs').readFileSync(__dirname + '/../examples/ModalOverlayMixin.js', 'utf8'),
+
ModalContained: require('fs').readFileSync(__dirname + '/../examples/ModalContained.js', 'utf8'),
ModalDefaultSizing: require('fs').readFileSync(__dirname + '/../examples/ModalDefaultSizing.js', 'utf8'),
ModalCustomSizing: require('fs').readFileSync(__dirname + '/../examples/ModalCustomSizing.js', 'utf8'),
@@ -99,5 +99,8 @@ export default {
InputValidation: require('fs').readFileSync(__dirname + '/../examples/InputValidation.js', 'utf8'),
InputHorizontal: require('fs').readFileSync(__dirname + '/../examples/InputHorizontal.js', 'utf8'),
InputWrapper: require('fs').readFileSync(__dirname + '/../examples/InputWrapper.js', 'utf8'),
- MenuItem: require('fs').readFileSync(__dirname + '/../examples/MenuItem.js', 'utf8')
+ MenuItem: require('fs').readFileSync(__dirname + '/../examples/MenuItem.js', 'utf8'),
+
+ Overlay: require('fs').readFileSync(__dirname + '/../examples/Overlay.js', 'utf8'),
+ OverlayCustom: require('fs').readFileSync(__dirname + '/../examples/OverlayCustom.js', 'utf8')
};
diff --git a/docs/src/SupportPage.js b/docs/src/SupportPage.js
new file mode 100644
index 0000000000..0a792dfbd5
--- /dev/null
+++ b/docs/src/SupportPage.js
@@ -0,0 +1,48 @@
+import React from 'react';
+
+import NavMain from './NavMain';
+import PageHeader from './PageHeader';
+import PageFooter from './PageFooter';
+
+export default class Page extends React.Component {
+ render() {
+ return (
+
+
+
+
+
+
+
+
+
+
Stay up to date on the development of React-Bootstrap and reach out to the community with these helpful resources.
+
+
Stack Overflow
+
Ask questions about specific problems you have faced, including details about what exactly you are trying to do. Make sure you tag your question with react-bootstrap. You can also read through existing React-Bootstrap questions.
+
+
Live help
+
Bring your questions and pair with other react-bootstrap users in a live Thinkful hangout. Hear about the challenges other developers are running into, or screenshare your own code with the group for feedback.
The issue tracker is the preferred channel for bug reports, features requests and submitting pull requests. See more about how we use issues in the contribution guidelines.
+
);
}
});
diff --git a/src/Modal.js b/src/Modal.js
index 3a28a7ff62..70b5945bab 100644
--- a/src/Modal.js
+++ b/src/Modal.js
@@ -1,15 +1,21 @@
-import React from 'react';
+/*eslint-disable react/prop-types */
+import React, { cloneElement } from 'react';
+
import classNames from 'classnames';
+import createChainedFunction from './utils/createChainedFunction';
import BootstrapMixin from './BootstrapMixin';
import FadeMixin from './FadeMixin';
import domUtils from './utils/domUtils';
import EventListener from './utils/EventListener';
+import deprecationWarning from './utils/deprecationWarning';
+
+import Portal from './Portal';
+import Body from './ModalBody';
+import Header from './ModalHeader';
+import Title from './ModalTitle';
+import Footer from './ModalFooter';
-// TODO:
-// - aria-labelledby
-// - Add `modal-body` div if only one child passed in that doesn't already have it
-// - Tests
/**
* Gets the correct clientHeight of the modal container
@@ -31,9 +37,32 @@ 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;
+
/**
* Firefox doesn't have a focusin event so using capture is easiest way to get bubbling
* IE8 can't do addEventListener, but does have onfocusin, so we use that in ie8
+ *
+ * We only allow one Listener at a time to avoid stack overflows
+ *
* @param {ReactElement|HTMLElement} context
* @param {Function} handler
*/
@@ -42,6 +71,10 @@ function onFocus(context, handler) {
let useFocusin = !doc.addEventListener;
let remove;
+ if ( currentFocusListener ) {
+ currentFocusListener.remove();
+ }
+
if (useFocusin) {
document.attachEvent('onfocusin', handler);
remove = () => document.detachEvent('onfocusin', handler);
@@ -49,7 +82,10 @@ function onFocus(context, handler) {
document.addEventListener('focus', handler, true);
remove = () => document.removeEventListener('focus', handler, true);
}
- return { remove };
+
+ currentFocusListener = { remove };
+
+ return currentFocusListener;
}
let scrollbarSize;
@@ -75,19 +111,64 @@ function getScrollbarSize(){
}
-const Modal = React.createClass({
+const ModalMarkup = React.createClass({
mixins: [BootstrapMixin, FadeMixin],
propTypes: {
+ /**
+ * The Modal title text
+ * @deprecated Use the "Modal.Header" component instead
+ */
title: React.PropTypes.node,
+ /**
+ * Include a backdrop component. Specify 'static' for a backdrop that doesn't trigger an "onHide" when clicked.
+ */
backdrop: React.PropTypes.oneOf(['static', true, false]),
+ /**
+ * Include a backdrop component. Specify 'static' for a backdrop that doesn't trigger an "onHide" when clicked.
+ */
keyboard: React.PropTypes.bool,
+
+ /**
+ * Specify whether the Modal heading should contain a close button
+ * @deprecated Use the "Modal.Header" Component instead
+ */
closeButton: React.PropTypes.bool,
- container: React.PropTypes.object,
+
+ /**
+ * Open and close the Modal with a slide and fade animation.
+ */
animation: React.PropTypes.bool,
- onRequestHide: React.PropTypes.func.isRequired,
+ /**
+ * A Callback fired when the header closeButton or non-static backdrop is clicked.
+ * @type {function}
+ * @required
+ */
+ onHide: requiredIfNot('onRequestHide', React.PropTypes.func),
+
+ /**
+ * A Callback fired when the header closeButton or non-static backdrop is clicked.
+ * @deprecated Replaced by `onHide`.
+ */
+ onRequestHide: React.PropTypes.func,
+
+ /**
+ * A css class to apply to the Modal dialog DOM node.
+ */
dialogClassName: React.PropTypes.string,
+
+ /**
+ * When `true` The modal will automatically shift focus to itself when it opens, and replace it to the last focused element when it closes.
+ * Generally this should never be set to false as it makes the Modal less accessible to assistive technologies, like screen-readers.
+ */
+ autoFocus: React.PropTypes.bool,
+
+ /**
+ * When `true` The modal will prevent focus from leaving the Modal while open.
+ * Consider leaving the default value here, as it is necessary to make the Modal work well with assistive technologies,
+ * such as screen readers.
+ */
enforceFocus: React.PropTypes.bool
},
@@ -98,6 +179,8 @@ const Modal = React.createClass({
keyboard: true,
animation: true,
closeButton: true,
+
+ autoFocus: true,
enforceFocus: true
};
},
@@ -131,9 +214,8 @@ const Modal = React.createClass({
onClick={this.props.backdrop === true ? this.handleBackdropClick : null}
ref="modal">
+ );
+ }
+}
+
+//used in liue of parent contexts right now to auto wire the close button
+ModalHeader.__isModalHeader = true;
+
+ModalHeader.propTypes = {
+ /**
+ * A css class applied to the Component
+ */
+ modalClassName: React.PropTypes.string,
+ /**
+ * Specify whether the Component should contain a close button
+ */
+ closeButton: React.PropTypes.bool,
+ /**
+ * A Callback fired when the close button is clicked. If used directly inside a Modal component, the onHide will automatically
+ * be propagated up to the parent Modal `onHide`.
+ */
+ onHide: React.PropTypes.func
+};
+
+ModalHeader.defaultProps = {
+ modalClassName: 'modal-header',
+ closeButton: false
+};
+
+
+export default ModalHeader;
diff --git a/src/ModalTitle.js b/src/ModalTitle.js
new file mode 100644
index 0000000000..0c44bbaee5
--- /dev/null
+++ b/src/ModalTitle.js
@@ -0,0 +1,26 @@
+import React from 'react';
+import classnames from 'classnames';
+
+class ModalTitle extends React.Component {
+
+ render() {
+ return (
+