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

* 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
This commit is contained in:
աӄա 2024-05-13 10:00:28 +08:00 committed by GitHub
parent 5f735a9892
commit e34c5e307d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 104 additions and 22 deletions

View File

@ -0,0 +1,6 @@
---
"@nextui-org/tabs": patch
"@nextui-org/theme": patch
---
Add `destroyInactiveTabPanel` prop for Tabs component (#1562)

View File

@ -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

View File

@ -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);
});
});

View File

@ -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"
>

View File

@ -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,
});
@ -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}
/>
);
})}
</>
);

View File

@ -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 &
@ -90,6 +95,7 @@ export function useTabs<T extends object>(originalProps: UseTabsProps<T>) {
motionProps,
isVertical = false,
shouldSelectOnPressUp = true,
destroyInactiveTabPanel = true,
...otherProps
} = props;
@ -182,6 +188,7 @@ export function useTabs<T extends object>(originalProps: UseTabsProps<T>) {
domRef,
state,
values,
destroyInactiveTabPanel,
getBaseProps,
getTabListProps,
getWrapperProps,

View File

@ -68,6 +68,7 @@ const tabs = tv({
"py-3",
"px-1",
"outline-none",
"data-[inert=true]:hidden",
// focus ring
...dataFocusVisibleClasses,
],