From 43bb38074dff4400efe610c438515dbde29f7682 Mon Sep 17 00:00:00 2001 From: Amy Resnik Date: Tue, 18 Nov 2025 15:11:01 -0500 Subject: [PATCH 1/5] add shouldForceMount --- packages/gamut/src/Tabs/TabPanel.tsx | 41 ++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/packages/gamut/src/Tabs/TabPanel.tsx b/packages/gamut/src/Tabs/TabPanel.tsx index 75dc40f8db1..bf311254e36 100644 --- a/packages/gamut/src/Tabs/TabPanel.tsx +++ b/packages/gamut/src/Tabs/TabPanel.tsx @@ -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, @@ -10,13 +12,34 @@ interface TabPanelBaseProps extends TabElementStyleProps, ReactAriaTabPanelProps {} -export type TabPanelProps = TabPanelBaseProps & - Omit & { - /** - * 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)(tabElementBaseProps); +const tabPanelStates = system.states({ + shouldForceMount: { + "&[data-inert='true']": { + display: 'none', + }, + }, +}); + +const StyledTabPanel = styled(ReactAriaTabPanel)( + tabElementBaseProps, + tabPanelStates +); + +export const TabPanel: React.FC = (props) => { + return ; +}; From 5beb6daf4ab8ac20e302188946296dca4a352365 Mon Sep 17 00:00:00 2001 From: Amy Resnik Date: Tue, 18 Nov 2025 15:11:06 -0500 Subject: [PATCH 2/5] update type --- packages/gamut/src/Tabs/Tab.tsx | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/gamut/src/Tabs/Tab.tsx b/packages/gamut/src/Tabs/Tab.tsx index d8e8ccc79e9..ae0baa30f67 100644 --- a/packages/gamut/src/Tabs/Tab.tsx +++ b/packages/gamut/src/Tabs/Tab.tsx @@ -11,13 +11,12 @@ import { useTab } from './TabProvider'; interface TabBaseProps extends TabButtonProps, ReactAriaTabProps {} -export type TabProps = TabBaseProps & - Omit & { - /** - * the id matches up the tab and tab panel - */ - id: string; - }; +export type TabProps = Omit & { + /** + * the id matches up the tab and tab panel + */ + id: string; +}; const TabBase = styled(ReactAriaTab)( tabVariants, From 250254ffbb2cfbf21ddfdc6bf744eb9d5ec000b7 Mon Sep 17 00:00:00 2001 From: Amy Resnik Date: Tue, 18 Nov 2025 15:15:50 -0500 Subject: [PATCH 3/5] add story --- .../src/lib/Molecules/Tabs/Tabs.mdx | 8 ++++++ .../src/lib/Molecules/Tabs/Tabs.stories.tsx | 28 ++++++++++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/packages/styleguide/src/lib/Molecules/Tabs/Tabs.mdx b/packages/styleguide/src/lib/Molecules/Tabs/Tabs.mdx index 124c9ef5f99..3a70a3d4eba 100644 --- a/packages/styleguide/src/lib/Molecules/Tabs/Tabs.mdx +++ b/packages/styleguide/src/lib/Molecules/Tabs/Tabs.mdx @@ -79,6 +79,14 @@ You can disable a tab by passing the `isDisabled` prop to a specific `Tab` compo +### 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 that also visually hides the inactive tab panel. + + + ## Playground diff --git a/packages/styleguide/src/lib/Molecules/Tabs/Tabs.stories.tsx b/packages/styleguide/src/lib/Molecules/Tabs/Tabs.stories.tsx index 4217cb2d4b0..82fb462de95 100644 --- a/packages/styleguide/src/lib/Molecules/Tabs/Tabs.stories.tsx +++ b/packages/styleguide/src/lib/Molecules/Tabs/Tabs.stories.tsx @@ -74,7 +74,7 @@ export const Default: Story = { Welcome to Tab 1 Hi there! I'm the contents inside Tab 1. Yippee! - + Welcome to Tab 2 Hi there! I'm the contents inside Tab 2. Yippee! @@ -263,3 +263,29 @@ export const Disabled: Story = { ), }; + +export const ForceMount: Story = { + render: (args) => ( + + + Tab 1 + Tab 2 + Tab 3 + + + + Welcome to Tab 1 + Hi there! I'm the contents inside Tab 1. Yippee! + + + Welcome to Tab 2 + Hi there! I'm the contents inside Tab 2. Yippee! + + + Welcome to Tab 3 + Hi there! I'm the contents inside Tab 3. Yippee! + + + + ), +}; From 4361e65892dc4b498167c104eb601a46b5c57958 Mon Sep 17 00:00:00 2001 From: Amy Resnik Date: Tue, 18 Nov 2025 15:31:59 -0500 Subject: [PATCH 4/5] add test --- .../gamut/src/Tabs/__tests__/Tabs.test.tsx | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/packages/gamut/src/Tabs/__tests__/Tabs.test.tsx b/packages/gamut/src/Tabs/__tests__/Tabs.test.tsx index fae5ea90914..9cf3164c8c3 100644 --- a/packages/gamut/src/Tabs/__tests__/Tabs.test.tsx +++ b/packages/gamut/src/Tabs/__tests__/Tabs.test.tsx @@ -20,7 +20,7 @@ const FullTabs = (props: TabsProps) => (

tab 2 content

- +

tab 3 content

@@ -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', () => { @@ -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', () => { @@ -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 + }); + }); }); From ea98e9248a33bb873a45c075ee435dc7f5b320cf Mon Sep 17 00:00:00 2001 From: Amy Resnik Date: Wed, 3 Dec 2025 12:48:28 -0500 Subject: [PATCH 5/5] add link --- packages/styleguide/src/lib/Molecules/Tabs/Tabs.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/styleguide/src/lib/Molecules/Tabs/Tabs.mdx b/packages/styleguide/src/lib/Molecules/Tabs/Tabs.mdx index 3a70a3d4eba..a57ab59b6a3 100644 --- a/packages/styleguide/src/lib/Molecules/Tabs/Tabs.mdx +++ b/packages/styleguide/src/lib/Molecules/Tabs/Tabs.mdx @@ -83,7 +83,7 @@ You can disable a tab by passing the `isDisabled` prop to a specific `Tab` compo 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 that also visually hides the inactive tab panel. +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.