Skip to content

Commit

Permalink
feat(tabs): add support for secondaryLabel (#13366)
Browse files Browse the repository at this point in the history
* 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 <tay1orjones@users.noreply.github.com>
  • Loading branch information
francinelucca and tay1orjones authored Mar 24, 2023
1 parent b6c24cb commit 4bf871b
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 8 deletions.
8 changes: 8 additions & 0 deletions e2e/components/Tabs/Tabs-test.e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
});
});
});

Expand Down
6 changes: 6 additions & 0 deletions packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -7062,6 +7062,9 @@ Map {
"disabled": Object {
"type": "bool",
},
"hasSecondaryLabel": Object {
"type": "bool",
},
"onClick": Object {
"type": "func",
},
Expand All @@ -7071,6 +7074,9 @@ Map {
"renderButton": Object {
"type": "func",
},
"secondaryLabel": Object {
"type": "string",
},
},
"render": [Function],
},
Expand Down
52 changes: 47 additions & 5 deletions packages/react/src/components/Tabs/Tabs-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ describe('Tabs', () => {
</Tabs>
);

expect(screen.getByText('Tab Label 2')).toHaveAttribute(
expect(screen.getByText('Tab Label 2').parentElement).toHaveAttribute(
'aria-selected',
'true'
);
Expand Down Expand Up @@ -67,7 +67,9 @@ describe('Tab', () => {
</Tabs>
);

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', () => {
Expand All @@ -86,13 +88,13 @@ describe('Tab', () => {
</Tabs>
);

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'
);
Expand All @@ -113,7 +115,47 @@ describe('Tab', () => {
</TabPanels>
</Tabs>
);
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(
<Tabs>
<TabList aria-label="List of tabs" contained>
<Tab as="div" secondaryLabel="test-secondary-label">
Tab Label 1
</Tab>
<Tab>Tab Label 2</Tab>
<Tab>Tab Label 3</Tab>
</TabList>
<TabPanels>
<TabPanel>Tab Panel 1</TabPanel>
<TabPanel>Tab Panel 2</TabPanel>
<TabPanel>Tab Panel 3</TabPanel>
</TabPanels>
</Tabs>
);
expect(screen.getByText('test-secondary-label')).toBeInTheDocument();
});

it('should not render secondaryLabel in non-contained tabs', () => {
render(
<Tabs>
<TabList aria-label="List of tabs">
<Tab as="div" secondaryLabel="test-secondary-label">
Tab Label 1
</Tab>
<Tab>Tab Label 2</Tab>
<Tab>Tab Label 3</Tab>
</TabList>
<TabPanels>
<TabPanel>Tab Panel 1</TabPanel>
<TabPanel>Tab Panel 2</TabPanel>
<TabPanel>Tab Panel 3</TabPanel>
</TabPanels>
</Tabs>
);
expect(screen.queryByText('test-secondary-label')).toBeNull();
});

it('should call onClick from props if provided', () => {
Expand Down
27 changes: 26 additions & 1 deletion packages/react/src/components/Tabs/Tabs.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -344,6 +350,7 @@ function TabList({
ref: (node) => {
tabs.current[index] = node;
},
hasSecondaryLabel: hasSecondaryLabelTabs,
})}
</TabContext.Provider>
);
Expand Down Expand Up @@ -481,6 +488,8 @@ const Tab = React.forwardRef(function Tab(
disabled,
onClick,
onKeyDown,
secondaryLabel,
hasSecondaryLabel,
...rest
},
ref
Expand Down Expand Up @@ -524,7 +533,12 @@ const Tab = React.forwardRef(function Tab(
onKeyDown={onKeyDown}
tabIndex={selectedIndex === index ? '0' : '-1'}
type="button">
{children}
<span className={`${prefix}--tabs__nav-item-label`}>{children}</span>
{hasSecondaryLabel && (
<div className={`${prefix}--tabs__nav-item-secondary-label`}>
{secondaryLabel}
</div>
)}
</BaseComponent>
);
});
Expand All @@ -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
*/
Expand All @@ -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(
Expand Down
38 changes: 38 additions & 0 deletions packages/react/src/components/Tabs/Tabs.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,44 @@ export const Contained = () => (
</Tabs>
);

export const ContainedWithSecondaryLabel = () => (
<Tabs>
<TabList aria-label="List of tabs" contained>
<Tab secondaryLabel="367">Tab Label 1</Tab>
<Tab secondaryLabel="367">Tab Label 2</Tab>
<Tab disabled secondaryLabel="367">
Tab Label 3
</Tab>
<Tab title="Tab Label 4" secondaryLabel="367">
Tab Label 4
</Tab>
<Tab secondaryLabel="367">Tab Label 5</Tab>
</TabList>
<TabPanels>
<TabPanel>Tab Panel 1</TabPanel>
<TabPanel>
<form style={{ margin: '2em' }}>
<legend className={`cds--label`}>Validation example</legend>
<Checkbox id="cb" labelText="Accept privacy policy" />
<Button
style={{ marginTop: '1rem', marginBottom: '1rem' }}
type="submit">
Submit
</Button>
<TextInput
type="text"
labelText="Text input label"
helperText="Optional help text"
/>
</form>
</TabPanel>
<TabPanel>Tab Panel 3</TabPanel>
<TabPanel>Tab Panel 4</TabPanel>
<TabPanel>Tab Panel 5</TabPanel>
</TabPanels>
</Tabs>
);

export const Skeleton = () => {
return (
<div style={{ maxWidth: '100%' }}>
Expand Down
29 changes: 27 additions & 2 deletions packages/styles/scss/components/tabs/_tabs.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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
//-----------------------------
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down

0 comments on commit 4bf871b

Please sign in to comment.