mirror of
https://github.com/nextui-org/nextui.git
synced 2025-12-08 19:26:11 +00:00
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:
parent
5f735a9892
commit
e34c5e307d
6
.changeset/mean-parrots-cheat.md
Normal file
6
.changeset/mean-parrots-cheat.md
Normal file
@ -0,0 +1,6 @@
|
||||
---
|
||||
"@nextui-org/tabs": patch
|
||||
"@nextui-org/theme": patch
|
||||
---
|
||||
|
||||
Add `destroyInactiveTabPanel` prop for Tabs component (#1562)
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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"
|
||||
>
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -68,6 +68,7 @@ const tabs = tv({
|
||||
"py-3",
|
||||
"px-1",
|
||||
"outline-none",
|
||||
"data-[inert=true]:hidden",
|
||||
// focus ring
|
||||
...dataFocusVisibleClasses,
|
||||
],
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user