From 5f0ac647e883510fddd30e6fdfd76107d3fc6c74 Mon Sep 17 00:00:00 2001 From: Caroline Taymor Date: Mon, 10 Aug 2015 16:25:22 -0700 Subject: [PATCH] [added] Implements a generalized left-aligned version of tabs as an option on tabs, via the position and tabWidth props. Signed-off-by: Dominick Reinhold --- docs/examples/LeftTabs.js | 9 ++ docs/src/ComponentsPage.js | 4 + docs/src/Samples.js | 1 + src/Tabs.js | 170 ++++++++++++++++++++++++++++++++----- src/styleMaps.js | 3 +- test/TabsSpec.js | 164 ++++++++++++++++++++++++++++++++++- 6 files changed, 324 insertions(+), 27 deletions(-) create mode 100644 docs/examples/LeftTabs.js 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({

Set the animation prop to false

+

Left tabs

+

Set position to 'left'. Optionally, tabWidth can be passed the number of columns for the tabs.

+ +

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;