Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Tabs): Contained tabs on the grid #13927

Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
698a6ec
feat(Tabs): allow tabs to distribute width with new normalizeWidth prop
francinelucca Jun 5, 2023
06eba0e
Merge branch 'main' of github.com:carbon-design-system/carbon into 13…
francinelucca Jun 7, 2023
054642d
feat(Tabs): add ellipsis overflow to fullWidth tabs and add test stories
francinelucca Jun 7, 2023
1cacef1
Merge branch 'main' into 13353-tabs-implement-contained-tabs-on-the-c…
francinelucca Jun 7, 2023
65b6472
Merge branch 'main' of github.com:carbon-design-system/carbon into 13…
francinelucca Jun 12, 2023
9a5e81e
fix(Tabs): mock useMatchMedia for tests and add fullWidth tests
francinelucca Jun 12, 2023
1a56114
fix: update snapshot
francinelucca Jun 12, 2023
f45617d
Merge branch 'main' into 13353-tabs-implement-contained-tabs-on-the-c…
guidari Jun 16, 2023
d6dce49
fix: add more docs around tabs in the grid
francinelucca Jun 16, 2023
6b92b17
Merge branch 'main' into 13353-tabs-implement-contained-tabs-on-the-c…
francinelucca Jun 19, 2023
a1924e2
Merge branch 'main' of github.com:carbon-design-system/carbon into 13…
francinelucca Jun 20, 2023
2e89178
fix: format
francinelucca Jun 20, 2023
f407b2a
Merge branch 'main' into 13353-tabs-implement-contained-tabs-on-the-c…
francinelucca Jun 20, 2023
ef33660
chore(Tabs): remove test story
francinelucca Jun 21, 2023
2d908a9
Merge branch '13353-tabs-implement-contained-tabs-on-the-css-grid' of…
francinelucca Jun 21, 2023
b842a48
Merge branch 'main' into 13353-tabs-implement-contained-tabs-on-the-c…
francinelucca Jun 21, 2023
fec0348
Merge branch 'main' into 13353-tabs-implement-contained-tabs-on-the-c…
guidari Jun 22, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -70,6 +70,14 @@ test.describe('Tabs', () => {
});
});

test('contained fullWidth @vrt', async ({ page }) => {
await snapshotStory(page, {
component: 'Tabs',
id: 'components-tabs--contained-full-width',
theme,
});
});

