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(APP-3954): Update Tab component to not render the TabList when there is a single child #407

Merged
merged 12 commits into from
Feb 18, 2025
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

## [Unreleased]

### Changed

- Update `Tabs.List` component to only render if we have multiple tabs.

## [1.0.66] - 2025-02-11

### Changed
Expand Down
24 changes: 19 additions & 5 deletions src/core/components/tabs/tabsList/tabsList.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,18 @@ import { render, screen } from '@testing-library/react';
import { Tabs, type ITabsListProps } from '../../tabs';

describe('<Tabs.Root /> component', () => {
const createTestComponent = (props?: Partial<ITabsListProps>) => {
const completeProps: ITabsListProps = {
...props,
};
const createSingleTabTestComponent = (props?: Partial<ITabsListProps>) => {
const completeProps: ITabsListProps = { ...props };
return (
<Tabs.Root>
<Tabs.List {...completeProps}>
<Tabs.Trigger label="Tab 1" value="1" />
</Tabs.List>
</Tabs.Root>
);
};
const createMultiTabsTestComponent = (props?: Partial<ITabsListProps>) => {
const completeProps: ITabsListProps = { ...props };
return (
<Tabs.Root>
<Tabs.List {...completeProps}>
Expand All @@ -18,9 +26,15 @@ describe('<Tabs.Root /> component', () => {
};

it('should render multiple tab triggers without crashing', () => {
render(createTestComponent());
render(createMultiTabsTestComponent());

expect(screen.getByText('Tab 1')).toBeInTheDocument();
expect(screen.getByText('Tab 2')).toBeInTheDocument();
});

it('should render null when only a single tab trigger is present', () => {
render(createSingleTabTestComponent());

expect(screen.queryByText('Tab 1')).not.toBeInTheDocument();
});
});
10 changes: 9 additions & 1 deletion src/core/components/tabs/tabsList/tabsList.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { TabsList as RadixTabsList } from '@radix-ui/react-tabs';
import classNames from 'classnames';
import { useContext, type ComponentProps } from 'react';
import { Children, useContext, type ComponentProps } from 'react';
import { TabsContext } from '../tabsRoot/tabsRoot';

export interface ITabsListProps extends ComponentProps<'div'> {}
Expand All @@ -11,6 +11,14 @@ export const TabsList: React.FC<ITabsListProps> = (props) => {

const tabsListClassNames = classNames('flex gap-x-6', { 'border-b border-neutral-100': isUnderlined }, className);

/* If there is only a single child then the tabs are redundant and we just show the content
In this case we don't render the tabs list. Note that we should always set either a defaultValue or value
prop on the Tabs.Root component if there is a possibility of only a single child being present
Otherwise no tab will be selected and no content will render */
if (Children.count(children) === 1) {
return null;
}

return (
<RadixTabsList className={tabsListClassNames} {...otherProps}>
{children}
Expand Down
20 changes: 20 additions & 0 deletions src/core/components/tabs/tabsRoot/tabsRoot.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,24 @@ export const InsideCard: Story = {
render: (args) => <Card className="p-6">{reusableStoryComponent(args)}</Card>,
};

/**
* Usage example of a Tabs component with a single tab with the default value.
* If there is a chance of only a single tab then we must set either defaultValue or value property
*/
export const SingleTab: Story = {
args: { defaultValue: '1' },
render: (args) => (
<Tabs.Root {...args}>
<Tabs.List>
<Tabs.Trigger label="Default Tab" value="1" />
</Tabs.List>
<Tabs.Content value="1">
<div className="flex h-24 w-96 items-center justify-center border border-dashed border-info-300 bg-info-100">
Item 1 Content
</div>
</Tabs.Content>
</Tabs.Root>
),
};

export default meta;
59 changes: 38 additions & 21 deletions src/core/components/tabs/tabsTrigger/tabsTrigger.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,38 +3,55 @@ import { IconType } from '../../icon';
import { Tabs, type ITabsTriggerProps } from '../../tabs';

describe('<Tabs.Trigger /> component', () => {
const createTestComponent = (props?: Partial<ITabsTriggerProps>) => {
const completeProps: ITabsTriggerProps = {
label: 'Tab 1',
value: '1',
...props,
};

return (
<Tabs.Root>
<Tabs.List>
<Tabs.Trigger {...completeProps} />
</Tabs.List>
</Tabs.Root>
);
};
const createTestComponent = (tabs: ITabsTriggerProps[]) => (
<Tabs.Root defaultValue="1">
<Tabs.List>
{tabs.map((props, index) => (
<Tabs.Trigger key={index} {...props} />
))}
</Tabs.List>
</Tabs.Root>
);

it('renders a tab', () => {
render(createTestComponent());
const tab = screen.getByRole('tab');
render(
createTestComponent([
{ label: 'Tab 1', value: '1' },
{ label: 'Tab 2', value: '2' },
]),
);

const tab = screen.getByRole('tab', { name: 'Tab 1' });
expect(tab).toBeInTheDocument();
expect(tab.getAttribute('disabled')).toBeNull();
});

it('renders the icon when iconRight is provided', () => {
const iconRight = IconType.BLOCKCHAIN_BLOCK;
render(createTestComponent({ iconRight }));
render(
createTestComponent([
{ label: 'Tab 1', value: '1', iconRight },
{ label: 'Tab 2', value: '2' },
]),
);

expect(screen.getByTestId(iconRight)).toBeInTheDocument();
});

it('disables the tab when the disabled property is set to true', () => {
const disabled = true;
render(createTestComponent({ disabled }));
expect(screen.getByRole('tab').getAttribute('disabled')).toEqual('');
render(
createTestComponent([
{ label: 'Tab 1', value: '1', disabled: true },
{ label: 'Tab 2', value: '2' },
]),
);

const tab = screen.getByRole('tab', { name: 'Tab 1' });
expect(tab.getAttribute('disabled')).toEqual('');
});

it('does not render the Tabs.List or Tabs.Trigger in a single tab setup', () => {
render(createTestComponent([{ label: 'Tab 1', value: '1' }]));
expect(screen.queryByRole('tab')).not.toBeInTheDocument();
});
});