From 2cdacc85886b0f2240294981838cec2f5ad45bda Mon Sep 17 00:00:00 2001 From: Josefina Mancilla <josefinanoemi4@gmail.com> Date: Wed, 22 Sep 2021 12:44:02 -0500 Subject: [PATCH 01/22] feat(Tab): make tab functional component --- packages/react/src/components/Tab/index.js | 8 +- packages/react/src/components/Tab/next/Tab.js | 174 ++++++++++++++++++ .../react/src/components/Tabs/Tabs-story.js | 2 +- 3 files changed, 182 insertions(+), 2 deletions(-) create mode 100644 packages/react/src/components/Tab/next/Tab.js diff --git a/packages/react/src/components/Tab/index.js b/packages/react/src/components/Tab/index.js index e664e8d8850b..83ad52cd2666 100644 --- a/packages/react/src/components/Tab/index.js +++ b/packages/react/src/components/Tab/index.js @@ -5,4 +5,10 @@ * LICENSE file in the root directory of this source tree. */ -export default from './Tab'; +import * as FeatureFlags from '@carbon/feature-flags'; +import { Tab as TabNext } from './next/Tab'; +import TabClassic from './Tab'; + +export const Tab = FeatureFlags.enabled('enable-v11-release') + ? TabNext + : TabClassic; diff --git a/packages/react/src/components/Tab/next/Tab.js b/packages/react/src/components/Tab/next/Tab.js new file mode 100644 index 000000000000..bad4ff032694 --- /dev/null +++ b/packages/react/src/components/Tab/next/Tab.js @@ -0,0 +1,174 @@ +/** + * Copyright IBM Corp. 2016, 2018 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import PropTypes from 'prop-types'; +import React from 'react'; +import classNames from 'classnames'; +import { settings } from 'carbon-components'; +import deprecate from '../../../prop-types/deprecate'; + +const { prefix } = settings; + +const Tab = React.forwardRef(function Tab( + { + className, + disabled, + handleTabClick, + handleTabKeyDown, + id, + index, + label = 'provide a label', + onClick = () => {}, + onKeyDown = () => {}, + renderButton, + renderContent, // eslint-disable-line no-unused-vars + selected = false, + tabIndex = 0, + ...other + }, + ref +) { + const classes = classNames(className, `${prefix}--tabs__nav-link`, { + [`${prefix}--tabs__nav-item--disabled`]: disabled, + [`${prefix}--tabs__nav-item--selected`]: selected, + }); + + const buttonProps = { + ['aria-selected']: selected, + ['aria-disabled']: disabled, + ['aria-controls']: id && `${id}__panel`, + id, + className: `${prefix}--tabs__nav-link`, + tabIndex: !disabled ? tabIndex : -1, + ref: ref, + }; + + return ( + <li + {...other} + className={classes} + onClick={(evt) => { + if (disabled) { + return; + } + if (handleTabClick) { + handleTabClick(index, evt); + } + onClick(evt); + }} + onKeyDown={(evt) => { + if (disabled) { + return; + } + if (handleTabKeyDown) { + handleTabKeyDown(index, evt); + } + onKeyDown(evt); + }} + role="presentation"> + {renderButton ? ( + renderButton(buttonProps) + ) : ( + <button type="button" role="tab" {...buttonProps}> + {label} + </button> + )} + </li> + ); +}); + +Tab.propTypes = { + /** + * Specify an optional className to be added to your Tab + */ + className: PropTypes.string, + + /** + * Whether your Tab is disabled. + */ + disabled: PropTypes.bool, + + /** + * A handler that is invoked when a user clicks on the control. + * Reserved for usage in Tabs + */ + handleTabClick: PropTypes.func, + + /** + * A handler that is invoked on the key down event for the control. + * Reserved for usage in Tabs + */ + handleTabKeyDown: PropTypes.func, + + /** + * Provide a string that represents the `href` of the Tab + */ + href: deprecate( + PropTypes.string, + 'The href prop has been deprecated and removed in v11.' + ), + + /** + * The element ID for the top-level element. + */ + id: PropTypes.string, + + /** + * The index of your Tab in your Tabs. Reserved for usage in Tabs + */ + index: PropTypes.number, + + /** + * Provide the contents of your Tab + */ + label: PropTypes.node, + + /** + * Provide a handler that is invoked when a user clicks on the control + */ + onClick: PropTypes.func.isRequired, + + /** + * Provide a handler that is invoked on the key down event for the control + */ + onKeyDown: PropTypes.func.isRequired, + + /* + * An optional parameter to allow overriding the anchor rendering. + * Useful for using Tab along with react-router or other client + * side router libraries. + **/ + renderAnchor: deprecate( + PropTypes.func, + 'The renderAnchor prop has been deprecated in favor of renderButton. It has been removed in v11.' + ), + renderButton: PropTypes.func, + + /* + * An optional parameter to allow overriding the content rendering. + **/ + renderContent: PropTypes.func, + + /** + * Provide an accessibility role for your Tab + */ + role: deprecate( + PropTypes.string, + 'The role prop has been deprecated and removed in v11. Role is now built into the element.' + ), + + /** + * Whether your Tab is selected. + * Reserved for usage in Tabs + */ + selected: PropTypes.bool.isRequired, + + /** + * Specify the tab index of the `<button>` node + */ + tabIndex: PropTypes.number, +}; diff --git a/packages/react/src/components/Tabs/Tabs-story.js b/packages/react/src/components/Tabs/Tabs-story.js index 80d5ff17289a..a2b6d5f1650c 100644 --- a/packages/react/src/components/Tabs/Tabs-story.js +++ b/packages/react/src/components/Tabs/Tabs-story.js @@ -20,7 +20,7 @@ import './Tabs-story.scss'; import CodeSnippet from '../CodeSnippet'; import Button from '../Button'; import Tabs from '../Tabs'; -import Tab from '../Tab'; +import { Tab } from '../Tab'; import TabsSkeleton from '../Tabs/Tabs.Skeleton'; import mdx from './Tabs.mdx'; From be88f6ac236d1da24f4043e118291b833c71d519 Mon Sep 17 00:00:00 2001 From: Josefina Mancilla <josefinanoemi4@gmail.com> Date: Fri, 1 Oct 2021 00:58:51 -0500 Subject: [PATCH 02/22] feat(Tabs): convert to functional --- packages/react/src/components/Tab/index.js | 2 +- packages/react/src/components/Tab/next/Tab.js | 8 +- .../react/src/components/Tabs/Tabs-story.js | 2 +- packages/react/src/components/Tabs/Tabs.js | 6 + packages/react/src/components/Tabs/index.js | 10 +- .../react/src/components/Tabs/next/Tabs.js | 557 ++++++++++++++++++ 6 files changed, 578 insertions(+), 7 deletions(-) create mode 100644 packages/react/src/components/Tabs/next/Tabs.js diff --git a/packages/react/src/components/Tab/index.js b/packages/react/src/components/Tab/index.js index 83ad52cd2666..754fdd1a0a2f 100644 --- a/packages/react/src/components/Tab/index.js +++ b/packages/react/src/components/Tab/index.js @@ -7,7 +7,7 @@ import * as FeatureFlags from '@carbon/feature-flags'; import { Tab as TabNext } from './next/Tab'; -import TabClassic from './Tab'; +import { Tab as TabClassic } from './Tab'; export const Tab = FeatureFlags.enabled('enable-v11-release') ? TabNext diff --git a/packages/react/src/components/Tab/next/Tab.js b/packages/react/src/components/Tab/next/Tab.js index bad4ff032694..50e76b44f537 100644 --- a/packages/react/src/components/Tab/next/Tab.js +++ b/packages/react/src/components/Tab/next/Tab.js @@ -8,12 +8,10 @@ import PropTypes from 'prop-types'; import React from 'react'; import classNames from 'classnames'; -import { settings } from 'carbon-components'; import deprecate from '../../../prop-types/deprecate'; +import { usePrefix } from '../../../internal/usePrefix'; -const { prefix } = settings; - -const Tab = React.forwardRef(function Tab( +export const Tab = React.forwardRef(function Tab( { className, disabled, @@ -32,6 +30,8 @@ const Tab = React.forwardRef(function Tab( }, ref ) { + const { prefix } = usePrefix(); + const classes = classNames(className, `${prefix}--tabs__nav-link`, { [`${prefix}--tabs__nav-item--disabled`]: disabled, [`${prefix}--tabs__nav-item--selected`]: selected, diff --git a/packages/react/src/components/Tabs/Tabs-story.js b/packages/react/src/components/Tabs/Tabs-story.js index a2b6d5f1650c..80d5ff17289a 100644 --- a/packages/react/src/components/Tabs/Tabs-story.js +++ b/packages/react/src/components/Tabs/Tabs-story.js @@ -20,7 +20,7 @@ import './Tabs-story.scss'; import CodeSnippet from '../CodeSnippet'; import Button from '../Button'; import Tabs from '../Tabs'; -import { Tab } from '../Tab'; +import Tab from '../Tab'; import TabsSkeleton from '../Tabs/Tabs.Skeleton'; import mdx from './Tabs.mdx'; diff --git a/packages/react/src/components/Tabs/Tabs.js b/packages/react/src/components/Tabs/Tabs.js index bd259a91b4f6..2483538d2bd2 100644 --- a/packages/react/src/components/Tabs/Tabs.js +++ b/packages/react/src/components/Tabs/Tabs.js @@ -170,6 +170,7 @@ export default class Tabs extends React.Component { scrollLeft: tablistScrollLeft, scrollWidth: tablistScrollWidth, } = this.tablist?.current || {}; + const tab = this.getTabAt(this.state.selected); const horizontalOverflow = tablistScrollWidth > tablistClientWidth; @@ -187,6 +188,8 @@ export default class Tabs extends React.Component { this.tablist.current.scrollLeft += this.OVERFLOW_BUTTON_OFFSET * 2; } } + + console.log('THIS', this); } componentWillUnmount() { @@ -203,6 +206,7 @@ export default class Tabs extends React.Component { scrollLeft: tablistScrollLeft, scrollWidth: tablistScrollWidth, } = this.tablist.current; + const { tablistClientWidth: currentStateClientWidth, tablistScrollLeft: currentStateScrollLeft, @@ -337,6 +341,8 @@ export default class Tabs extends React.Component { setTabAt = (index, tabRef) => { this[`tab${index}`] = tabRef; + console.log(this[`tab${index}`]); + console.log(tabRef); }; overflowNavInterval = null; diff --git a/packages/react/src/components/Tabs/index.js b/packages/react/src/components/Tabs/index.js index 531a69be74b1..60eead8232eb 100644 --- a/packages/react/src/components/Tabs/index.js +++ b/packages/react/src/components/Tabs/index.js @@ -5,5 +5,13 @@ * LICENSE file in the root directory of this source tree. */ +import * as FeatureFlags from '@carbon/feature-flags'; +import { Tabs as TabsNext } from './next/Tabs'; +import { Tabs as TabsClassic } from './Tabs'; + +export const Tabs = FeatureFlags.enabled('enable-v11-release') + ? TabsNext + : TabsClassic; + export * from './Tabs.Skeleton'; -export default from './Tabs'; +// export default from './Tabs'; diff --git a/packages/react/src/components/Tabs/next/Tabs.js b/packages/react/src/components/Tabs/next/Tabs.js new file mode 100644 index 000000000000..40de9f14c441 --- /dev/null +++ b/packages/react/src/components/Tabs/next/Tabs.js @@ -0,0 +1,557 @@ +/** + * Copyright IBM Corp. 2016, 2018 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import PropTypes from 'prop-types'; +import React, { + useState, + useRef, + useEffect, + useCallback, + useMemo, +} from 'react'; +import classNames from 'classnames'; +import { ChevronLeft16, ChevronRight16 } from '@carbon/icons-react'; +import debounce from 'lodash.debounce'; +import { keys, match, matches } from '../../../internal/keyboard'; +import TabContent from '../../TabContent'; +import deprecate from '../../../prop-types/deprecate'; +import { usePrefix } from '../../../internal/usePrefix'; + +export const Tabs = React.forwardRef(function Tabs( + { + children, + className, + leftOverflowButtonProps, + onSelectionChange, + rightOverflowButtonProps, + scrollIntoView = true, + selected = 0, + selectionMode = 'automatic', + tabContentClassName, + type = 'default', + ...other + }, + ref +) { + const { prefix } = usePrefix(); + + //refs + const tablist = useRef(); + const leftOverflowNavButton = useRef(); + const rightOverflowNavButton = useRef(); + + //state + const [horizontalOverflow, setHorizontalOverflow] = useState(false); + const [tablistClientWidth, setTablistClientWidth] = useState( + tablist.current.clientWidth + ); + const [tablistScrollWidth, setTablistScrollWidth] = useState( + tablist.current.scrollWidth + ); + const [tablistScrollLeft, setTablistScrollLeft] = useState( + tablist.current.scrollLeft + ); + + let tabs = useMemo(() => {}, []); + + //prop + state alignment - getDerivedStateFromProps + const [isSelected, setIsSelected] = useState(selected); + const [prevSelected, setPrevSelected] = useState(isSelected); + if (selected !== prevSelected) { + setIsSelected(selected); + setPrevSelected(selected); + } + + // width of the overflow buttons + let OVERFLOW_BUTTON_OFFSET = 40; + + /** + * `scroll` event handler to save tablist clientWidth, scrollWidth, and + * scrollLeft + */ + const handleScroll = () => { + if (!tablist?.current) { + return; + } + const { clientWidth, scrollLeft, scrollWidth } = tablist.current; + + setTablistClientWidth(clientWidth); + setTablistScrollWidth(scrollWidth); + setTablistScrollLeft(scrollLeft); + setHorizontalOverflow(scrollWidth > clientWidth); + }; + + /** + * The debounced version of the `resize` event handler. + * @type {Function} + * @private + */ + const _debouncedHandleWindowResize = debounce(_handleWindowResize, 200); + + const _handleWindowResize = handleScroll(); + + const getEnabledTabs = () => + React.Children.toArray(children).reduce( + (enabledTabs, tab, index) => + !tab.props.disabled ? enabledTabs.concat(index) : enabledTabs, + [] + ); + + const getNextIndex = (index, direction) => { + const enabledTabs = getEnabledTabs(); + const nextIndex = Math.max( + enabledTabs.indexOf(index) + direction, + // For `tab` not found in `enabledTabs` + -1 + ); + const nextIndexLooped = + nextIndex >= 0 && nextIndex < enabledTabs.length + ? nextIndex + : nextIndex - Math.sign(nextIndex) * enabledTabs.length; + return enabledTabs[nextIndexLooped]; + }; + + const getDirection = (evt) => { + if (match(evt, keys.ArrowLeft)) { + return -1; + } + if (match(evt, keys.ArrowRight)) { + return 1; + } + return 0; + }; + + const getTabAt = useCallback( + (index, useFresh) => + (!useFresh && tabs[`tab${index}`]) || + React.Children.toArray(children)[index], + [tabs, children] + ); + + const scrollTabIntoView = (event, { index }) => { + const tab = getTabAt(index); + if ( + matches(event, [keys.ArrowLeft, keys.ArrowRight]) || + event.type === 'click' + ) { + const currentScrollLeft = tablistScrollLeft; + tab?.tabAnchor?.scrollIntoView({ + block: 'nearest', + inline: 'nearest', + }); + tab?.tabAnchor?.focus(); + const newScrollLeft = tablist.current.scrollLeft; + if (newScrollLeft > currentScrollLeft) { + tablist.current.scrollLeft += OVERFLOW_BUTTON_OFFSET; + } + } + }; + + const selectTabAt = (event, { index, onSelectionChange }) => { + scrollTabIntoView(event, { index }); + if (isSelected !== index) { + setIsSelected(index); + setPrevSelected(index); + if (typeof onSelectionChange === 'function') { + onSelectionChange(index); + } + } + }; + + const handleTabKeyDown = (onSelectionChange) => { + return (index, evt) => { + if (matches(evt, [keys.Enter, keys.Space])) { + selectTabAt(evt, { index, onSelectionChange }); + } + + const nextIndex = (() => { + if (matches(evt, [keys.ArrowLeft, keys.ArrowRight])) { + return getNextIndex(index, getDirection(evt)); + } + if (match(evt, keys.Home)) { + return 0; + } + if (match(evt, keys.End)) { + return getEnabledTabs().pop(); + } + })(); + const tab = getTabAt(nextIndex); + + if ( + matches(evt, [keys.ArrowLeft, keys.ArrowRight, keys.Home, keys.End]) + ) { + evt.preventDefault(); + if (selectionMode !== 'manual') { + selectTabAt(evt, { index: nextIndex, onSelectionChange }); + } else { + scrollTabIntoView(evt, { index: nextIndex }); + } + tab?.tabAnchor?.focus(); + } + }; + }; + + const getTabs = () => React.Children.map(children, (tab) => tab); + + // following functions (handle*) are Props on Tab.js, see Tab.js for parameters + const handleTabClick = (onSelectionChange) => (index, evt) => { + evt.preventDefault(); + selectTabAt(evt, { index, onSelectionChange }); + }; + + const setTabAt = (index, tabRef) => { + tabs[`tab${index}`] = tabRef; + }; + + let overflowNavInterval = null; + + const handleOverflowNavClick = (_, { direction, multiplier = 10 }) => { + // account for overflow button appearing and causing tablist width change + const { clientWidth, scrollLeft, scrollWidth } = tablist?.current; + if (direction === 1 && !scrollLeft) { + tablist.current.scrollLeft += OVERFLOW_BUTTON_OFFSET; + } + + tablist.current.scrollLeft += direction * multiplier; + + const leftEdgeReached = + direction === -1 && scrollLeft < OVERFLOW_BUTTON_OFFSET; + + const rightEdgeReached = + direction === 1 && + scrollLeft + clientWidth >= scrollWidth - OVERFLOW_BUTTON_OFFSET; + + if (leftEdgeReached || rightEdgeReached) { + if (leftEdgeReached) { + rightOverflowNavButton?.current?.focus(); + } + if (rightEdgeReached) { + leftOverflowNavButton?.current?.focus(); + } + } + }; + + const handleOverflowNavMouseDown = (event, { direction }) => { + // disregard mouse buttons aside from LMB + if (event.buttons !== 1) { + return; + } + + overflowNavInterval = setInterval(() => { + const { clientWidth, scrollLeft, scrollWidth } = tablist?.current; + + // clear interval if scroll reaches left or right edge + const leftEdgeReached = + direction === -1 && scrollLeft < OVERFLOW_BUTTON_OFFSET; + + const rightEdgeReached = + direction === 1 && + scrollLeft + clientWidth >= scrollWidth - OVERFLOW_BUTTON_OFFSET; + + if (leftEdgeReached || rightEdgeReached) { + clearInterval(overflowNavInterval); + } + + // account for overflow button appearing and causing tablist width change + handleOverflowNavClick(event, { direction }); + }); + }; + + const handleOverflowNavMouseUp = () => { + clearInterval(overflowNavInterval); + }; + + //component did mount equivalent + useEffect(() => { + _handleWindowResize(); + window.addEventListener('resize', _debouncedHandleWindowResize); + + // scroll selected tab into view on mount + const { clientWidth, scrollLeft, scrollWidth } = tablist?.current || {}; + const tab = getTabAt(isSelected); + const horizontalOverflow = scrollWidth > clientWidth; + + if (horizontalOverflow) { + const leftOverflowNavButtonHidden = + tab?.tabAnchor?.getBoundingClientRect().right < + tab?.tabAnchor?.offsetParent.getBoundingClientRect().right; + + const rightOverflowNavButtonHidden = + scrollLeft + clientWidth === scrollWidth; + scrollIntoView && + tab?.tabAnchor?.scrollIntoView({ + block: 'nearest', + inline: 'nearest', + }); + + // account for overflow buttons in scroll position on mount + if (!leftOverflowNavButtonHidden && !rightOverflowNavButtonHidden) { + tablist.current.scrollLeft += OVERFLOW_BUTTON_OFFSET * 2; + } + } + + //component will unmount equivalent + return () => { + if (_debouncedHandleWindowResize) { + _debouncedHandleWindowResize.cancel(); + } + window.removeEventListener('resize', _debouncedHandleWindowResize); + }; + }, [ + isSelected, + scrollIntoView, + getTabAt, + OVERFLOW_BUTTON_OFFSET, + _debouncedHandleWindowResize, + _handleWindowResize, + ]); + + //component did update equivalent + useEffect(() => { + // compare current tablist properties to current state + const { + clientWidth: currentTablistClientWidth, + scrollLeft: currentTablistScrollLeft, + scrollWidth: currentTablistScrollWidth, + } = tablist.current; + + if ( + currentTablistClientWidth !== tablistClientWidth || + currentTablistScrollLeft !== tablistScrollLeft || + currentTablistScrollWidth !== tablistScrollWidth + ) { + setTablistClientWidth(currentTablistClientWidth); + setTablistScrollWidth(currentTablistScrollWidth); + setTablistScrollLeft(currentTablistScrollLeft); + setHorizontalOverflow( + currentTablistScrollWidth > currentTablistClientWidth + ); + } + + if (scrollIntoView && prevSelected !== isSelected) { + getTabAt(isSelected)?.tabAnchor?.scrollIntoView({ + block: 'nearest', + inline: 'nearest', + }); + } + }, [ + isSelected, + prevSelected, + scrollIntoView, + tablistClientWidth, + tablistScrollLeft, + tablistScrollWidth, + getTabAt, + ]); + + /** + * The tab panel acts like a tab panel when the screen is wider, but acts + * like a select list when the screen is narrow. In the wide case we want + * to allow the user to use the tab key to set the focus in the tab panel + * and then use the left and right arrow keys to navigate the tabs. In the + * narrow case we want to use the tab key to select different options in + * the list. + * + * We set the tab index based on the different states so the browser will treat + * the whole tab panel as a single focus component when it looks like a tab + * panel and separate components when it looks like a select list. + */ + const tabsWithProps = getTabs().map((tab, index) => { + const tabIndex = index === isSelected ? 0 : -1; + const newTab = React.cloneElement(tab, { + index, + selected: index === isSelected, + handleTabClick: handleTabClick(onSelectionChange), + tabIndex, + ref: (e) => { + setTabAt(index, e); + }, + handleTabKeyDown: handleTabKeyDown(onSelectionChange), + }); + + return newTab; + }); + + const tabContentWithProps = React.Children.map(tabsWithProps, (tab) => { + const { + id: tabId, + children, + selected, + renderContent: Content = TabContent, + } = tab.props; + + return ( + <Content + id={tabId && `${tabId}__panel`} + className={tabContentClassName} + hidden={!selected} + selected={selected} + aria-labelledby={tabId}> + {children} + </Content> + ); + }); + + const leftOverflowNavButtonHidden = !horizontalOverflow || !tablistScrollLeft; + + const rightOverflowNavButtonHidden = + !horizontalOverflow || + tablistScrollLeft + tablistClientWidth === tablistScrollWidth; + + const classes = { + tabs: classNames(className, `${prefix}--tabs`, { + [`${prefix}--tabs--container`]: type === 'container', + // [`${prefix}--tabs--light`]: light, + }), + tablist: classNames(`${prefix}--tabs__nav`), + leftOverflowButtonClasses: classNames({ + [`${prefix}--tab--overflow-nav-button`]: horizontalOverflow, + [`${prefix}--tab--overflow-nav-button--hidden`]: leftOverflowNavButtonHidden, + }), + rightOverflowButtonClasses: classNames({ + [`${prefix}--tab--overflow-nav-button`]: horizontalOverflow, + [`${prefix}--tab--overflow-nav-button--hidden`]: rightOverflowNavButtonHidden, + }), + }; + + return ( + <> + <div + className={classes.tabs} + onScroll={handleScroll} + ref={ref} + {...other}> + <button + aria-hidden="true" + aria-label="Scroll left" + className={classes.leftOverflowButtonClasses} + onClick={(_) => handleOverflowNavClick(_, { direction: -1 })} + onMouseDown={(event) => + handleOverflowNavMouseDown(event, { direction: -1 }) + } + onMouseUp={handleOverflowNavMouseUp} + ref={leftOverflowNavButton.current} + tabIndex="-1" + type="button" + {...leftOverflowButtonProps}> + <ChevronLeft16 /> + </button> + {!leftOverflowNavButtonHidden && ( + <div className={`${prefix}--tabs__overflow-indicator--left`} /> + )} + <ul + role="tablist" + tabIndex={-1} + className={classes.tablist} + ref={tablist.current}> + {tabsWithProps} + </ul> + {!rightOverflowNavButtonHidden && ( + <div className={`${prefix}--tabs__overflow-indicator--right`} /> + )} + <button + aria-hidden="true" + aria-label="Scroll right" + className={classes.rightOverflowButtonClasses} + onClick={(_) => handleOverflowNavClick(_, { direction: 1 })} + onMouseDown={(event) => + handleOverflowNavMouseDown(event, { direction: 1 }) + } + onMouseUp={handleOverflowNavMouseUp} + ref={rightOverflowNavButton.current} + tabIndex="-1" + type="button" + {...rightOverflowButtonProps}> + <ChevronRight16 /> + </button> + </div> + {tabContentWithProps} + </> + ); +}); + +Tabs.propTypes = { + /** + * Pass in a collection of <Tab> children to be rendered depending on the + * currently selected tab + */ + children: PropTypes.node, + + /** + * Provide a className that is applied to the root <div> component for the + * <Tabs> + */ + className: PropTypes.string, + + /** + * Specify whether the Tab content is hidden + */ + hidden: PropTypes.bool, + + /** + * Provide the props that describe the left overflow button + */ + leftOverflowButtonProps: PropTypes.object, + + /** + * Specify whether or not to use the light component variant + */ + light: deprecate( + PropTypes.bool, + 'The light prop has been deprecated in v11 in favor of our new layering model that uses the Layer component' + ), + + /** + * Optionally provide an `onClick` handler that is invoked when a <Tab> is + * clicked + */ + onClick: PropTypes.func, + + /** + * Optionally provide an `onKeyDown` handler that is invoked when keyed + * navigation is triggered + */ + onKeyDown: PropTypes.func, + + /** + * Provide an optional handler that is called whenever the selection + * changes. This method is called with the index of the tab that was + * selected + */ + onSelectionChange: PropTypes.func, + + /** + * Provide the props that describe the right overflow button + */ + rightOverflowButtonProps: PropTypes.object, + + /** + * Choose whether or not to automatically scroll to newly selected tabs + * on component rerender + */ + scrollIntoView: PropTypes.bool, + + /** + * Optionally provide an index for the currently selected <Tab> + */ + selected: PropTypes.number, + + /** + * Choose whether or not to automatically change selection on focus + */ + selectionMode: PropTypes.oneOf(['automatic', 'manual']), + + /** + * Provide a className that is applied to the <TabContent> components + */ + tabContentClassName: PropTypes.string, + + /** + * Provide the type of Tab + */ + type: PropTypes.oneOf(['default', 'container']), +}; From 6d9e103819ab53e108b0ee3f571be0668221e7cc Mon Sep 17 00:00:00 2001 From: Josefina Mancilla <josefinanoemi4@gmail.com> Date: Mon, 4 Oct 2021 11:20:14 -0500 Subject: [PATCH 03/22] fix: remove console.logs --- packages/react/src/components/Tabs/Tabs.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/react/src/components/Tabs/Tabs.js b/packages/react/src/components/Tabs/Tabs.js index 23318658ee04..fa15fb5aba4c 100644 --- a/packages/react/src/components/Tabs/Tabs.js +++ b/packages/react/src/components/Tabs/Tabs.js @@ -170,7 +170,6 @@ export default class Tabs extends React.Component { scrollLeft: tablistScrollLeft, scrollWidth: tablistScrollWidth, } = this.tablist?.current || {}; - const tab = this.getTabAt(this.state.selected); const horizontalOverflow = tablistScrollWidth > tablistClientWidth; @@ -188,7 +187,7 @@ export default class Tabs extends React.Component { this.tablist.current.scrollLeft += this.OVERFLOW_BUTTON_OFFSET * 2; } } - + // remove this before merging! console.log('THIS', this); } @@ -206,7 +205,6 @@ export default class Tabs extends React.Component { scrollLeft: tablistScrollLeft, scrollWidth: tablistScrollWidth, } = this.tablist.current; - const { tablistClientWidth: currentStateClientWidth, tablistScrollLeft: currentStateScrollLeft, @@ -341,8 +339,6 @@ export default class Tabs extends React.Component { setTabAt = (index, tabRef) => { this[`tab${index}`] = tabRef; - console.log(this[`tab${index}`]); - console.log(tabRef); }; overflowNavInterval = null; From 762860d40f1d39538cac70e4fe861c2ddd634853 Mon Sep 17 00:00:00 2001 From: Josefina Mancilla <josefinanoemi4@gmail.com> Date: Mon, 4 Oct 2021 14:42:51 -0500 Subject: [PATCH 04/22] fix: export components as default --- packages/react/src/components/Tab/index.js | 10 +++++----- packages/react/src/components/Tab/next/Tab.js | 4 +++- packages/react/src/components/Tabs/index.js | 8 +++++--- packages/react/src/components/Tabs/next/Tabs.js | 4 +++- 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/packages/react/src/components/Tab/index.js b/packages/react/src/components/Tab/index.js index 754fdd1a0a2f..d76f1b004194 100644 --- a/packages/react/src/components/Tab/index.js +++ b/packages/react/src/components/Tab/index.js @@ -6,9 +6,9 @@ */ import * as FeatureFlags from '@carbon/feature-flags'; -import { Tab as TabNext } from './next/Tab'; -import { Tab as TabClassic } from './Tab'; +import { default as TabNext } from './next/Tab'; +import { default as TabClassic } from './Tab'; -export const Tab = FeatureFlags.enabled('enable-v11-release') - ? TabNext - : TabClassic; +const Tab = FeatureFlags.enabled('enable-v11-release') ? TabNext : TabClassic; + +export default Tab; diff --git a/packages/react/src/components/Tab/next/Tab.js b/packages/react/src/components/Tab/next/Tab.js index 50e76b44f537..a728b2ffee88 100644 --- a/packages/react/src/components/Tab/next/Tab.js +++ b/packages/react/src/components/Tab/next/Tab.js @@ -11,7 +11,7 @@ import classNames from 'classnames'; import deprecate from '../../../prop-types/deprecate'; import { usePrefix } from '../../../internal/usePrefix'; -export const Tab = React.forwardRef(function Tab( +const Tab = React.forwardRef(function Tab( { className, disabled, @@ -172,3 +172,5 @@ Tab.propTypes = { */ tabIndex: PropTypes.number, }; + +export default Tab; diff --git a/packages/react/src/components/Tabs/index.js b/packages/react/src/components/Tabs/index.js index 60eead8232eb..ec7ffc66fae8 100644 --- a/packages/react/src/components/Tabs/index.js +++ b/packages/react/src/components/Tabs/index.js @@ -6,12 +6,14 @@ */ import * as FeatureFlags from '@carbon/feature-flags'; -import { Tabs as TabsNext } from './next/Tabs'; -import { Tabs as TabsClassic } from './Tabs'; +import { default as TabsNext } from './next/Tabs'; +import { default as TabsClassic } from './Tabs'; -export const Tabs = FeatureFlags.enabled('enable-v11-release') +const Tabs = FeatureFlags.enabled('enable-v11-release') ? TabsNext : TabsClassic; export * from './Tabs.Skeleton'; // export default from './Tabs'; + +export default Tabs; diff --git a/packages/react/src/components/Tabs/next/Tabs.js b/packages/react/src/components/Tabs/next/Tabs.js index 40de9f14c441..535c17fe69aa 100644 --- a/packages/react/src/components/Tabs/next/Tabs.js +++ b/packages/react/src/components/Tabs/next/Tabs.js @@ -21,7 +21,7 @@ import TabContent from '../../TabContent'; import deprecate from '../../../prop-types/deprecate'; import { usePrefix } from '../../../internal/usePrefix'; -export const Tabs = React.forwardRef(function Tabs( +const Tabs = React.forwardRef(function Tabs( { children, className, @@ -555,3 +555,5 @@ Tabs.propTypes = { */ type: PropTypes.oneOf(['default', 'container']), }; + +export default Tabs; From b9d0e4317390ce2569b622026df181528e1a1bc4 Mon Sep 17 00:00:00 2001 From: Josefina Mancilla <josefinanoemi4@gmail.com> Date: Mon, 4 Oct 2021 16:12:00 -0500 Subject: [PATCH 05/22] fix: tabs state and refs --- .../react/src/components/Tabs/next/Tabs.js | 57 +++++++++---------- 1 file changed, 28 insertions(+), 29 deletions(-) diff --git a/packages/react/src/components/Tabs/next/Tabs.js b/packages/react/src/components/Tabs/next/Tabs.js index 535c17fe69aa..3eb3746f9be0 100644 --- a/packages/react/src/components/Tabs/next/Tabs.js +++ b/packages/react/src/components/Tabs/next/Tabs.js @@ -6,13 +6,7 @@ */ import PropTypes from 'prop-types'; -import React, { - useState, - useRef, - useEffect, - useCallback, - useMemo, -} from 'react'; +import React, { useState, useRef, useEffect, useCallback } from 'react'; import classNames from 'classnames'; import { ChevronLeft16, ChevronRight16 } from '@carbon/icons-react'; import debounce from 'lodash.debounce'; @@ -43,20 +37,13 @@ const Tabs = React.forwardRef(function Tabs( const tablist = useRef(); const leftOverflowNavButton = useRef(); const rightOverflowNavButton = useRef(); + const tabs = useRef([]); //state const [horizontalOverflow, setHorizontalOverflow] = useState(false); - const [tablistClientWidth, setTablistClientWidth] = useState( - tablist.current.clientWidth - ); - const [tablistScrollWidth, setTablistScrollWidth] = useState( - tablist.current.scrollWidth - ); - const [tablistScrollLeft, setTablistScrollLeft] = useState( - tablist.current.scrollLeft - ); - - let tabs = useMemo(() => {}, []); + const [tablistClientWidth, setTablistClientWidth] = useState(null); + const [tablistScrollWidth, setTablistScrollWidth] = useState(null); + const [tablistScrollLeft, setTablistScrollLeft] = useState(null); //prop + state alignment - getDerivedStateFromProps const [isSelected, setIsSelected] = useState(selected); @@ -90,9 +77,13 @@ const Tabs = React.forwardRef(function Tabs( * @type {Function} * @private */ - const _debouncedHandleWindowResize = debounce(_handleWindowResize, 200); + const _debouncedHandleWindowResize = useRef(); + + const _handleWindowResize = handleScroll; - const _handleWindowResize = handleScroll(); + useEffect(() => { + _debouncedHandleWindowResize.current = debounce(_handleWindowResize, 200); + }, []); // eslint-disable-line react-hooks/exhaustive-deps const getEnabledTabs = () => React.Children.toArray(children).reduce( @@ -127,7 +118,7 @@ const Tabs = React.forwardRef(function Tabs( const getTabAt = useCallback( (index, useFresh) => - (!useFresh && tabs[`tab${index}`]) || + (!useFresh && tabs.current[index]) || React.Children.toArray(children)[index], [tabs, children] ); @@ -204,7 +195,7 @@ const Tabs = React.forwardRef(function Tabs( }; const setTabAt = (index, tabRef) => { - tabs[`tab${index}`] = tabRef; + tabs.current[index] = tabRef; }; let overflowNavInterval = null; @@ -268,10 +259,15 @@ const Tabs = React.forwardRef(function Tabs( //component did mount equivalent useEffect(() => { _handleWindowResize(); - window.addEventListener('resize', _debouncedHandleWindowResize); + window.addEventListener('resize', _debouncedHandleWindowResize.current); // scroll selected tab into view on mount const { clientWidth, scrollLeft, scrollWidth } = tablist?.current || {}; + + setTablistClientWidth(clientWidth); + setTablistScrollWidth(scrollWidth); + setTablistScrollLeft(scrollLeft); + const tab = getTabAt(isSelected); const horizontalOverflow = scrollWidth > clientWidth; @@ -296,10 +292,13 @@ const Tabs = React.forwardRef(function Tabs( //component will unmount equivalent return () => { - if (_debouncedHandleWindowResize) { - _debouncedHandleWindowResize.cancel(); + if (_debouncedHandleWindowResize.current) { + _debouncedHandleWindowResize.current.cancel(); } - window.removeEventListener('resize', _debouncedHandleWindowResize); + window.removeEventListener( + 'resize', + _debouncedHandleWindowResize.current + ); }; }, [ isSelected, @@ -434,7 +433,7 @@ const Tabs = React.forwardRef(function Tabs( handleOverflowNavMouseDown(event, { direction: -1 }) } onMouseUp={handleOverflowNavMouseUp} - ref={leftOverflowNavButton.current} + ref={leftOverflowNavButton} tabIndex="-1" type="button" {...leftOverflowButtonProps}> @@ -447,7 +446,7 @@ const Tabs = React.forwardRef(function Tabs( role="tablist" tabIndex={-1} className={classes.tablist} - ref={tablist.current}> + ref={tablist}> {tabsWithProps} </ul> {!rightOverflowNavButtonHidden && ( @@ -462,7 +461,7 @@ const Tabs = React.forwardRef(function Tabs( handleOverflowNavMouseDown(event, { direction: 1 }) } onMouseUp={handleOverflowNavMouseUp} - ref={rightOverflowNavButton.current} + ref={rightOverflowNavButton} tabIndex="-1" type="button" {...rightOverflowButtonProps}> From 047a141f06d56e677645348d0e253381683d64bd Mon Sep 17 00:00:00 2001 From: Josefina Mancilla <josefinanoemi4@gmail.com> Date: Tue, 5 Oct 2021 10:08:40 -0500 Subject: [PATCH 06/22] fix: use prefix --- packages/react/src/components/Tab/next/Tab.js | 2 +- packages/react/src/components/Tabs/next/Tabs.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react/src/components/Tab/next/Tab.js b/packages/react/src/components/Tab/next/Tab.js index a728b2ffee88..4a5637c5e181 100644 --- a/packages/react/src/components/Tab/next/Tab.js +++ b/packages/react/src/components/Tab/next/Tab.js @@ -30,7 +30,7 @@ const Tab = React.forwardRef(function Tab( }, ref ) { - const { prefix } = usePrefix(); + const prefix = usePrefix(); const classes = classNames(className, `${prefix}--tabs__nav-link`, { [`${prefix}--tabs__nav-item--disabled`]: disabled, diff --git a/packages/react/src/components/Tabs/next/Tabs.js b/packages/react/src/components/Tabs/next/Tabs.js index 3eb3746f9be0..09ad4e867e59 100644 --- a/packages/react/src/components/Tabs/next/Tabs.js +++ b/packages/react/src/components/Tabs/next/Tabs.js @@ -31,7 +31,7 @@ const Tabs = React.forwardRef(function Tabs( }, ref ) { - const { prefix } = usePrefix(); + const prefix = usePrefix(); //refs const tablist = useRef(); From de12b3aa3e34d21b5360e29f8201033d53d98c4e Mon Sep 17 00:00:00 2001 From: Josefina Mancilla <josefinanoemi4@gmail.com> Date: Tue, 5 Oct 2021 10:09:08 -0500 Subject: [PATCH 07/22] fix: add feature flag v11 story --- packages/carbon-react/src/components/Tabs/Tabs.stories.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/carbon-react/src/components/Tabs/Tabs.stories.js b/packages/carbon-react/src/components/Tabs/Tabs.stories.js index 08482fdb9108..6d740c04bd60 100644 --- a/packages/carbon-react/src/components/Tabs/Tabs.stories.js +++ b/packages/carbon-react/src/components/Tabs/Tabs.stories.js @@ -7,9 +7,17 @@ import React from 'react'; import { Button, Tab, Tabs, TabsSkeleton } from '../Tabs'; +import { unstable_FeatureFlags as FeatureFlags } from 'carbon-components-react'; export default { title: 'Components/Tabs', + decorators: [ + (Story) => ( + <FeatureFlags flags={{ 'enable-v11-release': true }}> + <Story /> + </FeatureFlags> + ), + ], parameters: { component: Tabs, subcomponents: { From 19aaa757264f533cf4c2b83025b16a0224ef9c77 Mon Sep 17 00:00:00 2001 From: Josefina Mancilla <josefinanoemi4@gmail.com> Date: Tue, 5 Oct 2021 23:20:51 -0500 Subject: [PATCH 08/22] fix: temp use v10 classNames --- .../components/src/components/tabs/_tabs.scss | 408 ++++++++++++++++++ packages/react/src/components/Tab/next/Tab.js | 21 +- .../react/src/components/Tabs/next/Tabs.js | 21 +- 3 files changed, 440 insertions(+), 10 deletions(-) diff --git a/packages/components/src/components/tabs/_tabs.scss b/packages/components/src/components/tabs/_tabs.scss index 8921c57308ba..9856776d533a 100644 --- a/packages/components/src/components/tabs/_tabs.scss +++ b/packages/components/src/components/tabs/_tabs.scss @@ -447,6 +447,414 @@ @include hidden; } + // ----------------------------- + // v11 styles + // ----------------------------- + @if feature-flag-enabled('enable-v11-release') { + .#{$prefix}--skeleton.#{$prefix}--tabs:not(.#{$prefix}--tabs--container) + .#{$prefix}--tabs__nav-item { + border-bottom: 2px solid $skeleton-02; + } + + .#{$prefix}--tabs { + @include reset; + @include type-style('body-short-01'); + + display: flex; + width: 100%; + height: auto; + min-height: rem(40px); + color: $text-01; + + &.#{$prefix}--tabs--container { + min-height: rem(48px); + } + + .#{$prefix}--tabs__nav { + display: flex; + overflow: auto hidden; + width: auto; + max-width: 100%; + flex-direction: row; + padding: 0; + margin: 0; + list-style: none; + outline: 0; + // hide scrollbars + scrollbar-width: none; + transition: max-height $duration--fast-01 motion(standard, productive); + + &::-webkit-scrollbar { + display: none; + } + } + + //----------------------------- + // Overflow Nav Buttons + //----------------------------- + .#{$prefix}--tabs__overflow-indicator--left, + .#{$prefix}--tabs__overflow-indicator--right { + z-index: 1; + width: $carbon--spacing-03; + flex: 1 0 auto; + } + + .#{$prefix}--tabs__overflow-indicator--left { + margin-right: -$carbon--spacing-03; + background-image: linear-gradient(to left, transparent, $ui-background); + } + + .#{$prefix}--tabs__overflow-indicator--right { + margin-left: -$carbon--spacing-03; + background-image: linear-gradient( + to right, + transparent, + $ui-background + ); + } + + // .#{$prefix}--tabs--light + // .#{$prefix}--tabs__overflow-indicator--left { + // background-image: linear-gradient(to left, transparent, $ui-01); + // } + + // .#{$prefix}--tabs--light + // .#{$prefix}--tabs__overflow-indicator--right { + // background-image: linear-gradient(to right, transparent, $ui-01); + // } + + &.#{$prefix}--tabs--container + .#{$prefix}--tabs__overflow-indicator--left { + background-image: linear-gradient(to left, transparent, $ui-03); + } + + &.#{$prefix}--tabs--container + .#{$prefix}--tabs__overflow-indicator--right { + background-image: linear-gradient(to right, transparent, $ui-03); + } + + // Safari-only media query + // won't appear correctly with CSS custom properties + // see: code snippet and modal overflow indicators + @media not all and (min-resolution: 0.001dpcm) { + @supports (-webkit-appearance: none) and (stroke-color: transparent) { + .#{$prefix}--tabs__overflow-indicator--left { + background-image: linear-gradient( + to left, + rgba($ui-background, 0), + $ui-background + ); + } + + .#{$prefix}--tabs__overflow-indicator--right { + background-image: linear-gradient( + to right, + rgba($ui-background, 0), + $ui-background + ); + } + + &.#{$prefix}--tabs--container + .#{$prefix}--tabs__overflow-indicator--left { + background-image: linear-gradient(to left, rgba($ui-03, 0), $ui-03); + } + + &.#{$prefix}--tabs--container + .#{$prefix}--tabs__overflow-indicator--right { + background-image: linear-gradient( + to right, + rgba($ui-03, 0), + $ui-03 + ); + } + } + } + + .#{$prefix}--tab--overflow-nav-button { + @include button-reset; + + display: flex; + width: $carbon--spacing-08; + flex-shrink: 0; + align-items: center; + justify-content: center; + + &:focus { + @include focus-outline('outline'); + } + } + + .#{$prefix}--tab--overflow-nav-button--hidden { + display: none; + } + + &.#{$prefix}--tabs--container .#{$prefix}--tab--overflow-nav-button { + width: $carbon--spacing-09; + margin: 0; + background-color: $ui-03; + } + + .#{$prefix}--tab--overflow-nav-button svg { + fill: $icon-01; + } + + //----------------------------- + // Item + //----------------------------- + .#{$prefix}--tabs__nav-item { + @include reset; + + display: flex; + padding: 0; + cursor: pointer; + transition: background-color + $duration--fast-01 + motion(standard, productive); + } + + .#{$prefix}--tabs__nav-item + .#{$prefix}--tabs__nav-item { + margin-left: rem(1px); + } + + &.#{$prefix}--tabs--container .#{$prefix}--tabs__nav-item { + background-color: $ui-03; + } + + &.#{$prefix}--tabs--container + .#{$prefix}--tabs__nav-item + + .#{$prefix}--tabs__nav-item { + margin-left: 0; + // Draws the border without affecting the inner-content + box-shadow: rem(-1px) 0 0 0 $ui-04; + } + + &.#{$prefix}--tabs--container + .#{$prefix}--tabs__nav-item + + .#{$prefix}--tabs__nav-item.#{$prefix}--tabs__nav-item--selected, + &.#{$prefix}--tabs--container + .#{$prefix}--tabs__nav-item.#{$prefix}--tabs__nav-item--selected + + .#{$prefix}--tabs__nav-item { + box-shadow: none; + } + + .#{$prefix}--tabs__nav-item .#{$prefix}--tabs__nav-link { + transition: color $duration--fast-01 motion(standard, productive), + border-bottom-color $duration--fast-01 motion(standard, productive), + outline $duration--fast-01 motion(standard, productive); + } + + //----------------------------- + // Item Hover + //----------------------------- + &.#{$prefix}--tabs--container .#{$prefix}--tabs__nav-item:hover { + background-color: $hover-selected-ui; + } + + //--------------------------------------------- + // Item Disabled + //--------------------------------------------- + .#{$prefix}--tabs__nav-item--disabled, + .#{$prefix}--tabs__nav-item--disabled:hover { + background-color: transparent; + cursor: not-allowed; + outline: none; + } + + &.#{$prefix}--tabs--container + .#{$prefix}--tabs__nav-item.#{$prefix}--tabs__nav-item--disabled, + &.#{$prefix}--tabs--container + .#{$prefix}--tabs__nav-item.#{$prefix}--tabs__nav-item--disabled:hover { + background-color: $disabled-02; + } + + //----------------------------- + // Item Selected + //----------------------------- + .#{$prefix}--tabs__nav-item--selected { + transition: color $duration--fast-01 motion(standard, productive); + } + + .#{$prefix}--tabs__nav-item--selected .#{$prefix}--tabs__nav-link, + .#{$prefix}--tabs__nav-item--selected .#{$prefix}--tabs__nav-link:focus, + .#{$prefix}--tabs__nav-item--selected .#{$prefix}--tabs__nav-link:active { + @include type-style('productive-heading-01'); + + border-bottom: 2px solid $interactive-04; + color: $text-01; + } + + &.#{$prefix}--tabs--container .#{$prefix}--tabs__nav-item--selected, + &.#{$prefix}--tabs--container + .#{$prefix}--tabs__nav-item--selected:hover { + background-color: $ui-01; + + .#{$prefix}--tabs__nav-link:focus, + .#{$prefix}--tabs__nav-link:active { + box-shadow: none; + } + } + + &.#{$prefix}--tabs--container + .#{$prefix}--tabs__nav-item--selected + .#{$prefix}--tabs__nav-link { + // Draws the border without affecting the inner-content + box-shadow: inset 0 2px 0 0 $interactive-04; + // height - vertical padding + line-height: calc(#{rem(48px)} - (#{$spacing-03} * 2)); + } + + &.#{$prefix}--tabs--light.#{$prefix}--tabs--container + .#{$prefix}--tabs__nav-item--selected, + &.#{$prefix}--tabs--light.#{$prefix}--tabs--container + .#{$prefix}--tabs__nav-item--selected:hover { + background-color: $ui-background; + } + + //----------------------------- + // Link + //----------------------------- + .#{$prefix}--tabs__nav-link { + @include button-reset($width: false); + @include focus-outline('reset'); + @include type-style('body-short-01'); + + overflow: hidden; + width: rem(160px); + padding: $spacing-04 $spacing-05 $spacing-03; + border-bottom: $tab-underline-color; + color: $text-02; + text-align: left; + text-decoration: none; + text-overflow: ellipsis; + transition: border $duration--fast-01 motion(standard, productive), + outline $duration--fast-01 motion(standard, productive); + white-space: nowrap; + + &:focus, + &:active { + @include focus-outline('outline'); + } + } + + &.#{$prefix}--tabs--container .#{$prefix}--tabs__nav-link { + height: rem(48px); + padding: $spacing-03 $spacing-05; + border-bottom: 0; + // height - vertical padding + line-height: calc(#{rem(48px)} - (#{$spacing-03} * 2)); + } + + //----------------------------- + // Link Hover + //----------------------------- + .#{$prefix}--tabs__nav-item:hover .#{$prefix}--tabs__nav-link { + border-bottom: $tab-underline-color-hover; + color: $text-01; + } + + &.#{$prefix}--tabs--container + .#{$prefix}--tabs__nav-item + .#{$prefix}--tabs__nav-link { + border-bottom: none; + } + + //----------------------------- + // Link Disabled + //----------------------------- + .#{$prefix}--tabs__nav-item--disabled .#{$prefix}--tabs__nav-link { + border-bottom: $tab-underline-disabled; + color: $tab-text-disabled; + } + + .#{$prefix}--tabs__nav-item--disabled:hover .#{$prefix}--tabs__nav-link { + border-bottom: $tab-underline-disabled; + color: $tab-text-disabled; + cursor: not-allowed; + pointer-events: none; + } + + .#{$prefix}--tabs__nav-item--disabled .#{$prefix}--tabs__nav-link:focus, + .#{$prefix}--tabs__nav-item--disabled .#{$prefix}--tabs__nav-link:active { + border-bottom: $tab-underline-disabled; + outline: none; + } + + .#{$prefix}--tabs--light + .#{$prefix}--tabs__nav-item--disabled + .#{$prefix}--tabs__nav-link { + border-bottom-color: $ui-03; + } + + .#{$prefix}--tabs--light + .#{$prefix}--tabs__nav-item--disabled:hover + .#{$prefix}--tabs__nav-link { + border-bottom-color: $ui-03; + } + + .#{$prefix}--tabs--light + .#{$prefix}--tabs__nav-item--disabled + .#{$prefix}--tabs__nav-link:focus, + .#{$prefix}--tabs--light + .#{$prefix}--tabs__nav-item--disabled + .#{$prefix}--tabs__nav-link:active { + border-bottom-color: $ui-03; + } + + &.#{$prefix}--tabs--container + .#{$prefix}--tabs__nav-item--disabled + .#{$prefix}--tabs__nav-link { + border-bottom: none; + color: $disabled-03; + } + + //----------------------------- + // Tab Content Container + //----------------------------- + .#{$prefix}--tab-content { + padding: $carbon--spacing-05; + } + + //----------------------------- + // Skeleton state + //----------------------------- + .#{$prefix}--tabs.#{$prefix}--skeleton { + cursor: default; + pointer-events: none; + } + + .#{$prefix}--tabs.#{$prefix}--skeleton .#{$prefix}--tabs__nav-link { + @include skeleton; + + width: rem(75px); + } + + .#{$prefix}--tabs.#{$prefix}--skeleton .#{$prefix}--tabs-trigger { + @include skeleton; + + width: rem(75px); + margin-right: rem(1px); + } + + .#{$prefix}--tabs.#{$prefix}--skeleton .#{$prefix}--tabs-trigger svg { + @include hidden; + } + } + + // Windows HCM fix + .#{$prefix}--tabs__nav-item + .#{$prefix}--tabs__nav-item--selected + .#{$prefix}--tabs__nav-item--selected { + @include high-contrast-mode('focus'); + } + + // stylelint-disable-next-line no-duplicate-selectors + .#{$prefix}--tabs + .#{$prefix}--tabs__nav-item--disabled + .#{$prefix}--tabs__nav-link { + @include high-contrast-mode('disabled'); + } + } + // TODO: remove namespace and suffix in next major release .#{$prefix}--tabs--scrollable { @include reset; diff --git a/packages/react/src/components/Tab/next/Tab.js b/packages/react/src/components/Tab/next/Tab.js index 4a5637c5e181..f519b6de9d9a 100644 --- a/packages/react/src/components/Tab/next/Tab.js +++ b/packages/react/src/components/Tab/next/Tab.js @@ -32,17 +32,28 @@ const Tab = React.forwardRef(function Tab( ) { const prefix = usePrefix(); - const classes = classNames(className, `${prefix}--tabs__nav-link`, { - [`${prefix}--tabs__nav-item--disabled`]: disabled, - [`${prefix}--tabs__nav-item--selected`]: selected, - }); + const classes = classNames( + className, + // TODO: remove scrollable in next major release + // `${prefix}--tabs__nav-item`, + `${prefix}--tabs--scrollable__nav-item`, + { + [`${prefix}--tabs__nav-item--disabled`]: disabled, + [`${prefix}--tabs__nav-item--selected`]: selected, + // TODO: remove scrollable in next major release + [`${prefix}--tabs--scrollable__nav-item--disabled`]: disabled, + [`${prefix}--tabs--scrollable__nav-item--selected`]: selected, + } + ); const buttonProps = { ['aria-selected']: selected, ['aria-disabled']: disabled, ['aria-controls']: id && `${id}__panel`, id, - className: `${prefix}--tabs__nav-link`, + // TODO: remove scrollable in next major release + // className: `${prefix}--tabs__nav-link`, + className: `${prefix}--tabs--scrollable__nav-link`, tabIndex: !disabled ? tabIndex : -1, ref: ref, }; diff --git a/packages/react/src/components/Tabs/next/Tabs.js b/packages/react/src/components/Tabs/next/Tabs.js index 09ad4e867e59..ee9cfdea98f1 100644 --- a/packages/react/src/components/Tabs/next/Tabs.js +++ b/packages/react/src/components/Tabs/next/Tabs.js @@ -402,11 +402,22 @@ const Tabs = React.forwardRef(function Tabs( tablistScrollLeft + tablistClientWidth === tablistScrollWidth; const classes = { - tabs: classNames(className, `${prefix}--tabs`, { - [`${prefix}--tabs--container`]: type === 'container', - // [`${prefix}--tabs--light`]: light, - }), - tablist: classNames(`${prefix}--tabs__nav`), + // TODO: remove scrollable from classnames in next major release and uncomment classnames that don't contain scrollable + tabs: classNames( + className, + // `${prefix}--tabs`, + `${prefix}--tabs--scrollable`, + { + // [`${prefix}--tabs--container`]: type === 'container', + [`${prefix}--tabs--scrollable--container`]: type === 'container', + // [`${prefix}--tabs--light`]: light, + } + ), + // TODO: remove scrollable from classnames in next major release and uncomment classnames that don't contain scrollable + tablist: classNames( + // `${prefix}--tabs__nav`, + `${prefix}--tabs--scrollable__nav` + ), leftOverflowButtonClasses: classNames({ [`${prefix}--tab--overflow-nav-button`]: horizontalOverflow, [`${prefix}--tab--overflow-nav-button--hidden`]: leftOverflowNavButtonHidden, From 0442bd7917c849c9154a355a2008b3ee7bdf9ca3 Mon Sep 17 00:00:00 2001 From: Josefina Mancilla <josefinanoemi4@gmail.com> Date: Wed, 6 Oct 2021 00:51:45 -0500 Subject: [PATCH 09/22] fix: clean up, comment, and fix select --- .../react/src/components/Tabs/next/Tabs.js | 84 ++++++++++++++----- 1 file changed, 62 insertions(+), 22 deletions(-) diff --git a/packages/react/src/components/Tabs/next/Tabs.js b/packages/react/src/components/Tabs/next/Tabs.js index ee9cfdea98f1..57d079096717 100644 --- a/packages/react/src/components/Tabs/next/Tabs.js +++ b/packages/react/src/components/Tabs/next/Tabs.js @@ -39,19 +39,20 @@ const Tabs = React.forwardRef(function Tabs( const rightOverflowNavButton = useRef(); const tabs = useRef([]); - //state + //states const [horizontalOverflow, setHorizontalOverflow] = useState(false); const [tablistClientWidth, setTablistClientWidth] = useState(null); const [tablistScrollWidth, setTablistScrollWidth] = useState(null); const [tablistScrollLeft, setTablistScrollLeft] = useState(null); + const [isSelected, setIsSelected] = useState(selected); + const [prevSelected, setPrevSelected] = useState(null); //prop + state alignment - getDerivedStateFromProps - const [isSelected, setIsSelected] = useState(selected); - const [prevSelected, setPrevSelected] = useState(isSelected); - if (selected !== prevSelected) { - setIsSelected(selected); - setPrevSelected(selected); - } + // THIS IS NOT WORKING!!!! + // if (selected !== prevSelected) { + // setIsSelected(selected); + // setPrevSelected(selected); + // } // width of the overflow buttons let OVERFLOW_BUTTON_OFFSET = 40; @@ -81,10 +82,10 @@ const Tabs = React.forwardRef(function Tabs( const _handleWindowResize = handleScroll; - useEffect(() => { - _debouncedHandleWindowResize.current = debounce(_handleWindowResize, 200); - }, []); // eslint-disable-line react-hooks/exhaustive-deps - + /** + * returns all tabs that are not disabled + * used for keyboard navigation + */ const getEnabledTabs = () => React.Children.toArray(children).reduce( (enabledTabs, tab, index) => @@ -92,6 +93,10 @@ const Tabs = React.forwardRef(function Tabs( [] ); + /** + * returns the index of the next tab we are going to when navigating L/R arrow keys (i.e. 0, 1, 2) + * used in handleTabKeyDown to get the next index after keyboard arrow evt, which then updates selected tab + */ const getNextIndex = (index, direction) => { const enabledTabs = getEnabledTabs(); const nextIndex = Math.max( @@ -106,6 +111,11 @@ const Tabs = React.forwardRef(function Tabs( return enabledTabs[nextIndexLooped]; }; + /** + * used as second argument for getNextIndex(i,d) + * returns -1, 1 or 0 depending on arrow key + * number is then used in math calculations to find the index of the next tab we are navigating to + */ const getDirection = (evt) => { if (match(evt, keys.ArrowLeft)) { return -1; @@ -116,6 +126,9 @@ const Tabs = React.forwardRef(function Tabs( return 0; }; + /** + * creates an array of all the child tab items + */ const getTabAt = useCallback( (index, useFresh) => (!useFresh && tabs.current[index]) || @@ -142,6 +155,11 @@ const Tabs = React.forwardRef(function Tabs( } }; + /** + * selecting tab on click and on keyboard nav + * index = tab to be selected, returned in handleTabKeyDown + * onSelectionChange = optional prop for event handler + */ const selectTabAt = (event, { index, onSelectionChange }) => { scrollTabIntoView(event, { index }); if (isSelected !== index) { @@ -153,6 +171,9 @@ const Tabs = React.forwardRef(function Tabs( } }; + /** + * keyboard event handler + */ const handleTabKeyDown = (onSelectionChange) => { return (index, evt) => { if (matches(evt, [keys.Enter, keys.Space])) { @@ -172,6 +193,7 @@ const Tabs = React.forwardRef(function Tabs( })(); const tab = getTabAt(nextIndex); + // updating selected tab if ( matches(evt, [keys.ArrowLeft, keys.ArrowRight, keys.Home, keys.End]) ) { @@ -188,7 +210,11 @@ const Tabs = React.forwardRef(function Tabs( const getTabs = () => React.Children.map(children, (tab) => tab); - // following functions (handle*) are Props on Tab.js, see Tab.js for parameters + /** + * click handler + * passed down to Tab children as a prop in `tabsWithProps` + * following functions (handle*) are Props on Tab.js, see Tab.js for parameters + */ const handleTabClick = (onSelectionChange) => (index, evt) => { evt.preventDefault(); selectTabAt(evt, { index, onSelectionChange }); @@ -200,6 +226,11 @@ const Tabs = React.forwardRef(function Tabs( let overflowNavInterval = null; + /** + * group - overflow scroll + * scrolling via overflow btn click + * click handler for scrollable tabs L/R arrow buttons + */ const handleOverflowNavClick = (_, { direction, multiplier = 10 }) => { // account for overflow button appearing and causing tablist width change const { clientWidth, scrollLeft, scrollWidth } = tablist?.current; @@ -226,6 +257,11 @@ const Tabs = React.forwardRef(function Tabs( } }; + /** + * group - overflow scroll + * scrolling w/ mouse event + * mousedown handler for scrollable tabs + */ const handleOverflowNavMouseDown = (event, { direction }) => { // disregard mouse buttons aside from LMB if (event.buttons !== 1) { @@ -252,12 +288,21 @@ const Tabs = React.forwardRef(function Tabs( }); }; + /** + * group - overflow scroll + * scrolling w/ mouse event + * mouseup handler for scrollable tabs + */ const handleOverflowNavMouseUp = () => { clearInterval(overflowNavInterval); }; - //component did mount equivalent + /** + * only run once - component did mount equivalent + */ useEffect(() => { + _debouncedHandleWindowResize.current = debounce(_handleWindowResize, 200); + _handleWindowResize(); window.addEventListener('resize', _debouncedHandleWindowResize.current); @@ -300,16 +345,11 @@ const Tabs = React.forwardRef(function Tabs( _debouncedHandleWindowResize.current ); }; - }, [ - isSelected, - scrollIntoView, - getTabAt, - OVERFLOW_BUTTON_OFFSET, - _debouncedHandleWindowResize, - _handleWindowResize, - ]); + }, []); // eslint-disable-line react-hooks/exhaustive-deps - //component did update equivalent + /** + * component did update equivalent + */ useEffect(() => { // compare current tablist properties to current state const { From d52043ed722f7badbdd60aac2ae7a6580c77ed7b Mon Sep 17 00:00:00 2001 From: Josefina Mancilla <josefinanoemi4@gmail.com> Date: Wed, 6 Oct 2021 01:09:02 -0500 Subject: [PATCH 10/22] fix: keyboard navigation --- .../react/src/components/Tabs/next/Tabs.js | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/packages/react/src/components/Tabs/next/Tabs.js b/packages/react/src/components/Tabs/next/Tabs.js index 57d079096717..7df5bdf4cce2 100644 --- a/packages/react/src/components/Tabs/next/Tabs.js +++ b/packages/react/src/components/Tabs/next/Tabs.js @@ -45,14 +45,18 @@ const Tabs = React.forwardRef(function Tabs( const [tablistScrollWidth, setTablistScrollWidth] = useState(null); const [tablistScrollLeft, setTablistScrollLeft] = useState(null); const [isSelected, setIsSelected] = useState(selected); - const [prevSelected, setPrevSelected] = useState(null); + const [prevSelected, setPrevSelected] = useState(isSelected); - //prop + state alignment - getDerivedStateFromProps - // THIS IS NOT WORKING!!!! - // if (selected !== prevSelected) { - // setIsSelected(selected); - // setPrevSelected(selected); - // } + /** + * prop + state alignment - getDerivedStateFromProps + * only update if selected prop changes + */ + useEffect(() => { + if (selected !== prevSelected) { + setIsSelected(selected); + setPrevSelected(selected); + } + }, [selected]); //eslint-disable-line react-hooks/exhaustive-deps // width of the overflow buttons let OVERFLOW_BUTTON_OFFSET = 40; @@ -203,7 +207,7 @@ const Tabs = React.forwardRef(function Tabs( } else { scrollTabIntoView(evt, { index: nextIndex }); } - tab?.tabAnchor?.focus(); + tab?.focus(); } }; }; From dec0a57318e3d7c9dbc91991f2305c0d301eaf92 Mon Sep 17 00:00:00 2001 From: Josefina Mancilla <josefinanoemi4@gmail.com> Date: Wed, 6 Oct 2021 01:33:55 -0500 Subject: [PATCH 11/22] fix: remove console log --- packages/react/src/components/Tabs/Tabs.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/react/src/components/Tabs/Tabs.js b/packages/react/src/components/Tabs/Tabs.js index fa15fb5aba4c..7ad962e509cc 100644 --- a/packages/react/src/components/Tabs/Tabs.js +++ b/packages/react/src/components/Tabs/Tabs.js @@ -187,8 +187,6 @@ export default class Tabs extends React.Component { this.tablist.current.scrollLeft += this.OVERFLOW_BUTTON_OFFSET * 2; } } - // remove this before merging! - console.log('THIS', this); } componentWillUnmount() { From 9917fd252b6101d79c1660291632508d380aa27e Mon Sep 17 00:00:00 2001 From: Josefina Mancilla <josefinanoemi4@gmail.com> Date: Mon, 11 Oct 2021 16:25:16 -0500 Subject: [PATCH 12/22] chore: tabs comment --- packages/react/src/components/Tabs/next/Tabs.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/react/src/components/Tabs/next/Tabs.js b/packages/react/src/components/Tabs/next/Tabs.js index 7df5bdf4cce2..07198cce2250 100644 --- a/packages/react/src/components/Tabs/next/Tabs.js +++ b/packages/react/src/components/Tabs/next/Tabs.js @@ -130,9 +130,6 @@ const Tabs = React.forwardRef(function Tabs( return 0; }; - /** - * creates an array of all the child tab items - */ const getTabAt = useCallback( (index, useFresh) => (!useFresh && tabs.current[index]) || @@ -224,6 +221,9 @@ const Tabs = React.forwardRef(function Tabs( selectTabAt(evt, { index, onSelectionChange }); }; + /** + * creates an array of all the child tab items + */ const setTabAt = (index, tabRef) => { tabs.current[index] = tabRef; }; From 22849a8e9323a785dc015e088b49ba2f5d99adca Mon Sep 17 00:00:00 2001 From: Josefina Mancilla <josefinanoemi4@gmail.com> Date: Mon, 11 Oct 2021 16:33:18 -0500 Subject: [PATCH 13/22] feat: add tab tests --- .../react/src/components/Tab/next/Tab-test.js | 206 ++++++++++++++++++ 1 file changed, 206 insertions(+) create mode 100644 packages/react/src/components/Tab/next/Tab-test.js diff --git a/packages/react/src/components/Tab/next/Tab-test.js b/packages/react/src/components/Tab/next/Tab-test.js new file mode 100644 index 000000000000..735136ed3ab8 --- /dev/null +++ b/packages/react/src/components/Tab/next/Tab-test.js @@ -0,0 +1,206 @@ +import React from 'react'; +import { default as Tab } from './Tab'; +import { cleanup, render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +describe('Tab', () => { + afterEach(cleanup); + + it('adds extra classes that are passed via className', async () => { + render( + <Tab + className="custom-class" + label="Tab 1" + onClick={() => {}} + onKeyDown={() => {}} + selected={false}> + <p>Content for first tab goes here.</p> + </Tab> + ); + + await expect( + screen.getByRole('presentation').classList.contains('custom-class') + ).toBe(true); + }); + + it('sets tabIndex on <button> if one is passed via props', async () => { + render( + <Tab + label="Tab 1" + // eslint-disable-next-line jsx-a11y/tabindex-no-positive + tabIndex={2} + onClick={() => {}} + onKeyDown={() => {}} + selected={false}> + <p>Content for first tab goes here.</p> + </Tab> + ); + + await expect(screen.getByRole('tab').tabIndex).toEqual(2); + }); + + it('renders <li> with [role="presentation"]', async () => { + render( + <Tab + className="custom-class" + label="Tab 1" + onClick={() => {}} + onKeyDown={() => {}} + selected={false}> + <p>Content for first tab goes here.</p> + </Tab> + ); + + await expect(screen.getByRole('presentation')).toBeTruthy(); + }); + + it('renders <button> with tabindex set to 0 by default', async () => { + render( + <Tab + label="Tab 1" + onClick={() => {}} + onKeyDown={() => {}} + selected={false}> + <p>Content for first tab goes here.</p> + </Tab> + ); + + await expect(screen.getByRole('tab').tabIndex).toEqual(0); + }); + + it('renders <button> with tabindex set to -1 if disabled', async () => { + render( + <Tab + label="Tab 1" + onClick={() => {}} + onKeyDown={() => {}} + selected={false} + disabled> + <p>Content for first tab goes here.</p> + </Tab> + ); + + await expect(screen.getByRole('tab').tabIndex).toEqual(-1); + }); + + it('uses label to set children on <button> when passed via props', async () => { + render( + <Tab + label="Tab 1" + onClick={() => {}} + onKeyDown={() => {}} + selected={false}> + <p>Content for first tab goes here.</p> + </Tab> + ); + + await expect(screen.getByRole('tab').textContent).toBe('Tab 1'); + }); + + it('has aria-disabled that matches disabled', async () => { + render( + <Tab + label="Tab 1" + onClick={() => {}} + onKeyDown={() => {}} + selected={false} + disabled> + <p>Content for first tab goes here.</p> + </Tab> + ); + + await expect(screen.getByRole('tab')).toHaveAttribute('aria-disabled'); + }); +}); + +describe('Click events', () => { + it('invokes handleTabClick from handleTabClick prop', async () => { + const handleTabClick = jest.fn(); + render( + <Tab + label="Tab 1" + handleTabClick={handleTabClick} + onClick={() => {}} + onKeyDown={() => {}} + selected={false}> + <p>Content for first tab goes here.</p> + </Tab> + ); + + const button = screen.getByRole('tab'); + userEvent.click(button); + await expect(handleTabClick).toHaveBeenCalled(); + }); + + it('invokes onClick when a function is passed to onClick prop', async () => { + const onClick = jest.fn(); + + render( + <Tab + label="Tab 1" + onClick={onClick} + onKeyDown={() => {}} + selected={false}> + <p>Content for first tab goes here.</p> + </Tab> + ); + + const button = screen.getByRole('tab'); + userEvent.click(button); + await expect(onClick).toHaveBeenCalled(); + }); + + it('does not invoke click handler if tab is disabled', async () => { + const onClick = jest.fn(); + + render( + <Tab + label="Tab 1" + onClick={onClick} + onKeyDown={() => {}} + selected={false} + disabled> + <p>Content for first tab goes here.</p> + </Tab> + ); + + const button = screen.getByRole('tab'); + userEvent.click(button); + await expect(onClick).not.toHaveBeenCalled(); + }); +}); + +describe('Keyboard events', () => { + it('invokes onKeyDown from onKeyDown prop', async () => { + const onKeyDown = jest.fn(); + render( + <Tab label="Tab 1" onClick={() => {}} onKeyDown={onKeyDown} selected> + <p>Content for first tab goes here.</p> + </Tab> + ); + + const button = screen.getByRole('tab'); + userEvent.type(button, '[ArrowLeft]'); + + await expect(onKeyDown).toHaveBeenCalled(); + }); + + it('invokes handleTabKeyDown from handleTabKeyDown prop', async () => { + const handleTabKeyDown = jest.fn(); + render( + <Tab + label="Tab 1" + onClick={() => {}} + onKeyDown={() => {}} + handleTabKeyDown={handleTabKeyDown} + selected> + <p>Content for first tab goes here.</p> + </Tab> + ); + + const button = screen.getByRole('tab'); + userEvent.type(button, '[ArrowRight]'); + + await expect(handleTabKeyDown).toHaveBeenCalled(); + }); +}); From 024a47ebc8d8c42288dba0752a8fa35c8b977be4 Mon Sep 17 00:00:00 2001 From: Josefina Mancilla <josefinanoemi4@gmail.com> Date: Wed, 13 Oct 2021 11:49:56 -0500 Subject: [PATCH 14/22] feat: add tabs tests --- packages/react/src/components/Tab/next/Tab.js | 6 +- .../src/components/Tabs/next/Tabs-test.js | 264 ++++++++++++++++++ 2 files changed, 267 insertions(+), 3 deletions(-) create mode 100644 packages/react/src/components/Tabs/next/Tabs-test.js diff --git a/packages/react/src/components/Tab/next/Tab.js b/packages/react/src/components/Tab/next/Tab.js index f519b6de9d9a..ad06cd59c92e 100644 --- a/packages/react/src/components/Tab/next/Tab.js +++ b/packages/react/src/components/Tab/next/Tab.js @@ -141,12 +141,12 @@ Tab.propTypes = { /** * Provide a handler that is invoked when a user clicks on the control */ - onClick: PropTypes.func.isRequired, + onClick: PropTypes.func, /** * Provide a handler that is invoked on the key down event for the control */ - onKeyDown: PropTypes.func.isRequired, + onKeyDown: PropTypes.func, /* * An optional parameter to allow overriding the anchor rendering. @@ -176,7 +176,7 @@ Tab.propTypes = { * Whether your Tab is selected. * Reserved for usage in Tabs */ - selected: PropTypes.bool.isRequired, + selected: PropTypes.bool, /** * Specify the tab index of the `<button>` node diff --git a/packages/react/src/components/Tabs/next/Tabs-test.js b/packages/react/src/components/Tabs/next/Tabs-test.js new file mode 100644 index 000000000000..c95e6731711a --- /dev/null +++ b/packages/react/src/components/Tabs/next/Tabs-test.js @@ -0,0 +1,264 @@ +import React from 'react'; +import { default as Tabs } from './Tabs'; +import { default as Tab } from '../../Tab/next/Tab'; +import { render, screen } from '@testing-library/react'; +import { fireEvent } from '@testing-library/dom'; + +describe('Tabs', () => { + it('adds extra classes that are passed via className prop', async () => { + render( + <Tabs className="custom-class" data-testid="tabs-test"> + <Tab label="firstTab">content1</Tab> + <Tab label="lastTab">content2</Tab> + </Tabs> + ); + + const tabs = screen.getByTestId('tabs-test'); + await expect(tabs.classList.contains('custom-class')).toBe(true); + }); + + it('renders <ul> with tablist role by default', async () => { + render( + <Tabs className="custom-class"> + <Tab label="firstTab">content1</Tab> + <Tab label="lastTab">content2</Tab> + </Tabs> + ); + + const tablist = screen.getByRole('tablist'); + await expect(tablist).toBeTruthy(); + }); +}); + +describe('Children tabs', () => { + it('renders children', async () => { + render( + <Tabs className="custom-class"> + <Tab label="firstTab">content1</Tab> + <Tab label="lastTab">content2</Tab> + </Tabs> + ); + + const tabArray = screen.getAllByRole('presentation'); + await expect(tabArray.length).toEqual(2); + }); + + it('first tab is selected by default', async () => { + render( + <Tabs className="custom-class"> + <Tab label="firstTab" data-testid="first-tab"> + content1 + </Tab> + <Tab label="lastTab">content2</Tab> + </Tabs> + ); + + const firstTab = screen.getByTestId('first-tab'); + await expect( + firstTab.classList.contains('bx--tabs__nav-item--selected') + ).toBe(true); + }); + + it('overrides default selected tab when selected prop is provided', async () => { + render( + <Tabs className="custom-class" selected={1}> + <Tab label="firstTab" data-testid="first-tab"> + content1 + </Tab> + <Tab label="lastTab" data-testid="second-tab"> + content2 + </Tab> + </Tabs> + ); + + const firstTab = screen.getByTestId('first-tab'); + const secondTab = screen.getByTestId('second-tab'); + + await expect( + firstTab.classList.contains('bx--tabs__nav-item--selected') + ).toBe(false); + await expect( + secondTab.classList.contains('bx--tabs__nav-item--selected') + ).toBe(true); + }); +}); + +describe('Children tab content', () => { + it('renders correct number of children content as expected', async () => { + render( + <Tabs className="custom-class"> + <Tab label="firstTab">content1</Tab> + <Tab label="lastTab">content2</Tab> + </Tabs> + ); + + const contentArray = screen.getAllByRole('tabpanel', { hidden: true }); + await expect(contentArray.length).toEqual(2); + }); + + it('only shows one content tabpanel at a time', async () => { + render( + <Tabs className="custom-class"> + <Tab label="firstTab">content1</Tab> + <Tab label="secondTab">content2</Tab> + <Tab label="lastTab">content3</Tab> + </Tabs> + ); + + const contentArray = screen.getAllByRole('tabpanel'); + await expect(contentArray.length).toEqual(1); + }); + + it('adds extra classes that are passed via tabContentClassName prop', async () => { + render( + <Tabs tabContentClassName="content-class"> + <Tab label="firstTab">content1</Tab> + <Tab label="lastTab">content2</Tab> + </Tabs> + ); + + const content = screen.getByRole('tabpanel'); + await expect(content.classList.contains('content-class')).toBe(true); + }); + + it('renders unselected tab content with hidden attribute', async () => { + render( + <Tabs className="custom-class"> + <Tab label="firstTab">content1</Tab> + <Tab label="lastTab">content2</Tab> + </Tabs> + ); + + const contentArray = screen.getAllByRole('tabpanel', { hidden: true }); + await expect(contentArray[1]).toHaveAttribute('hidden'); + }); +}); + +describe('Keyboard events', () => { + it('updates selected tab and content, and loops from first tab to last tab when pressing left arrow key', async () => { + render( + <Tabs> + <Tab label="firstTab" data-testid="tab1"> + content1 + </Tab> + <Tab label="lastTab" data-testid="tab2"> + content2 + </Tab> + </Tabs> + ); + + const tab1 = screen.getByTestId('tab1'); + const tab2 = screen.getByTestId('tab2'); + + const tabContent = screen.getAllByRole('tabpanel'); + const tab1Content = tabContent[0]; + fireEvent.keyDown(tab1, { + key: 'ArrowLeft', + code: 'ArrowLeft', + charCode: 37, + }); + await expect(tab2.classList.contains('bx--tabs__nav-item--selected')).toBe( + true + ); + await expect(tab1Content).toHaveAttribute('hidden'); + }); + + it('updates selected tab and content when pressing right arrow key', async () => { + render( + <Tabs> + <Tab label="firstTab" data-testid="tab1"> + content1 + </Tab> + <Tab label="lastTab" data-testid="tab2"> + content2 + </Tab> + </Tabs> + ); + + const tab1 = screen.getByTestId('tab1'); + const tab2 = screen.getByTestId('tab2'); + const tabContent = screen.getAllByRole('tabpanel'); + const tab1Content = tabContent[0]; + + fireEvent.keyDown(tab1, { + key: 'ArrowRight', + code: 'ArrowRight', + charCode: 39, + }); + await expect(tab2.classList.contains('bx--tabs__nav-item--selected')).toBe( + true + ); + await expect(tab1Content).toHaveAttribute('hidden'); + }); + + it('ignores disabled tabs', async () => { + render( + <Tabs> + <Tab label="firstTab" data-testid="tab1"> + content1 + </Tab> + <Tab label="lastTab" data-testid="tab2" disabled> + content2 + </Tab> + <Tab label="thirdTab" data-testid="tab3"> + content3 + </Tab> + </Tabs> + ); + const tab1 = screen.getByTestId('tab1'); + const tab3 = screen.getByTestId('tab3'); + fireEvent.keyDown(tab1, { + key: 'ArrowRight', + code: 'ArrowRight', + charCode: 39, + }); + + await expect(tab3.classList.contains('bx--tabs__nav-item--selected')).toBe( + true + ); + }); +}); + +describe('Click events', () => { + it('updates selected tab and content on click', async () => { + render( + <Tabs> + <Tab label="firstTab" data-testid="tab1"> + content1 + </Tab> + <Tab label="lastTab" data-testid="tab2"> + content2 + </Tab> + </Tabs> + ); + const tab2 = screen.getByTestId('tab2'); + const tabContent = screen.getAllByRole('tabpanel'); + const tab1Content = tabContent[0]; + fireEvent.click(tab2); + await expect(tab2.classList.contains('bx--tabs__nav-item--selected')).toBe( + true + ); + await expect(tab1Content).toHaveAttribute('hidden'); + }); + + it('ignores disabled tab on click', async () => { + render( + <Tabs> + <Tab label="firstTab" data-testid="tab1"> + content1 + </Tab> + <Tab label="lastTab" data-testid="tab2" disabled> + content2 + </Tab> + </Tabs> + ); + const tab2 = screen.getByTestId('tab2'); + const tabContent = screen.getAllByRole('tabpanel', { hidden: true }); + const tab2Content = tabContent[1]; + fireEvent.click(tab2); + await expect(tab2.classList.contains('bx--tabs__nav-item--selected')).toBe( + false + ); + await expect(tab2Content).toHaveAttribute('hidden'); + }); +}); From f10bda4697717099e2dae1d76403a72b3256de79 Mon Sep 17 00:00:00 2001 From: Josefina Mancilla <josefinanoemi4@gmail.com> Date: Wed, 13 Oct 2021 12:56:51 -0500 Subject: [PATCH 15/22] fix: add back light --- .../components/src/components/tabs/_tabs.scss | 16 +++++++--------- packages/react/src/components/Tabs/next/Tabs.js | 2 ++ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/components/src/components/tabs/_tabs.scss b/packages/components/src/components/tabs/_tabs.scss index 9856776d533a..1815512518ed 100644 --- a/packages/components/src/components/tabs/_tabs.scss +++ b/packages/components/src/components/tabs/_tabs.scss @@ -513,15 +513,13 @@ ); } - // .#{$prefix}--tabs--light - // .#{$prefix}--tabs__overflow-indicator--left { - // background-image: linear-gradient(to left, transparent, $ui-01); - // } - - // .#{$prefix}--tabs--light - // .#{$prefix}--tabs__overflow-indicator--right { - // background-image: linear-gradient(to right, transparent, $ui-01); - // } + .#{$prefix}--tabs--light .#{$prefix}--tabs__overflow-indicator--left { + background-image: linear-gradient(to left, transparent, $ui-01); + } + + .#{$prefix}--tabs--light .#{$prefix}--tabs__overflow-indicator--right { + background-image: linear-gradient(to right, transparent, $ui-01); + } &.#{$prefix}--tabs--container .#{$prefix}--tabs__overflow-indicator--left { diff --git a/packages/react/src/components/Tabs/next/Tabs.js b/packages/react/src/components/Tabs/next/Tabs.js index 07198cce2250..ecb54b8c2ec5 100644 --- a/packages/react/src/components/Tabs/next/Tabs.js +++ b/packages/react/src/components/Tabs/next/Tabs.js @@ -20,6 +20,7 @@ const Tabs = React.forwardRef(function Tabs( children, className, leftOverflowButtonProps, + light = false, onSelectionChange, rightOverflowButtonProps, scrollIntoView = true, @@ -455,6 +456,7 @@ const Tabs = React.forwardRef(function Tabs( // [`${prefix}--tabs--container`]: type === 'container', [`${prefix}--tabs--scrollable--container`]: type === 'container', // [`${prefix}--tabs--light`]: light, + [`${prefix}--tabs--scrollable--light`]: light, } ), // TODO: remove scrollable from classnames in next major release and uncomment classnames that don't contain scrollable From 0ef6abddb4310e3b0d9bbadea49a85902a79f63f Mon Sep 17 00:00:00 2001 From: Josefina Mancilla <josefinanoemi4@gmail.com> Date: Wed, 13 Oct 2021 13:15:50 -0500 Subject: [PATCH 16/22] fix: remove v11 styles --- .../components/src/components/tabs/_tabs.scss | 406 ------------------ 1 file changed, 406 deletions(-) diff --git a/packages/components/src/components/tabs/_tabs.scss b/packages/components/src/components/tabs/_tabs.scss index 1815512518ed..8921c57308ba 100644 --- a/packages/components/src/components/tabs/_tabs.scss +++ b/packages/components/src/components/tabs/_tabs.scss @@ -447,412 +447,6 @@ @include hidden; } - // ----------------------------- - // v11 styles - // ----------------------------- - @if feature-flag-enabled('enable-v11-release') { - .#{$prefix}--skeleton.#{$prefix}--tabs:not(.#{$prefix}--tabs--container) - .#{$prefix}--tabs__nav-item { - border-bottom: 2px solid $skeleton-02; - } - - .#{$prefix}--tabs { - @include reset; - @include type-style('body-short-01'); - - display: flex; - width: 100%; - height: auto; - min-height: rem(40px); - color: $text-01; - - &.#{$prefix}--tabs--container { - min-height: rem(48px); - } - - .#{$prefix}--tabs__nav { - display: flex; - overflow: auto hidden; - width: auto; - max-width: 100%; - flex-direction: row; - padding: 0; - margin: 0; - list-style: none; - outline: 0; - // hide scrollbars - scrollbar-width: none; - transition: max-height $duration--fast-01 motion(standard, productive); - - &::-webkit-scrollbar { - display: none; - } - } - - //----------------------------- - // Overflow Nav Buttons - //----------------------------- - .#{$prefix}--tabs__overflow-indicator--left, - .#{$prefix}--tabs__overflow-indicator--right { - z-index: 1; - width: $carbon--spacing-03; - flex: 1 0 auto; - } - - .#{$prefix}--tabs__overflow-indicator--left { - margin-right: -$carbon--spacing-03; - background-image: linear-gradient(to left, transparent, $ui-background); - } - - .#{$prefix}--tabs__overflow-indicator--right { - margin-left: -$carbon--spacing-03; - background-image: linear-gradient( - to right, - transparent, - $ui-background - ); - } - - .#{$prefix}--tabs--light .#{$prefix}--tabs__overflow-indicator--left { - background-image: linear-gradient(to left, transparent, $ui-01); - } - - .#{$prefix}--tabs--light .#{$prefix}--tabs__overflow-indicator--right { - background-image: linear-gradient(to right, transparent, $ui-01); - } - - &.#{$prefix}--tabs--container - .#{$prefix}--tabs__overflow-indicator--left { - background-image: linear-gradient(to left, transparent, $ui-03); - } - - &.#{$prefix}--tabs--container - .#{$prefix}--tabs__overflow-indicator--right { - background-image: linear-gradient(to right, transparent, $ui-03); - } - - // Safari-only media query - // won't appear correctly with CSS custom properties - // see: code snippet and modal overflow indicators - @media not all and (min-resolution: 0.001dpcm) { - @supports (-webkit-appearance: none) and (stroke-color: transparent) { - .#{$prefix}--tabs__overflow-indicator--left { - background-image: linear-gradient( - to left, - rgba($ui-background, 0), - $ui-background - ); - } - - .#{$prefix}--tabs__overflow-indicator--right { - background-image: linear-gradient( - to right, - rgba($ui-background, 0), - $ui-background - ); - } - - &.#{$prefix}--tabs--container - .#{$prefix}--tabs__overflow-indicator--left { - background-image: linear-gradient(to left, rgba($ui-03, 0), $ui-03); - } - - &.#{$prefix}--tabs--container - .#{$prefix}--tabs__overflow-indicator--right { - background-image: linear-gradient( - to right, - rgba($ui-03, 0), - $ui-03 - ); - } - } - } - - .#{$prefix}--tab--overflow-nav-button { - @include button-reset; - - display: flex; - width: $carbon--spacing-08; - flex-shrink: 0; - align-items: center; - justify-content: center; - - &:focus { - @include focus-outline('outline'); - } - } - - .#{$prefix}--tab--overflow-nav-button--hidden { - display: none; - } - - &.#{$prefix}--tabs--container .#{$prefix}--tab--overflow-nav-button { - width: $carbon--spacing-09; - margin: 0; - background-color: $ui-03; - } - - .#{$prefix}--tab--overflow-nav-button svg { - fill: $icon-01; - } - - //----------------------------- - // Item - //----------------------------- - .#{$prefix}--tabs__nav-item { - @include reset; - - display: flex; - padding: 0; - cursor: pointer; - transition: background-color - $duration--fast-01 - motion(standard, productive); - } - - .#{$prefix}--tabs__nav-item + .#{$prefix}--tabs__nav-item { - margin-left: rem(1px); - } - - &.#{$prefix}--tabs--container .#{$prefix}--tabs__nav-item { - background-color: $ui-03; - } - - &.#{$prefix}--tabs--container - .#{$prefix}--tabs__nav-item - + .#{$prefix}--tabs__nav-item { - margin-left: 0; - // Draws the border without affecting the inner-content - box-shadow: rem(-1px) 0 0 0 $ui-04; - } - - &.#{$prefix}--tabs--container - .#{$prefix}--tabs__nav-item - + .#{$prefix}--tabs__nav-item.#{$prefix}--tabs__nav-item--selected, - &.#{$prefix}--tabs--container - .#{$prefix}--tabs__nav-item.#{$prefix}--tabs__nav-item--selected - + .#{$prefix}--tabs__nav-item { - box-shadow: none; - } - - .#{$prefix}--tabs__nav-item .#{$prefix}--tabs__nav-link { - transition: color $duration--fast-01 motion(standard, productive), - border-bottom-color $duration--fast-01 motion(standard, productive), - outline $duration--fast-01 motion(standard, productive); - } - - //----------------------------- - // Item Hover - //----------------------------- - &.#{$prefix}--tabs--container .#{$prefix}--tabs__nav-item:hover { - background-color: $hover-selected-ui; - } - - //--------------------------------------------- - // Item Disabled - //--------------------------------------------- - .#{$prefix}--tabs__nav-item--disabled, - .#{$prefix}--tabs__nav-item--disabled:hover { - background-color: transparent; - cursor: not-allowed; - outline: none; - } - - &.#{$prefix}--tabs--container - .#{$prefix}--tabs__nav-item.#{$prefix}--tabs__nav-item--disabled, - &.#{$prefix}--tabs--container - .#{$prefix}--tabs__nav-item.#{$prefix}--tabs__nav-item--disabled:hover { - background-color: $disabled-02; - } - - //----------------------------- - // Item Selected - //----------------------------- - .#{$prefix}--tabs__nav-item--selected { - transition: color $duration--fast-01 motion(standard, productive); - } - - .#{$prefix}--tabs__nav-item--selected .#{$prefix}--tabs__nav-link, - .#{$prefix}--tabs__nav-item--selected .#{$prefix}--tabs__nav-link:focus, - .#{$prefix}--tabs__nav-item--selected .#{$prefix}--tabs__nav-link:active { - @include type-style('productive-heading-01'); - - border-bottom: 2px solid $interactive-04; - color: $text-01; - } - - &.#{$prefix}--tabs--container .#{$prefix}--tabs__nav-item--selected, - &.#{$prefix}--tabs--container - .#{$prefix}--tabs__nav-item--selected:hover { - background-color: $ui-01; - - .#{$prefix}--tabs__nav-link:focus, - .#{$prefix}--tabs__nav-link:active { - box-shadow: none; - } - } - - &.#{$prefix}--tabs--container - .#{$prefix}--tabs__nav-item--selected - .#{$prefix}--tabs__nav-link { - // Draws the border without affecting the inner-content - box-shadow: inset 0 2px 0 0 $interactive-04; - // height - vertical padding - line-height: calc(#{rem(48px)} - (#{$spacing-03} * 2)); - } - - &.#{$prefix}--tabs--light.#{$prefix}--tabs--container - .#{$prefix}--tabs__nav-item--selected, - &.#{$prefix}--tabs--light.#{$prefix}--tabs--container - .#{$prefix}--tabs__nav-item--selected:hover { - background-color: $ui-background; - } - - //----------------------------- - // Link - //----------------------------- - .#{$prefix}--tabs__nav-link { - @include button-reset($width: false); - @include focus-outline('reset'); - @include type-style('body-short-01'); - - overflow: hidden; - width: rem(160px); - padding: $spacing-04 $spacing-05 $spacing-03; - border-bottom: $tab-underline-color; - color: $text-02; - text-align: left; - text-decoration: none; - text-overflow: ellipsis; - transition: border $duration--fast-01 motion(standard, productive), - outline $duration--fast-01 motion(standard, productive); - white-space: nowrap; - - &:focus, - &:active { - @include focus-outline('outline'); - } - } - - &.#{$prefix}--tabs--container .#{$prefix}--tabs__nav-link { - height: rem(48px); - padding: $spacing-03 $spacing-05; - border-bottom: 0; - // height - vertical padding - line-height: calc(#{rem(48px)} - (#{$spacing-03} * 2)); - } - - //----------------------------- - // Link Hover - //----------------------------- - .#{$prefix}--tabs__nav-item:hover .#{$prefix}--tabs__nav-link { - border-bottom: $tab-underline-color-hover; - color: $text-01; - } - - &.#{$prefix}--tabs--container - .#{$prefix}--tabs__nav-item - .#{$prefix}--tabs__nav-link { - border-bottom: none; - } - - //----------------------------- - // Link Disabled - //----------------------------- - .#{$prefix}--tabs__nav-item--disabled .#{$prefix}--tabs__nav-link { - border-bottom: $tab-underline-disabled; - color: $tab-text-disabled; - } - - .#{$prefix}--tabs__nav-item--disabled:hover .#{$prefix}--tabs__nav-link { - border-bottom: $tab-underline-disabled; - color: $tab-text-disabled; - cursor: not-allowed; - pointer-events: none; - } - - .#{$prefix}--tabs__nav-item--disabled .#{$prefix}--tabs__nav-link:focus, - .#{$prefix}--tabs__nav-item--disabled .#{$prefix}--tabs__nav-link:active { - border-bottom: $tab-underline-disabled; - outline: none; - } - - .#{$prefix}--tabs--light - .#{$prefix}--tabs__nav-item--disabled - .#{$prefix}--tabs__nav-link { - border-bottom-color: $ui-03; - } - - .#{$prefix}--tabs--light - .#{$prefix}--tabs__nav-item--disabled:hover - .#{$prefix}--tabs__nav-link { - border-bottom-color: $ui-03; - } - - .#{$prefix}--tabs--light - .#{$prefix}--tabs__nav-item--disabled - .#{$prefix}--tabs__nav-link:focus, - .#{$prefix}--tabs--light - .#{$prefix}--tabs__nav-item--disabled - .#{$prefix}--tabs__nav-link:active { - border-bottom-color: $ui-03; - } - - &.#{$prefix}--tabs--container - .#{$prefix}--tabs__nav-item--disabled - .#{$prefix}--tabs__nav-link { - border-bottom: none; - color: $disabled-03; - } - - //----------------------------- - // Tab Content Container - //----------------------------- - .#{$prefix}--tab-content { - padding: $carbon--spacing-05; - } - - //----------------------------- - // Skeleton state - //----------------------------- - .#{$prefix}--tabs.#{$prefix}--skeleton { - cursor: default; - pointer-events: none; - } - - .#{$prefix}--tabs.#{$prefix}--skeleton .#{$prefix}--tabs__nav-link { - @include skeleton; - - width: rem(75px); - } - - .#{$prefix}--tabs.#{$prefix}--skeleton .#{$prefix}--tabs-trigger { - @include skeleton; - - width: rem(75px); - margin-right: rem(1px); - } - - .#{$prefix}--tabs.#{$prefix}--skeleton .#{$prefix}--tabs-trigger svg { - @include hidden; - } - } - - // Windows HCM fix - .#{$prefix}--tabs__nav-item - .#{$prefix}--tabs__nav-item--selected - .#{$prefix}--tabs__nav-item--selected { - @include high-contrast-mode('focus'); - } - - // stylelint-disable-next-line no-duplicate-selectors - .#{$prefix}--tabs - .#{$prefix}--tabs__nav-item--disabled - .#{$prefix}--tabs__nav-link { - @include high-contrast-mode('disabled'); - } - } - // TODO: remove namespace and suffix in next major release .#{$prefix}--tabs--scrollable { @include reset; From a5d0e6550272ad42dbdde4e3da0e8d945db41634 Mon Sep 17 00:00:00 2001 From: Josefina Mancilla <32556167+jnm2377@users.noreply.github.com> Date: Mon, 18 Oct 2021 15:21:54 -0500 Subject: [PATCH 17/22] Update packages/react/src/components/Tabs/index.js Co-authored-by: Taylor Jones <tay1orjones@users.noreply.github.com> --- packages/react/src/components/Tabs/index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/react/src/components/Tabs/index.js b/packages/react/src/components/Tabs/index.js index ec7ffc66fae8..989fb9845fb8 100644 --- a/packages/react/src/components/Tabs/index.js +++ b/packages/react/src/components/Tabs/index.js @@ -14,6 +14,5 @@ const Tabs = FeatureFlags.enabled('enable-v11-release') : TabsClassic; export * from './Tabs.Skeleton'; -// export default from './Tabs'; export default Tabs; From e7ab851d13a7caae89b076c9f285d375abd8cc3b Mon Sep 17 00:00:00 2001 From: Josefina Mancilla <josefinanoemi4@gmail.com> Date: Mon, 18 Oct 2021 16:09:00 -0500 Subject: [PATCH 18/22] fix: next tab test and deprecation --- .../react/src/components/Tab/next/Tab-test.js | 4 +-- packages/react/src/components/Tab/next/Tab.js | 27 ------------------- 2 files changed, 1 insertion(+), 30 deletions(-) diff --git a/packages/react/src/components/Tab/next/Tab-test.js b/packages/react/src/components/Tab/next/Tab-test.js index 735136ed3ab8..78a2f5447278 100644 --- a/packages/react/src/components/Tab/next/Tab-test.js +++ b/packages/react/src/components/Tab/next/Tab-test.js @@ -1,11 +1,9 @@ import React from 'react'; import { default as Tab } from './Tab'; -import { cleanup, render, screen } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; describe('Tab', () => { - afterEach(cleanup); - it('adds extra classes that are passed via className', async () => { render( <Tab diff --git a/packages/react/src/components/Tab/next/Tab.js b/packages/react/src/components/Tab/next/Tab.js index ad06cd59c92e..bc2a7705e592 100644 --- a/packages/react/src/components/Tab/next/Tab.js +++ b/packages/react/src/components/Tab/next/Tab.js @@ -8,7 +8,6 @@ import PropTypes from 'prop-types'; import React from 'react'; import classNames from 'classnames'; -import deprecate from '../../../prop-types/deprecate'; import { usePrefix } from '../../../internal/usePrefix'; const Tab = React.forwardRef(function Tab( @@ -23,7 +22,6 @@ const Tab = React.forwardRef(function Tab( onClick = () => {}, onKeyDown = () => {}, renderButton, - renderContent, // eslint-disable-line no-unused-vars selected = false, tabIndex = 0, ...other @@ -115,14 +113,6 @@ Tab.propTypes = { */ handleTabKeyDown: PropTypes.func, - /** - * Provide a string that represents the `href` of the Tab - */ - href: deprecate( - PropTypes.string, - 'The href prop has been deprecated and removed in v11.' - ), - /** * The element ID for the top-level element. */ @@ -153,25 +143,8 @@ Tab.propTypes = { * Useful for using Tab along with react-router or other client * side router libraries. **/ - renderAnchor: deprecate( - PropTypes.func, - 'The renderAnchor prop has been deprecated in favor of renderButton. It has been removed in v11.' - ), renderButton: PropTypes.func, - /* - * An optional parameter to allow overriding the content rendering. - **/ - renderContent: PropTypes.func, - - /** - * Provide an accessibility role for your Tab - */ - role: deprecate( - PropTypes.string, - 'The role prop has been deprecated and removed in v11. Role is now built into the element.' - ), - /** * Whether your Tab is selected. * Reserved for usage in Tabs From 730f9d8e5c86863dcc1cd979050917a03235d40f Mon Sep 17 00:00:00 2001 From: Josefina Mancilla <josefinanoemi4@gmail.com> Date: Mon, 18 Oct 2021 16:10:04 -0500 Subject: [PATCH 19/22] fix: tabs deprecate unused props --- .../react/__tests__/__snapshots__/PublicAPI-test.js.snap | 9 ++------- packages/react/src/components/Tab/Tab.js | 5 +++-- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap index 248de768906b..071ff6d93c30 100644 --- a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap +++ b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap @@ -5865,13 +5865,8 @@ Map { "renderButton": Object { "type": "func", }, - "renderContent": Object { - "type": "func", - }, - "role": Object { - "isRequired": true, - "type": "string", - }, + "renderContent": [Function], + "role": [Function], "selected": Object { "isRequired": true, "type": "bool", diff --git a/packages/react/src/components/Tab/Tab.js b/packages/react/src/components/Tab/Tab.js index 1a746eb731c1..eea4669bb686 100644 --- a/packages/react/src/components/Tab/Tab.js +++ b/packages/react/src/components/Tab/Tab.js @@ -77,12 +77,12 @@ export default class Tab extends React.Component { /* * An optional parameter to allow overriding the content rendering. **/ - renderContent: PropTypes.func, + renderContent: deprecate(PropTypes.func), /** * Provide an accessibility role for your Tab */ - role: PropTypes.string.isRequired, + role: deprecate(PropTypes.string), /** * Whether your Tab is selected. @@ -122,6 +122,7 @@ export default class Tab extends React.Component { renderAnchor, renderButton, renderContent, // eslint-disable-line no-unused-vars + role, // eslint-disable-line no-unused-vars ...other } = this.props; From 2e8925ff902db1afcc00c9ea5e20085ed0ed0fa7 Mon Sep 17 00:00:00 2001 From: Josefina Mancilla <josefinanoemi4@gmail.com> Date: Mon, 18 Oct 2021 17:00:15 -0500 Subject: [PATCH 20/22] fix: deprecations' --- .../react/__tests__/__snapshots__/PublicAPI-test.js.snap | 5 +++-- packages/react/src/components/Tab/Tab.js | 3 +-- packages/react/src/components/Tab/next/Tab.js | 6 ++++++ 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap index 35a054f26a32..6223d424aed7 100644 --- a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap +++ b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap @@ -5905,7 +5905,6 @@ Map { "label": "provide a label", "onClick": [Function], "onKeyDown": [Function], - "role": "presentation", "selected": false, }, "propTypes": Object { @@ -5943,7 +5942,9 @@ Map { "renderButton": Object { "type": "func", }, - "renderContent": [Function], + "renderContent": Object { + "type": "func", + }, "role": [Function], "selected": Object { "isRequired": true, diff --git a/packages/react/src/components/Tab/Tab.js b/packages/react/src/components/Tab/Tab.js index eea4669bb686..6329e625edc0 100644 --- a/packages/react/src/components/Tab/Tab.js +++ b/packages/react/src/components/Tab/Tab.js @@ -77,7 +77,7 @@ export default class Tab extends React.Component { /* * An optional parameter to allow overriding the content rendering. **/ - renderContent: deprecate(PropTypes.func), + renderContent: PropTypes.func, /** * Provide an accessibility role for your Tab @@ -97,7 +97,6 @@ export default class Tab extends React.Component { }; static defaultProps = { - role: 'presentation', label: 'provide a label', selected: false, onClick: () => {}, diff --git a/packages/react/src/components/Tab/next/Tab.js b/packages/react/src/components/Tab/next/Tab.js index bc2a7705e592..40be07b3b1cb 100644 --- a/packages/react/src/components/Tab/next/Tab.js +++ b/packages/react/src/components/Tab/next/Tab.js @@ -22,6 +22,7 @@ const Tab = React.forwardRef(function Tab( onClick = () => {}, onKeyDown = () => {}, renderButton, + renderContent, // eslint-disable-line no-unused-vars selected = false, tabIndex = 0, ...other @@ -145,6 +146,11 @@ Tab.propTypes = { **/ renderButton: PropTypes.func, + /* + * An optional parameter to allow overriding the content rendering. + **/ + renderContent: PropTypes.func, + /** * Whether your Tab is selected. * Reserved for usage in Tabs From 6be4cb3b18058bd88061104866e75620140e90cf Mon Sep 17 00:00:00 2001 From: Josefina Mancilla <josefinanoemi4@gmail.com> Date: Mon, 18 Oct 2021 17:11:32 -0500 Subject: [PATCH 21/22] fix: tab role presentation test --- packages/react/src/components/Tab/Tab-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react/src/components/Tab/Tab-test.js b/packages/react/src/components/Tab/Tab-test.js index f28a9e157f99..f84b5668a6ac 100644 --- a/packages/react/src/components/Tab/Tab-test.js +++ b/packages/react/src/components/Tab/Tab-test.js @@ -45,7 +45,7 @@ describe('Tab', () => { }); it('renders <li> with [role="presentation"]', () => { - expect(wrapper.props().role).toEqual('presentation'); + expect(wrapper.find('li').prop('role')).toEqual('presentation'); }); it('renders <button> with tabindex set to 0', () => { From aa706cc31f930779d05e4fe11df7ab42aeabce9d Mon Sep 17 00:00:00 2001 From: Josefina Mancilla <josefinanoemi4@gmail.com> Date: Tue, 19 Oct 2021 13:22:20 -0500 Subject: [PATCH 22/22] fix: remove unused arg' --- packages/react/src/components/Tabs/next/Tabs.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/react/src/components/Tabs/next/Tabs.js b/packages/react/src/components/Tabs/next/Tabs.js index ecb54b8c2ec5..c94a46d8719d 100644 --- a/packages/react/src/components/Tabs/next/Tabs.js +++ b/packages/react/src/components/Tabs/next/Tabs.js @@ -132,9 +132,7 @@ const Tabs = React.forwardRef(function Tabs( }; const getTabAt = useCallback( - (index, useFresh) => - (!useFresh && tabs.current[index]) || - React.Children.toArray(children)[index], + (index) => tabs.current[index] || React.Children.toArray(children)[index], [tabs, children] );