Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(tabs): add destroyInactiveTabPanel prop for Tabs component #2973

Merged
merged 11 commits into from
May 13, 2024
Merged
6 changes: 6 additions & 0 deletions .changeset/mean-parrots-cheat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@nextui-org/tabs": patch
"@nextui-org/theme": patch
---

Add `destroyInactiveTabPanel` prop for Tabs component (#1562)
25 changes: 13 additions & 12 deletions apps/docs/content/docs/components/tabs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -274,18 +274,19 @@ You can customize the `Tabs` component by passing custom Tailwind CSS classes to

### Tab Props

| Attribute | Type | Description | Default |
| --------------------- | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| children\* | `ReactNode` | The content of the tab. | - |
| title | `ReactNode` | The title of the tab. | - |
| titleValue | `string` | A string representation of the item's contents. Use this when the `title` is not readable. | - |
| href | `string` | A URL to link to. See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#href). | - |
| target | `HTMLAttributeAnchorTarget` | The target window for the link. See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#target). | - |
| rel | `string` | The relationship between the linked resource and the current page. See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel). | - |
| download | `boolean` \| `string` | Causes the browser to download the linked URL. A string may be provided to suggest a file name. See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#download). | - |
| ping | `string` | A space-separated list of URLs to ping when the link is followed. See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#ping). | - |
| referrerPolicy | `HTMLAttributeReferrerPolicy` | How much of the referrer to send when following the link. See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#referrerpolicy). | - |
| shouldSelectOnPressUp | `boolean` | Whether the tab selection should occur on press up instead of press down. | - |
| Attribute | Type | Description | Default |
|-------------------------|-------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------|
| children\* | `ReactNode` | The content of the tab. | - |
| title | `ReactNode` | The title of the tab. | - |
| titleValue | `string` | A string representation of the item's contents. Use this when the `title` is not readable. | - |
| href | `string` | A URL to link to. See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#href). | - |
| target | `HTMLAttributeAnchorTarget` | The target window for the link. See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#target). | - |
| rel | `string` | The relationship between the linked resource and the current page. See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel). | - |
| download | `boolean` \| `string` | Causes the browser to download the linked URL. A string may be provided to suggest a file name. See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#download). | - |
| ping | `string` | A space-separated list of URLs to ping when the link is followed. See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#ping). | - |
| referrerPolicy | `HTMLAttributeReferrerPolicy` | How much of the referrer to send when following the link. See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#referrerpolicy). | - |
| shouldSelectOnPressUp | `boolean` | Whether the tab selection should occur on press up instead of press down. | - |
| destroyInactiveTabPanel | `boolean` | Whether to destroy inactive tab panel when switching tabs. Inactive tab panels are inert and cannot be interacted with. | `true` |

#### Motion Props

Expand Down
36 changes: 36 additions & 0 deletions packages/components/tabs/__tests__/tabs.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -318,4 +318,40 @@ describe("Tabs", () => {
expect(tabWrapper).toHaveAttribute("data-placement", "top");
expect(tabWrapper).toHaveAttribute("data-vertical", "horizontal");
});

test("should destory inactive tab panels", () => {
const {container} = render(
<Tabs aria-label="Tabs test (destroyInactiveTabPanel=true)">
<Tab key="item1" title="Item 1">
<div>Content 1</div>
</Tab>
<Tab key="item2" title="Item 2">
<div>Content 2</div>
</Tab>
<Tab key="item3" title="Item 3">
<div>Content 3</div>
</Tab>
</Tabs>,
);

expect(container.querySelectorAll("[data-slot='panel']")).toHaveLength(1);
});

test("should destory inactive tab panels", () => {
const {container} = render(
<Tabs aria-label="Tabs test (destroyInactiveTabPanel=false)" destroyInactiveTabPanel={false}>
<Tab key="item1" title="Item 1">
<div>Content 1</div>
</Tab>
<Tab key="item2" title="Item 2">
<div>Content 2</div>
</Tab>
<Tab key="item3" title="Item 3">
<div>Content 3</div>
</Tab>
</Tabs>,
);

expect(container.querySelectorAll("[data-slot='panel']")).toHaveLength(3);
});
});
23 changes: 20 additions & 3 deletions packages/components/tabs/src/tab-panel.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type {AriaTabPanelProps} from "@react-aria/tabs";

import {Key} from "@react-types/shared";
import {forwardRef, HTMLNextUIProps} from "@nextui-org/system";
import {useDOMRef} from "@nextui-org/react-utils";
import {clsx} from "@nextui-org/shared-utils";
Expand All @@ -10,6 +11,15 @@ import {useFocusRing} from "@react-aria/focus";
import {ValuesType} from "./use-tabs";

