diff --git a/docs/examples/LeftTabs.js b/docs/examples/LeftTabs.js
new file mode 100644
index 0000000000..ee0146cce7
--- /dev/null
+++ b/docs/examples/LeftTabs.js
@@ -0,0 +1,9 @@
+const tabsInstance = (
+
+ Tab 1 content
+ Tab 2 content
+ Tab 3 content
+
+);
+
+React.render(tabsInstance, mountNode);
diff --git a/docs/src/ComponentsPage.js b/docs/src/ComponentsPage.js
index c0a60d00a4..69047afdf9 100644
--- a/docs/src/ComponentsPage.js
+++ b/docs/src/ComponentsPage.js
@@ -529,6 +529,10 @@ const ComponentsPage = React.createClass({
Extends tabbed navigation
This plugin extends the tabbed navigation component to add tabbable areas.
diff --git a/docs/src/Samples.js b/docs/src/Samples.js
index 14ed90a163..66e141a051 100644
--- a/docs/src/Samples.js
+++ b/docs/src/Samples.js
@@ -64,6 +64,7 @@ export default {
TabsUncontrolled: require('fs').readFileSync(__dirname + '/../examples/TabsUncontrolled.js', 'utf8'),
TabsControlled: require('fs').readFileSync(__dirname + '/../examples/TabsControlled.js', 'utf8'),
TabsNoAnimation: require('fs').readFileSync(__dirname + '/../examples/TabsNoAnimation.js', 'utf8'),
+ LeftTabs: require('fs').readFileSync(__dirname + '/../examples/LeftTabs.js', 'utf8'),
PagerDefault: require('fs').readFileSync(__dirname + '/../examples/PagerDefault.js', 'utf8'),
PagerAligned: require('fs').readFileSync(__dirname + '/../examples/PagerAligned.js', 'utf8'),
PagerDisabled: require('fs').readFileSync(__dirname + '/../examples/PagerDisabled.js', 'utf8'),
diff --git a/src/Tabs.js b/src/Tabs.js
index d8a4718be4..38ad3a4ed2 100644
--- a/src/Tabs.js
+++ b/src/Tabs.js
@@ -1,9 +1,15 @@
import React, { cloneElement } from 'react';
-import ValidComponentChildren from './utils/ValidComponentChildren';
+
+import Col from './Col';
+import Grid from './Grid';
import Nav from './Nav';
import NavItem from './NavItem';
+import Row from './Row';
+import styleMaps from './styleMaps';
+
+import ValidComponentChildren from './utils/ValidComponentChildren';
-let panelId = (props, child) => child.props.id ? child.props.id : props.id && (props.id + '___panel___' + child.props.eventKey);
+let paneId = (props, child) => child.props.id ? child.props.id : props.id && (props.id + '___pane___' + child.props.eventKey);
let tabId = (props, child) => child.props.id ? child.props.id + '___tab' : props.id && (props.id + '___tab___' + child.props.eventKey);
function getDefaultActiveKeyFromChildren(children) {
@@ -22,16 +28,44 @@ const Tabs = React.createClass({
propTypes: {
activeKey: React.PropTypes.any,
defaultActiveKey: React.PropTypes.any,
+ /**
+ * Navigation style for tabs
+ *
+ * If not specified, it will be treated as `'tabs'` when vertically
+ * positioned and `'pills'` when horizontally positioned.
+ */
bsStyle: React.PropTypes.oneOf(['tabs', 'pills']),
animation: React.PropTypes.bool,
id: React.PropTypes.string,
- onSelect: React.PropTypes.func
+ onSelect: React.PropTypes.func,
+ position: React.PropTypes.oneOf(['top', 'left', 'right']),
+ /**
+ * Number of grid columns for the tabs if horizontally positioned
+ *
+ * This accepts either a single width or a mapping of size to width.
+ */
+ tabWidth: React.PropTypes.oneOfType([
+ React.PropTypes.number,
+ React.PropTypes.object
+ ]),
+ /**
+ * Number of grid columns for the panes if horizontally positioned
+ *
+ * This accepts either a single width or a mapping of size to width. If not
+ * specified, it will be treated as `styleMaps.GRID_COLUMNS` minus
+ * `tabWidth`.
+ */
+ paneWidth: React.PropTypes.oneOfType([
+ React.PropTypes.number,
+ React.PropTypes.object
+ ])
},
getDefaultProps() {
return {
- bsStyle: 'tabs',
- animation: true
+ animation: true,
+ tabWidth: 2,
+ position: 'top'
};
},
@@ -73,26 +107,89 @@ const Tabs = React.createClass({
id,
className,
style, // eslint-disable-line react/prop-types
- ...props } = this.props;
+ position,
+ bsStyle,
+ tabWidth,
+ paneWidth,
+ children,
+ ...props
+ } = this.props;
- function renderTabIfSet(child) {
- return child.props.title != null ? this.renderTab(child) : null;
+ const isHorizontal = position === 'left' || position === 'right';
+
+ if (bsStyle == null) {
+ bsStyle = isHorizontal ? 'pills' : 'tabs';
}
- let nav = (
-
- );
+ const containerProps = {id, className, style};
- return (
-
- {nav}
-
- {ValidComponentChildren.map(this.props.children, this.renderPane)}
+ const tabsProps = {
+ ...props,
+ bsStyle,
+ stacked: isHorizontal,
+ activeKey: this.getActiveKey(),
+ onSelect: this.handleSelect,
+ ref: 'tabs',
+ role: 'tablist'
+ };
+ const childTabs = ValidComponentChildren.map(children, this.renderTab);
+
+ const panesProps = {
+ className: 'tab-content',
+ ref: 'panes'
+ };
+ const childPanes = ValidComponentChildren.map(children, this.renderPane);
+
+ if (isHorizontal) {
+ const {tabsColProps, panesColProps} =
+ this.getColProps({tabWidth, paneWidth});
+
+ const tabs = (
+
+ {childTabs}
+
+ );
+ const panes = (
+
+ {childPanes}
+
+ );
+
+ let body;
+ if (position === 'left') {
+ body = (
+
+ {tabs}
+ {panes}
+
+ );
+ } else {
+ body = (
+
+ {panes}
+ {tabs}
+
+ );
+ }
+
+ return (
+
+ {body}
+
+ );
+ } else {
+ return (
+
+
+
+
+ {childPanes}
+
-
- );
+ );
+ }
},
getActiveKey() {
@@ -111,7 +208,7 @@ const Tabs = React.createClass({
child,
{
active: shouldPaneBeSetActive && (thereIsNoActivePane || !this.props.animation),
- id: panelId(this.props, child),
+ id: paneId(this.props, child),
'aria-labelledby': tabId(this.props, child),
key: child.key ? child.key : index,
animation: this.props.animation,
@@ -121,13 +218,17 @@ const Tabs = React.createClass({
},
renderTab(child) {
- let {eventKey, title, disabled } = child.props;
+ if (child.props.title == null) {
+ return null;
+ }
+
+ let {eventKey, title, disabled} = child.props;
return (
{title}
@@ -135,6 +236,29 @@ const Tabs = React.createClass({
);
},
+ getColProps({tabWidth, paneWidth}) {
+ let tabsColProps;
+ if (tabWidth instanceof Object) {
+ tabsColProps = tabWidth;
+ } else {
+ tabsColProps = {xs: tabWidth};
+ }
+
+ let panesColProps;
+ if (paneWidth == null) {
+ panesColProps = {};
+ Object.keys(tabsColProps).forEach(function (size) {
+ panesColProps[size] = styleMaps.GRID_COLUMNS - tabsColProps[size];
+ });
+ } else if (paneWidth instanceof Object) {
+ panesColProps = paneWidth;
+ } else {
+ panesColProps = {xs: paneWidth};
+ }
+
+ return {tabsColProps, panesColProps};
+ },
+
shouldComponentUpdate() {
// Defer any updates to this component during the `onSelect` handler.
return !this._isChanging;
diff --git a/src/styleMaps.js b/src/styleMaps.js
index b64252732a..5a0f1bb859 100644
--- a/src/styleMaps.js
+++ b/src/styleMaps.js
@@ -306,7 +306,8 @@ const styleMaps = {
'menu-right',
'menu-down',
'menu-up'
- ]
+ ],
+ GRID_COLUMNS: 12
};
export default styleMaps;
diff --git a/test/TabsSpec.js b/test/TabsSpec.js
index d866887cb7..e4242d91d5 100644
--- a/test/TabsSpec.js
+++ b/test/TabsSpec.js
@@ -1,10 +1,16 @@
import React from 'react';
import ReactTestUtils from 'react/lib/ReactTestUtils';
-import Tabs from '../src/Tabs';
-import Tab from '../src/Tab';
-import NavItem from '../src/NavItem';
+
+import Col from '../src/Col';
+import Grid from '../src/Grid';
import Nav from '../src/Nav';
+import NavItem from '../src/NavItem';
+import Row from '../src/Row';
+import Tab from '../src/Tab';
+import Tabs from '../src/Tabs';
+
import ValidComponentChildren from '../src/utils/ValidComponentChildren';
+
import { render } from './helpers';
describe('Tabs', function () {
@@ -218,6 +224,158 @@ describe('Tabs', function () {
assert.equal(tabs.refs.tabs.props.activeKey, 2);
});
+
+ describe('when the position prop is not provided', function() {
+ let instance;
+
+ beforeEach(function() {
+ instance = ReactTestUtils.renderIntoDocument(
+
+ Tab content
+
+ );
+ });
+
+ it('doesn\'t stack the tabs', function () {
+ let nav = ReactTestUtils.findRenderedComponentWithType(instance, Nav);
+
+ expect(nav.props.bsStyle).to.equal('tabs');
+ expect(nav.props.stacked).to.not.be.ok;
+ });
+
+ it('doesn\'t apply column styling', function () {
+ let tabs = instance.refs.tabs;
+ let panes = instance.refs.panes;
+
+ expect(React.findDOMNode(tabs).className).to.not.match(/\bcol\b/);
+ expect(React.findDOMNode(panes).className).to.not.match(/\bcol\b/);
+ });
+
+ it('doesn\'t render grid elements', function () {
+ const grids = ReactTestUtils.scryRenderedComponentsWithType(
+ instance, Grid
+ );
+ const rows = ReactTestUtils.scryRenderedComponentsWithType(
+ instance, Row
+ );
+ const cols = ReactTestUtils.scryRenderedComponentsWithType(
+ instance, Col
+ );
+
+ expect(grids).to.be.empty;
+ expect(rows).to.be.empty;
+ expect(cols).to.be.empty;
+ });
+ });
+
+
+ describe('when the position prop is "left"', function() {
+ describe('when tabWidth is not provided', function() {
+ let instance;
+
+ beforeEach(function () {
+ instance = ReactTestUtils.renderIntoDocument(
+
+ Tab content
+
+ );
+ });
+
+ it('Should stack the tabs', function () {
+ let nav = ReactTestUtils.findRenderedComponentWithType(instance, Nav);
+
+ expect(nav.props.bsStyle).to.equal('pills');
+ expect(nav.props.stacked).to.be.ok;
+ });
+
+ it('Should have a left nav with a width of 2', function() {
+ let tabs = instance.refs.tabs;
+ let panes = instance.refs.panes;
+
+ expect(React.findDOMNode(tabs).className).to.match(/\bcol-xs-2\b/);
+ expect(React.findDOMNode(panes).className).to.match(/\bcol-xs-10\b/);
+ });
+
+ it('renders grid elements', function () {
+ const grids = ReactTestUtils.scryRenderedComponentsWithType(
+ instance, Grid
+ );
+ const rows = ReactTestUtils.scryRenderedComponentsWithType(
+ instance, Row
+ );
+ const cols = ReactTestUtils.scryRenderedComponentsWithType(
+ instance, Col
+ );
+
+ expect(grids).to.have.length(1);
+ expect(rows).to.have.length(1);
+ expect(cols).to.have.length(2);
+ });
+ });
+
+ describe('when only tabWidth is provided', function() {
+ it('Should have a left nav with the width that was provided', function() {
+ let instance = ReactTestUtils.renderIntoDocument(
+
+ Tab content
+
+ );
+
+ let tabs = instance.refs.tabs;
+ let panes = instance.refs.panes;
+
+ expect(React.findDOMNode(tabs).className).to.match(/\bcol-xs-3\b/);
+ expect(React.findDOMNode(panes).className).to.match(/\bcol-xs-9\b/);
+ });
+ });
+
+ describe('when simple tabWidth and paneWidth are provided', function() {
+ let instance;
+
+ beforeEach(function () {
+ instance = ReactTestUtils.renderIntoDocument(
+
+ Tab content
+
+ );
+ });
+
+ it('Should have the provided widths', function() {
+ let tabs = instance.refs.tabs;
+ let panes = instance.refs.panes;
+
+ expect(React.findDOMNode(tabs).className).to.match(/\bcol-xs-4\b/);
+ expect(React.findDOMNode(panes).className).to.match(/\bcol-xs-7\b/);
+ });
+ });
+
+ describe('when complex tabWidth and paneWidth are provided', function() {
+ let instance;
+
+ beforeEach(function () {
+ instance = ReactTestUtils.renderIntoDocument(
+
+ Tab content
+
+ );
+ });
+
+ it('Should have the provided widths', function() {
+ let tabs = instance.refs.tabs;
+ let panes = instance.refs.panes;
+
+ expect(React.findDOMNode(tabs).className)
+ .to.match(/\bcol-xs-4\b/).and.to.match(/\bcol-md-3\b/);
+ expect(React.findDOMNode(panes).className)
+ .to.match(/\bcol-xs-7\b/).and.to.match(/\bcol-md-8\b/);
+ });
+ });
+ });
+
describe('animation', function () {
let mountPoint;