Skip to content
Merged
79 changes: 79 additions & 0 deletions src/Tabs/Tab.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import classnames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';

export const Tab = (props) => {
const { title,
className,
disabled,
glyph,
id,
selected,
onClick,
tabContentProps,
linkProps,
index,
...rest } = props;

const tabClasses = classnames(
className,
'fd-tabs__item'
);

// css classes used for tabs
const linkClasses = classnames(
'fd-tabs__link',
{
[`sap-icon--${glyph}`]: !!glyph
}
);

return (
<li
{...rest}
className={tabClasses}
key={id}>
<a
{...linkProps}
aria-controls={id}
aria-disabled={disabled}
aria-selected={selected}
className={linkClasses}
href={!disabled ? `#${id}` : null}
onClick={!disabled ? (event) => {
props.onClick(event, index);
} : null}
role='tab'>
{title}
</a>
</li>
);
};
Tab.displayName = 'Tab';

Tab.defaultProps = {
onClick: () => { }
};

Tab.propTypes = {
className: PropTypes.string,
disabled: PropTypes.bool,
glyph: PropTypes.string,
id: PropTypes.string,
index: PropTypes.number,
linkProps: PropTypes.object,
selected: PropTypes.bool,
tabContentProps: PropTypes.object,
title: PropTypes.string,
onClick: PropTypes.func
};

Tab.propDescriptions = {
glyph: 'Icon to display on the tab.',
index: '_INTERNAL USE ONLY._',
selected: '_INTERNAL USE ONLY._',
title: 'Localized text to display on the tab.',
tabContentProps: 'Additional props to be spread to the tab content\'s <div> element.',
linkProps: 'Additional props to be spread to the tab\'s <a> element.',
onClick: '_INTERNAL USE ONLY._'
};
76 changes: 76 additions & 0 deletions src/Tabs/Tab.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { mount } from 'enzyme';
import React from 'react';
import renderer from 'react-test-renderer';
import { Tab } from './Tab';

describe('<Tabs />', () => {
const mockOnClick = jest.fn();

const defaultTab = (
<Tab
id='1'
onClick={mockOnClick}
title='Tab 1' >
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 ?
</Tab>);

const disabledTab = (
<Tab
disabled
id='3'
title='Tab 3'>
Lorem ipsum dolor sit amet consectetur adipisicing elit.
</Tab>);

const glyphTab = (
<Tab glyph='cart' id='4'>
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.
</Tab>);


test('create tabs component', () => {
let component = renderer.create(defaultTab);
let tree = component.toJSON();
expect(tree).toMatchSnapshot();

component = renderer.create(disabledTab);
tree = component.toJSON();
expect(tree).toMatchSnapshot();

component = renderer.create(glyphTab);
tree = component.toJSON();
expect(tree).toMatchSnapshot();
});

test('onClick of tab', () => {
const wrapper = mount(defaultTab);
wrapper.find('a').simulate('click');
expect(wrapper.prop('onClick')).toBeCalledTimes(1);
});

describe('Prop spreading', () => {
test('should allow props to be spread to the Tab component', () => {
const element = mount(<Tab data-sample='Sample' id='testId' />);

expect(
element.getDOMNode().attributes['data-sample'].value
).toBe('Sample');
});

test('should allow props to be spread to the Tab component\'s li elements', () => {
const element = mount(<Tab id='testId' {...{ 'data-sample': 'Sample' }} />);

expect(
element.find('li').at(0).getDOMNode().attributes['data-sample'].value
).toBe('Sample');
});

test('should allow props to be spread to the Tab component\'s a elements', () => {
const element = mount(<Tab id='1' linkProps={{ 'data-sample': 'Sample' }} />);

expect(
element.find('li a').at(0).getDOMNode().attributes['data-sample'].value
).toBe('Sample');
});
});
});
109 changes: 109 additions & 0 deletions src/Tabs/TabGroup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import classnames from 'classnames';
import PropTypes from 'prop-types';
import { TabContent } from './_TabContent';
import React, { Component } from 'react';

