From 4bf871b5d2d34703d7b2b43fce0d7fa418fe298e Mon Sep 17 00:00:00 2001 From: Francine Lucca <40550942+francinelucca@users.noreply.github.com> Date: Fri, 24 Mar 2023 15:36:46 -0400 Subject: [PATCH] feat(tabs): add support for secondaryLabel (#13366) * feat(tabs): add support for secondaryLabel * test(Tabs): adjust tests to account for new child elements * fix(Tabs): add new Tab props to publicAPI * test(Tabs): add contained tabs with secondary label story & e2e testing * fix(Tabs): add more description to secondaryLabel prop * test(Tabs): add test cases for new secondaryLabel prop * fix(tabs): set span line-height to 0 for icon tabs --------- Co-authored-by: Taylor Jones --- e2e/components/Tabs/Tabs-test.e2e.js | 8 +++ .../__snapshots__/PublicAPI-test.js.snap | 6 +++ .../react/src/components/Tabs/Tabs-test.js | 52 +++++++++++++++++-- packages/react/src/components/Tabs/Tabs.js | 27 +++++++++- .../react/src/components/Tabs/Tabs.stories.js | 38 ++++++++++++++ .../styles/scss/components/tabs/_tabs.scss | 29 ++++++++++- 6 files changed, 152 insertions(+), 8 deletions(-) diff --git a/e2e/components/Tabs/Tabs-test.e2e.js b/e2e/components/Tabs/Tabs-test.e2e.js index 1b6bab593acb..567089d9b96f 100644 --- a/e2e/components/Tabs/Tabs-test.e2e.js +++ b/e2e/components/Tabs/Tabs-test.e2e.js @@ -53,6 +53,14 @@ test.describe('Tabs', () => { theme, }); }); + + test('contained with secondary labels @vrt', async ({ page }) => { + await snapshotStory(page, { + component: 'Tabs', + id: 'components-tabs--contained-with-secondary-label', + theme, + }); + }); }); }); diff --git a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap index eb8e7a11f0ea..db0f8eaec927 100644 --- a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap +++ b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap @@ -7062,6 +7062,9 @@ Map { "disabled": Object { "type": "bool", }, + "hasSecondaryLabel": Object { + "type": "bool", + }, "onClick": Object { "type": "func", }, @@ -7071,6 +7074,9 @@ Map { "renderButton": Object { "type": "func", }, + "secondaryLabel": Object { + "type": "string", + }, }, "render": [Function], }, diff --git a/packages/react/src/components/Tabs/Tabs-test.js b/packages/react/src/components/Tabs/Tabs-test.js index 0355db2dd185..341b84d95767 100644 --- a/packages/react/src/components/Tabs/Tabs-test.js +++ b/packages/react/src/components/Tabs/Tabs-test.js @@ -21,7 +21,7 @@ describe('Tabs', () => { ); - expect(screen.getByText('Tab Label 2')).toHaveAttribute( + expect(screen.getByText('Tab Label 2').parentElement).toHaveAttribute( 'aria-selected', 'true' ); @@ -67,7 +67,9 @@ describe('Tab', () => { ); - expect(screen.getByText('Tab Label 2')).toHaveClass('custom-class'); + expect(screen.getByText('Tab Label 2').parentElement).toHaveClass( + 'custom-class' + ); }); it('should not select a disabled tab and select next tab', () => { @@ -86,13 +88,13 @@ describe('Tab', () => { ); - expect(screen.getByText('Tab Label 1')).toHaveAttribute( + expect(screen.getByText('Tab Label 1').parentElement).toHaveAttribute( 'aria-selected', 'false' ); // By default, if a Tab is disabled, the next Tab should be selected - expect(screen.getByText('Tab Label 2')).toHaveAttribute( + expect(screen.getByText('Tab Label 2').parentElement).toHaveAttribute( 'aria-selected', 'true' ); @@ -113,7 +115,47 @@ describe('Tab', () => { ); - expect(screen.getByText('Tab Label 1').tagName).toBe('DIV'); + expect(screen.getByText('Tab Label 1').parentElement.tagName).toBe('DIV'); + }); + + it('should render secondaryLabel in contained tabs if provided', () => { + render( + + + + Tab Label 1 + + Tab Label 2 + Tab Label 3 + + + Tab Panel 1 + Tab Panel 2 + Tab Panel 3 + + + ); + expect(screen.getByText('test-secondary-label')).toBeInTheDocument(); + }); + + it('should not render secondaryLabel in non-contained tabs', () => { + render( + + + + Tab Label 1 + + Tab Label 2 + Tab Label 3 + + + Tab Panel 1 + Tab Panel 2 + Tab Panel 3 + + + ); + expect(screen.queryByText('test-secondary-label')).toBeNull(); }); it('should call onClick from props if provided', () => { diff --git a/packages/react/src/components/Tabs/Tabs.js b/packages/react/src/components/Tabs/Tabs.js index c1517b51f17c..ab657641cf83 100644 --- a/packages/react/src/components/Tabs/Tabs.js +++ b/packages/react/src/components/Tabs/Tabs.js @@ -130,11 +130,17 @@ function TabList({ const nextButton = useRef(null); const [isScrollable, setIsScrollable] = useState(false); const [scrollLeft, setScrollLeft] = useState(null); + const hasSecondaryLabelTabs = + contained && + !!React.Children.toArray(children).filter( + (child) => child.props.secondaryLabel + ).length; const className = cx(`${prefix}--tabs`, customClassName, { [`${prefix}--tabs--contained`]: contained, [`${prefix}--tabs--light`]: light, [`${prefix}--tabs__icon--default`]: iconSize === 'default', [`${prefix}--tabs__icon--lg`]: iconSize === 'lg', + [`${prefix}--tabs--tall`]: hasSecondaryLabelTabs, }); // Previous Button @@ -344,6 +350,7 @@ function TabList({ ref: (node) => { tabs.current[index] = node; }, + hasSecondaryLabel: hasSecondaryLabelTabs, })} ); @@ -481,6 +488,8 @@ const Tab = React.forwardRef(function Tab( disabled, onClick, onKeyDown, + secondaryLabel, + hasSecondaryLabel, ...rest }, ref @@ -524,7 +533,12 @@ const Tab = React.forwardRef(function Tab( onKeyDown={onKeyDown} tabIndex={selectedIndex === index ? '0' : '-1'} type="button"> - {children} + {children} + {hasSecondaryLabel && ( +
+ {secondaryLabel} +
+ )} ); }); @@ -550,6 +564,11 @@ Tab.propTypes = { */ disabled: PropTypes.bool, + /* + * Internal use only, determines wether a tab should render as a secondary label tab + **/ + hasSecondaryLabel: PropTypes.bool, + /** * Provide a handler that is invoked when a user clicks on the control */ @@ -566,6 +585,12 @@ Tab.propTypes = { * side router libraries. **/ renderButton: PropTypes.func, + + /* + * An optional label to render under the primary tab label. + /* This prop is only useful for conained tabs + **/ + secondaryLabel: PropTypes.string, }; const IconTab = React.forwardRef(function IconTab( diff --git a/packages/react/src/components/Tabs/Tabs.stories.js b/packages/react/src/components/Tabs/Tabs.stories.js index 091cf54d87b8..47de9d304259 100644 --- a/packages/react/src/components/Tabs/Tabs.stories.js +++ b/packages/react/src/components/Tabs/Tabs.stories.js @@ -182,6 +182,44 @@ export const Contained = () => ( ); +export const ContainedWithSecondaryLabel = () => ( + + + Tab Label 1 + Tab Label 2 + + Tab Label 3 + + + Tab Label 4 + + Tab Label 5 + + + Tab Panel 1 + +
+ Validation example + + + + +
+ Tab Panel 3 + Tab Panel 4 + Tab Panel 5 +
+
+); + export const Skeleton = () => { return (
diff --git a/packages/styles/scss/components/tabs/_tabs.scss b/packages/styles/scss/components/tabs/_tabs.scss index 1d05fcc82995..93cb209bdcee 100644 --- a/packages/styles/scss/components/tabs/_tabs.scss +++ b/packages/styles/scss/components/tabs/_tabs.scss @@ -299,10 +299,25 @@ $icon-tab-size: custom-property.get-var('icon-tab-size', rem(40px)); height: rem(48px); padding: $spacing-03 $spacing-05; border-bottom: 0; + } + + &.#{$prefix}--tabs--contained:not(.#{$prefix}--tabs--tall) + .#{$prefix}--tabs__nav-item-label { // height - vertical padding line-height: calc(#{rem(48px)} - (#{$spacing-03} * 2)); } + &.#{$prefix}--tabs--contained .#{$prefix}--tabs__nav-item-secondary-label { + @include type-style('label-01'); + + min-height: rem(16px); + } + + &.#{$prefix}--tabs--contained.#{$prefix}--tabs--tall + .#{$prefix}--tabs__nav-link { + height: rem(64px); + } + //----------------------------- // Icon Item //----------------------------- @@ -319,6 +334,10 @@ $icon-tab-size: custom-property.get-var('icon-tab-size', rem(40px)); align-items: center; justify-content: center; padding: 0; + + .#{$prefix}--tabs__nav-item-label { + line-height: 0; + } } &.#{$prefix}--tabs__icon--lg { @@ -363,11 +382,17 @@ $icon-tab-size: custom-property.get-var('icon-tab-size', rem(40px)); color: $text-primary; } + &.#{$prefix}--tabs--contained:not(.#{$prefix}--tabs--tall) + .#{$prefix}--tabs__nav-item--selected, + &.#{$prefix}--tabs--contained:not(.#{$prefix}--tabs--tall) + .#{$prefix}--tabs__nav-item--selected:hover { + // height - vertical padding + line-height: calc(#{rem(48px)} - (#{$spacing-03} * 2)); + } + &.#{$prefix}--tabs--contained .#{$prefix}--tabs__nav-item--selected, &.#{$prefix}--tabs--contained .#{$prefix}--tabs__nav-item--selected:hover { background-color: $layer; - // height - vertical padding - line-height: calc(#{rem(48px)} - (#{$spacing-03} * 2)); .#{$prefix}--tabs__nav-link:focus, .#{$prefix}--tabs__nav-link:active {