Skip to content

Conversation

@chrismanciero
Copy link
Contributor

Description

This is a complete refactor of the Tabs component which replaces the single file that contained both the TabGroup and Tab components with separate files for each component. This PR also adds a new component, TabContent, which is used to display the content of a tab. Each component resides in the /Tabs directory

  • /Tabs/Tab.js - a component used to display the clickable link of the tab strip
  • /Tabs/TabContent.js - a component used to display the contents of a selected tab
  • /Tabs/TabGroup.js - a component that wraps the Tab and TabContent components. The TabGroup handles the placement of the Tab strip as well as the content.

As seen on the Fiori Fundamentals Tab page (https://sap.github.io/fundamental/components/tabs.html). The Tabs with list element is the implementation used. The Tabs with nav element is not implemented as there does not seem to be a valid use-case to have 2 implementations.

fixes #344

@chrismanciero chrismanciero requested a review from a team February 15, 2019 23:35
@mobot11 mobot11 changed the title Fix/tabs component refactor fix: tabs component refactor Feb 16, 2019
@@ -0,0 +1,36 @@
import classnames from 'classnames';
Copy link
Contributor

@bcullman bcullman Feb 16, 2019

Choose a reason for hiding this comment

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

@chrismanciero - the SAP Concur team discussed the pattern to be used for subcomponents, and wanted to use a "private" component structure.

to do this we typically prefix private components with underscores, and import/export them as part of the parent component. Reach out to @greg-a-smith or others for more guidance.

Copy link
Contributor

Choose a reason for hiding this comment

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

@chrismanciero Feel free to connect with me on Monday and I can explain more.

src/Tabs/Tab.js Outdated
return (
<a
{...rest}
{...tabLinkProps}
Copy link
Contributor

Choose a reason for hiding this comment

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

Now that these are all separate components, I don't think tabLinkProps is needed anymore. Any "other" props are already going to be included in the ...rest spread.

src/Tabs/Tab.js Outdated
className={linkClasses}
href={!disabled ? `#${id}` : null}
onClick={!disabled ? (event) => {
props.onClick(event, id);
Copy link
Contributor

Choose a reason for hiding this comment

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

onClick isn't defined as a prop and should be. It should also have an empty function as a default value. More on this below in the PR review...

@@ -0,0 +1,36 @@
import classnames from 'classnames';
Copy link
Contributor

Choose a reason for hiding this comment

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

@chrismanciero Feel free to connect with me on Monday and I can explain more.


return (
<div
{...tabContentProps}
Copy link
Contributor

Choose a reason for hiding this comment

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

Not a huge deal, but with this being a separate component, the final spread (...tabContentProps) should probably be named something like ...rest or ...otherProps so it doesn't appear like it's a named prop.

className='fd-tabs__item'
key={child.props.id}>
<Tab {...child.props} onClick={this.handleTabSelection}
selected={this.state.selectedId === child.props.id} />
Copy link
Contributor

Choose a reason for hiding this comment

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

The children of TabGroup should already be Tab components so you should be able to use React.cloneElement to pass additional/internal props to it.

TabGroup.propTypes = {
children: PropTypes.node,
className: PropTypes.string,
selectedId: PropTypes.string,
Copy link
Contributor

Choose a reason for hiding this comment

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

It's good there is a selectedId prop so the consumer can decide which tab is currently selected, however, there should also be a getDerivedStateFromProps (formerly componentWillReceiveProps) method to properly update state if the consumer changes that prop.

Additionally, there should be a callback function prop (named something like onTabChange) that the consumer can get updates in case they are managing any state related to these tabs.

src/Tabs/Tab.js Outdated
} : null}
role='tab'>
{title}
</a>
Copy link
Contributor

Choose a reason for hiding this comment

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

According to the WAI specs for Tabs, the element with role='tab' should actually be a <button>. Right out of the box, we will get better keyboard navigation support and have less to code ourselves.

That said, this may actually need to be an issue brought to the Fiori Fundamentals team since all libraries should adopt this change.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, this needs to be brought up to Fiori Fundamentals because of styling issues. If i change from <a> tags to <button> tags the design looks bad

Copy link
Contributor

Choose a reason for hiding this comment

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

@greg-a-smith This change is part of the a11y story we have in the backlog for Tabs.

Copy link
Contributor

Choose a reason for hiding this comment

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

Awesome! 👍

{...this.props.tabContentProps}
selected={this.state.selectedId === child.props.id}>
{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".

src/index.js Outdated
export { SearchInput } from './SearchInput/SearchInput';
export { SideNav, SideNavList, SideNavListItem } from './SideNavigation/SideNavigation';
export { Tab } from './Tabs/Tab';
export { TabContent } from './Tabs/TabContent';
Copy link
Contributor

Choose a reason for hiding this comment

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

I think the TabContent component should be internal-only and should not be exported.

@@ -1,101 +1,56 @@
import { mount } from 'enzyme';
Copy link
Contributor

Choose a reason for hiding this comment

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

Now that these are separate components, we should have unit tests for each individually.

src/Tabs/Tab.js Outdated
import React from 'react';

export const Tab = (props) => {
const { title, className, disabled, glyph, id, selected, ...rest } = props;
Copy link
Contributor

Choose a reason for hiding this comment

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

Should probably add onClick to the list of props being pruned out.

className: PropTypes.string,
selectedId: PropTypes.string,
tabContentProps: PropTypes.object,
tabLinkProps: PropTypes.object,
Copy link
Contributor

Choose a reason for hiding this comment

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

The props tabContentProps and tabLinkProps should be moved to the Tab component. Otherwise, a consumer will not be able to provide different props for each tab and its content. They can still be referenced in the render methods in this file, but would just come from child.props within those loops.

Taking that a step further, once moved to the Tab component, I think tabLinkProps could just become linkProps and those would be spread to the <a> element. Additionally, the Tab component could render its own <li> and spread ...rest to that element.

className='fd-tabs__item'
key={child.props.id}>
{this.cloneElement(child)}
</li>);
Copy link
Contributor

Choose a reason for hiding this comment

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

I think it might be better to have Tab render both the <li> and the <a> elements for each tab. I just think keeps "like" elements together. Otherwise, Tab looks like a misnamed link renderer. The map can stay here and then className and key could be moved to Tab as well.

return React.Children.map(this.props.children, (child) => {
return (
<TabContent
{...this.props.tabContentProps}
Copy link
Contributor

Choose a reason for hiding this comment

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

See comment below, but I think tabContentProps should be coming from child.props.

TabGroup.displayName = 'TabGroup';

TabGroup.defaultProps = {
selectedId: '1'
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this may be problematic. This relies on the consumer setting the id prop on the first tab to 1. Maybe this should be changed to a number prop and just simply be the index of the selected tab rather than a specific id value.

});

if (this.props.onTabClick) {
this.props.onTabClick(event, id);
Copy link
Contributor

Choose a reason for hiding this comment

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

TIP: If you give onTabClick a default value of an empty function, you won't need to do the "if exists" check here.

TabContent.propTypes = {
children: PropTypes.node,
className: PropTypes.string,
id: PropTypes.string,
Copy link
Contributor

Choose a reason for hiding this comment

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

The id prop has no way of getting set since this is a non-exported component. The way to do it would be to include id in the tabContentProps object, but then that would end up on the <div> by virtue of the ...rest spread. The className prop is in a similar situation, however, that can stay since you need to have a way to get the fd-tabs__panel class on the <div>.

Copy link
Contributor

@greg-a-smith greg-a-smith left a comment

Choose a reason for hiding this comment

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

This looks pretty good. Just a few more comments and I think this will be ready to go.

import React from 'react';

export const TabContent = (props) => {
const { children, id, selected, className, ...rest } = props;
Copy link
Contributor

Choose a reason for hiding this comment

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

id is no longer a prop on this component so that could be removed from here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done.

src/Tabs/Tab.js Outdated

// css classes used for tabs
const linkClasses = classnames(
className,
Copy link
Contributor

Choose a reason for hiding this comment

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

Now that this renders both the <li> and the <a>, I think className should map to the <li> and be combined with 'fd-tabs__item'. I don't think we need a linkClasses prop simply to add classes to the <a> element since they could write a selector using any attributes from the <li>.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done.

TabGroup.displayName = 'TabGroup';

TabGroup.defaultProps = {
selectedId: '1',
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this prop's type needs to change to a number and represent the index of the selected tab. The reason I say that is because using id attribute values requires the use of the id prop in the Tab component and the selectedId prop in TabGroup, both of which are currently optional. It would be difficult to set a default selectedId since the tab's id value is not easily known.

The index of each tab could be bound to the onClick function being passed (in cloneElement ) so there would be no other work needed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done.

Copy link
Contributor

@greg-a-smith greg-a-smith left a comment

Choose a reason for hiding this comment

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

@chrismanciero I contributed back a couple minor edits, but I'm ready to ship this. I am now technically a partial author, so I'll just give a ⛵️ . Nice work!

@chrismanciero chrismanciero merged commit 938268a into master Feb 20, 2019
@chrismanciero chrismanciero deleted the fix/Tabs-Component-refactor branch February 20, 2019 15:29
greg-a-smith pushed a commit that referenced this pull request Mar 5, 2019
* 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Tabs shift in size when content changes

5 participants