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 ( +
+ +
{content}
+
+ ); + } +} + +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,