diff --git a/.changeset/nine-moles-sort.md b/.changeset/nine-moles-sort.md new file mode 100644 index 0000000000..3e3d65dc45 --- /dev/null +++ b/.changeset/nine-moles-sort.md @@ -0,0 +1,5 @@ +--- +"@heroui/tabs": patch +--- + +fix unresponsive modal after switching tabs inside (#5543) diff --git a/packages/components/tabs/__tests__/tabs.test.tsx b/packages/components/tabs/__tests__/tabs.test.tsx index 9734e5c1f7..89fd7ec857 100644 --- a/packages/components/tabs/__tests__/tabs.test.tsx +++ b/packages/components/tabs/__tests__/tabs.test.tsx @@ -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 ( + <> + + + + {(onClose) => ( + <> + Test Modal with Tabs + + + +
Content for Tab 1
+
+ +
Content for Tab 2
+
+ +
Content for Tab 3
+
+
+
+ + + + + )} +
+
+ + ); + }; + + const {getByTestId, getByRole, queryByRole} = render(); + + 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"); + }); + }); }); diff --git a/packages/components/tabs/package.json b/packages/components/tabs/package.json index 24fb54adca..8a28d438ad 100644 --- a/packages/components/tabs/package.json +++ b/packages/components/tabs/package.json @@ -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", diff --git a/packages/components/tabs/src/tab.tsx b/packages/components/tabs/src/tab.tsx index d3f68ad4ac..d346c545d3 100644 --- a/packages/components/tabs/src/tab.tsx +++ b/packages/components/tabs/src/tab.tsx @@ -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 diff --git a/packages/components/tabs/src/tabs.tsx b/packages/components/tabs/src/tabs.tsx index 2d8c0ffb79..a1b289388b 100644 --- a/packages/components/tabs/src/tabs.tsx +++ b/packages/components/tabs/src/tabs.tsx @@ -21,6 +21,7 @@ const Tabs = forwardRef(function Tabs( Component, values, state, + domRef, destroyInactiveTabPanel, getBaseProps, getTabListProps, @@ -32,7 +33,9 @@ const Tabs = forwardRef(function Tabs( 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, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 20859ca7c0..cfcdceed94 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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