Skip to content

Instantly share code, notes, and snippets.

@diegocasmo
Last active September 3, 2021 06:06
Show Gist options
  • Save diegocasmo/5cd978e9c5695aefca0c6a8a19fa4c69 to your computer and use it in GitHub Desktop.
Save diegocasmo/5cd978e9c5695aefca0c6a8a19fa4c69 to your computer and use it in GitHub Desktop.
Source code for implementing a React <Tabs/> component.
import React, {PropTypes} from 'react';
export const Tab = (props) => {
return (
<li className="tab">
<a className={`tab-link ${props.linkClassName} ${props.isActive ? 'active' : ''}`}
onClick={(event) => {
event.preventDefault();
props.onClick(props.tabIndex);
}}>
<i className={`tab-icon ${props.iconClassName}`}/>
</a>
</li>
)
}
Tab.propTypes = {
onClick : PropTypes.func,
tabIndex : PropTypes.number,
isActive : PropTypes.bool,
iconClassName: PropTypes.string.isRequired,
linkClassName: PropTypes.string.isRequired
};
import React from 'react';
import {mount} from 'enzyme';
import {Tab} from '../Tab';
describe('<Tab/>', () => {
let Component = null;
let onClickHandler = null;
beforeEach(() => {
onClickHandler = jasmine.createSpy('onClickHandler');
Component = mount(
<Tab onClick={onClickHandler}
tabIndex={2}
isActive={true}
iconClassName={'foo'}
linkClassName={'test'}/>
);
});
it('should render', () => {
expect(Component.length).toBeTruthy();
});
it('should call onClick() prop when link is clicked', () => {
Component.find('.tab-link').simulate('click', {preventDefault: () => {}});
expect(onClickHandler).toHaveBeenCalledWith(Component.props().tabIndex);
});
it("should add '.active' className to component if tab is active", () => {
const Component = mount(
<Tab isActive={true}
iconClassName={'foo'}
linkClassName={'test'}/>
);
expect(Component.find('.tab-link').hasClass('active')).toBeTruthy();
});
it("should not add '.active' className to component if tab is inactive", () => {
const Component = mount(
<Tab isActive={false}
iconClassName={'foo'}
linkClassName={'test'}/>
);
expect(Component.find('.tab-link').hasClass('active')).toBeFalsy();
});
it('should add correct className to tab link', () => {
expect(Component.find('.tab-link').hasClass(Component.props().linkClassName)).toBeTruthy();
});
it('should add correct className to tab icon', () => {
expect(Component.find('.tab-icon').hasClass(Component.props().iconClassName)).toBeTruthy();
});
});
import React, {Component, PropTypes} from 'react';
export class Tabs extends Component {
constructor(props, context) {
super(props, context);
this.state = {
activeTabIndex: this.props.defaultActiveTabIndex
};
this.handleTabClick = this.handleTabClick.bind(this);
}
handleTabClick(tabIndex) {
this.setState({
activeTabIndex: tabIndex === this.state.activeTabIndex ? this.props.defaultActiveTabIndex : tabIndex
});
}
// Encapsulate <Tabs/> component API as props for <Tab/> children
renderChildrenWithTabsApiAsProps() {
return React.Children.map(this.props.children, (child, index) => {
return React.cloneElement(child, {
onClick : this.handleTabClick,
tabIndex: index,
isActive: index === this.state.activeTabIndex
});
});
}
// Render current active tab content
renderActiveTabContent() {
const {children} = this.props;
const {activeTabIndex} = this.state;
if(children[activeTabIndex]) {
return children[activeTabIndex].props.children;
}
}
render() {
return (
<div className="tabs">
<ul className="tabs-nav nav navbar-nav navbar-left">
{this.renderChildrenWithTabsApiAsProps()}
</ul>
<div className="tabs-active-content">
{this.renderActiveTabContent()}
</div>
</div>
);
}
};
Tabs.propTypes = {
defaultActiveTabIndex: PropTypes.number
};
Tabs.defaultProps = {
defaultActiveTabIndex: 0
};
import React from 'react';
import {mount} from 'enzyme';
import {Tabs} from '../Tabs';
import {Tab} from '../Tab';
describe('<Tabs/>', () => {
let Component = null;
beforeEach(() => {
Component = mount(
<Tabs>
<Tab iconClassName={'icon-class-0'}
linkClassName={'link-class-0'}>
<p>content 0</p>
</Tab>
<Tab iconClassName={'icon-class-1'}
linkClassName={'link-class-1'}>
<p>content 1</p>
</Tab>
</Tabs>
);
});
it('should render', () => {
expect(Component.length).toBeTruthy();
});
it('should render tab 0 content by default', () => {
expect(Component.find('.tabs-active-content').text()).toEqual('content 0');
});
it("should allow to specify the 'defaultActiveTabIndex'", () => {
const Component = mount(
<Tabs defaultActiveTabIndex={1}>
<Tab iconClassName={'icon-class-0'}
linkClassName={'link-class-0'}>
<p>content 0</p>
</Tab>
<Tab iconClassName={'icon-class-1'}
linkClassName={'link-class-1'}>
<p>content 1</p>
</Tab>
</Tabs>
);
// Active tab content should be 1 instead of 0 now
expect(Component.find('.tabs-active-content').text()).toEqual('content 1');
});
it('should change tab content to the selected tab', () => {
expect(Component.find('.tabs-active-content').text()).toEqual('content 0');
// Select tab at index 1, and expect text to change
Component.find('.link-class-1').simulate('click', {preventDefault: () => {}});
expect(Component.find('.tabs-active-content').text()).toEqual('content 1');
});
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment