diff --git a/.changeset/mean-parrots-cheat.md b/.changeset/mean-parrots-cheat.md new file mode 100644 index 0000000000..90ff415959 --- /dev/null +++ b/.changeset/mean-parrots-cheat.md @@ -0,0 +1,6 @@ +--- +"@nextui-org/tabs": patch +"@nextui-org/theme": patch +--- + +Add `destroyInactiveTabPanel` prop for Tabs component (#1562) diff --git a/apps/docs/content/docs/components/tabs.mdx b/apps/docs/content/docs/components/tabs.mdx index 63718e7358..3e283f55e4 100644 --- a/apps/docs/content/docs/components/tabs.mdx +++ b/apps/docs/content/docs/components/tabs.mdx @@ -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 diff --git a/packages/components/tabs/__tests__/tabs.test.tsx b/packages/components/tabs/__tests__/tabs.test.tsx index a5117c5d6a..ff87798522 100644 --- a/packages/components/tabs/__tests__/tabs.test.tsx +++ b/packages/components/tabs/__tests__/tabs.test.tsx @@ -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( + + +
Content 1
+
+ +
Content 2
+
+ +
Content 3
+
+
, + ); + + expect(container.querySelectorAll("[data-slot='panel']")).toHaveLength(1); + }); + + test("should destory inactive tab panels", () => { + const {container} = render( + + +
Content 1
+
+ +
Content 2
+
+ +
Content 3
+
+
, + ); + + expect(container.querySelectorAll("[data-slot='panel']")).toHaveLength(3); + }); }); diff --git a/packages/components/tabs/src/tab-panel.tsx b/packages/components/tabs/src/tab-panel.tsx index 58968cf97f..fe51d65efb 100644 --- a/packages/components/tabs/src/tab-panel.tsx +++ b/packages/components/tabs/src/tab-panel.tsx @@ -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"; @@ -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. */ @@ -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; @@ -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; } @@ -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" > diff --git a/packages/components/tabs/src/tabs.tsx b/packages/components/tabs/src/tabs.tsx index a4c618ac82..e22d48ef28 100644 --- a/packages/components/tabs/src/tabs.tsx +++ b/packages/components/tabs/src/tabs.tsx @@ -9,7 +9,15 @@ import TabPanel from "./tab-panel"; interface Props extends UseTabsProps {} function Tabs(props: Props, ref: ForwardedRef) { - const {Component, values, state, getBaseProps, getTabListProps, getWrapperProps} = useTabs({ + const { + Component, + values, + state, + destroyInactiveTabPanel, + getBaseProps, + getTabListProps, + getWrapperProps, + } = useTabs({ ...props, ref, }); @@ -41,12 +49,18 @@ function Tabs(props: Props, ref: ForwardedRef{tabs} : tabs} - + {[...state.collection].map((item) => { + return ( + + ); + })} ); diff --git a/packages/components/tabs/src/use-tabs.ts b/packages/components/tabs/src/use-tabs.ts index aaea6f4c12..d3a39e5849 100644 --- a/packages/components/tabs/src/use-tabs.ts +++ b/packages/components/tabs/src/use-tabs.ts @@ -57,6 +57,11 @@ export interface Props extends Omit { * @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 = Props & @@ -90,6 +95,7 @@ export function useTabs(originalProps: UseTabsProps) { motionProps, isVertical = false, shouldSelectOnPressUp = true, + destroyInactiveTabPanel = true, ...otherProps } = props; @@ -182,6 +188,7 @@ export function useTabs(originalProps: UseTabsProps) { domRef, state, values, + destroyInactiveTabPanel, getBaseProps, getTabListProps, getWrapperProps, diff --git a/packages/core/theme/src/components/tabs.ts b/packages/core/theme/src/components/tabs.ts index 3e126deb7f..22806989ac 100644 --- a/packages/core/theme/src/components/tabs.ts +++ b/packages/core/theme/src/components/tabs.ts @@ -68,6 +68,7 @@ const tabs = tv({ "py-3", "px-1", "outline-none", + "data-[inert=true]:hidden", // focus ring ...dataFocusVisibleClasses, ],