-
Notifications
You must be signed in to change notification settings - Fork 77
fix: tabs component refactor #414
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
| @@ -0,0 +1,36 @@ | |||
| import classnames from 'classnames'; | |||
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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} |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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'; | |||
There was a problem hiding this comment.
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/TabContent.js
Outdated
|
|
||
| return ( | ||
| <div | ||
| {...tabContentProps} |
There was a problem hiding this comment.
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.
src/Tabs/TabGroup.js
Outdated
| className='fd-tabs__item' | ||
| key={child.props.id}> | ||
| <Tab {...child.props} onClick={this.handleTabSelection} | ||
| selected={this.state.selectedId === child.props.id} /> |
There was a problem hiding this comment.
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.
src/Tabs/TabGroup.js
Outdated
| TabGroup.propTypes = { | ||
| children: PropTypes.node, | ||
| className: PropTypes.string, | ||
| selectedId: PropTypes.string, |
There was a problem hiding this comment.
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> |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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>); |
There was a problem hiding this comment.
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'; |
There was a problem hiding this comment.
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'; | |||
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
src/Tabs/TabGroup.js
Outdated
| className: PropTypes.string, | ||
| selectedId: PropTypes.string, | ||
| tabContentProps: PropTypes.object, | ||
| tabLinkProps: PropTypes.object, |
There was a problem hiding this comment.
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.
src/Tabs/TabGroup.js
Outdated
| className='fd-tabs__item' | ||
| key={child.props.id}> | ||
| {this.cloneElement(child)} | ||
| </li>); |
There was a problem hiding this comment.
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.
src/Tabs/TabGroup.js
Outdated
| return React.Children.map(this.props.children, (child) => { | ||
| return ( | ||
| <TabContent | ||
| {...this.props.tabContentProps} |
There was a problem hiding this comment.
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.
src/Tabs/TabGroup.js
Outdated
| TabGroup.displayName = 'TabGroup'; | ||
|
|
||
| TabGroup.defaultProps = { | ||
| selectedId: '1' |
There was a problem hiding this comment.
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.
src/Tabs/TabGroup.js
Outdated
| }); | ||
|
|
||
| if (this.props.onTabClick) { | ||
| this.props.onTabClick(event, id); |
There was a problem hiding this comment.
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.
src/Tabs/_TabContent.js
Outdated
| TabContent.propTypes = { | ||
| children: PropTypes.node, | ||
| className: PropTypes.string, | ||
| id: PropTypes.string, |
There was a problem hiding this comment.
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>.
greg-a-smith
left a comment
There was a problem hiding this 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.
src/Tabs/_TabContent.js
Outdated
| import React from 'react'; | ||
|
|
||
| export const TabContent = (props) => { | ||
| const { children, id, selected, className, ...rest } = props; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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>.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done.
src/Tabs/TabGroup.js
Outdated
| TabGroup.displayName = 'TabGroup'; | ||
|
|
||
| TabGroup.defaultProps = { | ||
| selectedId: '1', |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done.
greg-a-smith
left a comment
There was a problem hiding this 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!
* 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
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
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