Skip to content

Commit

Permalink
LG-4147: forceRenderAllTabPanels prop with test util updates
Browse files Browse the repository at this point in the history
  • Loading branch information
stephl3 committed May 30, 2024
1 parent 9053f74 commit 8129aa6
Show file tree
Hide file tree
Showing 16 changed files with 231 additions and 85 deletions.
31 changes: 18 additions & 13 deletions packages/tabs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,10 @@ test('tabs', () => {
</Tabs>
);

const { getAllTabsInTabList, getTabUtilsByName, getSelectedPanel } = getTestUtils();
const { getAllTabPanelsInDOM, getAllTabsInTabList, getSelectedPanel, getTabUtilsByName } = getTestUtils();

expect(getAllTabsInTabList()).toHaveLength(3);
expect(getAllTabPanelsInDOM()).toHaveLength(1);

const firstTabUtils = getTabUtilsByName('First');
expect(firstTabUtils.isSelected()).toBeTruthy();
Expand Down Expand Up @@ -144,7 +145,7 @@ test('tabs', () => {
Content C
</Tab>
</Tabs>
<Tabs aria-label="Label XY" data-lgid="tabs-xy">
<Tabs aria-label="Label XY" data-lgid="tabs-xy" forceRenderAllTabPanels={true}>
<Tab name="X">
Content X
</Tab>
Expand All @@ -160,33 +161,37 @@ test('tabs', () => {

// First tabs
expect(testUtils1.getAllTabsInTabList()).toHaveLength(3);
expect(testUtils1.getAllTabPanelsInDOM()).toHaveLength(1);
expect(testUtils1.getSelectedPanel()).toHaveTextContent('Content A');

// Second tabs
expect(testUtils1.getAllTabsInTabList()).toHaveLength(2);
expect(testUtils1.getSelectedPanel()).toHaveTextContent('Content Y');
expect(testUtils2.getAllTabsInTabList()).toHaveLength(2);
expect(testUtils2.getAllTabPanelsInDOM()).toHaveLength(2);
expect(testUtils2.getSelectedPanel()).toHaveTextContent('Content Y');
});
```

### Test Utils

```tsx
const {
getAllTabPanelsInDOM,
getAllTabsInTabList,
getTabUtilsByName: { getTab, isSelected, isDisabled },
getSelectedPanel,
} = getTestUtils();
```

| Util | Description | Returns |
| ----------------------- | ---------------------------------------------------- | ----------------------- |
| `getAllTabsInTabList()` | Returns an array of tabs | `Array<HTMLElement>` |
| `getSelectedPanel()` | Returns the selected tab panel | `HTMLElement` \| `null` |
| `getTabUtilsByName()` | Returns tab utils if tab with matching name is found | `TabUtils` \| `null` |
| TabUtils | | |
| `getTab()` | Returns the tab | `HTMLElement` |
| `isSelected()` | Returns whether the tab is selected | `boolean` |
| `isDisabled()` | Returns whether the tab is disabled | `boolean` |
| Util | Description | Returns |
| ------------------------ | ---------------------------------------------------- | ----------------------- |
| `getAllTabPanelsInDOM()` | Returns an array of tab panels | `Array<HTMLElement>` |
| `getAllTabsInTabList()` | Returns an array of tabs | `Array<HTMLElement>` |
| `getSelectedPanel()` | Returns the selected tab panel | `HTMLElement` \| `null` |
| `getTabUtilsByName()` | Returns tab utils if tab with matching name is found | `TabUtils` \| `null` |
| TabUtils | | |
| `getTab()` | Returns the tab | `HTMLElement` |
| `isSelected()` | Returns whether the tab is selected | `boolean` |
| `isDisabled()` | Returns whether the tab is disabled | `boolean` |

## Reference

Expand Down
12 changes: 10 additions & 2 deletions packages/tabs/src/Tab/Tab.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import React from 'react';
import PropTypes from 'prop-types';

import { useTabsContext } from '../context';

import { TabProps } from './Tab.types';

/**
Expand All @@ -20,14 +22,20 @@ 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, disabled, selected, ...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;

const { forceRenderAllTabPanels } = useTabsContext();

const shouldRender = !disabled && (forceRenderAllTabPanels || selected);

if (!shouldRender) return null;

return (
<div {...rest} role="tabpanel">
{selected ? children : null}
{children}
</div>
);
}
Expand Down
5 changes: 5 additions & 0 deletions packages/tabs/src/TabPanel/TabPanel.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { css } from '@leafygreen-ui/emotion';

export const hiddenTabPanelStyle = css`
display: none;
`;
13 changes: 11 additions & 2 deletions packages/tabs/src/TabPanel/TabPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,36 @@
import React, { useMemo } from 'react';

import { useDescendant } from '@leafygreen-ui/descendants';
import { cx } from '@leafygreen-ui/emotion';

import {
TabPanelDescendantsContext,
useTabDescendantsContext,
useTabsContext,
} from '../context';

import { hiddenTabPanelStyle } from './TabPanel.styles';
import { TabPanelProps } from './TabPanel.types';

const TabPanel = ({ child, selectedIndex }: TabPanelProps) => {
const TabPanel = ({ child }: TabPanelProps) => {
const { id, index, ref } = useDescendant(TabPanelDescendantsContext);
const { tabDescendants } = useTabDescendantsContext();
const { selectedIndex } = useTabsContext();

const relatedTab = useMemo(() => {
return tabDescendants.find(tabDescendant => tabDescendant.index === index);
}, [tabDescendants, index]);

const selected = index === selectedIndex;

return (
<div ref={ref}>
{React.cloneElement(child, {
id,
selected: !child.props.disabled && index === selectedIndex,
selected: index === selectedIndex,
className: cx({
[hiddenTabPanelStyle]: !selected,
}),
['aria-labelledby']: relatedTab?.id,
})}
</div>
Expand Down
1 change: 0 additions & 1 deletion packages/tabs/src/TabPanel/TabPanel.types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export interface TabPanelProps {
child: React.ReactElement;
selectedIndex: number;
}
13 changes: 3 additions & 10 deletions packages/tabs/src/TabTitle/TabTitle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { useUpdatedBaseFontSize } from '@leafygreen-ui/typography';
import {
TabDescendantsContext,
useTabPanelDescendantsContext,
useTabsContext,
} from '../context';

import {
Expand All @@ -25,22 +26,14 @@ import { BaseTabTitleProps } from './TabTitle.types';

const TabTitle = InferredPolymorphic<BaseTabTitleProps, 'button'>(
(
{
as,
children,
className,
darkMode,
disabled = false,
onClick,
selectedIndex,
...rest
},
{ as, children, className, darkMode, disabled = false, onClick, ...rest },
fwdRef,
) => {
const baseFontSize: BaseFontSize = useUpdatedBaseFontSize();
const { Component } = useInferredPolymorphic(as, rest, 'button');
const { index, ref, id } = useDescendant(TabDescendantsContext);
const { tabPanelDescendants } = useTabPanelDescendantsContext();
const { selectedIndex } = useTabsContext();

const theme = darkMode ? Theme.Dark : Theme.Light;
const selected = index === selectedIndex;
Expand Down
1 change: 0 additions & 1 deletion packages/tabs/src/TabTitle/TabTitle.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,5 @@ export interface BaseTabTitleProps {
className?: string;
disabled?: boolean;
isAnyTabFocused?: boolean;
selectedIndex: number;
[key: string]: any;
}
43 changes: 43 additions & 0 deletions packages/tabs/src/Tabs.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,49 @@ describe('packages/tabs', () => {

expect(getByTestId('inline-children')).toBeInTheDocument();
});

describe('forceRenderAllTabPanels', () => {
test('renders only selected panel in DOM when prop is false', () => {
const { getAllTabsInTabList, getAllTabPanelsInDOM, getSelectedPanel } =
renderTabs({
forceRenderAllTabPanels: false,
});
const tabs = getAllTabsInTabList();
const tabPanels = getAllTabPanelsInDOM();
expect(tabs).toHaveLength(3);
expect(tabPanels).toHaveLength(1);

const selectedPanel = getSelectedPanel();
const hiddenPanels = tabPanels.filter(
panel => panel.id !== selectedPanel?.id,
);

expect(selectedPanel).toBeVisible();
expect(hiddenPanels).toHaveLength(0);
});

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).toHaveLength(3);
expect(tabPanels).toHaveLength(3);

const selectedPanel = getSelectedPanel();
const hiddenPanels = tabPanels.filter(
panel => panel.id !== selectedPanel?.id,
);

expect(selectedPanel).toBeVisible();
hiddenPanels.forEach(panel => {
expect(panel).not.toBeVisible();
expect(panel).toBeInTheDocument();
});
});
});
});

describe('when controlled', () => {
Expand Down
3 changes: 2 additions & 1 deletion packages/tabs/src/Tabs.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,13 +88,14 @@ const meta: StoryMetaType<typeof Tabs> = {
</CardWithMargin>
</Tab>,
],
forceRenderAllTabPanels: false,
},
argTypes: {
as: storybookArgTypes.as,
forceRenderAllTabPanels: { control: 'boolean' },
selected: { control: 'number' },
},
// TODO: Add subcomponent controls for Tab when supported by Storybook
// @ts-expect-error
subcomponents: { tab: Tab },
};
export default meta;
Expand Down
63 changes: 37 additions & 26 deletions packages/tabs/src/Tabs/Tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ import { BaseFontSize } from '@leafygreen-ui/tokens';
import { useUpdatedBaseFontSize } from '@leafygreen-ui/typography';

import { LGIDS_TABS } from '../constants';
import { TabDescendantsContext, TabPanelDescendantsContext } from '../context';
import {
TabDescendantsContext,
TabPanelDescendantsContext,
TabsContext,
} from '../context';
import TabPanel from '../TabPanel';
import TabTitle from '../TabTitle';
import { getActiveAndEnabledIndices } from '../utils';
Expand Down Expand Up @@ -60,6 +64,7 @@ const Tabs = (props: AccessibleTabsProps) => {
inlineChildren,
selected: controlledSelected,
setSelected: setControlledSelected,
forceRenderAllTabPanels = false,
'data-lgid': dataLgId = LGIDS_TABS.root,
'aria-labelledby': ariaLabelledby,
'aria-label': ariaLabel,
Expand Down Expand Up @@ -143,7 +148,6 @@ const Tabs = (props: AccessibleTabsProps) => {
handleClickTab(event, index);
}
: undefined,
selectedIndex: selected,
...rest,
} as const;

Expand All @@ -155,7 +159,7 @@ const Tabs = (props: AccessibleTabsProps) => {
return child;
}

return <TabPanel child={child} selectedIndex={selected} />;
return <TabPanel child={child} />;
});

return (
Expand All @@ -170,33 +174,40 @@ const Tabs = (props: AccessibleTabsProps) => {
descendants={tabPanelDescendants}
dispatch={tabPanelDispatch}
>
<div {...rest} className={className} data-lgid={dataLgId}>
<div className={tabContainerStyle} id={id}>
<TabsContext.Provider
value={{
forceRenderAllTabPanels,
selectedIndex: selected,
}}
>
<div {...rest} className={className} data-lgid={dataLgId}>
<div className={tabContainerStyle} id={id}>
<div
className={cx(
getListThemeStyles(theme),
tabListElementClassName,
)}
data-lgid={LGIDS_TABS.tabList}
role="tablist"
aria-orientation="horizontal"
{...accessibilityProps}
>
{renderedTabs}
</div>
<div className={inlineChildrenContainerStyle}>
<div className={inlineChildrenWrapperStyle}>
{inlineChildren}
</div>
</div>
</div>
<div
className={cx(
getListThemeStyles(theme),
tabListElementClassName,
)}
data-lgid={LGIDS_TABS.tabList}
role="tablist"
aria-orientation="horizontal"
{...accessibilityProps}
className={tabPanelsElementClassName}
data-lgid={LGIDS_TABS.tabPanels}
>
{renderedTabs}
{renderedTabPanels}
</div>
<div className={inlineChildrenContainerStyle}>
<div className={inlineChildrenWrapperStyle}>
{inlineChildren}
</div>
</div>
</div>
<div
className={tabPanelsElementClassName}
data-lgid={LGIDS_TABS.tabPanels}
>
{renderedTabPanels}
</div>
</div>
</TabsContext.Provider>
</DescendantsProvider>
</DescendantsProvider>
</LeafyGreenProvider>
Expand Down
Loading

0 comments on commit 8129aa6

Please sign in to comment.