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 */} +
+

Breadcrumbs Breadcrumb, BreadcrumbItems

+

Breadcrumbs are used to indicate the current page's location. Add active attribute to active BreadcrumbItem.

+

Do not set both active and href attributes. active overrides href and span element is rendered instead of a.

+ +

Breadcrumbs Example

+ + +

Props

+

Breadcrumb component itself doesn't have any specific public properties

+ +

BreadcrumbItem

+ +
+ {/* Tabbed Areas */}

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'); + }); +});