From 3c710f94d59019b26fb005906e056b3f71bcc4c2 Mon Sep 17 00:00:00 2001 From: ycavatars Date: Wed, 2 Sep 2015 11:21:10 +0800 Subject: [PATCH] [added] Breadcrumb component --- docs/examples/.eslintrc | 2 + docs/examples/Breadcrumb.js | 15 ++++ docs/src/ComponentsPage.js | 18 +++++ docs/src/ReactPlayground.js | 2 + docs/src/Samples.js | 1 + src/Breadcrumb.js | 37 ++++++++++ src/BreadcrumbItem.js | 61 ++++++++++++++++ src/index.js | 2 + src/styleMaps.js | 1 + test/BreadcrumbItemSpec.js | 137 ++++++++++++++++++++++++++++++++++++ test/BreadcrumbSpec.js | 120 +++++++++++++++++++++++++++++++ 11 files changed, 396 insertions(+) create mode 100644 docs/examples/Breadcrumb.js create mode 100644 src/Breadcrumb.js create mode 100644 src/BreadcrumbItem.js create mode 100644 test/BreadcrumbItemSpec.js create mode 100644 test/BreadcrumbSpec.js 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 */} +
+

Breadcrumbs Breadcrumb, BreadcrumbItems

+

Breadcrumbs are used to indicate the current page's location. An active class is added to a BreadcrumbItem if there's no href property for it.

+ +

Breadcrumbs Example

+ + +

Props

+ +

Breadcrumb

+ + +

BreadcrumbItem

+ +
+ {/* Tabbed Areas */}

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