Skip to content

Commit 4fda27c

Browse files
authored
feat(Tabs): add shouldForceMount prop to TabPanel
Adds `shouldForceMount` prop to `TabPanel` to render the `TabPanel` in the DOM and hide it visually. This is a request from the Author team to match functionality from reach/tabs that was removed when we migrated over to react-aria-components tabs.
1 parent 744ff3c commit 4fda27c

File tree

5 files changed

+93
-18
lines changed

5 files changed

+93
-18
lines changed

packages/gamut/src/Tabs/Tab.tsx

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,12 @@ import { useTab } from './TabProvider';
1111

1212
interface TabBaseProps extends TabButtonProps, ReactAriaTabProps {}
1313

14-
export type TabProps = TabBaseProps &
15-
Omit<ReactAriaTabProps, 'id'> & {
16-
/**
17-
* the id matches up the tab and tab panel
18-
*/
19-
id: string;
20-
};
14+
export type TabProps = Omit<TabBaseProps, 'id'> & {
15+
/**
16+
* the id matches up the tab and tab panel
17+
*/
18+
id: string;
19+
};
2120

2221
const TabBase = styled(ReactAriaTab)<TabProps>(
2322
tabVariants,
Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import { system } from '@codecademy/gamut-styles';
12
import styled from '@emotion/styled';
3+
import * as React from 'react';
24
import {
35
TabPanel as ReactAriaTabPanel,
46
TabPanelProps as ReactAriaTabPanelProps,
@@ -10,13 +12,34 @@ interface TabPanelBaseProps
1012
extends TabElementStyleProps,
1113
ReactAriaTabPanelProps {}
1214

13-
export type TabPanelProps = TabPanelBaseProps &
14-
Omit<ReactAriaTabPanelProps, 'id'> & {
15-
/**
16-
* the id matches up the tab and tab panel
17-
*/
18-
id: string;
19-
};
15+
export type TabPanelProps = Omit<
16+
TabPanelBaseProps,
17+
'id' | 'shouldForceMount'
18+
> & {
19+
/**
20+
* the id matches up the tab and tab panel
21+
*/
22+
id: string;
23+
/**
24+
* Whether to mount the tab panel in the DOM even when it is not currently selected.
25+
* This is a wrapper around the `shouldForceMount` prop in react-aria-components that also visually hides the inactive tab panel.
26+
*/
27+
shouldForceMount?: boolean;
28+
};
2029

21-
export const TabPanel =
22-
styled(ReactAriaTabPanel)<TabPanelProps>(tabElementBaseProps);
30+
const tabPanelStates = system.states({
31+
shouldForceMount: {
32+
"&[data-inert='true']": {
33+
display: 'none',
34+
},
35+
},
36+
});
37+
38+
const StyledTabPanel = styled(ReactAriaTabPanel)<TabPanelProps>(
39+
tabElementBaseProps,
40+
tabPanelStates
41+
);
42+
43+
export const TabPanel: React.FC<TabPanelProps> = (props) => {
44+
return <StyledTabPanel {...props} />;
45+
};

packages/gamut/src/Tabs/__tests__/Tabs.test.tsx

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ const FullTabs = (props: TabsProps) => (
2020
<TabPanel id="tab2">
2121
<p>tab 2 content</p>
2222
</TabPanel>
23-
<TabPanel id="tab3">
23+
<TabPanel id="tab3" shouldForceMount>
2424
<p>tab 3 content</p>
2525
</TabPanel>
2626
</TabPanels>
@@ -75,17 +75,20 @@ describe('Tabs', () => {
7575

7676
view.getByText('Tab 1');
7777
view.getByText('tab 1 content');
78+
expect(view.queryByText('tab 2 content')).toBeNull();
7879
});
7980

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

8384
view.getByText('tab 1 content');
85+
expect(view.queryByText('tab 2 content')).toBeNull();
8486

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

8789
view.getByText('tab 2 content');
8890
expect(mockOnSelectionChange).toHaveBeenCalledWith('tab2');
91+
expect(view.queryByText('tab 1 content')).toBeNull();
8992
});
9093

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

102105
view.getByText('Tab 1');
103106
view.getByText('tab 1 content');
107+
expect(view.queryByText('tab 2 content')).toBeNull();
104108
});
105109

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

