Skip to content

Commit 5f0ac64

Browse files
Caroline TaymorAugust Toman-Yih
Caroline Taymor
authored and
August Toman-Yih
committed
[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 <[email protected]>
1 parent e2c4c6a commit 5f0ac64

File tree

6 files changed

+324
-27
lines changed

6 files changed

+324
-27
lines changed

docs/examples/LeftTabs.js

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
const tabsInstance = (
2+
<Tabs defaultActiveKey={2} position='left' tabWidth={3}>
3+
<Tab eventKey={1} title='Tab 1'>Tab 1 content</Tab>
4+
<Tab eventKey={2} title='Tab 2'>Tab 2 content</Tab>
5+
<Tab eventKey={3} title='Tab 3' disabled>Tab 3 content</Tab>
6+
</Tabs>
7+
);
8+
9+
React.render(tabsInstance, mountNode);

docs/src/ComponentsPage.js

+4
Original file line numberDiff line numberDiff line change
@@ -529,6 +529,10 @@ const ComponentsPage = React.createClass({
529529
<p>Set the <code>animation</code> prop to <code>false</code></p>
530530
<ReactPlayground codeText={Samples.TabsNoAnimation} exampleClassName='bs-example-tabs' />
531531

532+
<h3><Anchor id='left-tabs'>Left tabs</Anchor></h3>
533+
<p>Set <code>position</code> to <code>'left'</code>. Optionally, <code>tabWidth</code> can be passed the number of columns for the tabs.</p>
534+
<ReactPlayground codeText={Samples.LeftTabs} exampleClassName='bs-example-tabs' />
535+
532536
<div className='bs-callout bs-callout-info'>
533537
<h4>Extends tabbed navigation</h4>
534538
<p>This plugin extends the <a href='#navs'>tabbed navigation component</a> to add tabbable areas.</p>

docs/src/Samples.js

+1
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ export default {
6464
TabsUncontrolled: require('fs').readFileSync(__dirname + '/../examples/TabsUncontrolled.js', 'utf8'),
6565
TabsControlled: require('fs').readFileSync(__dirname + '/../examples/TabsControlled.js', 'utf8'),
6666
TabsNoAnimation: require('fs').readFileSync(__dirname + '/../examples/TabsNoAnimation.js', 'utf8'),
67+
LeftTabs: require('fs').readFileSync(__dirname + '/../examples/LeftTabs.js', 'utf8'),
6768
PagerDefault: require('fs').readFileSync(__dirname + '/../examples/PagerDefault.js', 'utf8'),
6869
PagerAligned: require('fs').readFileSync(__dirname + '/../examples/PagerAligned.js', 'utf8'),
6970
PagerDisabled: require('fs').readFileSync(__dirname + '/../examples/PagerDisabled.js', 'utf8'),

src/Tabs.js

+147-23
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
import React, { cloneElement } from 'react';
2-
import ValidComponentChildren from './utils/ValidComponentChildren';
2+
3+
import Col from './Col';
4+
import Grid from './Grid';
35
import Nav from './Nav';
46
import NavItem from './NavItem';
7+
import Row from './Row';
8+
import styleMaps from './styleMaps';
9+
10+
import ValidComponentChildren from './utils/ValidComponentChildren';
511

6-
let panelId = (props, child) => child.props.id ? child.props.id : props.id && (props.id + '___panel___' + child.props.eventKey);
12+
let paneId = (props, child) => child.props.id ? child.props.id : props.id && (props.id + '___pane___' + child.props.eventKey);
713
let tabId = (props, child) => child.props.id ? child.props.id + '___tab' : props.id && (props.id + '___tab___' + child.props.eventKey);
814

915
function getDefaultActiveKeyFromChildren(children) {
@@ -22,16 +28,44 @@ const Tabs = React.createClass({
2228
propTypes: {
2329
activeKey: React.PropTypes.any,
2430
defaultActiveKey: React.PropTypes.any,
31+
/**
32+
* Navigation style for tabs
33+
*
34+
* If not specified, it will be treated as `'tabs'` when vertically
35+
* positioned and `'pills'` when horizontally positioned.
36+
*/
2537
bsStyle: React.PropTypes.oneOf(['tabs', 'pills']),
2638
animation: React.PropTypes.bool,
2739
id: React.PropTypes.string,
28-
onSelect: React.PropTypes.func
40+
onSelect: React.PropTypes.func,
41+
position: React.PropTypes.oneOf(['top', 'left', 'right']),
42+
/**
43+
* Number of grid columns for the tabs if horizontally positioned
44+
*
45+
* This accepts either a single width or a mapping of size to width.
46+
*/
47+
tabWidth: React.PropTypes.oneOfType([
48+
React.PropTypes.number,
49+
React.PropTypes.object
50+
]),
51+
/**
52+
* Number of grid columns for the panes if horizontally positioned
53+
*
54+
* This accepts either a single width or a mapping of size to width. If not
55+
* specified, it will be treated as `styleMaps.GRID_COLUMNS` minus
56+
* `tabWidth`.
57+
*/
58+
paneWidth: React.PropTypes.oneOfType([
59+
React.PropTypes.number,
60+
React.PropTypes.object
61+
])
2962
},
3063

3164
getDefaultProps() {
3265
return {
33-
bsStyle: 'tabs',
34-
animation: true
66+
animation: true,
67+
tabWidth: 2,
68+
position: 'top'
3569
};
3670
},
3771

@@ -73,26 +107,89 @@ const Tabs = React.createClass({
73107
id,
74108
className,
75109
style, // eslint-disable-line react/prop-types
76-
...props } = this.props;
110+
position,
111+
bsStyle,
112+
tabWidth,
113+
paneWidth,
114+
children,
115+
...props
116+
} = this.props;
77117

78-
function renderTabIfSet(child) {
79-
return child.props.title != null ? this.renderTab(child) : null;
118+
const isHorizontal = position === 'left' || position === 'right';
119+
120+
if (bsStyle == null) {
121+
bsStyle = isHorizontal ? 'pills' : 'tabs';
80122
}
81123

82-
let nav = (
83-
<Nav {...props} activeKey={this.getActiveKey()} onSelect={this.handleSelect} ref="tabs" role="tablist">
84-
{ValidComponentChildren.map(this.props.children, renderTabIfSet, this)}
85-
</Nav>
86-
);
124+
const containerProps = {id, className, style};
87125

88-
return (
89-
<div id={id} className={className} style={style}>
90-
{nav}
91-
<div className="tab-content" ref="panes">
92-
{ValidComponentChildren.map(this.props.children, this.renderPane)}
126+
const tabsProps = {
127+
...props,
128+
bsStyle,
129+
stacked: isHorizontal,
130+
activeKey: this.getActiveKey(),
131+
onSelect: this.handleSelect,
132+
ref: 'tabs',
133+
role: 'tablist'
134+
};
135+
const childTabs = ValidComponentChildren.map(children, this.renderTab);
136+
137+
const panesProps = {
138+
className: 'tab-content',
139+
ref: 'panes'
140+
};
141+
const childPanes = ValidComponentChildren.map(children, this.renderPane);
142+
143+
if (isHorizontal) {
144+
const {tabsColProps, panesColProps} =
145+
this.getColProps({tabWidth, paneWidth});
146+
147+
const tabs = (
148+
<Col componentClass={Nav} {...tabsProps} {...tabsColProps}>
149+
{childTabs}
150+
</Col>
151+
);
152+
const panes = (
153+
<Col {...panesProps} {...panesColProps}>
154+
{childPanes}
155+
</Col>
156+
);
157+
158+
let body;
159+
if (position === 'left') {
160+
body = (
161+
<Row {...containerProps}>
162+
{tabs}
163+
{panes}
164+
</Row>
165+
);
166+
} else {
167+
body = (
168+
<Row {...containerProps}>
169+
{panes}
170+
{tabs}
171+
</Row>
172+
);
173+
}
174+
175+
return (
176+
<Grid>
177+
{body}
178+
</Grid>
179+
);
180+
} else {
181+
return (
182+
<div {...containerProps}>
183+
<Nav {...tabsProps}>
184+
{childTabs}
185+
</Nav>
186+
187+
<div {...panesProps}>
188+
{childPanes}
189+
</div>
93190
</div>
94-
</div>
95-
);
191+
);
192+
}
96193
},
97194

98195
getActiveKey() {
@@ -111,7 +208,7 @@ const Tabs = React.createClass({
111208
child,
112209
{
113210
active: shouldPaneBeSetActive && (thereIsNoActivePane || !this.props.animation),
114-
id: panelId(this.props, child),
211+
id: paneId(this.props, child),
115212
'aria-labelledby': tabId(this.props, child),
116213
key: child.key ? child.key : index,
117214
animation: this.props.animation,
@@ -121,20 +218,47 @@ const Tabs = React.createClass({
121218
},
122219

123220
renderTab(child) {
124-
let {eventKey, title, disabled } = child.props;
221+
if (child.props.title == null) {
222+
return null;
223+
}
224+
225+
let {eventKey, title, disabled} = child.props;
125226

126227
return (
127228
<NavItem
128229
linkId={tabId(this.props, child)}
129230
ref={'tab' + eventKey}
130-
aria-controls={panelId(this.props, child)}
231+
aria-controls={paneId(this.props, child)}
131232
eventKey={eventKey}
132233
disabled={disabled}>
133234
{title}
134235
</NavItem>
135236
);
136237
},
137238

239+
getColProps({tabWidth, paneWidth}) {
240+
let tabsColProps;
241+
if (tabWidth instanceof Object) {
242+
tabsColProps = tabWidth;
243+
} else {
244+
tabsColProps = {xs: tabWidth};
245+
}
246+
247+
let panesColProps;
248+
if (paneWidth == null) {
249+
panesColProps = {};
250+
Object.keys(tabsColProps).forEach(function (size) {
251+
panesColProps[size] = styleMaps.GRID_COLUMNS - tabsColProps[size];
252+
});
253+
} else if (paneWidth instanceof Object) {
254+
panesColProps = paneWidth;
255+
} else {
256+
panesColProps = {xs: paneWidth};
257+
}
258+
259+
return {tabsColProps, panesColProps};
260+
},
261+
138262
shouldComponentUpdate() {
139263
// Defer any updates to this component during the `onSelect` handler.
140264
return !this._isChanging;

src/styleMaps.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,8 @@ const styleMaps = {
306306
'menu-right',
307307
'menu-down',
308308
'menu-up'
309-
]
309+
],
310+
GRID_COLUMNS: 12
310311
};
311312

312313
export default styleMaps;

0 commit comments

Comments
 (0)