Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 6 additions & 7 deletions packages/gamut/src/Tabs/Tab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,12 @@ import { useTab } from './TabProvider';

interface TabBaseProps extends TabButtonProps, ReactAriaTabProps {}

export type TabProps = TabBaseProps &
Omit<ReactAriaTabProps, 'id'> & {
/**
* the id matches up the tab and tab panel
*/
id: string;
};
export type TabProps = Omit<TabBaseProps, 'id'> & {
/**
* the id matches up the tab and tab panel
*/
id: string;
};

const TabBase = styled(ReactAriaTab)<TabProps>(
tabVariants,
Expand Down
41 changes: 32 additions & 9 deletions packages/gamut/src/Tabs/TabPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { system } from '@codecademy/gamut-styles';
import styled from '@emotion/styled';
import * as React from 'react';
import {
TabPanel as ReactAriaTabPanel,
TabPanelProps as ReactAriaTabPanelProps,
Expand All @@ -10,13 +12,34 @@ interface TabPanelBaseProps
extends TabElementStyleProps,
ReactAriaTabPanelProps {}

export type TabPanelProps = TabPanelBaseProps &
Omit<ReactAriaTabPanelProps, 'id'> & {
/**
* the id matches up the tab and tab panel
*/
id: string;
};
export type TabPanelProps = Omit<
TabPanelBaseProps,
'id' | 'shouldForceMount'
> & {
/**
* the id matches up the tab and tab panel
*/
id: string;
/**
* Whether to mount the tab panel in the DOM even when it is not currently selected.
* This is a wrapper around the `shouldForceMount` prop in react-aria-components that also visually hides the inactive tab panel.
*/
shouldForceMount?: boolean;
};

export const TabPanel =
styled(ReactAriaTabPanel)<TabPanelProps>(tabElementBaseProps);
const tabPanelStates = system.states({
shouldForceMount: {
"&[data-inert='true']": {
display: 'none',
},
},
});

const StyledTabPanel = styled(ReactAriaTabPanel)<TabPanelProps>(
tabElementBaseProps,
tabPanelStates
);

export const TabPanel: React.FC<TabPanelProps> = (props) => {
return <StyledTabPanel {...props} />;
};
21 changes: 20 additions & 1 deletion packages/gamut/src/Tabs/__tests__/Tabs.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const FullTabs = (props: TabsProps) => (
<TabPanel id="tab2">
<p>tab 2 content</p>
</TabPanel>
<TabPanel id="tab3">
<TabPanel id="tab3" shouldForceMount>
<p>tab 3 content</p>
</TabPanel>
</TabPanels>
Expand Down Expand Up @@ -75,17 +75,20 @@ describe('Tabs', () => {

view.getByText('Tab 1');
view.getByText('tab 1 content');
expect(view.queryByText('tab 2 content')).toBeNull();
});

it('renders the second tab tab panel and calls onSelectionChange when second tab is clicked', async () => {
const { view } = renderView();

view.getByText('tab 1 content');
expect(view.queryByText('tab 2 content')).toBeNull();

await act(() => userEvent.click(view.getByText('Tab 2')));

view.getByText('tab 2 content');
expect(mockOnSelectionChange).toHaveBeenCalledWith('tab2');
expect(view.queryByText('tab 1 content')).toBeNull();
});

it('renders the default selected key when passed', () => {
Expand All @@ -101,17 +104,20 @@ describe('Tabs', () => {

view.getByText('Tab 1');
view.getByText('tab 1 content');
expect(view.queryByText('tab 2 content')).toBeNull();
});

it('renders new tab panel and calls onSelectionChange when a tab is clicked', async () => {
const { view } = renderViewControlled();

view.getByText('tab 1 content');
expect(view.queryByText('tab 2 content')).toBeNull();

await act(() => userEvent.click(view.getByText('Tab 2')));

expect(mockOnSelectionChange).toHaveBeenCalledWith('tab2');
view.getByText('tab 2 content');
expect(view.queryByText('tab 1 content')).toBeNull();
});
});
describe('Disabled', () => {
Expand All @@ -131,4 +137,17 @@ describe('Tabs', () => {
expect(tab).toHaveStyle('opacity: 0.25');
});
});

describe('Force mount', () => {
it('renders the tab panel when shouldForceMount is passed', () => {
const { view } = renderView();

view.getByText('tab 1 content'); // default tab
view.getByText('tab 3 content'); // force mounted tab
expect(view.getByText('tab 3 content').parentElement).toHaveStyle(
'display: none'
);
expect(view.queryByText('tab 2 content')).toBeNull(); // not force mounted tab
});
});
});
8 changes: 8 additions & 0 deletions packages/styleguide/src/lib/Molecules/Tabs/Tabs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,14 @@ You can disable a tab by passing the `isDisabled` prop to a specific `Tab` compo

<Canvas of={TabStories.Disabled} />

### Force mount

By default, when a tab is not selected, the `TabPanel` is not mounted in the DOM. This is to improve performance and avoid unnecessary rendering. However, if you need to force mount the `TabPanel` even when it is not selected, you can pass the `shouldForceMount` prop to the `TabPanel` component.

The `shouldForceMount` prop is a wrapper around the `shouldForceMount` prop in [react-aria-components](https://react-spectrum.adobe.com/react-aria/Tabs.html#tabpanel) that also visually hides the inactive tab panel.

<Canvas of={TabStories.ForceMount} />

## Playground

<Canvas sourceState="shown" of={TabStories.Default} />
Expand Down
28 changes: 27 additions & 1 deletion packages/styleguide/src/lib/Molecules/Tabs/Tabs.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export const Default: Story = {
<Text as="h2">Welcome to Tab 1</Text>
<Text>Hi there! I&apos;m the contents inside Tab 1. Yippee!</Text>
</TabPanel>
<TabPanel id="2">
<TabPanel id="2" shouldForceMount>
<Text as="h2">Welcome to Tab 2</Text>
<Text>Hi there! I&apos;m the contents inside Tab 2. Yippee!</Text>
</TabPanel>
Expand Down Expand Up @@ -263,3 +263,29 @@ export const Disabled: Story = {
</Tabs>
),
};

export const ForceMount: Story = {
render: (args) => (
<Tabs {...args}>
<TabList>
<Tab id="1">Tab 1</Tab>
<Tab id="2">Tab 2</Tab>
<Tab id="3">Tab 3</Tab>
</TabList>
<TabPanels my={24}>
<TabPanel id="1" shouldForceMount>
<Text as="h2">Welcome to Tab 1</Text>
<Text>Hi there! I&apos;m the contents inside Tab 1. Yippee!</Text>
</TabPanel>
<TabPanel id="2" shouldForceMount>
<Text as="h2">Welcome to Tab 2</Text>
<Text>Hi there! I&apos;m the contents inside Tab 2. Yippee!</Text>
</TabPanel>
<TabPanel id="3" shouldForceMount>
<Text as="h2">Welcome to Tab 3</Text>
<Text>Hi there! I&apos;m the contents inside Tab 3. Yippee!</Text>
</TabPanel>
</TabPanels>
</Tabs>
),
};
Loading