fix(tabs): unresponsive modal after switching tabs inside (#5582)

* fix(tabs): unresponsive modal after switching tabs inside

* chore(deps): remove self
This commit is contained in:
WK 2025-08-30 22:53:45 +08:00 committed by GitHub
parent 97a1c4a4f1
commit 8eb269df9e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 121 additions and 3 deletions

View File

@ -0,0 +1,5 @@
---
"@heroui/tabs": patch
---
fix unresponsive modal after switching tabs inside (#5543)

View File

@ -2,10 +2,12 @@ import type {UserEvent} from "@testing-library/user-event";
import type {TabsProps} from "../src";
import * as React from "react";
import {act, render, fireEvent, within} from "@testing-library/react";
import {act, render, fireEvent, within, waitFor} from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import {focus} from "@heroui/test-utils";
import {spy, shouldIgnoreReactWarning} from "@heroui/test-utils";
import {Modal, ModalContent, ModalHeader, ModalBody, ModalFooter} from "@heroui/modal";
import {Button} from "@heroui/button";
import {Tabs, Tab} from "../src";
@ -435,4 +437,106 @@ describe("Tabs", () => {
expect(item2Click).toHaveBeenCalledTimes(2);
expect(tab2).toHaveAttribute("aria-selected", "true");
});
it("should allow reopening modal with tabs without blocking", async () => {
const TestComponent = () => {
const [isOpen, setIsOpen] = React.useState(false);
return (
<>
<Button data-testid="open-modal-btn" onPress={() => setIsOpen(true)}>
Open Modal
</Button>
<Modal data-testid="test-modal" isOpen={isOpen} onOpenChange={setIsOpen}>
<ModalContent>
{(onClose) => (
<>
<ModalHeader>Test Modal with Tabs</ModalHeader>
<ModalBody>
<Tabs aria-label="Test tabs" data-testid="modal-tabs">
<Tab key="tab1" data-testid="tab-1" title="Tab 1">
<div data-testid="tab1-content">Content for Tab 1</div>
</Tab>
<Tab key="tab2" data-testid="tab-2" title="Tab 2">
<div data-testid="tab2-content">Content for Tab 2</div>
</Tab>
<Tab key="tab3" data-testid="tab-3" title="Tab 3">
<div data-testid="tab3-content">Content for Tab 3</div>
</Tab>
</Tabs>
</ModalBody>
<ModalFooter>
<Button data-testid="close-modal-btn" onPress={onClose}>
Close
</Button>
</ModalFooter>
</>
)}
</ModalContent>
</Modal>
</>
);
};
const {getByTestId, getByRole, queryByRole} = render(<TestComponent />);
const openButton = getByTestId("open-modal-btn");
await act(async () => {
fireEvent.click(openButton);
});
await waitFor(() => {
const modal = getByRole("dialog");
expect(modal).toBeInTheDocument();
});
const tabButtons = getByRole("dialog").querySelectorAll('[role="tab"]');
expect(tabButtons).toHaveLength(3);
await act(async () => {
fireEvent.click(tabButtons[1]);
});
await waitFor(() => {
expect(tabButtons[1]).toHaveAttribute("aria-selected", "true");
});
const closeButton = getByTestId("close-modal-btn");
await act(async () => {
fireEvent.click(closeButton);
});
await waitFor(
() => {
expect(queryByRole("dialog")).not.toBeInTheDocument();
},
{timeout: 1000},
);
await act(async () => {
fireEvent.click(openButton);
});
await waitFor(() => {
const modal = getByRole("dialog");
expect(modal).toBeInTheDocument();
});
const newTabButtons = getByRole("dialog").querySelectorAll('[role="tab"]');
expect(newTabButtons).toHaveLength(3);
await act(async () => {
fireEvent.click(newTabButtons[2]);
});
await waitFor(() => {
expect(newTabButtons[2]).toHaveAttribute("aria-selected", "true");
});
});
});

View File

@ -61,6 +61,7 @@
"@heroui/input": "workspace:*",
"@heroui/test-utils": "workspace:*",
"@heroui/button": "workspace:*",
"@heroui/modal": "workspace:*",
"@heroui/shared-icons": "workspace:*",
"clean-package": "2.2.0",
"react": "18.3.0",

View File

@ -76,6 +76,8 @@ const Tab = forwardRef<"button", TabItemProps>((props, ref) => {
rerender: true,
});
const isInModal = domRef?.current?.closest('[aria-modal="true"]') !== null;
const handleClick = () => {
if (!domRef?.current || !listRef?.current) return;
@ -120,7 +122,7 @@ const Tab = forwardRef<"button", TabItemProps>((props, ref) => {
title={otherProps?.titleValue}
type={Component === "button" ? "button" : undefined}
>
{isSelected && !disableAnimation && !disableCursorAnimation && isMounted ? (
{isSelected && !disableAnimation && !disableCursorAnimation && isMounted && !isInModal ? (
// use synchronous loading for domMax here
// since lazy loading produces different behaviour
<LazyMotion features={domMax}>

View File

@ -21,6 +21,7 @@ const Tabs = forwardRef(function Tabs<T extends object>(
Component,
values,
state,
domRef,
destroyInactiveTabPanel,
getBaseProps,
getTabListProps,
@ -32,7 +33,9 @@ const Tabs = forwardRef(function Tabs<T extends object>(
const layoutId = useId();
const layoutGroupEnabled = !props.disableAnimation && !props.disableCursorAnimation;
const isInModal = domRef?.current?.closest('[aria-modal="true"]') !== null;
const layoutGroupEnabled = !props.disableAnimation && !props.disableCursorAnimation && !isInModal;
const tabsProps = {
state,

3
pnpm-lock.yaml generated
View File

@ -2811,6 +2811,9 @@ importers:
'@heroui/input':
specifier: workspace:*
version: link:../input
'@heroui/modal':
specifier: workspace:*
version: link:../modal
'@heroui/shared-icons':
specifier: workspace:*
version: link:../../utilities/shared-icons