test('contained with secondary labels @vrt', async ({ page }) => {
await snapshotStory(page, {
component: 'Tabs',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7587,6 +7587,9 @@ Map {
"contained": Object {
"type": "bool",
},
"fullWidth": Object {
"type": "bool",
},
"iconSize": Object {
"args": Array [
Array [
Expand Down
106 changes: 106 additions & 0 deletions packages/react/src/components/Tabs/Tabs-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,16 @@ import { Tabs, Tab, TabPanel, TabPanels, TabList } from './Tabs';
import { act } from 'react-dom/test-utils';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import * as hooks from '../../internal/useMatchMedia';

const prefix = 'cds';

describe('Tabs', () => {
beforeEach(() => {
jest.resetModules();
jest.spyOn(hooks, 'useMatchMedia').mockImplementation(() => true);
});

it('should update selected index based on the default provided', () => {
render(
<Tabs defaultSelectedIndex={1}>
Expand Down Expand Up @@ -53,6 +59,11 @@ describe('Tabs', () => {
});

describe('Tab', () => {
beforeEach(() => {
jest.resetModules();
jest.spyOn(hooks, 'useMatchMedia').mockImplementation(() => true);
});

it('should set a className from props on outermost element in Tab', () => {
render(
<Tabs>
Expand Down Expand Up @@ -437,6 +448,11 @@ describe('Tab', () => {
});

describe('TabPanel', () => {
beforeEach(() => {
jest.resetModules();
jest.spyOn(hooks, 'useMatchMedia').mockImplementation(() => true);
});

it('should have a className if provided by props', () => {
render(
<Tabs>
Expand Down Expand Up @@ -536,3 +552,93 @@ describe('TabPanel', () => {
expect(screen.getByText('Tab Panel 1')).toHaveAttribute('tabIndex', '0');
});
});

describe('TabList', () => {
it('should span fullWidth if lg and fullWidth prop is passed in', () => {
jest.spyOn(hooks, 'useMatchMedia').mockImplementation(() => true);
const { container } = render(
<Tabs>
<TabList aria-label="List of tabs" contained fullWidth>
<Tab>Tab Label 1</Tab>
<Tab>Tab Label 2</Tab>
<Tab>Tab Label 3</Tab>
</TabList>
<TabPanels>
<TabPanel className="custom-class">
Tab Panel 1<button type="button">Submit</button>
</TabPanel>
<TabPanel>Tab Panel 2</TabPanel>
<TabPanel>Tab Panel 3</TabPanel>
</TabPanels>
</Tabs>
);

expect(container.firstChild).toHaveClass(`${prefix}--tabs--full-width`);
});

it('should ignore fullWidth prop if screen smaller than lg', () => {
jest.spyOn(hooks, 'useMatchMedia').mockImplementation(() => false);
const { container } = render(
<Tabs>
<TabList aria-label="List of tabs" contained fullWidth>
<Tab>Tab Label 1</Tab>
<Tab>Tab Label 2</Tab>
<Tab>Tab Label 3</Tab>
</TabList>
<TabPanels>
<TabPanel className="custom-class">
Tab Panel 1<button type="button">Submit</button>
</TabPanel>
<TabPanel>Tab Panel 2</TabPanel>
<TabPanel>Tab Panel 3</TabPanel>
</TabPanels>
</Tabs>
);

expect(container.firstChild).not.toHaveClass(`${prefix}--tabs--full-width`);
});

it('should ignore fullWidth prop if tabs are not contained', () => {
jest.spyOn(hooks, 'useMatchMedia').mockImplementation(() => true);
const { container } = render(
<Tabs>
<TabList aria-label="List of tabs" fullWidth>
<Tab>Tab Label 1</Tab>
<Tab>Tab Label 2</Tab>
<Tab>Tab Label 3</Tab>
</TabList>
<TabPanels>
<TabPanel className="custom-class">
Tab Panel 1<button type="button">Submit</button>
</TabPanel>
<TabPanel>Tab Panel 2</TabPanel>
<TabPanel>Tab Panel 3</TabPanel>
</TabPanels>
</Tabs>
);

expect(container.firstChild).not.toHaveClass(`${prefix}--tabs--full-width`);
});

it('should not be fullWidth in default state', () => {
jest.spyOn(hooks, 'useMatchMedia').mockImplementation(() => true);
const { container } = render(
<Tabs>
<TabList aria-label="List of tabs" contained>
<Tab>Tab Label 1</Tab>
<Tab>Tab Label 2</Tab>
<Tab>Tab Label 3</Tab>
</TabList>
<TabPanels>
<TabPanel className="custom-class">
Tab Panel 1<button type="button">Submit</button>
</TabPanel>
<TabPanel>Tab Panel 2</TabPanel>
<TabPanel>Tab Panel 3</TabPanel>
</TabPanels>
</Tabs>
);

expect(container.firstChild).not.toHaveClass(`${prefix}--tabs--full-width`);
});
});
28 changes: 26 additions & 2 deletions packages/react/src/components/Tabs/Tabs.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/

import { ChevronLeft, ChevronRight } from '@carbon/icons-react';
import { breakpoints } from '@carbon/layout';
import cx from 'classnames';
import debounce from 'lodash.debounce';
import PropTypes from 'prop-types';
Expand All @@ -23,6 +24,7 @@ import { usePressable } from './usePressable';
import deprecate from '../../prop-types/deprecate';
import { Close } from '@carbon/icons-react';
import { useEvent } from '../../internal/useEvent';
import { useMatchMedia } from '../../internal/useMatchMedia';

// Used to manage the overall state of the Tabs
const TabsContext = React.createContext();
Expand All @@ -32,6 +34,9 @@ const TabContext = React.createContext();

// Used to keep track of position in a list of tab panels
const TabPanelContext = React.createContext();

const lgMediaQuery = `(min-width: ${breakpoints.lg.width})`;

function Tabs({
children,
defaultSelectedIndex = 0,
Expand Down Expand Up @@ -138,6 +143,7 @@ function TabList({
children,
className: customClassName,
contained = false,
fullWidth = false,
iconSize,
leftOverflowButtonProps,
light,
Expand All @@ -160,6 +166,14 @@ function TabList({
const [isScrollable, setIsScrollable] = useState(false);
const [scrollLeft, setScrollLeft] = useState(null);

const isLg = useMatchMedia(lgMediaQuery);

const distributeWidth =
fullWidth &&
contained &&
isLg &&
React.Children.toArray(children).length < 9;

// Previous Button
// VISIBLE IF:
// SCROLLABLE
Expand All @@ -186,6 +200,7 @@ function TabList({
[`${prefix}--tabs__icon--default`]: iconSize === 'default',
[`${prefix}--tabs__icon--lg`]: iconSize === 'lg',
[`${prefix}--tabs--tall`]: hasSecondaryLabelTabs,
[`${prefix}--tabs--full-width`]: distributeWidth,
});

const isPreviousButtonVisible = ref.current
Expand Down Expand Up @@ -451,6 +466,11 @@ TabList.propTypes = {
*/
contained: PropTypes.bool,

/**
* Used for tabs within a grid, this makes it so tabs span the full container width and have the same width. Only available on contained tabs with <9 children
*/
fullWidth: PropTypes.bool,

/**
* If using `IconTab`, specify the size of the icon being used.
*/
Expand Down Expand Up @@ -651,7 +671,9 @@ const Tab = React.forwardRef(function Tab(
{<Icon size={16} />}
</div>
)}
<span className={`${prefix}--tabs__nav-item-label`}>{children}</span>
<span className={`${prefix}--tabs__nav-item-label`} title={children}>
{children}
</span>
{/* always rendering dismissIcon so we don't lose reference to it, otherwise events do not work when switching from/to dismissable state */}
<div
className={cx(`${prefix}--tabs__nav-item--icon`, {
Expand All @@ -662,7 +684,9 @@ const Tab = React.forwardRef(function Tab(
</div>
</div>
{hasSecondaryLabel && (
<div className={`${prefix}--tabs__nav-item-secondary-label`}>
<div
className={`${prefix}--tabs__nav-item-secondary-label`}
title={secondaryLabel}>
{secondaryLabel}
</div>
)}
Expand Down
Loading