Skip to content

Commit 938268a

Browse files
fix: tabs component refactor (#414)
* fix: Refactor Tabs component, separated controls * updated spreading props * converted TabGroup to React class component * added changes based of code review * update prop descriptions * update component based on code review * removed unneeded testing code, update unit tests * changes based on code review * Minor prop description edits
1 parent 23341f0 commit 938268a

File tree

14 files changed

+732
-685
lines changed

14 files changed

+732
-685
lines changed

src/Tabs/Tab.js

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import classnames from 'classnames';
2+
import PropTypes from 'prop-types';
3+
import React from 'react';
4+
5+
export const Tab = (props) => {
6+
const { title,
7+
className,
8+
disabled,
9+
glyph,
10+
id,
11+
selected,
12+
onClick,
13+
tabContentProps,
14+
linkProps,
15+
index,
16+
...rest } = props;
17+
18+
const tabClasses = classnames(
19+
className,
20+
'fd-tabs__item'
21+
);
22+
23+
// css classes used for tabs
24+
const linkClasses = classnames(
25+
'fd-tabs__link',
26+
{
27+
[`sap-icon--${glyph}`]: !!glyph
28+
}
29+
);
30+
31+
return (
32+
<li
33+
{...rest}
34+
className={tabClasses}
35+
key={id}>
36+
<a
37+
{...linkProps}
38+
aria-controls={id}
39+
aria-disabled={disabled}
40+
aria-selected={selected}
41+
className={linkClasses}
42+
href={!disabled ? `#${id}` : null}
43+
onClick={!disabled ? (event) => {
44+
props.onClick(event, index);
45+
} : null}
46+
role='tab'>
47+
{title}
48+
</a>
49+
</li>
50+
);
51+
};
52+
Tab.displayName = 'Tab';
53+
54+
Tab.defaultProps = {
55+
onClick: () => { }
56+
};
57+
58+
Tab.propTypes = {
59+
className: PropTypes.string,
60+
disabled: PropTypes.bool,
61+
glyph: PropTypes.string,
62+
id: PropTypes.string,
63+
index: PropTypes.number,
64+
linkProps: PropTypes.object,
65+
selected: PropTypes.bool,
66+
tabContentProps: PropTypes.object,
67+
title: PropTypes.string,
68+
onClick: PropTypes.func
69+
};
70+
71+
Tab.propDescriptions = {
72+
glyph: 'Icon to display on the tab.',
73+
index: '_INTERNAL USE ONLY._',
74+
selected: '_INTERNAL USE ONLY._',
75+
title: 'Localized text to display on the tab.',
76+
tabContentProps: 'Additional props to be spread to the tab content\'s <div> element.',
77+
linkProps: 'Additional props to be spread to the tab\'s <a> element.',
78+
onClick: '_INTERNAL USE ONLY._'
79+
};

src/Tabs/Tab.test.js

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { mount } from 'enzyme';
2+
import React from 'react';
3+
import renderer from 'react-test-renderer';
4+
import { Tab } from './Tab';
5+
6+
describe('<Tabs />', () => {
7+
const mockOnClick = jest.fn();
8+
9+
const defaultTab = (
10+
<Tab
11+
id='1'
12+
onClick={mockOnClick}
13+
title='Tab 1' >
14+
Lorem ipsum dolor sit amet consectetur adipisicing elit.Dolore et ducimus veritatis officiis amet ? Vitae officia optio dolor exercitationem incidunt magnam non, suscipit, illo quisquam numquam fugiat ? Debitis, delectus sequi ?
15+
</Tab>);
16+
17+
const disabledTab = (
18+
<Tab
19+
disabled
20+
id='3'
21+
title='Tab 3'>
22+
Lorem ipsum dolor sit amet consectetur adipisicing elit.
23+
</Tab>);
24+
25+
const glyphTab = (
26+
<Tab glyph='cart' id='4'>
27+
Lorem ipsum dolor sit amet consectetur adipisicing elit. A quibusdam ipsa cumque soluta debitis accusantium iste alias quas vel perferendis voluptatibus quod asperiores praesentium quaerat, iusto repellendus nulla, maiores eius.
28+
</Tab>);
29+
30+
31+
test('create tabs component', () => {
32+
let component = renderer.create(defaultTab);
33+
let tree = component.toJSON();
34+
expect(tree).toMatchSnapshot();
35+
36+
component = renderer.create(disabledTab);
37+
tree = component.toJSON();
38+
expect(tree).toMatchSnapshot();
39+
40+
component = renderer.create(glyphTab);
41+
tree = component.toJSON();
42+
expect(tree).toMatchSnapshot();
43+
});
44+
45+
test('onClick of tab', () => {
46+
const wrapper = mount(defaultTab);
47+
wrapper.find('a').simulate('click');
48+
expect(wrapper.prop('onClick')).toBeCalledTimes(1);
49+
});
50+
51+
describe('Prop spreading', () => {
52+
test('should allow props to be spread to the Tab component', () => {
53+
const element = mount(<Tab data-sample='Sample' id='testId' />);
54+
55+
expect(
56+
element.getDOMNode().attributes['data-sample'].value
57+
).toBe('Sample');
58+
});
59+
60+
test('should allow props to be spread to the Tab component\'s li elements', () => {
61+
const element = mount(<Tab id='testId' {...{ 'data-sample': 'Sample' }} />);
62+
63+
expect(
64+
element.find('li').at(0).getDOMNode().attributes['data-sample'].value
65+
).toBe('Sample');
66+
});
67+
68+
test('should allow props to be spread to the Tab component\'s a elements', () => {
69+
const element = mount(<Tab id='1' linkProps={{ 'data-sample': 'Sample' }} />);
70+
71+
expect(
72+
element.find('li a').at(0).getDOMNode().attributes['data-sample'].value
73+
).toBe('Sample');
74+
});
75+
});
76+
});

src/Tabs/TabGroup.js

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import classnames from 'classnames';
2+
import PropTypes from 'prop-types';
3+
import { TabContent } from './_TabContent';
4+
import React, { Component } from 'react';
5+
6+
export class TabGroup extends Component {
7+
constructor(props) {
8+
super(props);
9+
this.state = {
10+
selectedIndex: props.selectedIndex
11+
};
12+
}
13+
14+
static getDerivedStateFromProps(props, state) {
15+
const prevProps = state.prevProps || {};
16+
// Compare the incoming prop to previous prop
17+
const selectedIndex =
18+
prevProps.selectedIndex !== props.selectedIndex
19+
? props.selectedIndex
20+
: state.selectedIndex;
21+
return {
22+
// Store the previous props in state
23+
prevProps: props,
24+
selectedIndex
25+
};
26+
}
27+
28+
// set selected tab
29+
handleTabSelection = (event, index) => {
30+
event.preventDefault();
31+
this.setState({
32+
selectedIndex: index
33+
});
34+
35+
this.props.onTabClick(event, index);
36+
};
37+
38+
// clone Tab element
39+
cloneElement = (child, index) => {
40+
return (React.cloneElement(child, {
41+
onClick: this.handleTabSelection,
42+
selected: this.state.selectedIndex === index,
43+
index: index
44+
}));
45+
}
46+
47+
// create tab list
48+
renderTabs = () => {
49+
return React.Children.map(this.props.children, (child, index) => {
50+
return this.cloneElement(child, index);
51+
});
52+
};
53+
54+
// create content to show below tab list
55+
renderContent = () => {
56+
return React.Children.map(this.props.children, (child, index) => {
57+
return (
58+
<TabContent
59+
{...child.props.tabContentProps}
60+
selected={this.state.selectedIndex === index}>
61+
{child.props.children}
62+
</TabContent>);
63+
});
64+
};
65+
66+
render() {
67+
const {
68+
children,
69+
className,
70+
selectedIndex,
71+
tabGroupProps,
72+
onTabClick,
73+
...rest } = this.props;
74+
75+
// css classes to use for tab group
76+
const tabGroupClasses = classnames(
77+
'fd-tabs',
78+
className
79+
);
80+
return (
81+
<React.Fragment>
82+
<ul {...rest} className={tabGroupClasses}
83+
role='tablist'>
84+
{this.renderTabs(children)}
85+
</ul>
86+
{this.renderContent(children)}
87+
</React.Fragment>
88+
);
89+
}
90+
}
91+
TabGroup.displayName = 'TabGroup';
92+
93+
TabGroup.defaultProps = {
94+
selectedIndex: 0,
95+
onTabClick: () => { }
96+
};
97+
98+
TabGroup.propTypes = {
99+
children: PropTypes.node,
100+
className: PropTypes.string,
101+
selectedIndex: PropTypes.number,
102+
onTabClick: PropTypes.func
103+
};
104+
105+
TabGroup.propDescriptions = {
106+
children: 'One or more `Tab` components to render within the component.',
107+
selectedIndex: 'The index of the selected tab.',
108+
onTabClick: 'Callback function when the user clicks on a tab. Parameters passed to the function are `event` and `index`.'
109+
};

src/Tabs/TabGroup.test.js

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { mount } from 'enzyme';
2+
import React from 'react';
3+
import renderer from 'react-test-renderer';
4+
import { Tab } from './Tab';
5+
import { TabGroup } from './TabGroup';
6+
7+
describe('<Tabs />', () => {
8+
const defaultTabs = (
9+
<TabGroup>
10+
<Tab
11+
id='1'
12+
title='Tab 1'>
13+
Lorem ipsum dolor sit amet consectetur adipisicing elit. Dolore et ducimus veritatis officiis amet? Vitae officia optio dolor exercitationem incidunt magnam non, suscipit, illo quisquam numquam fugiat? Debitis, delectus sequi?
14+
</Tab>
15+
<Tab id='2' title='Tab 2'>
16+
Lorem ipsum dolor sit amet consectetur adipisicing elit. Numquam libero id corporis odit animi voluptat, Lorem ipsum dolor sit amet consectetur adipisicing elit. Possimus quia tempore eligendi tempora repellat officia rerum laudantium, veritatis officiis asperiores ipsum nam, distinctio, dolor provident culpa voluptatibus esse deserunt animi?
17+
</Tab>
18+
<Tab
19+
disabled
20+
id='3'
21+
title='Tab 3'>
22+
Lorem ipsum dolor sit amet consectetur adipisicing elit.
23+
</Tab>
24+
<Tab glyph='cart' id='4'>
25+
Lorem ipsum dolor sit amet consectetur adipisicing elit. A quibusdam ipsa cumque soluta debitis accusantium iste alias quas vel perferendis voluptatibus quod asperiores praesentium quaerat, iusto repellendus nulla, maiores eius.
26+
</Tab>
27+
</TabGroup>
28+
);
29+
const defaultTabsWithClass = (
30+
<TabGroup
31+
className='blue'
32+
selectedIndex={1}>
33+
<Tab
34+
id='1'
35+
title='Tab 1'>
36+
Lorem ipsum dolor sit amet consectetur adipisicing elit. Dolore et ducimus veritatis officiis amet? Vitae officia optio dolor exercitationem incidunt magnam non, suscipit, illo quisquam numquam fugiat? Debitis, delectus sequi?
37+
</Tab>
38+
<Tab id='2' title='Tab 2'>
39+
Lorem ipsum dolor sit amet consectetur adipisicing elit. Numquam libero id corporis odit animi voluptat, Lorem ipsum dolor sit amet consectetur adipisicing elit. Possimus quia tempore eligendi tempora repellat officia rerum laudantium, veritatis officiis asperiores ipsum nam, distinctio, dolor provident culpa voluptatibus esse deserunt animi?
40+
</Tab>
41+
<Tab
42+
disabled
43+
id='3'
44+
title='Tab 3'>
45+
Lorem ipsum dolor sit amet consectetur adipisicing elit.
46+
</Tab>
47+
<Tab glyph='cart' id='4'>
48+
Lorem ipsum dolor sit amet consectetur adipisicing elit. A quibusdam ipsa cumque soluta debitis accusantium iste alias quas vel perferendis voluptatibus quod asperiores praesentium quaerat, iusto repellendus nulla, maiores eius.
49+
</Tab>
50+
</TabGroup>
51+
);
52+
53+
test('create tabs component', () => {
54+
let component = renderer.create(defaultTabs);
55+
let tree = component.toJSON();
56+
expect(tree).toMatchSnapshot();
57+
58+
component = renderer.create(defaultTabsWithClass);
59+
tree = component.toJSON();
60+
expect(tree).toMatchSnapshot();
61+
});
62+
63+
test('tab selection', () => {
64+
const wrapper = mount(defaultTabsWithClass);
65+
66+
// check selected tab
67+
expect(wrapper.state(['selectedIndex'])).toEqual(1);
68+
69+
wrapper
70+
.find('ul.fd-tabs li.fd-tabs__item a.fd-tabs__link')
71+
.at(1)
72+
.simulate('click');
73+
74+
wrapper
75+
.find('ul.fd-tabs li.fd-tabs__item a.fd-tabs__link')
76+
.at(3)
77+
.simulate('click');
78+
79+
// check selected tab changed
80+
expect(wrapper.state(['selectedIndex'])).toEqual(3);
81+
});
82+
83+
describe('Prop spreading', () => {
84+
test('should allow props to be spread to the TabGroup component', () => {
85+
const element = mount(<TabGroup data-sample='Sample' />);
86+
87+
expect(
88+
element.getDOMNode().attributes['data-sample'].value
89+
).toBe('Sample');
90+
});
91+
});
92+
});

0 commit comments

Comments
 (0)