Skip to content

Commit

Permalink
Merge pull request #804 from Adslot/re-apply-tab-implementation
Browse files Browse the repository at this point in the history
Re-apply the implementation of tab&tabs
  • Loading branch information
vinteo authored Jan 31, 2019
2 parents 28ece26 + 2c3a0e2 commit c2e083e
Show file tree
Hide file tree
Showing 7 changed files with 263 additions and 18 deletions.
60 changes: 44 additions & 16 deletions docs/examples/TabExample.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<Tabs defaultActiveKey="Audience" animation={false} id="audience-tab">
<Tabs activeKey={this.state.activeTab} onSelect={this.switchTab} id="audience-tab">
<Tab
eventKey="Audience"
title={
Expand Down Expand Up @@ -55,32 +65,28 @@ const exampleProps = {
),
notes: (
<p>
See{' '}
<a href="https://getbootstrap.com/docs/3.3/components/#nav-tabs" target="_blank" rel="noopener noreferrer">
Bootstrap documentation
</a>{' '}
or{' '}
<a href="https://react-bootstrap.github.io/components/tabs/" target="_blank" rel="noopener noreferrer">
React Bootstrap documentation
</a>
.
This is not a react-bootstrap component. However it implements the same API for switching tabs. Only the props
listed are supported.
</p>
),
exampleCodeSnippet: `
<Tabs defaultActiveKey="Audience" animation={false} id="audience-tab">
<Tabs activeKey={this.state.activeTab} onSelect={this.switchTab} id="audience-tab">
<Tab
eventKey="Audience"
title={
<span className="flexible-wrapper-inline">
<SvgSymbol href="./docs/assets/svg-symbols.svg#list" />
<FlexibleSpacer />
Audience
</span>}
</span>
}
>
<Empty
collection={[]}
text="No audience details."
svgSymbol={{ href: './docs/assets/svg-symbols.svg#checklist-incomplete' }}
svgSymbol={{
href: './docs/assets/svg-symbols.svg#checklist-incomplete',
}}
/>
</Tab>
<Tab
Expand All @@ -90,7 +96,8 @@ const exampleProps = {
<SvgSymbol href="./docs/assets/svg-symbols.svg#calendar" />
<FlexibleSpacer />
Billing
</span>}
</span>
}
>
<Empty
collection={[]}
Expand All @@ -99,7 +106,28 @@ const exampleProps = {
/>
</Tab>
</Tabs>`,
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 () => (
Expand Down
23 changes: 23 additions & 0 deletions src/components/adslot-ui/Tab/index.jsx
Original file line number Diff line number Diff line change
@@ -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 }) => (
<div role="tabpanel" aria-hidden={show} className={classnames(['tab-pane', 'fade', { active: show, in: show }])}>
{children}
</div>
);

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;
22 changes: 22 additions & 0 deletions src/components/adslot-ui/Tab/index.spec.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React from 'react';
import { shallow } from 'enzyme';
import Tab from '.';

describe('<Tab />', () => {
it('should render with props', () => {
expect(
shallow(
<Tab eventKey="first" show={false} title="First">
hi
</Tab>
).props().className
).to.equal('tab-pane fade');
expect(
shallow(
<Tab eventKey="first" show title="First">
hi
</Tab>
).props().className
).to.equal('tab-pane fade active in');
});
});
94 changes: 94 additions & 0 deletions src/components/adslot-ui/Tabs/index.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<div id={id}>
<ul role="tablist" className="nav nav-tabs">
{tabs.map(tab => (
<li
role="presentation"
className={classnames(tab.props.tabClassName, { active: tab.props.show, disabled: tab.props.disabled })}
key={tab.props.eventKey}
>
<a
id={`${id}-tab-${tab.props.eventKey}`}
role="tab"
tabIndex={-1}
aria-selected={tab.props.show}
onClick={this.switchTab(tab.props.eventKey)}
style={tab.props.disabled ? { pointerEvents: 'none' } : {}}
>
{tab.props.title}
</a>
</li>
))}
</ul>
<div className="tab-content">{content}</div>
</div>
);
}
}

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;
74 changes: 74 additions & 0 deletions src/components/adslot-ui/Tabs/index.spec.jsx
Original file line number Diff line number Diff line change
@@ -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('<Tabs />', () => {
it('should render with props', () => {
const wrapper = shallow(
<Tabs defaultActiveKey="first" id="test">
<Tab eventKey="first" title="Fist">
Tab1
</Tab>
<Tab eventKey="last" title="Second" disabled>
Tab2
</Tab>
</Tabs>
);
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(
<Tabs activeKey="first" onSelect={selectSpy} id="test">
<Tab eventKey="first" title="Fist">
Tab1
</Tab>
<Tab eventKey="last" title="Second">
Tab2
</Tab>
<div>other</div>
</Tabs>
);
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(
<Tabs defaultActiveKey="first" id="test">
<Tab eventKey="first" title="Fist">
Tab1
</Tab>
<Tab eventKey="last" title="Second">
Tab2
</Tab>
</Tabs>
);
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);
});
});
4 changes: 4 additions & 0 deletions src/components/adslot-ui/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -51,6 +53,8 @@ export {
Search,
SearchBar,
SplitPane,
Tab,
Tabs,
Textarea,
TextEllipsis,
TreePickerGrid,
Expand Down
4 changes: 2 additions & 2 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -66,6 +64,8 @@ import {
Search,
SearchBar,
SplitPane,
Tab,
Tabs,
Textarea,
TextEllipsis,
TreePickerGrid,
Expand Down

0 comments on commit c2e083e

Please sign in to comment.