diff --git a/packages/tabs/src/Tab/Tab.tsx b/packages/tabs/src/Tab/Tab.tsx
index 5409e388c5..2bac929073 100644
--- a/packages/tabs/src/Tab/Tab.tsx
+++ b/packages/tabs/src/Tab/Tab.tsx
@@ -20,14 +20,16 @@ import { TabProps } from './Tab.types';
* @param props.to Destination when name is rendered as `Link` tag.
*
*/
-function Tab({ selected, children, ...rest }: TabProps) {
+function Tab({ children, _shouldRender, ...rest }: TabProps) {
// default and name are not an HTML properties
// onClick applies to TabTitle component, not Tab component
delete rest.default, delete rest.name, delete rest.onClick, delete rest.href;
+ if (!_shouldRender) return null;
+
return (
- {selected ? children : null}
+ {children}
);
}
diff --git a/packages/tabs/src/Tab/Tab.types.ts b/packages/tabs/src/Tab/Tab.types.ts
index c5976d011e..fcdf25dbf8 100644
--- a/packages/tabs/src/Tab/Tab.types.ts
+++ b/packages/tabs/src/Tab/Tab.types.ts
@@ -42,6 +42,12 @@ export interface TabProps extends HTMLElementProps<'div'> {
*/
selected?: boolean;
+ /**
+ * Boolean that determines if tab panel should render in DOM
+ * @private
+ */
+ _shouldRender?: boolean;
+
// Done in order to support any Router system, such that TabTitle component can accept any URL destination prop.
[key: string]: any;
}
diff --git a/packages/tabs/src/TabPanel/TabPanel.styles.ts b/packages/tabs/src/TabPanel/TabPanel.styles.ts
new file mode 100644
index 0000000000..60beae1802
--- /dev/null
+++ b/packages/tabs/src/TabPanel/TabPanel.styles.ts
@@ -0,0 +1,5 @@
+import { css } from '@leafygreen-ui/emotion';
+
+export const hiddenTabPanelStyle = css`
+ display: none;
+`;
diff --git a/packages/tabs/src/TabPanel/TabPanel.tsx b/packages/tabs/src/TabPanel/TabPanel.tsx
index af43582cae..3cae8a53f0 100644
--- a/packages/tabs/src/TabPanel/TabPanel.tsx
+++ b/packages/tabs/src/TabPanel/TabPanel.tsx
@@ -1,15 +1,17 @@
import React, { useMemo } from 'react';
import { useDescendant } from '@leafygreen-ui/descendants';
+import { cx } from '@leafygreen-ui/emotion';
import {
TabPanelDescendantsContext,
useTabDescendantsContext,
} from '../context';
+import { hiddenTabPanelStyle } from './TabPanel.styles';
import { TabPanelProps } from './TabPanel.types';
-const TabPanel = ({ child, selectedIndex }: TabPanelProps) => {
+const TabPanel = ({ child, forceRender, selectedIndex }: TabPanelProps) => {
const { id, index, ref } = useDescendant(TabPanelDescendantsContext);
const { tabDescendants } = useTabDescendantsContext();
@@ -17,11 +19,17 @@ const TabPanel = ({ child, selectedIndex }: TabPanelProps) => {
return tabDescendants.find(tabDescendant => tabDescendant.index === index);
}, [tabDescendants, index]);
+ const selected = index === selectedIndex;
+
return (
{React.cloneElement(child, {
id,
- selected: !child.props.disabled && index === selectedIndex,
+ selected,
+ _shouldRender: !child.props.disabled && (forceRender || selected),
+ className: cx({
+ [hiddenTabPanelStyle]: !selected,
+ }),
['aria-labelledby']: relatedTab?.id,
})}
diff --git a/packages/tabs/src/TabPanel/TabPanel.types.ts b/packages/tabs/src/TabPanel/TabPanel.types.ts
index 1ccbaaa0ae..bfaa402028 100644
--- a/packages/tabs/src/TabPanel/TabPanel.types.ts
+++ b/packages/tabs/src/TabPanel/TabPanel.types.ts
@@ -1,4 +1,5 @@
export interface TabPanelProps {
child: React.ReactElement;
+ forceRender: boolean;
selectedIndex: number;
}
diff --git a/packages/tabs/src/Tabs.spec.tsx b/packages/tabs/src/Tabs.spec.tsx
index 90a911a09c..ad66316790 100644
--- a/packages/tabs/src/Tabs.spec.tsx
+++ b/packages/tabs/src/Tabs.spec.tsx
@@ -74,6 +74,35 @@ describe('packages/tabs', () => {
expect(getByTestId('inline-children')).toBeInTheDocument();
});
+
+ describe('forceRenderAllTabPanels', () => {
+ test('renders only selected panel when prop is false', () => {
+ const { getAllTabsInTabList, getAllTabPanelsInDOM } = renderTabs({
+ forceRenderAllTabPanels: false,
+ });
+ const tabs = getAllTabsInTabList();
+ const tabPanels = getAllTabPanelsInDOM();
+ expect(tabs.length).not.toEqual(tabPanels.length);
+ });
+
+ test('renders all tab panels in DOM but only selected panel is visible when prop is true', () => {
+ const { getAllTabsInTabList, getAllTabPanelsInDOM, getSelectedPanel } =
+ renderTabs({
+ forceRenderAllTabPanels: true,
+ });
+ const tabs = getAllTabsInTabList();
+ const tabPanels = getAllTabPanelsInDOM();
+ expect(tabs.length).toEqual(tabPanels.length);
+
+ const selectedPanel = getSelectedPanel();
+ const hiddenPanels = tabPanels.filter(
+ panel => panel.id !== selectedPanel?.id,
+ );
+
+ expect(selectedPanel).toBeVisible();
+ hiddenPanels.forEach(panel => expect(panel).not.toBeVisible());
+ });
+ });
});
describe('when controlled', () => {
diff --git a/packages/tabs/src/Tabs.stories.tsx b/packages/tabs/src/Tabs.stories.tsx
index 5ddd9ddc11..795ed246ec 100644
--- a/packages/tabs/src/Tabs.stories.tsx
+++ b/packages/tabs/src/Tabs.stories.tsx
@@ -105,6 +105,7 @@ export const LiveExample: StoryFn = ({
max-width: 66vw;
`}
aria-label="Tabs to demonstrate usage of Leafygreen UI Tab Components"
+ forceRenderAllTabPanels={true}
{...props}
/>
diff --git a/packages/tabs/src/Tabs/Tabs.tsx b/packages/tabs/src/Tabs/Tabs.tsx
index 9ecdf30d44..67f5a3c1fc 100644
--- a/packages/tabs/src/Tabs/Tabs.tsx
+++ b/packages/tabs/src/Tabs/Tabs.tsx
@@ -60,6 +60,7 @@ const Tabs = (props: AccessibleTabsProps) => {
inlineChildren,
selected: controlledSelected,
setSelected: setControlledSelected,
+ forceRenderAllTabPanels = false,
'data-lgid': dataLgId = LGIDS_TABS.root,
'aria-labelledby': ariaLabelledby,
'aria-label': ariaLabel,
@@ -155,7 +156,13 @@ const Tabs = (props: AccessibleTabsProps) => {
return child;
}
- return ;
+ return (
+
+ );
});
return (
diff --git a/packages/tabs/src/Tabs/Tabs.types.ts b/packages/tabs/src/Tabs/Tabs.types.ts
index 507d2b8239..b8e023756b 100644
--- a/packages/tabs/src/Tabs/Tabs.types.ts
+++ b/packages/tabs/src/Tabs/Tabs.types.ts
@@ -1,57 +1,66 @@
-import { Either, HTMLElementProps, LgIdProps } from '@leafygreen-ui/lib';
+import {
+ DarkModeProps,
+ Either,
+ HTMLElementProps,
+ LgIdProps,
+} from '@leafygreen-ui/lib';
import { BaseFontSize } from '@leafygreen-ui/tokens';
-export interface TabsProps extends HTMLElementProps<'div'>, LgIdProps {
+export interface TabsProps
+ extends HTMLElementProps<'div'>,
+ LgIdProps,
+ DarkModeProps {
/**
- * Content that will appear inside of Tabs component.
- * Should be comprised of at least two `` components.
+ * Accessible label that describes the set of tabs
*/
- children: React.ReactNode;
+ ['aria-label']?: string;
/**
- * Content that will appear inline after the `` components. `inlineChildren` are wrapped in a flexbox container.
+ * References id of label external to the component that describes the set of tabs
*/
- inlineChildren?: React.ReactNode;
+ ['aria-labelledby']?: string;
/**
- * Callback to be executed when Tab is selected. Receives index of activated Tab as the first argument.
+ * HTML Element that wraps title in Tab List.
*
- * @type (index: number) => void
+ * @type HTMLElement | React.Component
*/
- setSelected?: React.Dispatch;
+ as?: React.ElementType;
/**
- * Index of the Tab that should appear active. If value passed to selected prop, component will be controlled by consumer.
+ * The base font size of the title and text rendered in children.
*/
- selected?: number;
+ baseFontSize?: BaseFontSize;
/**
- * determines if component will appear for Dark Mode
- * @default false
+ * Content that will appear inside of Tabs component.
+ * Should be comprised of at least two `` components.
*/
- darkMode?: boolean;
+ children: React.ReactNode;
/**
- * HTML Element that wraps title in Tab List.
- *
- * @type HTMLElement | React.Component
+ * When set to true, all tab panels will forcibly render in DOM.
+ * Tab panels that are not selected will be hidden with `display: none;`
+ * This will not apply for disabled tabs
*/
- as?: React.ElementType;
+ forceRenderAllTabPanels?: boolean;
/**
- * Accessible label that describes the set of tabs
+ * Content that will appear inline after the `` components. `inlineChildren` are wrapped in a flexbox container.
*/
- ['aria-label']?: string;
+ inlineChildren?: React.ReactNode;
/**
- * References id of label external to the component that describes the set of tabs
+ * Index of the Tab that should appear active. If value passed to selected prop, component will be controlled by consumer.
*/
- ['aria-labelledby']?: string;
+ selected?: number;
/**
- * The base font size of the title and text rendered in children.
+ * Callback to be executed when Tab is selected. Receives index of activated Tab as the first argument.
+ *
+ * @type (index: number) => void
*/
- baseFontSize?: BaseFontSize;
+ setSelected?: React.Dispatch;
}
type AriaLabels = 'aria-label' | 'aria-labelledby';
diff --git a/packages/tabs/src/utils/getTestUtils/getTestUtils.spec.tsx b/packages/tabs/src/utils/getTestUtils/getTestUtils.spec.tsx
index c9f50d3a25..ce43ec3251 100644
--- a/packages/tabs/src/utils/getTestUtils/getTestUtils.spec.tsx
+++ b/packages/tabs/src/utils/getTestUtils/getTestUtils.spec.tsx
@@ -120,6 +120,16 @@ describe('packages/tabs/getTestUtils', () => {
});
});
+ describe('getAllTabPanelsInDOM', () => {
+ test('returns all tab panels in DOM', () => {
+ renderTabs();
+ const { getAllTabPanelsInDOM } = getTestUtils();
+ const allTabPanels = getAllTabPanelsInDOM();
+
+ expect(allTabPanels).toHaveLength(1);
+ });
+ });
+
describe('getSelectedPanel', () => {
test('is in the document', () => {
renderTabs();
diff --git a/packages/tabs/src/utils/getTestUtils/getTestUtils.ts b/packages/tabs/src/utils/getTestUtils/getTestUtils.ts
index d9d964a674..80543f951b 100644
--- a/packages/tabs/src/utils/getTestUtils/getTestUtils.ts
+++ b/packages/tabs/src/utils/getTestUtils/getTestUtils.ts
@@ -15,7 +15,6 @@ export const getTestUtils = (
/**
* Queries the `element` for the tab list element. Will throw if no element is found.
- *
* Then, finds and returns all elements with role=tab. Will throw if no tabs are found.
*/
const getAllTabsInTabList = (): Array => {
@@ -56,18 +55,50 @@ export const getTestUtils = (
};
/**
- * Queries the `element` for the selected panel. Returns null if selected panel is not found.
+ * Queries the `element` for the tab panels element. Will throw if no element is found.
+ * Then, finds and returns all elements with role=tabpanel. Will throw if no tab panels are found.
*/
- const getSelectedPanel = () => {
- return queryBySelector(
+ const getAllTabPanelsInDOM = () => {
+ const tabPanels = queryBySelector(
element,
- '[role="tabpanel"]:not(:empty)',
+ `[data-lgid=${LGIDS_TABS.tabPanels}]`,
+ );
+
+ if (!tabPanels) {
+ throw new Error('Unable to find tab panels container');
+ }
+
+ const allTabPanels =
+ tabPanels.querySelectorAll('[role="tabpanel"]');
+
+ if (allTabPanels.length === 0) {
+ throw new Error(
+ 'Unable to find any tabpanel elements in tab panels container',
+ );
+ }
+
+ return Array.from(allTabPanels);
+ };
+
+ /**
+ * Gets all tab panels and filters for the displayed tab panel. Returns null if selected panel is not found.
+ */
+ const getSelectedPanel = () => {
+ const allTabPanels = getAllTabPanelsInDOM();
+
+ const visibleTabPanel = allTabPanels.find(
+ tabPanel => getComputedStyle(tabPanel).display !== 'none',
);
+
+ if (!visibleTabPanel) return null;
+
+ return visibleTabPanel;
};
return {
getAllTabsInTabList: () => getAllTabsInTabList(),
getTabUtilsByName: (name: string) => getTabUtilsByName(name),
+ getAllTabPanelsInDOM: () => getAllTabPanelsInDOM(),
getSelectedPanel: () => getSelectedPanel(),
};
};
diff --git a/packages/tabs/src/utils/getTestUtils/getTestUtils.types.ts b/packages/tabs/src/utils/getTestUtils/getTestUtils.types.ts
index 9ac451deaa..522be24fc3 100644
--- a/packages/tabs/src/utils/getTestUtils/getTestUtils.types.ts
+++ b/packages/tabs/src/utils/getTestUtils/getTestUtils.types.ts
@@ -26,6 +26,11 @@ export interface TestUtilsReturnType {
*/
getTabUtilsByName: (name: string) => TabUtils | null;
+ /**
+ * Returns an array of tab panels in the tab panels container.
+ */
+ getAllTabPanelsInDOM: () => Array;
+
/**
* Returns selected tab panel or null if a selected tab panel is not found
*/