109113
view.getByText('tab 1 content');
114+
expect(view.queryByText('tab 2 content')).toBeNull();
110115

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

113118
expect(mockOnSelectionChange).toHaveBeenCalledWith('tab2');
114119
view.getByText('tab 2 content');
120+
expect(view.queryByText('tab 1 content')).toBeNull();
115121
});
116122
});
117123
describe('Disabled', () => {
@@ -131,4 +137,17 @@ describe('Tabs', () => {
131137
expect(tab).toHaveStyle('opacity: 0.25');
132138
});
133139
});
140+
141+
describe('Force mount', () => {
142+
it('renders the tab panel when shouldForceMount is passed', () => {
143+
const { view } = renderView();
144+
145+
view.getByText('tab 1 content'); // default tab
146+
view.getByText('tab 3 content'); // force mounted tab
147+
expect(view.getByText('tab 3 content').parentElement).toHaveStyle(
148+
'display: none'
149+
);
150+
expect(view.queryByText('tab 2 content')).toBeNull(); // not force mounted tab
151+
});
152+
});
134153
});

packages/styleguide/src/lib/Molecules/Tabs/Tabs.mdx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,14 @@ You can disable a tab by passing the `isDisabled` prop to a specific `Tab` compo
7979

8080
<Canvas of={TabStories.Disabled} />
8181

82+
### Force mount
83+
84+
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.
85+
86+
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.
87+
88+
<Canvas of={TabStories.ForceMount} />
89+
8290
## Playground
8391

8492
<Canvas sourceState="shown" of={TabStories.Default} />

packages/styleguide/src/lib/Molecules/Tabs/Tabs.stories.tsx

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ export const Default: Story = {
7474
<Text as="h2">Welcome to Tab 1</Text>
7575
<Text>Hi there! I&apos;m the contents inside Tab 1. Yippee!</Text>
7676
</TabPanel>
77-
<TabPanel id="2">
77+
<TabPanel id="2" shouldForceMount>
7878
<Text as="h2">Welcome to Tab 2</Text>
7979
<Text>Hi there! I&apos;m the contents inside Tab 2. Yippee!</Text>
8080
</TabPanel>
@@ -263,3 +263,29 @@ export const Disabled: Story = {
263263
</Tabs>
264264
),
265265
};
266+
267+
export const ForceMount: Story = {
268+
render: (args) => (
269+
<Tabs {...args}>
270+
<TabList>
271+
<Tab id="1">Tab 1</Tab>
272+
<Tab id="2">Tab 2</Tab>
273+
<Tab id="3">Tab 3</Tab>
274+
</TabList>
275+
<TabPanels my={24}>
276+
<TabPanel id="1" shouldForceMount>
277+
<Text as="h2">Welcome to Tab 1</Text>
278+
<Text>Hi there! I&apos;m the contents inside Tab 1. Yippee!</Text>
279+
</TabPanel>
280+
<TabPanel id="2" shouldForceMount>
281+
<Text as="h2">Welcome to Tab 2</Text>
282+
<Text>Hi there! I&apos;m the contents inside Tab 2. Yippee!</Text>
283+
</TabPanel>
284+
<TabPanel id="3" shouldForceMount>
285+
<Text as="h2">Welcome to Tab 3</Text>
286+
<Text>Hi there! I&apos;m the contents inside Tab 3. Yippee!</Text>
287+
</TabPanel>
288+
</TabPanels>
289+
</Tabs>
290+
),
291+
};

0 commit comments

Comments
 (0)