diff --git a/docs/examples/CollapsableNav.js b/docs/examples/CollapsableNav.js new file mode 100644 index 0000000000..7211b1342d --- /dev/null +++ b/docs/examples/CollapsableNav.js @@ -0,0 +1,23 @@ +var navbarInstance = ( + + {/* This is the eventKey referenced */} + + + + + ); + +React.render(navbarInstance, mountNode); diff --git a/docs/src/ComponentsPage.js b/docs/src/ComponentsPage.js index 7a611b42c6..315f8a8019 100644 --- a/docs/src/ComponentsPage.js +++ b/docs/src/ComponentsPage.js @@ -357,6 +357,16 @@ var ComponentsPage = React.createClass({ + +

Mobile Friendly (Multiple Nav Components)

+

To have a mobile friendly Navbar that handles multiple Nav components use CollapsableNav. The toggleNavKey must still be set, however, the corresponding eventKey must now be on the CollapsableNav component.

+
+

Div collapse

+

The navbar-collapse div gets created as the collapsable element which follows the bootstrap collapsable navbar documentation.

+
<div class="collapse navbar-collapse"></div>
+
+ + {/* Tabbed Areas */} diff --git a/docs/src/ReactPlayground.js b/docs/src/ReactPlayground.js index 8a23add10d..b91c26ee63 100644 --- a/docs/src/ReactPlayground.js +++ b/docs/src/ReactPlayground.js @@ -9,6 +9,7 @@ var Button = require('../../lib/Button'); var ButtonGroup = require('../../lib/ButtonGroup'); var ButtonToolbar = require('../../lib/ButtonToolbar'); var CollapsableMixin = require('../../lib/CollapsableMixin'); +var CollapsableNav = require('../../lib/CollapsableNav'); var Carousel = require('../../lib/Carousel'); var CarouselItem = require('../../lib/CarouselItem'); var Col = require('../../lib/Col'); diff --git a/src/CollapsableNav.jsx b/src/CollapsableNav.jsx new file mode 100644 index 0000000000..65e37931e7 --- /dev/null +++ b/src/CollapsableNav.jsx @@ -0,0 +1,110 @@ +var React = require('react'); +var joinClasses = require('./utils/joinClasses'); +var BootstrapMixin = require('./BootstrapMixin'); +var CollapsableMixin = require('./CollapsableMixin'); +var classSet = require('./utils/classSet'); +var domUtils = require('./utils/domUtils'); +var cloneWithProps = require('./utils/cloneWithProps'); + +var ValidComponentChildren = require('./utils/ValidComponentChildren'); +var createChainedFunction = require('./utils/createChainedFunction'); + + +var CollapsableNav = React.createClass({ + mixins: [BootstrapMixin, CollapsableMixin], + + propTypes: { + onSelect: React.PropTypes.func, + expanded: React.PropTypes.bool, + eventKey: React.PropTypes.any + }, + + getCollapsableDOMNode: function () { + return this.getDOMNode(); + }, + + getCollapsableDimensionValue: function () { + var height = 0; + var nodes = this.refs; + for (var key in nodes) { + if (nodes.hasOwnProperty(key)) { + + var n = nodes[key].getDOMNode() + , h = n.offsetHeight + , computedStyles = domUtils.getComputedStyles(n); + + height += (h + parseInt(computedStyles.marginTop, 10) + parseInt(computedStyles.marginBottom, 10)); + } + } + return height; + }, + + render: function () { + /* + * this.props.collapsable is set in NavBar when a eventKey is supplied. + */ + var classes = this.props.collapsable ? this.getCollapsableClassSet() : {}; + /* + * prevent duplicating navbar-collapse call if passed as prop. kind of overkill... good cadidate to have check implemented as a util that can + * also be used elsewhere. + */ + if (this.props.className == undefined || this.props.className.split(" ").indexOf('navbar-collapse') == -1) + classes['navbar-collapse'] = this.props.collapsable; + + return ( +
+ {ValidComponentChildren.map(this.props.children, (this.props.collapsable) ? this.renderCollapsableNavChildren : this.renderChildren )} +
+ ); + }, + + getChildActiveProp: function (child) { + if (child.props.active) { + return true; + } + if (this.props.activeKey != null) { + if (child.props.eventKey == this.props.activeKey) { + return true; + } + } + if (this.props.activeHref != null) { + if (child.props.href === this.props.activeHref) { + return true; + } + } + + return child.props.active; + }, + + renderChildren: function (child, index) { + var key = child.key ? child.key : index; + return cloneWithProps( + child, + { + activeKey: this.props.activeKey, + activeHref: this.props.activeHref, + ref: 'nocollapse_' + key, + key: key, + navItem: true + } + ); + }, + + renderCollapsableNavChildren: function (child, index) { + var key = child.key ? child.key : index; + return cloneWithProps( + child, + { + active: this.getChildActiveProp(child), + activeKey: this.props.activeKey, + activeHref: this.props.activeHref, + onSelect: createChainedFunction(child.props.onSelect, this.props.onSelect), + ref: 'collapsable_' + key, + key: key, + navItem: true + } + ); + } +}); + +module.exports = CollapsableNav; diff --git a/test/CollapsableNavSpec.jsx b/test/CollapsableNavSpec.jsx new file mode 100644 index 0000000000..e559ce810d --- /dev/null +++ b/test/CollapsableNavSpec.jsx @@ -0,0 +1,89 @@ +/*global describe, beforeEach, afterEach, it, assert */ + +var React = require('react'); +var ReactTestUtils = require('react/lib/ReactTestUtils'); +var Navbar = require('../lib/Navbar'); +var CollapsableNav = require('../lib/CollapsableNav'); +var Nav = require('../lib/Nav'); +var NavItem = require('../lib/NavItem'); + +describe('CollapsableNav', function () { + it('Should create div and add collapse class', function () { + var instance = ReactTestUtils.renderIntoDocument( + + + + + + ); + assert.ok(ReactTestUtils.findRenderedDOMComponentWithClass(instance, 'navbar-collapse')); + }); + + it('Should handle multiple Nav elements', function () { + var instance = ReactTestUtils.renderIntoDocument( + + + + + + + ); + assert.ok(ReactTestUtils.findRenderedComponentWithType(instance.refs.collapsable_object.refs.collapsable_0, Nav)); + assert.ok(ReactTestUtils.findRenderedComponentWithType(instance.refs.collapsable_object.refs.collapsable_1, Nav)); + }); + + it('Should just render children and move along if not in ', function () { + var instance = ReactTestUtils.renderIntoDocument( + + + + ); + assert.notOk(instance.getDOMNode().className.match(/\navbar-collapse\b/)); + assert.ok(ReactTestUtils.findRenderedComponentWithType(instance.refs.nocollapse_0, Nav)); + }); + + it('Should retain childrens classes set by className', function () { + var instance = ReactTestUtils.renderIntoDocument( + + + + + + ); + assert.ok(ReactTestUtils.findRenderedDOMComponentWithClass(instance.refs.collapsable_object.refs.collapsable_0, 'foo')); + assert.ok(ReactTestUtils.findRenderedDOMComponentWithClass(instance.refs.collapsable_object.refs.collapsable_0, 'bar')); + assert.ok(ReactTestUtils.findRenderedDOMComponentWithClass(instance.refs.collapsable_object.refs.collapsable_0, 'baz')); + }); + + it('Should should not duplicate classes', function () { + var instance = ReactTestUtils.renderIntoDocument( + + + + + + ); + var classDOM = ReactTestUtils.findRenderedDOMComponentWithTag(instance.refs.collapsable_object, 'DIV').props.className + , class_array = classDOM.split(" ") + , idx = class_array.indexOf('navbar-collapse'); + assert.equal(class_array.indexOf('navbar-collapse',idx+1), -1); + }); +}); diff --git a/tools/amd/index.js b/tools/amd/index.js index bc848179c6..d410f24e0c 100644 --- a/tools/amd/index.js +++ b/tools/amd/index.js @@ -16,6 +16,7 @@ define(function (require) { Carousel: require('./lib/Carousel'), CarouselItem: require('./lib/CarouselItem'), Col: require('./lib/Col'), + CollapsableNav: require('./lib/CollapsableNav'), CollapsableMixin: require('./lib/CollapsableMixin'), DropdownButton: require('./lib/DropdownButton'), DropdownMenu: require('./lib/DropdownMenu'), diff --git a/tools/cjs/main.js b/tools/cjs/main.js index 9ca6c92d8a..754cdd2d6d 100644 --- a/tools/cjs/main.js +++ b/tools/cjs/main.js @@ -11,6 +11,7 @@ module.exports = { Carousel: require('./Carousel'), CarouselItem: require('./CarouselItem'), Col: require('./Col'), + CollapsableNav: require('./CollapsableNav'), CollapsableMixin: require('./CollapsableMixin'), DropdownButton: require('./DropdownButton'), DropdownMenu: require('./DropdownMenu'),