diff --git a/.travis.yml b/.travis.yml
index a5468af91e..c5c6b6af80 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,7 +1,7 @@
sudo: false
language: node_js
node_js:
- - "iojs"
+- 4
cache:
directories:
- node_modules
diff --git a/docs/examples/.eslintrc b/docs/examples/.eslintrc
index 946c5f8337..d0f16ae46a 100644
--- a/docs/examples/.eslintrc
+++ b/docs/examples/.eslintrc
@@ -10,6 +10,8 @@
"Accordion",
"Alert",
"Badge",
+ "Breadcrumb",
+ "BreadcrumbItem",
"Button",
"ButtonGroup",
"ButtonInput",
diff --git a/docs/examples/Breadcrumb.js b/docs/examples/Breadcrumb.js
new file mode 100644
index 0000000000..d881f5f6f6
--- /dev/null
+++ b/docs/examples/Breadcrumb.js
@@ -0,0 +1,15 @@
+const breadcrumbInstance = (
+
+
+ Home
+
+
+ Library
+
+
+ Data
+
+
+);
+
+React.render(breadcrumbInstance, mountNode);
diff --git a/docs/src/ComponentsPage.js b/docs/src/ComponentsPage.js
index faeeff0b7b..9ba1aaf324 100644
--- a/docs/src/ComponentsPage.js
+++ b/docs/src/ComponentsPage.js
@@ -541,6 +541,22 @@ const ComponentsPage = React.createClass({
+ {/* Breadcrumb */}
+
Togglable tabs Tabs, Tab
@@ -947,6 +963,7 @@ const ComponentsPage = React.createClass({
Progress bars
Navs
Navbars
+
Breadcrumbs
Tabs
Pager
Pagination
diff --git a/docs/src/ReactPlayground.js b/docs/src/ReactPlayground.js
index 4dd53cb344..410dbaa6da 100644
--- a/docs/src/ReactPlayground.js
+++ b/docs/src/ReactPlayground.js
@@ -8,6 +8,8 @@ const React = require('react');
const Accordion = require('../../src/Accordion');
const Alert = require('../../src/Alert');
const Badge = require('../../src/Badge');
+const Breadcrumb = require('../../src/Breadcrumb');
+const BreadcrumbItem = require('../../src/BreadcrumbItem');
const Button = require('../../src/Button');
const ButtonGroup = require('../../src/ButtonGroup');
const ButtonInput = require('../../src/ButtonInput');
diff --git a/docs/src/Samples.js b/docs/src/Samples.js
index f2c8235683..dc2d4ee966 100644
--- a/docs/src/Samples.js
+++ b/docs/src/Samples.js
@@ -4,6 +4,7 @@ export default {
Collapse: require('fs').readFileSync(__dirname + '/../examples/Collapse.js', 'utf8'),
Fade: require('fs').readFileSync(__dirname + '/../examples/Fade.js', 'utf8'),
+ Breadcrumb: require('fs').readFileSync(__dirname + '/../examples/Breadcrumb.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'),
diff --git a/src/Breadcrumb.js b/src/Breadcrumb.js
new file mode 100644
index 0000000000..1f45d11f8f
--- /dev/null
+++ b/src/Breadcrumb.js
@@ -0,0 +1,39 @@
+import React, { cloneElement } from 'react';
+import classNames from 'classnames';
+import ValidComponentChildren from './utils/ValidComponentChildren';
+
+const Breadcrumb = React.createClass({
+ propTypes: {
+ /**
+ * bootstrap className
+ * @private
+ */
+ bsClass: React.PropTypes.string
+ },
+
+ getDefaultProps() {
+ return {
+ bsClass: 'breadcrumb'
+ };
+ },
+
+ render() {
+ const { className, ...props } = this.props;
+
+ return (
+
+ {ValidComponentChildren.map(this.props.children, this.renderBreadcrumbItem)}
+
+ );
+ },
+
+ renderBreadcrumbItem(child, index) {
+ return cloneElement( child, { key: child.key ? child.key : index } );
+ }
+});
+
+export default Breadcrumb;
diff --git a/src/BreadcrumbItem.js b/src/BreadcrumbItem.js
new file mode 100644
index 0000000000..ed3ee1d9f6
--- /dev/null
+++ b/src/BreadcrumbItem.js
@@ -0,0 +1,83 @@
+import React from 'react';
+import classNames from 'classnames';
+import SafeAnchor from './SafeAnchor';
+import warning from 'react/lib/warning';
+
+const BreadcrumbItem = React.createClass({
+ propTypes: {
+ /**
+ * If set to true, renders `span` instead of `a`
+ */
+ active: React.PropTypes.bool,
+ /**
+ * HTML id for the wrapper `li` element
+ */
+ id: React.PropTypes.oneOfType([
+ React.PropTypes.string,
+ React.PropTypes.number
+ ]),
+ /**
+ * HTML id for the inner `a` element
+ */
+ linkId: React.PropTypes.oneOfType([
+ React.PropTypes.string,
+ React.PropTypes.number
+ ]),
+ /**
+ * `href` attribute for the inner `a` element
+ */
+ href: React.PropTypes.string,
+ /**
+ * `title` attribute for the inner `a` element
+ */
+ title: React.PropTypes.node,
+ /**
+ * `target` attribute for the inner `a` element
+ */
+ target: React.PropTypes.string
+ },
+
+ getDefaultProps() {
+ return {
+ active: false,
+ };
+ },
+
+ render() {
+ const {
+ active,
+ className,
+ id,
+ linkId,
+ children,
+ href,
+ title,
+ target,
+ ...props } = this.props;
+
+ warning(!(href && active), '[react-bootstrap] `href` and `active` properties cannot be set at the same time');
+
+ const linkProps = {
+ href,
+ title,
+ target,
+ id: linkId
+ };
+
+ return (
+
+ {
+ active ?
+
+ { children }
+ :
+
+ { children }
+
+ }
+
+ );
+ }
+});
+
+export default BreadcrumbItem;
diff --git a/src/Input.js b/src/Input.js
index 800ab579bc..446d736d5f 100644
--- a/src/Input.js
+++ b/src/Input.js
@@ -6,7 +6,7 @@ import deprecationWarning from './utils/deprecationWarning';
class Input extends InputBase {
render() {
if (this.props.type === 'static') {
- deprecationWarning('Input type=static', 'StaticText');
+ deprecationWarning('Input type=static', 'FormControls.Static');
return
;
}
diff --git a/src/index.js b/src/index.js
index 80e9bb1a8b..270a9386d9 100644
--- a/src/index.js
+++ b/src/index.js
@@ -8,6 +8,8 @@ export Button from './Button';
export ButtonGroup from './ButtonGroup';
export ButtonInput from './ButtonInput';
export ButtonToolbar from './ButtonToolbar';
+export Breadcrumb from './Breadcrumb';
+export BreadcrumbItem from './BreadcrumbItem';
export Carousel from './Carousel';
export CarouselItem from './CarouselItem';
export Col from './Col';
diff --git a/test/BreadcrumbItemSpec.js b/test/BreadcrumbItemSpec.js
new file mode 100644
index 0000000000..0ce019fa01
--- /dev/null
+++ b/test/BreadcrumbItemSpec.js
@@ -0,0 +1,140 @@
+import React from 'react';
+import ReactTestUtils from 'react/lib/ReactTestUtils';
+import BreadcrumbItem from '../src/BreadcrumbItem';
+import { shouldWarn } from './helpers';
+
+describe('BreadcrumbItem', () => {
+ it('Should warn if `active` and `href` attributes set', () => {
+ ReactTestUtils.renderIntoDocument(
+
+ Crumb
+
+ );
+
+ shouldWarn('[react-bootstrap] `href` and `active` properties cannot be set at the same time');
+ });
+
+ it('Should render `a` as inner element when is not active', () => {
+ const instance = ReactTestUtils.renderIntoDocument(
+
+ Crumb
+
+ );
+
+ assert.ok(ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'a'));
+ assert.notInclude(React.findDOMNode(instance).className, 'active');
+ });
+
+ it('Should add `active` class with `active` attribute set.', () => {
+ const instance = ReactTestUtils.renderIntoDocument(
+
+ Active Crumb
+
+ );
+
+ assert.include(React.findDOMNode(instance).className, 'active');
+ });
+
+ it('Should render `span` as inner element when is active', () => {
+ const instance = ReactTestUtils.renderIntoDocument(
+
+ Crumb
+
+ );
+
+ assert.ok(ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'span'));
+ });
+
+ it('Should add custom classes onto `li` wrapper element', () => {
+ const instance = ReactTestUtils.renderIntoDocument(
+
+ Active Crumb
+
+ );
+
+ const classes = React.findDOMNode(instance).className;
+ assert.include(classes, 'custom-one');
+ assert.include(classes, 'custom-two');
+ });
+
+ it('Should spread additional props onto inner element', (done) => {
+ const handleClick = () => {
+ done();
+ };
+
+ const instance = ReactTestUtils.renderIntoDocument(
+
+ Crumb
+
+ );
+
+ const anchorNode = ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'a');
+ ReactTestUtils.Simulate.click(anchorNode);
+ });
+
+ it('Should apply id onto `li` wrapper element via `id` property', () => {
+ const instance = ReactTestUtils.renderIntoDocument(
+
+ Crumb
+
+ );
+
+ assert.equal(React.findDOMNode(instance).id, 'test-li-id');
+ });
+
+ it('Should apply id onto `a` inner alement via `linkId` property', () => {
+ const instance = ReactTestUtils.renderIntoDocument(
+
+ Crumb
+
+ );
+
+ const linkNode = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'a'));
+ assert.equal(linkNode.id, 'test-link-id');
+ });
+
+ it('Should apply `href` property onto `a` inner element', () => {
+ const instance = ReactTestUtils.renderIntoDocument(
+
+ Crumb
+
+ );
+
+ const linkNode = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'a'));
+ assert.equal(linkNode.href, 'http://getbootstrap.com/components/#breadcrumbs');
+ });
+
+ it('Should apply `title` property onto `a` inner element', () => {
+ const instance = ReactTestUtils.renderIntoDocument(
+
+ Crumb
+
+ );
+
+ const linkNode = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'a'));
+ assert.equal(linkNode.title, 'test-title');
+ });
+
+ it('Should not apply properties for inner `anchor` onto `li` wrapper element', () => {
+ const instance = ReactTestUtils.renderIntoDocument(
+
+ Crumb
+
+ );
+
+ const liNode = React.findDOMNode(instance);
+ assert.notOk(liNode.hasAttribute('href'));
+ assert.notOk(liNode.hasAttribute('title'));
+ });
+
+ it('Should set `target` attribute on `anchor`', () => {
+ const instance = ReactTestUtils.renderIntoDocument(
+
+ Crumb
+
+ );
+
+ const linkNode = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'a'));
+ assert.equal(linkNode.target, '_blank');
+ });
+});
diff --git a/test/BreadcrumbSpec.js b/test/BreadcrumbSpec.js
new file mode 100644
index 0000000000..eb74c1c2ac
--- /dev/null
+++ b/test/BreadcrumbSpec.js
@@ -0,0 +1,54 @@
+import React from 'react';
+import ReactTestUtils from 'react/lib/ReactTestUtils';
+import Breadcrumb from '../src/Breadcrumb';
+
+describe('Breadcrumb', () => {
+ it('Should apply id to the wrapper ol element', () => {
+ let instance = ReactTestUtils.renderIntoDocument(
+
+ );
+
+ let olNode = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'ol'));
+ assert.equal(olNode.id, 'custom-id');
+ });
+
+ it('Should have breadcrumb class', () => {
+ let instance = ReactTestUtils.renderIntoDocument(
+
+ );
+
+ let olNode = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'ol'));
+ assert.include(olNode.className, 'breadcrumb');
+ });
+
+ it('Should have custom classes', () => {
+ let instance = ReactTestUtils.renderIntoDocument(
+
+ );
+
+ let olNode = React.findDOMNode(ReactTestUtils.findRenderedComponentWithType(instance, Breadcrumb));
+
+ let classes = olNode.className;
+ assert.include(classes, 'breadcrumb');
+ assert.include(classes, 'custom-one');
+ assert.include(classes, 'custom-two');
+ });
+
+ it('Should have a navigation role', () => {
+ let instance = ReactTestUtils.renderIntoDocument(
+
+ );
+
+ let olNode = React.findDOMNode(ReactTestUtils.findRenderedComponentWithType(instance, Breadcrumb));
+ assert.equal(olNode.getAttribute('role'), 'navigation');
+ });
+
+ it('Should have an aria-label in ol', () => {
+ let instance = ReactTestUtils.renderIntoDocument(
+
+ );
+
+ let olNode = React.findDOMNode(ReactTestUtils.findRenderedComponentWithType(instance, Breadcrumb));
+ assert.equal(olNode.getAttribute('aria-label'), 'breadcrumbs');
+ });
+});