Skip to content

Commit 2957ede

Browse files
ajainarayananeddywashere
authored andcommitted
feat(Tabs): add TabContent & TabPane components (#131)
* feat(tabs): Adds TabContent and TabPane components #72 * feat(Tab): Adds docs for Tab (TabContent and TabPane) component * feat(Tab): Adds test for Tab component * feat(Tab): Adds \/tabs route to webpack.dev.config.js * Fixes TabContent and TabPane to use contextTypes for communicating active Tab * Updates Tabs.spec.js based on changes made to TabContent and TabPane + pushes code coverage to 100% * Adds extra check in TabContent while setting activeTab + moves classnames out to const variables from jsx
1 parent af874bc commit 2957ede

9 files changed

Lines changed: 245 additions & 2 deletions

File tree

docs/lib/Components/TabsPage.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/* eslint react/no-multi-comp: 0, react/prop-types: 0 */
2+
import React from 'react';
3+
import { PrismCode } from 'react-prism';
4+
import Helmet from 'react-helmet';
5+
6+
import TabsExample from '../examples/Tabs';
7+
const TabsExampleSource = require('!!raw!../examples/Tabs');
8+
9+
export default function TabsPage() {
10+
return (
11+
<div>
12+
<Helmet title="Tabs" />
13+
<h3>Tabs</h3>
14+
<hr />
15+
<div className="docs-example">
16+
<TabsExample />
17+
</div>
18+
<pre>
19+
<PrismCode className="language-jsx">
20+
{TabsExampleSource}
21+
</PrismCode>
22+
</pre>
23+
</div>
24+
);
25+
}

docs/lib/Components/index.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ const ComponentLink = (props) => {
1111
</NavItem>
1212
);
1313
};
14-
14+
const propTypes = {
15+
children: React.PropTypes.node
16+
};
1517

1618
class Components extends React.Component {
1719
constructor(props) {
@@ -95,6 +97,10 @@ class Components extends React.Component {
9597
name: 'Pagination',
9698
to: '/components/pagination/'
9799
},
100+
{
101+
name: 'Tabs',
102+
to: '/components/tabs/'
103+
},
98104
]
99105
};
100106
}
@@ -123,5 +129,5 @@ class Components extends React.Component {
123129
);
124130
}
125131
}
126-
132+
Components.propTypes = propTypes;
127133
export default Components;

docs/lib/examples/Tabs.js

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import React from 'react';
2+
import { TabContent, TabPane, Nav, NavItem, NavLink, Card, Button, CardTitle, CardText, Row, Col } from 'reactstrap';
3+
import classnames from 'classnames';
4+
5+
export default class Example extends React.Component {
6+
constructor(props) {
7+
super(props);
8+
9+
this.toggle = this.toggle.bind(this);
10+
this.state = {
11+
activeTab: '1'
12+
};
13+
}
14+
15+
toggle(tab) {
16+
if (this.state.activeTab !== tab) {
17+
this.setState({
18+
activeTab: tab
19+
});
20+
}
21+
}
22+
render() {
23+
return (
24+
<div>
25+
<Nav tabs>
26+
<NavItem>
27+
<NavLink
28+
className={classnames({ active: this.state.activeTab === '1' })}
29+
onClick={() => { this.toggle('1'); }}
30+
>
31+
Tab1
32+
</NavLink>
33+
</NavItem>
34+
<NavItem>
35+
<NavLink
36+
className={classnames({ active: this.state.activeTab === '2' })}
37+
onClick={() => { this.toggle('2'); }}
38+
>
39+
Moar Tabs
40+
</NavLink>
41+
</NavItem>
42+
</Nav>
43+
<TabContent activeTab={this.state.activeTab}>
44+
<TabPane tabId="1">
45+
<Row>
46+
<Col sm="12">
47+
<h4>Tab 1 Contents</h4>
48+
</Col>
49+
</Row>
50+
</TabPane>
51+
<TabPane tabId="2">
52+
<Row>
53+
<Col sm="6">
54+
<Card block>
55+
<CardTitle>Special Title Treatment</CardTitle>
56+
<CardText>With supporting text below as a natural lead-in to additional content.</CardText>
57+
<Button>Go somewhere</Button>
58+
</Card>
59+
</Col>
60+
<Col sm="6">
61+
<Card block>
62+
<CardTitle>Special Title Treatment</CardTitle>
63+
<CardText>With supporting text below as a natural lead-in to additional content.</CardText>
64+
<Button>Go somewhere</Button>
65+
</Card>
66+
</Col>
67+
</Row>
68+
</TabPane>
69+
</TabContent>
70+
</div>
71+
);
72+
}
73+
}

