diff --git a/docs/examples/TabExample.jsx b/docs/examples/TabExample.jsx
index d48598756..f4ab0b731 100644
--- a/docs/examples/TabExample.jsx
+++ b/docs/examples/TabExample.jsx
@@ -2,10 +2,20 @@ import React from 'react';
import Example from '../components/Example';
import { Empty, SvgSymbol, FlexibleSpacer, Tabs, Tab } from '../../src';
-class TabExample extends React.PureComponent {
+class TabExample extends React.Component {
+ state = {
+ activeTab: 'Audience',
+ };
+
+ switchTab = tabKey => {
+ this.setState({
+ activeTab: tabKey,
+ });
+ };
+
render() {
return (
-
+
- See{' '}
-
- Bootstrap documentation
- {' '}
- or{' '}
-
- React Bootstrap documentation
-
- .
+ This is not a react-bootstrap component. However it implements the same API for switching tabs. Only the props
+ listed are supported.
),
exampleCodeSnippet: `
-
+
Audience
- }
+
+ }
>
Billing
- }
+
+ }
>
`,
- propTypeSectionArray: [],
+ propTypeSectionArray: [
+ {
+ propTypes: [
+ {
+ propType: 'defaultActiveKey',
+ type: 'oneOfType [string, number]',
+ },
+ {
+ propType: 'activeKey',
+ type: 'oneOfType [string, number]',
+ },
+ {
+ propType: 'onSelect',
+ type: 'func',
+ },
+ {
+ propType: 'id',
+ type: 'string',
+ },
+ ],
+ },
+ ],
};
export default () => (
diff --git a/src/components/adslot-ui/Tab/index.jsx b/src/components/adslot-ui/Tab/index.jsx
new file mode 100644
index 000000000..9c14bb205
--- /dev/null
+++ b/src/components/adslot-ui/Tab/index.jsx
@@ -0,0 +1,23 @@
+/* eslint-disable react/no-unused-prop-types */
+import React from 'react';
+import PropTypes from 'prop-types';
+import classnames from 'classnames';
+
+const Tab = ({ children, show }) => (
+
+ {children}
+
+);
+
+Tab.innerName = 'au_tab';
+
+Tab.propTypes = {
+ children: PropTypes.node.isRequired,
+ eventKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
+ title: PropTypes.node.isRequired,
+ tabClassName: PropTypes.string,
+ disabled: PropTypes.bool,
+ show: PropTypes.bool,
+};
+
+export default Tab;
diff --git a/src/components/adslot-ui/Tab/index.spec.jsx b/src/components/adslot-ui/Tab/index.spec.jsx
new file mode 100644
index 000000000..7ac36eb59
--- /dev/null
+++ b/src/components/adslot-ui/Tab/index.spec.jsx
@@ -0,0 +1,22 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import Tab from '.';
+
+describe('', () => {
+ it('should render with props', () => {
+ expect(
+ shallow(
+
+ hi
+
+ ).props().className
+ ).to.equal('tab-pane fade');
+ expect(
+ shallow(
+
+ hi
+
+ ).props().className
+ ).to.equal('tab-pane fade active in');
+ });
+});
diff --git a/src/components/adslot-ui/Tabs/index.jsx b/src/components/adslot-ui/Tabs/index.jsx
new file mode 100644
index 000000000..65218dd39
--- /dev/null
+++ b/src/components/adslot-ui/Tabs/index.jsx
@@ -0,0 +1,94 @@
+/* eslint-disable jsx-a11y/anchor-is-valid */
+import React from 'react';
+import PropTypes from 'prop-types';
+import _ from 'lodash';
+import classnames from 'classnames';
+import Tab from '../Tab';
+
+class Tabs extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ activeKey: this.props.defaultActiveKey,
+ };
+
+ this.switchTab = this.switchTab.bind(this);
+ }
+
+ get isControlled() {
+ const { activeKey, onSelect } = this.props;
+
+ return !_.isNil(activeKey) && _.isFunction(onSelect);
+ }
+
+ get activeKey() {
+ return this.isControlled ? this.props.activeKey : this.state.activeKey;
+ }
+
+ switchTab(key) {
+ const { onSelect, activeKey } = this.props;
+ return event => {
+ event.preventDefault();
+
+ if (this.isControlled && key !== activeKey) {
+ onSelect(key);
+ } else if (key !== this.state.activeKey) {
+ this.setState({ activeKey: key });
+ }
+ };
+ }
+
+ render() {
+ const { id, children } = this.props;
+
+ const tabs = [];
+ const content = React.Children.map(children, child => {
+ if (_.isFunction(child.type) && child.type.innerName === Tab.innerName) {
+ const tab = React.cloneElement(child, {
+ show: this.activeKey === child.props.eventKey,
+ });
+ tabs.push(tab);
+
+ return tab;
+ }
+ return child;
+ });
+
+ return (
+
+ );
+ }
+}
+
+Tabs.propTypes = {
+ id: PropTypes.string.isRequired,
+ children: PropTypes.node.isRequired,
+ defaultActiveKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ activeKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ onSelect: PropTypes.func,
+};
+
+export default Tabs;
diff --git a/src/components/adslot-ui/Tabs/index.spec.jsx b/src/components/adslot-ui/Tabs/index.spec.jsx
new file mode 100644
index 000000000..d7bf7361b
--- /dev/null
+++ b/src/components/adslot-ui/Tabs/index.spec.jsx
@@ -0,0 +1,74 @@
+import React from 'react';
+import _ from 'lodash';
+import { shallow } from 'enzyme';
+import sinon from 'sinon';
+import Tabs from '.';
+import Tab from '../Tab';
+
+describe('', () => {
+ it('should render with props', () => {
+ const wrapper = shallow(
+
+
+ Tab1
+
+
+ Tab2
+
+
+ );
+ const links = wrapper.find('a');
+ let content = wrapper.find(Tab);
+ expect(links.length).to.equal(2);
+ expect(content.length).to.equal(2);
+
+ expect(links.last().props().style).to.eql({ pointerEvents: 'none' });
+
+ expect(content.first().props().show).to.equal(true);
+ expect(content.last().props().show).to.equal(false);
+
+ links.last().simulate('click', { preventDefault: _.noop });
+ content = wrapper.find(Tab);
+ expect(content.first().props().show).to.equal(false);
+ expect(content.last().props().show).to.equal(true);
+ });
+
+ it('should work as controlled', () => {
+ const selectSpy = sinon.spy();
+ const wrapper = shallow(
+
+
+ Tab1
+
+
+ Tab2
+
+ other
+
+ );
+ const links = wrapper.find('a');
+ expect(links.length).to.equal(2);
+
+ links.last().simulate('click', { preventDefault: _.noop });
+ expect(selectSpy.callCount).to.equal(1);
+ expect(selectSpy.calledWith('last')).to.equal(true);
+ });
+
+ it('should not re render when key is the same', () => {
+ const wrapper = shallow(
+
+
+ Tab1
+
+
+ Tab2
+
+
+ );
+ const spy = sinon.spy(wrapper.instance(), 'setState');
+ const links = wrapper.find('a');
+ links.last().simulate('click', { preventDefault: _.noop });
+ links.last().simulate('click', { preventDefault: _.noop });
+ expect(spy.callCount).to.equal(1);
+ });
+});
diff --git a/src/components/adslot-ui/index.js b/src/components/adslot-ui/index.js
index 54199c62c..5865be8d0 100644
--- a/src/components/adslot-ui/index.js
+++ b/src/components/adslot-ui/index.js
@@ -17,6 +17,8 @@ import RadioGroup from 'adslot-ui/RadioGroup';
import Search from 'adslot-ui/Search';
import SearchBar from 'adslot-ui/SearchBar';
import SplitPane from 'adslot-ui/SplitPane';
+import Tab from 'adslot-ui/Tab';
+import Tabs from 'adslot-ui/Tabs';
import Textarea from 'adslot-ui/Textarea';
import TextEllipsis from 'adslot-ui/TextEllipsis';
import TreePickerGrid from 'adslot-ui/TreePicker/Grid';
@@ -51,6 +53,8 @@ export {
Search,
SearchBar,
SplitPane,
+ Tab,
+ Tabs,
Textarea,
TextEllipsis,
TreePickerGrid,
diff --git a/src/index.js b/src/index.js
index 9fe68a4f1..3338a0ee0 100644
--- a/src/index.js
+++ b/src/index.js
@@ -12,8 +12,6 @@ import Modal from 'react-bootstrap/lib/Modal';
import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger';
import Pagination from 'react-bootstrap/lib/Pagination';
import ProgressBar from 'react-bootstrap/lib/ProgressBar';
-import Tab from 'react-bootstrap/lib/Tab';
-import Tabs from 'react-bootstrap/lib/Tabs';
import NavItem from 'react-bootstrap/lib/NavItem';
import 'styles/_bootstrap-custom.scss';
@@ -66,6 +64,8 @@ import {
Search,
SearchBar,
SplitPane,
+ Tab,
+ Tabs,
Textarea,
TextEllipsis,
TreePickerGrid,