export class TabGroup extends Component {
constructor(props) {
super(props);
this.state = {
selectedIndex: props.selectedIndex
};
}

static getDerivedStateFromProps(props, state) {
const prevProps = state.prevProps || {};
// Compare the incoming prop to previous prop
const selectedIndex =
prevProps.selectedIndex !== props.selectedIndex
? props.selectedIndex
: state.selectedIndex;
return {
// Store the previous props in state
prevProps: props,
selectedIndex
};
}

// set selected tab
handleTabSelection = (event, index) => {
event.preventDefault();
this.setState({
selectedIndex: index
});

this.props.onTabClick(event, index);
};

// clone Tab element
cloneElement = (child, index) => {
return (React.cloneElement(child, {
onClick: this.handleTabSelection,
selected: this.state.selectedIndex === index,
index: index
}));
}

// create tab list
renderTabs = () => {
return React.Children.map(this.props.children, (child, index) => {
return this.cloneElement(child, index);
});
};

// create content to show below tab list
renderContent = () => {
return React.Children.map(this.props.children, (child, index) => {
return (
<TabContent
{...child.props.tabContentProps}
selected={this.state.selectedIndex === index}>
{child.props.children}
</TabContent>);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an interesting approach (using an internal component) to help encapsulate the details for tab content. Per @bcullman's comment, we should just take a couple other steps to make sure this stays "private".

});
};

render() {
const {
children,
className,
selectedIndex,
tabGroupProps,
onTabClick,
...rest } = this.props;

// css classes to use for tab group
const tabGroupClasses = classnames(
'fd-tabs',
className
);
return (
<React.Fragment>
<ul {...rest} className={tabGroupClasses}
role='tablist'>
{this.renderTabs(children)}
</ul>
{this.renderContent(children)}
</React.Fragment>
);
}
}
TabGroup.displayName = 'TabGroup';

TabGroup.defaultProps = {
selectedIndex: 0,
onTabClick: () => { }
};

TabGroup.propTypes = {
children: PropTypes.node,
className: PropTypes.string,
selectedIndex: PropTypes.number,
onTabClick: PropTypes.func
};

TabGroup.propDescriptions = {
children: 'One or more `Tab` components to render within the component.',
selectedIndex: 'The index of the selected tab.',
onTabClick: 'Callback function when the user clicks on a tab. Parameters passed to the function are `event` and `index`.'
};
92 changes: 92 additions & 0 deletions src/Tabs/TabGroup.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { mount } from 'enzyme';
import React from 'react';
import renderer from 'react-test-renderer';
import { Tab } from './Tab';
import { TabGroup } from './TabGroup';

describe('<Tabs />', () => {
const defaultTabs = (
<TabGroup>
<Tab
id='1'
title='Tab 1'>
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?
</Tab>
<Tab id='2' title='Tab 2'>
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?
</Tab>
<Tab
disabled
id='3'
title='Tab 3'>
Lorem ipsum dolor sit amet consectetur adipisicing elit.
</Tab>
<Tab glyph='cart' id='4'>
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.
</Tab>
</TabGroup>
);
const defaultTabsWithClass = (
<TabGroup
className='blue'
selectedIndex={1}>
<Tab
id='1'
title='Tab 1'>
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?
</Tab>
<Tab id='2' title='Tab 2'>
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?
</Tab>
<Tab
disabled
id='3'
title='Tab 3'>
Lorem ipsum dolor sit amet consectetur adipisicing elit.
</Tab>
<Tab glyph='cart' id='4'>
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.
</Tab>
</TabGroup>
);

test('create tabs component', () => {
let component = renderer.create(defaultTabs);
let tree = component.toJSON();
expect(tree).toMatchSnapshot();

component = renderer.create(defaultTabsWithClass);
tree = component.toJSON();
expect(tree).toMatchSnapshot();
});

test('tab selection', () => {
const wrapper = mount(defaultTabsWithClass);

// check selected tab
expect(wrapper.state(['selectedIndex'])).toEqual(1);

wrapper
.find('ul.fd-tabs li.fd-tabs__item a.fd-tabs__link')
.at(1)
.simulate('click');

wrapper
.find('ul.fd-tabs li.fd-tabs__item a.fd-tabs__link')
.at(3)
.simulate('click');

// check selected tab changed
expect(wrapper.state(['selectedIndex'])).toEqual(3);
});

describe('Prop spreading', () => {
test('should allow props to be spread to the TabGroup component', () => {
const element = mount(<TabGroup data-sample='Sample' />);

expect(
element.getDOMNode().attributes['data-sample'].value
).toBe('Sample');
});
});
});
Loading