docs/lib/routes.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import ModalsPage from './Components/ModalsPage';
2020
import CardPage from './Components/CardPage';
2121
import TablesPage from './Components/TablesPage';
2222
import PaginationPage from './Components/PaginationPage';
23+
import TabsPage from './Components/TabsPage';
2324
import NotFound from './NotFound';
2425
import Components from './Components';
2526
import UI from './UI';
@@ -48,6 +49,7 @@ const routes = (
4849
<Route path="navbar/" component={NavbarPage} />
4950
<Route path="media/" component={MediaPage} />
5051
<Route path="pagination/" component={PaginationPage} />
52+
<Route path="tabs/" component={TabsPage} />
5153
</Route>
5254
<Route path="*" component={NotFound} />
5355
</Route>

src/TabContent.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import React, { PropTypes, Component } from 'react';
2+
import classnames from 'classnames';
3+
4+
const propTypes = {
5+
children: PropTypes.node,
6+
activeTab: PropTypes.any,
7+
className: PropTypes.string
8+
};
9+
10+
const childContextTypes = {
11+
activeTabId: PropTypes.any
12+
};
13+
14+
export default class TabContent extends Component {
15+
constructor(props) {
16+
super(props);
17+
this.state = {
18+
activeTab: this.props.activeTab
19+
};
20+
}
21+
getChildContext() {
22+
return {
23+
activeTabId: this.state.activeTab
24+
};
25+
}
26+
componentWillReceiveProps(nextProps) {
27+
if (this.state.activeTab !== nextProps.activeTab) {
28+
this.setState({
29+
activeTab: nextProps.activeTab
30+
});
31+
}
32+
}
33+
render() {
34+
const classes = classnames('tab-content', this.props.className);
35+
return (
36+
<div className={classes}>
37+
{ this.props.children }
38+
</div>
39+
);
40+
}
41+
}
42+
TabContent.propTypes = propTypes;
43+
TabContent.childContextTypes = childContextTypes;

src/TabPane.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
2+
import React, { PropTypes } from 'react';
3+
import classnames from 'classnames';
4+
5+
const propTypes = {
6+
children: PropTypes.node,
7+
className: PropTypes.string,
8+
tabId: PropTypes.any
9+
};
10+
const contextTypes = {
11+
activeTabId: PropTypes.any
12+
};
13+
14+
export default function TabPane(props, context) {
15+
const {
16+
className,
17+
tabId,
18+
children,
19+
...attributes
20+
} = props;
21+
const classes = classnames('tab-pane', className, { active: tabId === context.activeTabId });
22+
return (
23+
<div {...attributes} className={classes}>
24+
{children}
25+
</div>
26+
);
27+
}
28+
TabPane.propTypes = propTypes;
29+
TabPane.contextTypes = contextTypes;

src/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ import Media from './Media';
5858
import Pagination from './Pagination';
5959
import PaginationItem from './PaginationItem';
6060
import PaginationLink from './PaginationLink';
61+
import TabContent from './TabContent';
62+
import TabPane from './TabPane';
6163

6264
export {
6365
Container,
@@ -120,4 +122,6 @@ export {
120122
Pagination,
121123
PaginationItem,
122124
PaginationLink,
125+
TabContent,
126+
TabPane
123127
};

test/Tabs.spec.js

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/* eslint react/no-multi-comp: 0, react/prop-types: 0 */
2+
import React from 'react';
3+
import ReactDOM from 'react-dom';
4+
5+
import { mount } from 'enzyme';
6+
import { Nav, NavItem, NavLink, TabContent, TabPane } from 'reactstrap';
7+
import classnames from 'classnames';
8+
// Not sure if this is correct but didn't want to repeat a whole bunch of code.
9+
import TabsExample from '../docs/lib/examples/Tabs';
10+
11+
describe('Tabs', () => {
12+
it('should render', () => {
13+
let tab1 = mount(<TabsExample />);
14+
expect(tab1.find('.nav .nav-tabs').length).toBe(1);
15+
expect(tab1.find('.nav .nav-item').length).toBe(2);
16+
expect(tab1.find('.tab-content').length).toBe(1);
17+
expect(tab1.find('.tab-pane').length).toBe(2);
18+
});
19+
it('should have tab1 as active', () => {
20+
let tab1 = mount(<TabsExample />);
21+
expect(tab1.find('.nav .nav-item .nav-link').at(0).hasClass('active')).toBe(true);
22+
expect(tab1.find('.nav .nav-item .nav-link').at(1).hasClass('active')).toBe(false);
23+
expect(tab1.find('.tab-content .tab-pane').at(0).hasClass('active')).toBe(true);
24+
});
25+
it('should switch to tab2 as active when clicked', () => {
26+
let tab1 = mount(<TabsExample />);
27+
expect(tab1.find('.nav .nav-item .nav-link').at(0).hasClass('active')).toBe(true);
28+
expect(tab1.find('.nav .nav-item .nav-link').at(1).hasClass('active')).toBe(false);
29+
expect(tab1.find('.tab-content .tab-pane').at(0).hasClass('active')).toBe(true);
30+
tab1.find('.nav .nav-item .nav-link').at(1).simulate('click');
31+
expect(tab1.find('.nav .nav-item .nav-link').at(0).hasClass('active')).toBe(false);
32+
expect(tab1.find('.nav .nav-item .nav-link').at(1).hasClass('active')).toBe(true);
33+
expect(tab1.find('.tab-content .tab-pane').at(1).hasClass('active')).toBe(true);
34+
tab1.find('.nav .nav-item .nav-link').at(0).simulate('click');
35+
});
36+
it('should show no active tabs if active tab id is unknown', () => {
37+
let tab1 = mount(<TabsExample />);
38+
const instance = tab1.instance();
39+
expect(instance instanceof TabsExample).toBe(true);
40+
instance.toggle('3');
41+
/* Not sure if this is what we want. Toggling to an unknown tab id should
42+
render all tabs as inactive and should show no content.
43+
This could be a warning during development that the user is not having a proper tab ids.
44+
NOTE: Should this be different?
45+
*/
46+
expect(tab1.find('.nav .nav-item .nav-link').at(0).hasClass('active')).toBe(false);
47+
expect(tab1.find('.nav .nav-item .nav-link').at(1).hasClass('active')).toBe(false);
48+
expect(tab1.find('.tab-content .tab-pane').at(0).hasClass('active')).toBe(false);
49+
});
50+
it('should do nothing clicking on the same tab', () => {
51+
let tab1 = mount(<TabsExample />);
52+
expect(tab1.find('.nav .nav-item .nav-link').at(0).hasClass('active')).toBe(true);
53+
expect(tab1.find('.nav .nav-item .nav-link').at(1).hasClass('active')).toBe(false);
54+
expect(tab1.find('.tab-content .tab-pane').at(0).hasClass('active')).toBe(true);
55+
tab1.find('.nav .nav-item .nav-link').at(0).simulate('click');
56+
expect(tab1.find('.nav .nav-item .nav-link').at(0).hasClass('active')).toBe(true);
57+
expect(tab1.find('.nav .nav-item .nav-link').at(1).hasClass('active')).toBe(false);
58+
expect(tab1.find('.tab-content .tab-pane').at(0).hasClass('active')).toBe(true);
59+
});
60+
});

webpack.dev.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ var paths = [
3232
'/components/tables/',
3333
'/components/media/',
3434
'/components/pagination/',
35+
'/components/tabs/',
3536
'/404.html'
3637
];
3738

0 commit comments

Comments
 (0)