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..d8e9c4cc35
--- /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..79a1014f0a 100644
--- a/docs/src/ComponentsPage.js
+++ b/docs/src/ComponentsPage.js
@@ -541,6 +541,23 @@ const ComponentsPage = React.createClass({
+ {/* Breadcrumb */}
+
Togglable tabs Tabs, Tab
@@ -947,6 +964,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 232c8dba9f..db3cf8cbe1 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..0f7e172b71
--- /dev/null
+++ b/src/Breadcrumb.js
@@ -0,0 +1,37 @@
+import React, { cloneElement } from 'react';
+import classNames from 'classnames';
+import BootstrapMixin from './BootstrapMixin';
+import ValidComponentChildren from './utils/ValidComponentChildren';
+
+const Breadcrumb = React.createClass({
+ mixins: [BootstrapMixin],
+
+ getDefaultProps() {
+ return {
+ bsClass: 'breadcrumb'
+ };
+ },
+
+ render() {
+ const classes = this.getBsClassSet();
+ 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,
+ navItem: true
+ }
+ );
+ }
+});
+
+export default Breadcrumb;
diff --git a/src/BreadcrumbItem.js b/src/BreadcrumbItem.js
new file mode 100644
index 0000000000..823be75e57
--- /dev/null
+++ b/src/BreadcrumbItem.js
@@ -0,0 +1,61 @@
+import React from 'react';
+import classNames from 'classnames';
+import BootstrapMixin from './BootstrapMixin';
+import SafeAnchor from './SafeAnchor';
+import warning from 'react/lib/warning';
+
+const BreadcrumbItem = React.createClass({
+ mixins: [BootstrapMixin],
+
+ propTypes: {
+ id: React.PropTypes.string,
+ active: React.PropTypes.bool,
+ linkId: React.PropTypes.string,
+ href: React.PropTypes.string,
+ title: React.PropTypes.node,
+ target: React.PropTypes.string
+ },
+
+ getDefaultProps() {
+ return {
+ active: false,
+ };
+ },
+
+ render() {
+ warning(!(this.props.href && this.props.active), '[react-bootstrap] href and active properties cannot be set at the same time');
+
+ const {
+ id,
+ active,
+ linkId,
+ children,
+ href,
+ title,
+ target,
+ ...props } = this.props;
+ const classes = { active };
+ const linkProps = {
+ href,
+ title,
+ target,
+ id: linkId
+ };
+
+ return (
+
+ {
+ active ?
+
+ { children }
+ :
+
+ { children }
+
+ }
+
+ );
+ }
+});
+
+export default BreadcrumbItem;
diff --git a/src/index.js b/src/index.js
index 5f731695d9..fe708e21c7 100644
--- a/src/index.js
+++ b/src/index.js
@@ -10,6 +10,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/src/styleMaps.js b/src/styleMaps.js
index 4360d1a675..01f9abc6fc 100644
--- a/src/styleMaps.js
+++ b/src/styleMaps.js
@@ -1,6 +1,7 @@
const styleMaps = {
CLASSES: {
'alert': 'alert',
+ 'breadcrumb': 'breadcrumb',
'button': 'btn',
'button-group': 'btn-group',
'button-toolbar': 'btn-toolbar',
diff --git a/test/BreadcrumbItemSpec.js b/test/BreadcrumbItemSpec.js
new file mode 100644
index 0000000000..9f98531de2
--- /dev/null
+++ b/test/BreadcrumbItemSpec.js
@@ -0,0 +1,137 @@
+import React from 'react';
+import ReactTestUtils from 'react/lib/ReactTestUtils';
+import BreadcrumbItem from '../src/BreadcrumbItem';
+
+describe('BreadcrumbItem', function () {
+ it('Should add active class', function () {
+ let instance = ReactTestUtils.renderIntoDocument(
+
+ Active Crumb
+
+ );
+
+ assert.ok(ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'active'));
+ });
+
+ it('Should not add active class', function () {
+ let instance = ReactTestUtils.renderIntoDocument(
+
+ Crumb
+
+ );
+
+ let liNode = React.findDOMNode(instance);
+ assert.notInclude(liNode.className, 'active');
+ });
+
+ it('Should add custom classes', function () {
+ let instance = ReactTestUtils.renderIntoDocument(
+
+ Active Crumb
+
+ );
+
+ let liNode = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'active'));
+
+ let classes = liNode.className;
+ assert.include(classes, 'active');
+ assert.include(classes, 'custom-one');
+ assert.include(classes, 'custom-two');
+ });
+
+ it('Should spread props onto an active item', function() {
+ let instance = ReactTestUtils.renderIntoDocument(
+
+ Active Crumb
+
+ );
+
+ let spanNode = ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'span');
+
+ spanNode.props.herpa.should.equal('derpa');
+ });
+
+ it('Should spread props onto anchor', function(done) {
+ const handleClick = () => {
+ done();
+ };
+
+ let instance = ReactTestUtils.renderIntoDocument(
+
+ Crumb 1
+
+ );
+
+ let anchorNode = ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'a');
+ ReactTestUtils.Simulate.click(anchorNode);
+
+ anchorNode.props.herpa.should.equal('derpa');
+ });
+
+ it('Should add id for li element', function() {
+ let instance = ReactTestUtils.renderIntoDocument(
+
+ Crumb 1
+
+ );
+
+ let liNode = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'li'));
+ assert.equal(liNode.id, 'test-li-id');
+ });
+
+ it('Should add linkId', function() {
+ let instance = ReactTestUtils.renderIntoDocument(
+
+ Crumb 1
+
+ );
+
+ let linkNode = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'a'));
+ assert.equal(linkNode.id, 'test-link-id');
+ });
+
+ it('Should add href', function() {
+ let instance = ReactTestUtils.renderIntoDocument(
+
+ Crumb 1
+
+ );
+
+ let linkNode = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'a'));
+ assert.equal(linkNode.href, 'http://getbootstrap.com/components/#breadcrumbs');
+ });
+
+ it('Should have a title', function() {
+ let instance = ReactTestUtils.renderIntoDocument(
+
+ Crumb 1
+
+ );
+
+ let linkNode = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'a'));
+ assert.equal(linkNode.title, 'test-title');
+ });
+
+ it('Should not add anchor properties to li', function() {
+ let instance = ReactTestUtils.renderIntoDocument(
+
+ Crumb 1
+
+ );
+
+ let liNode = React.findDOMNode(instance);
+ assert.notOk(liNode.hasAttribute('href'));
+ assert.notOk(liNode.hasAttribute('title'));
+ });
+
+ it('Should set target attribute on anchor', function () {
+ let instance = ReactTestUtils.renderIntoDocument(
+
+ Crumb 1
+
+ );
+
+ let 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..9abbc24634
--- /dev/null
+++ b/test/BreadcrumbSpec.js
@@ -0,0 +1,120 @@
+import React from 'react';
+import ReactTestUtils from 'react/lib/ReactTestUtils';
+import Breadcrumb from '../src/Breadcrumb';
+import BreadcrumbItem from '../src/BreadcrumbItem';
+
+describe('Breadcrumb', function () {
+ it('Should able to wrap react component', function () {
+ let instance = ReactTestUtils.renderIntoDocument(
+
+
+ Crumb 1
+
+
+ Active Crumb Component
+
+
+ );
+
+ let items = ReactTestUtils.scryRenderedDOMComponentsWithTag(instance, 'li');
+
+ let linkNode = React.findDOMNode(items[0]).childNodes[0];
+ let spanNode = linkNode.childNodes[0];
+ assert.equal(spanNode.className, 'custom-span-class');
+
+ let divNode = React.findDOMNode(items[1]).childNodes[0].childNodes[0];
+ assert.equal(divNode.className, 'custom-div-class');
+ });
+
+ it('Should apply id to the wrapper ol element', function () {
+ let instance = ReactTestUtils.renderIntoDocument(
+
+ );
+
+ let olNode = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'ol'));
+ assert.equal(olNode.id, 'custom-id');
+ });
+
+ it('Should have breadcrumb class', function () {
+ let instance = ReactTestUtils.renderIntoDocument(
+
+
+ Crumb 1
+
+
+ Crumb 2
+
+
+ Active Crumb
+
+
+ );
+
+ assert.ok(ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'breadcrumb'));
+
+ let olNode = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithTag(instance, 'ol'));
+ assert.include(olNode.className, 'breadcrumb');
+ });
+
+ it('Should have custom classes', function () {
+ let instance = ReactTestUtils.renderIntoDocument(
+
+
+ Crumb 1
+
+
+ Crumb 2
+
+
+ Active Crumb
+
+
+ );
+
+ 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 in ol', function () {
+ let instance = ReactTestUtils.renderIntoDocument(
+
+
+ Crumb 1
+
+
+ Crumb 2
+
+
+ Active Crumb
+
+
+ );
+
+
+ let olNode = React.findDOMNode(ReactTestUtils.findRenderedComponentWithType(instance, Breadcrumb));
+ assert.equal(olNode.getAttribute('role'), 'navigation');
+ });
+
+ it('Should have a aria-label in ol', function () {
+ let instance = ReactTestUtils.renderIntoDocument(
+
+
+ Crumb 1
+
+
+ Crumb 2
+
+
+ Active Crumb
+
+
+ );
+
+ let olNode = React.findDOMNode(ReactTestUtils.findRenderedComponentWithType(instance, Breadcrumb));
+ assert.equal(olNode.getAttribute('aria-label'), 'breadcrumbs');
+ });
+});