Skip to content

Commit

Permalink
feat(tabs): add destroyInactiveTabPanel prop for Tabs component (#2973
Browse files Browse the repository at this point in the history
)

* feat(tabs): add destroyInactiveTabPanel and set default to false

* feat(tabs): integrate with destroyInactiveTabPanel

* feat(theme): hidden inert tab panel

* feat(changeset): add changeset

* chore(changeset): add issue number

* feat(docs): add `destroyInactiveTabPanel` prop to tabs page

* chore(docs): set destroyInactiveTabPanel to true by default

* chore(tabs): set destroyInactiveTabPanel to true by default

* chore(tabs): revise destroyInactiveTabPanel logic

* feat(tabs): add tests for destroyInactiveTabPanel

* chore(tabs): change the default value of destroyInactiveTabPanel to true
  • Loading branch information
wingkwong committed May 13, 2024
1 parent 5f735a9 commit e34c5e3
Show file tree
Hide file tree
Showing 7 changed files with 104 additions and 22 deletions.
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

0 comments on commit e34c5e3

Please sign in to comment.