interface Props extends HTMLNextUIProps<"div"> {
/**
* Whether to destroy inactive tab panel when switching tabs.
* Inactive tab panels are inert and cannot be interacted with.
*/
destroyInactiveTabPanel: boolean;
/**
* The current tab key.
*/
tabKey: Key;
/**
* The tab list state.
*/
Expand All @@ -30,12 +40,15 @@ export type TabPanelProps = Props & AriaTabPanelProps;
* @internal
*/
const TabPanel = forwardRef<"div", TabPanelProps>((props, ref) => {
const {as, state, className, slots, classNames, ...otherProps} = props;
const {as, tabKey, destroyInactiveTabPanel, state, className, slots, classNames, ...otherProps} =
props;

const Component = as || "div";

const domRef = useDOMRef(ref);

const {tabPanelProps} = useTabPanel(props, state, domRef);

const {focusProps, isFocused, isFocusVisible} = useFocusRing();

const selectedItem = state.selectedItem;
Expand All @@ -44,7 +57,9 @@ const TabPanel = forwardRef<"div", TabPanelProps>((props, ref) => {

const tabPanelStyles = clsx(classNames?.panel, className, selectedItem?.props?.className);

if (!content) {
const isSelected = tabKey === selectedItem?.key;

if (!content || (!isSelected && destroyInactiveTabPanel)) {
return null;
}

Expand All @@ -53,7 +68,9 @@ const TabPanel = forwardRef<"div", TabPanelProps>((props, ref) => {
ref={domRef}
data-focus={isFocused}
data-focus-visible={isFocusVisible}
{...mergeProps(tabPanelProps, focusProps, otherProps)}
data-inert={!isSelected ? "true" : undefined}
inert={!isSelected ? "true" : undefined}
{...(isSelected && mergeProps(tabPanelProps, focusProps, otherProps))}
className={slots.panel?.({class: tabPanelStyles})}
data-slot="panel"
>
Expand Down
28 changes: 21 additions & 7 deletions packages/components/tabs/src/tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,15 @@ import TabPanel from "./tab-panel";
interface Props<T> extends UseTabsProps<T> {}

function Tabs<T extends object>(props: Props<T>, ref: ForwardedRef<HTMLDivElement>) {
const {Component, values, state, getBaseProps, getTabListProps, getWrapperProps} = useTabs<T>({
const {
Component,
values,
state,
destroyInactiveTabPanel,
getBaseProps,
getTabListProps,
getWrapperProps,
} = useTabs<T>({
...props,
ref,
});
Expand Down Expand Up @@ -41,12 +49,18 @@ function Tabs<T extends object>(props: Props<T>, ref: ForwardedRef<HTMLDivElemen
{layoutGroupEnabled ? <LayoutGroup id={layoutId}>{tabs}</LayoutGroup> : tabs}
</Component>
</div>
<TabPanel
key={state.selectedItem?.key}
classNames={values.classNames}
slots={values.slots}
state={values.state}
/>
{[...state.collection].map((item) => {
return (
<TabPanel
key={item.key}
classNames={values.classNames}
destroyInactiveTabPanel={destroyInactiveTabPanel}
slots={values.slots}
state={values.state}
tabKey={item.key}
/>
);
})}
</>
);

Expand Down
7 changes: 7 additions & 0 deletions packages/components/tabs/src/use-tabs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ export interface Props extends Omit<HTMLNextUIProps, "children"> {
* @default false
*/
isVertical?: boolean;
/**
* Whether to destroy inactive tab panel when switching tabs. Inactive tab panels are inert and cannot be interacted with.
* @default true
*/
destroyInactiveTabPanel?: boolean;
}

export type UseTabsProps<T> = Props &
Expand Down Expand Up @@ -90,6 +95,7 @@ export function useTabs<T extends object>(originalProps: UseTabsProps<T>) {
motionProps,
isVertical = false,
shouldSelectOnPressUp = true,
destroyInactiveTabPanel = true,
...otherProps
} = props;

Expand Down Expand Up @@ -182,6 +188,7 @@ export function useTabs<T extends object>(originalProps: UseTabsProps<T>) {
domRef,
state,
values,
destroyInactiveTabPanel,
getBaseProps,
getTabListProps,
getWrapperProps,
Expand Down
1 change: 1 addition & 0 deletions packages/core/theme/src/components/tabs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ const tabs = tv({
"py-3",
"px-1",
"outline-none",
"data-[inert=true]:hidden",
// focus ring
...dataFocusVisibleClasses,
],
Expand Down
Loading