/* !
* (c) Copyright 2026 Palantir Technologies Inc. All rights reserved.
*/
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { createRef, useState } from "react";
import { afterAll, beforeEach, describe, expect, it, vi } from "@blueprintjs/test-commons/vitest";
import { Classes, mergeRefs } from "../../common";
import * as Errors from "../../common/errors";
import { Button, Dialog, DialogBody, InputGroup, PopupKind, Tooltip } from "../../components";
import type { PopoverInteractionKind } from "../popover/popoverProps";
import { PopoverNext } from "./popoverNext";
describe("", () => {
describe("validation", () => {
const warnSpy = vi.spyOn(console, "warn").mockImplementation(vi.fn());
beforeEach(() => warnSpy.mockClear());
afterAll(() => warnSpy.mockRestore());
it("throws error if given no target", () => {
render();
expect(warnSpy).toHaveBeenCalledWith(Errors.POPOVER_REQUIRES_TARGET);
});
it("warns if given > 1 target elements", () => {
render(
,
);
expect(warnSpy).toHaveBeenCalledWith(Errors.POPOVER_WARN_TOO_MANY_CHILDREN);
});
it("warns if given children and renderTarget prop", () => {
render( "boom"}>pow);
expect(warnSpy).toHaveBeenCalledWith(Errors.POPOVER_WARN_DOUBLE_TARGET);
});
it("warns if given targetProps and renderTarget", () => {
render( "boom"} />);
expect(warnSpy).toHaveBeenCalledWith(Errors.POPOVER_WARN_TARGET_PROPS_WITH_RENDER_TARGET);
});
it("warns if attempting to open a popover with empty content", () => {
render(
{"target"}
,
);
expect(warnSpy).toHaveBeenCalledWith(Errors.POPOVER_WARN_EMPTY_CONTENT);
});
it("warns if backdrop enabled when rendering inline", () => {
render(
{"target"}
,
);
expect(warnSpy).toHaveBeenCalledWith(Errors.POPOVER_WARN_HAS_BACKDROP_INLINE);
});
it("warns and disables if given undefined content", async () => {
const { container } = render(
,
);
expect(container.querySelector(`.${Classes.OVERLAY}`)).not.toBeInTheDocument();
expect(warnSpy).toHaveBeenCalledWith(Errors.POPOVER_WARN_EMPTY_CONTENT);
});
it("warns and disables if given empty string content", () => {
const EMPTY_STRING = " ";
const { container } = render(
,
);
expect(container.querySelector(`.${Classes.OVERLAY}`)).not.toBeInTheDocument();
expect(warnSpy).toHaveBeenCalledWith(Errors.POPOVER_WARN_EMPTY_CONTENT);
});
describe("throws error if backdrop enabled with non-CLICK interactionKind", () => {
runErrorTest("hover");
runErrorTest("hover-target");
runErrorTest("click-target");
it("doesn't throw error for CLICK", () => {
expect(() => ).not.toThrow();
});
function runErrorTest(interactionKind: PopoverInteractionKind) {
it(interactionKind, () => {
render(
} hasBackdrop={true} interactionKind={interactionKind}>
,
);
expect(warnSpy).toHaveBeenCalledWith(Errors.POPOVER_HAS_BACKDROP_INTERACTION);
});
}
});
});
describe("rendering", () => {
it("applies className to the target wrapper element", () => {
const { container } = render(
,
);
const popoverTarget = container.querySelector(`.${Classes.POPOVER_TARGET}`);
expect(popoverTarget).toHaveClass("my-custom-class");
});
it("adds POPOVER_OPEN class to target when the popover is open", async () => {
const user = userEvent.setup();
const { container } = render(
,
);
const popoverTarget = container.querySelector(`.${Classes.POPOVER_TARGET}`);
expect(popoverTarget).toBeInTheDocument();
expect(popoverTarget).not.toHaveClass(Classes.POPOVER_OPEN);
await user.click(screen.getByRole("button", { name: "target" }));
await waitFor(() => expect(popoverTarget).toHaveClass(Classes.POPOVER_OPEN));
});
it("renders Portal when usePortal=true", async () => {
const user = userEvent.setup();
const { baseElement } = render(
,
);
await user.click(screen.getByRole("button", { name: "target" }));
await waitFor(() => expect(screen.getByText("content")).toBeInTheDocument());
expect(baseElement.querySelector(`.${Classes.PORTAL}`)).toBeInTheDocument();
});
it("renders to specified container correctly", async () => {
// setup: create a container
const container = document.createElement("div");
document.body.appendChild(container);
render(
,
);
await waitFor(() => expect(screen.getByText("content")).toBeInTheDocument());
expect(container.querySelector(`.${Classes.POPOVER_CONTENT}`)).toBeInTheDocument();
// cleanup
document.body.removeChild(container);
});
it("does not render Portal when usePortal=false", async () => {
const user = userEvent.setup();
const { container } = render(
,
);
await user.click(screen.getByRole("button", { name: "target" }));
await waitFor(() => expect(screen.getByText("content")).toBeInTheDocument());
expect(container.querySelector(`.${Classes.PORTAL}`)).not.toBeInTheDocument();
});
it("hasBackdrop=true renders backdrop element", async () => {
const { baseElement } = render(
,
);
await waitFor(() => expect(screen.getByText("content")).toBeInTheDocument());
expect(baseElement.querySelector(`.${Classes.POPOVER_BACKDROP}`)).toBeInTheDocument();
});
it("hasBackdrop=false does not render backdrop element", async () => {
const { container } = render(
,
);
await waitFor(() => expect(screen.getByText("content")).toBeInTheDocument());
expect(container.querySelector(`.${Classes.POPOVER_BACKDROP}`)).not.toBeInTheDocument();
});
it("targetTagName renders the right elements", () => {
const { container } = render(
,
);
const popoverTarget = container.querySelector(`.${Classes.POPOVER_TARGET}`);
expect(popoverTarget).toBeInTheDocument();
expect(popoverTarget!.tagName).toBe("ADDRESS");
});
it("allows user to apply dark theme explicitly", () => {
const { container } = render(
,
);
const popoverElement = container.querySelector(`.${Classes.POPOVER}`);
expect(popoverElement).toBeInTheDocument();
expect(popoverElement).toHaveClass(Classes.DARK);
});
it("attaches popoverRef to the popover element", async () => {
const popoverRef = createRef();
const { container } = render(
,
);
await waitFor(() => expect(screen.getByText("content")).toBeInTheDocument());
const popoverElement = container.querySelector(`.${Classes.POPOVER}`);
expect(popoverElement).toBeInTheDocument();
expect(popoverRef.current).toBe(popoverElement);
});
it("popoverRef returns null when closed", () => {
const ref = createRef();
render(
,
);
expect(ref.current).toBeNull();
});
it("renders with aria-haspopup attr", () => {
const { container } = render(
,
);
expect(container.querySelector("[aria-haspopup='menu']")).toBeInTheDocument();
});
it("sets aria-haspopup attr base on popupKind", () => {
const { container } = render(
,
);
expect(container.querySelector("[aria-haspopup='dialog']")).toBeInTheDocument();
});
it("renders without aria-haspopup attr for hover interaction", () => {
const { container } = render(
,
);
expect(container.querySelector("[aria-haspopup]")).not.toBeInTheDocument();
});
it("applies FILL class to target when fill={true}", () => {
const { container } = render(
,
);
const popoverTarget = container.querySelector(`.${Classes.POPOVER_TARGET}`);
expect(popoverTarget).toBeInTheDocument();
expect(popoverTarget).toHaveClass(Classes.FILL);
});
it("does not apply FILL class to target when fill={false}", () => {
const { container } = render(
,
);
const popoverTarget = container.querySelector(`.${Classes.POPOVER_TARGET}`);
expect(popoverTarget).toBeInTheDocument();
expect(popoverTarget).not.toHaveClass(Classes.FILL);
});
});
describe("basic functionality", () => {
it("inherits dark theme from trigger ancestor", () => {
const { baseElement } = render(
,
);
const popoverElement = baseElement.querySelector(`.${Classes.POPOVER}`);
expect(popoverElement).toBeInTheDocument();
expect(popoverElement).toHaveClass(Classes.DARK);
});
it("inheritDarkTheme=false disables inheriting dark theme from trigger ancestor", () => {
const { baseElement } = render(
,
);
const popoverElement = baseElement.querySelector(`.${Classes.POPOVER}`);
expect(popoverElement).toBeInTheDocument();
expect(popoverElement).not.toHaveClass(Classes.DARK);
});
it("supports overlay lifecycle props", async () => {
const user = userEvent.setup();
const onOpening = vi.fn();
render(
,
);
await user.click(screen.getByRole("button", { name: "target" }));
expect(onOpening).toHaveBeenCalledOnce();
});
it("escape key closes popover", async () => {
const user = userEvent.setup();
render(
,
);
await user.click(screen.getByRole("button", { name: "target" }));
await waitFor(() => {
expect(screen.getByText("content")).toBeInTheDocument();
});
await user.keyboard("{Escape}");
await waitFor(() => {
expect(screen.queryByText("content")).not.toBeInTheDocument();
});
});
});
describe("focus management", () => {
it("returns focus to target element when closed with shouldReturnFocusOnClose={true} (default)", async () => {
const user = userEvent.setup();
const { container } = render(
close}
shouldReturnFocusOnClose={true}
usePortal={false}
>
,
);
const targetButton = screen.getByRole("button", { name: "target" });
await user.click(targetButton);
const overlay = container.querySelector(`.${Classes.OVERLAY}`);
await waitFor(() => {
expect(overlay).toBeInTheDocument();
expect(overlay).toHaveClass(Classes.OVERLAY_OPEN);
expect(overlay).toContainElement(document.activeElement as HTMLElement);
});
const closeButton = screen.getByRole("button", { name: "close" });
await user.click(closeButton);
await waitFor(() => {
expect(overlay).not.toHaveClass(Classes.OVERLAY_OPEN);
expect(targetButton).toBe(document.activeElement);
});
});
it("does not return focus to target element when closed with shouldReturnFocusOnClose={false}", async () => {
const user = userEvent.setup();
const { container } = render(
close}
shouldReturnFocusOnClose={false}
usePortal={false}
>
,
);
const targetButton = screen.getByRole("button", { name: "target" });
await user.click(targetButton);
const overlay = container.querySelector(`.${Classes.OVERLAY}`);
await waitFor(() => {
expect(overlay).toBeInTheDocument();
expect(overlay).toHaveClass(Classes.OVERLAY_OPEN);
expect(overlay).toContainElement(document.activeElement as HTMLElement);
});
const closeButton = screen.getByRole("button", { name: "close" });
await user.click(closeButton);
await waitFor(() => {
expect(overlay).not.toHaveClass(Classes.OVERLAY_OPEN);
expect(targetButton).not.toBe(document.activeElement);
});
});
});
describe("openOnTargetFocus", () => {
describe("if true (default)", () => {
it('adds tabindex="0" to target\'s child node when interactionKind is HOVER', () => {
render(
,
);
expect(screen.getByRole("button", { name: "target" })).toHaveAttribute("tabindex", "0");
});
it('adds tabindex="0" to target\'s child node when interactionKind is HOVER_TARGET_ONLY', () => {
render(
,
);
expect(screen.getByRole("button", { name: "target" })).toHaveAttribute("tabindex", "0");
});
it("does not add tabindex to target's child node when interactionKind is CLICK", () => {
render(
,
);
expect(screen.getByRole("button", { name: "target" })).not.toHaveAttribute("tabindex");
});
it("does not add tabindex to target's child node when interactionKind is CLICK_TARGET_ONLY", () => {
render(
,
);
expect(screen.getByRole("button", { name: "target" })).not.toHaveAttribute("tabindex");
});
it("does not add tabindex to target's child node when disabled=true", () => {
render(
,
);
expect(screen.getByRole("button", { name: "target" })).not.toHaveAttribute("tabindex");
});
it("opens popover on target focus when interactionKind is HOVER", async () => {
render(
,
);
const targetButton = screen.getByRole("button", { name: "target" });
targetButton.focus();
await waitFor(() => {
expect(screen.getByText("content")).toBeInTheDocument();
expect(targetButton).toBe(document.activeElement);
});
});
it("opens popover on target focus when interactionKind is HOVER_TARGET_ONLY", async () => {
render(
,
);
const targetButton = screen.getByRole("button", { name: "target" });
targetButton.focus();
await waitFor(() => {
expect(screen.getByText("content")).toBeInTheDocument();
expect(targetButton).toBe(document.activeElement);
});
});
it("does not open popover on target focus when interactionKind is CLICK", async () => {
render(
,
);
const targetButton = screen.getByRole("button", { name: "target" });
targetButton.focus();
expect(screen.queryByText("content")).not.toBeInTheDocument();
expect(targetButton).toBe(document.activeElement);
});
it("does not open popover on target focus when interactionKind is CLICK_TARGET_ONLY", () => {
render(
,
);
const targetButton = screen.getByRole("button", { name: "target" });
targetButton.focus();
expect(screen.queryByText("content")).not.toBeInTheDocument();
expect(targetButton).toBe(document.activeElement);
});
});
describe("if false", () => {
it("does not add tabindex to target's child node when interactionKind is HOVER", () => {
render(
,
);
expect(screen.getByRole("button", { name: "target" })).not.toHaveAttribute("tabindex");
});
it("should not add `tabindex` to target's child node when interactionKind is `HOVER_TARGET_ONLY`", () => {
render(
,
);
expect(screen.getByRole("button", { name: "target" })).not.toHaveAttribute("tabindex");
});
it("does not add tabindex to target's child node when interactionKind is CLICK", () => {
render(
,
);
expect(screen.getByRole("button", { name: "target" })).not.toHaveAttribute("tabindex");
});
it("does not add tabindex to target's child node when interactionKind is CLICK_TARGET_ONLY", () => {
render(
,
);
expect(screen.getByRole("button", { name: "target" })).not.toHaveAttribute("tabindex");
});
it("does not open popover on target focus when interactionKind is HOVER", () => {
render(
,
);
const targetButton = screen.getByRole("button", { name: "target" });
targetButton.focus();
expect(screen.queryByText("content")).not.toBeInTheDocument();
expect(targetButton).toBe(document.activeElement);
});
it("does not open popover on target focus when interactionKind is HOVER_TARGET_ONLY", () => {
render(
,
);
const targetButton = screen.getByRole("button", { name: "target" });
targetButton.focus();
expect(screen.queryByText("content")).not.toBeInTheDocument();
expect(targetButton).toBe(document.activeElement);
});
it("does not open popover on target focus when interactionKind is CLICK", () => {
render(
,
);
const targetButton = screen.getByRole("button", { name: "target" });
targetButton.focus();
expect(screen.queryByText("content")).not.toBeInTheDocument();
expect(targetButton).toBe(document.activeElement);
});
it("does not open popover on target focus when interactionKind is CLICK_TARGET_ONLY", () => {
render(
,
);
const targetButton = screen.getByRole("button", { name: "target" });
targetButton.focus();
expect(screen.queryByText("content")).not.toBeInTheDocument();
expect(targetButton).toBe(document.activeElement);
});
});
});
describe("in controlled mode", () => {
it("state respects isOpen prop", () => {
const { rerender } = render(
,
);
expect(screen.queryByText("content")).not.toBeInTheDocument();
rerender(
,
);
expect(screen.getByText("content")).toBeInTheDocument();
});
it("state does not update on user (click) interaction", async () => {
const user = userEvent.setup();
render(
,
);
await user.click(screen.getByRole("button", { name: "target" }));
expect(screen.queryByText("content")).not.toBeInTheDocument();
});
it("state does not update on user (key) interaction", async () => {
const user = userEvent.setup();
render(
,
);
expect(screen.getByText("content")).toBeInTheDocument();
await user.keyboard("{Escape}");
expect(screen.getByText("content")).toBeInTheDocument();
});
describe("disabled=true takes precedence over isOpen=true", () => {
it("on mount", () => {
render(
,
);
expect(screen.queryByText("content")).not.toBeInTheDocument();
});
it("onInteraction not called if changing from closed to open (b/c popover is still closed)", () => {
const onInteraction = vi.fn();
const { rerender } = render(
,
);
expect(onInteraction).not.toHaveBeenCalled();
rerender(
,
);
expect(screen.queryByText("content")).not.toBeInTheDocument();
expect(onInteraction).not.toHaveBeenCalled();
});
it("onInteraction not called if changing from open to closed (b/c popover was already closed)", () => {
const onInteraction = vi.fn();
const { rerender } = render(
,
);
expect(onInteraction).not.toHaveBeenCalled();
rerender(
,
);
expect(screen.queryByText("content")).not.toBeInTheDocument();
expect(onInteraction).not.toHaveBeenCalled();
});
it("onInteraction called if open and changing to disabled (b/c popover will close)", async () => {
const onInteraction = vi.fn();
const { rerender } = render(
,
);
await waitFor(() => expect(screen.getByText("content")).toBeInTheDocument());
expect(onInteraction).not.toHaveBeenCalled();
rerender(
,
);
await waitFor(() => expect(screen.queryByText("content")).not.toBeInTheDocument());
expect(onInteraction).toHaveBeenCalled();
});
it("onInteraction called if open and changing to not-disabled (b/c popover will open)", async () => {
const onInteraction = vi.fn();
const { rerender } = render(
,
);
expect(onInteraction).not.toHaveBeenCalled();
rerender(
,
);
await waitFor(() => expect(screen.getByText("content")).toBeInTheDocument());
expect(onInteraction).toHaveBeenCalled();
});
});
it("onClose is invoked with event when popover would close", async () => {
const user = userEvent.setup();
const onClose = vi.fn();
render(
close}
isOpen={true}
onClose={onClose}
>
,
);
const closeButton = screen.getByRole("button", { name: "close" });
await waitFor(() => expect(closeButton).toBeInTheDocument());
await user.click(screen.getByRole("button", { name: "close" }));
expect(onClose).toHaveBeenCalledOnce();
expect(onClose.mock.calls[0][0]).not.toBeNull();
});
describe("onInteraction()", () => {
it("is invoked with `true` when closed popover target is clicked", async () => {
const user = userEvent.setup();
const onInteraction = vi.fn();
render(
,
);
await user.click(screen.getByRole("button", { name: "target" }));
expect(onInteraction).toHaveBeenCalledOnce();
expect(onInteraction).toHaveBeenCalledWith(true, expect.anything());
});
it("is invoked with `false` when open popover target is clicked", async () => {
const user = userEvent.setup();
const onInteraction = vi.fn();
const { container } = render(
,
);
const target = container.querySelector(`.${Classes.POPOVER_TARGET}`);
expect(target).not.toBeNull();
await user.click(target!);
expect(onInteraction).toHaveBeenCalledOnce();
expect(onInteraction).toHaveBeenCalledWith(false, expect.anything());
});
it("is invoked with `false` when open modal popover backdrop is clicked", async () => {
const user = userEvent.setup();
const onInteraction = vi.fn();
render(
,
);
const backdrop = screen.getByTestId("test-backdrop");
await user.click(backdrop);
expect(onInteraction).toHaveBeenCalledOnce();
expect(onInteraction).toHaveBeenCalledWith(false, expect.anything());
});
it("is invoked with `false` when clicking POPOVER_DISMISS", async () => {
const user = userEvent.setup();
const onInteraction = vi.fn();
render(
close}
isOpen={true}
onInteraction={onInteraction}
>
,
);
const closeButton = screen.getByRole("button", { name: "close" });
await waitFor(() => expect(closeButton).toBeInTheDocument());
await user.click(closeButton);
expect(onInteraction).toHaveBeenCalledOnce();
expect(onInteraction).toHaveBeenCalledWith(false, expect.anything());
});
it("is invoked with `false` when the document is mousedowned", async () => {
const user = userEvent.setup();
const onInteraction = vi.fn();
render(
,
);
await user.click(document.documentElement);
expect(onInteraction).toHaveBeenCalledOnce();
expect(onInteraction).toHaveBeenCalledWith(false, expect.anything());
});
it("is not invoked when clicking inside a child Dialog rendered in popover content", async () => {
const user = userEvent.setup();
const onInteraction = vi.fn();
render(
}
isOpen={true}
onInteraction={onInteraction}
>
,
);
await waitFor(() => expect(screen.getByRole("button", { name: "dialog button" })).toBeInTheDocument());
await user.click(screen.getByRole("button", { name: "dialog button" }));
expect(onInteraction).not.toHaveBeenCalledWith(false, expect.anything());
});
it("is not invoked on HOVER popover when clicking inside a child Dialog rendered in popover content", async () => {
const user = userEvent.setup();
const onInteraction = vi.fn();
render(
}
interactionKind="hover"
isOpen={true}
onInteraction={onInteraction}
>
,
);
await waitFor(() => expect(screen.getByRole("button", { name: "dialog button" })).toBeInTheDocument());
await user.click(screen.getByRole("button", { name: "dialog button" }));
expect(onInteraction).not.toHaveBeenCalledWith(false, expect.anything());
});
});
it("does not apply active class to target when open", () => {
render(
,
);
const targetButton = screen.getByRole("button", { name: "target" });
expect(targetButton).not.toHaveClass(Classes.ACTIVE);
});
});
describe("in uncontrolled mode", () => {
it("setting defaultIsOpen=true renders open popover", async () => {
render(
,
);
await waitFor(() => expect(screen.getByText("content")).toBeInTheDocument());
});
it("with defaultIsOpen=true, popover can still be closed", async () => {
const user = userEvent.setup();
render(
close} defaultIsOpen={true}>
,
);
await waitFor(() => expect(screen.getByRole("button", { name: "close" })).toBeInTheDocument());
await user.click(screen.getByRole("button", { name: "close" }));
await waitFor(() => expect(screen.queryByRole("button", { name: "close" })).not.toBeInTheDocument());
});
it("CLICK_TARGET_ONLY works properly", async () => {
const user = userEvent.setup();
const { container } = render(
,
);
const target = container.querySelector(`.${Classes.POPOVER_TARGET}`);
expect(target).not.toBeNull();
await user.click(target!);
await waitFor(() => expect(screen.getByText("content")).toBeInTheDocument());
await user.click(target!);
await waitFor(() => expect(screen.queryByText("content")).not.toBeInTheDocument());
});
it("HOVER_TARGET_ONLY works properly", async () => {
const user = userEvent.setup();
const { container } = render(
,
);
const target = container.querySelector(`.${Classes.POPOVER_TARGET}`);
expect(target).not.toBeNull();
await user.hover(target!);
await waitFor(() => expect(screen.getByText("content")).toBeInTheDocument());
await user.unhover(target!);
await waitFor(() => expect(screen.queryByText("content")).not.toBeInTheDocument());
});
it("inline HOVER_TARGET_ONLY works properly when openOnTargetFocus={false}", async () => {
const user = userEvent.setup();
const { container } = render(
,
);
const target = container.querySelector(`.${Classes.POPOVER_TARGET}`);
expect(target).not.toBeNull();
await user.hover(target!);
await waitFor(() => expect(screen.getByText("content")).toBeInTheDocument());
await user.unhover(target!);
await waitFor(() => expect(screen.queryByText("content")).not.toBeInTheDocument());
});
it("inline HOVER works properly", async () => {
const user = userEvent.setup();
const { container } = render(
,
);
const target = container.querySelector(`.${Classes.POPOVER_TARGET}`);
expect(target).not.toBeNull();
await user.hover(target!);
await waitFor(() => expect(screen.getByText("content")).toBeInTheDocument());
await user.unhover(target!);
await waitFor(() => expect(screen.queryByText("content")).not.toBeInTheDocument());
});
it("clicking POPOVER_DISMISS closes popover when usePortal=true", async () => {
const user = userEvent.setup();
render(
close}
defaultIsOpen={true}
usePortal={true}
>
,
);
await waitFor(() => expect(screen.getByRole("button", { name: "close" })).toBeInTheDocument());
await user.click(screen.getByRole("button", { name: "close" }));
await waitFor(() => expect(screen.queryByRole("button", { name: "close" })).not.toBeInTheDocument());
});
it("clicking POPOVER_DISMISS closes popover when usePortal=false", async () => {
const user = userEvent.setup();
render(
close}
defaultIsOpen={true}
usePortal={false}
>
,
);
await waitFor(() => expect(screen.getByRole("button", { name: "close" })).toBeInTheDocument());
await user.click(screen.getByRole("button", { name: "close" }));
await waitFor(() => expect(screen.queryByRole("button", { name: "close" })).not.toBeInTheDocument());
});
it("pressing Escape closes popover when canEscapeKeyClose=true and usePortal=false", async () => {
const user = userEvent.setup();
render(
,
);
await user.click(screen.getByRole("button", { name: "target" }));
await waitFor(() => expect(screen.getByText("content")).toBeInTheDocument());
await user.keyboard("{Escape}");
await waitFor(() => expect(screen.queryByText("content")).not.toBeInTheDocument());
});
it("setting disabled=true prevents opening popover", async () => {
const user = userEvent.setup();
render(
,
);
await user.click(screen.getByRole("button", { name: "target" }));
expect(screen.queryByText("content")).not.toBeInTheDocument();
});
it("setting disabled=true hides open popover", async () => {
const user = userEvent.setup();
const { rerender } = render(
,
);
await user.click(screen.getByRole("button", { name: "target" }));
await waitFor(() => expect(screen.getByText("content")).toBeInTheDocument());
rerender(
,
);
await waitFor(() => expect(screen.queryByText("content")).not.toBeInTheDocument());
});
it("console.warns if onInteraction is set", () => {
const localWarnSpy = vi.spyOn(console, "warn").mockImplementation(vi.fn());
render(
false}>
,
);
expect(localWarnSpy.mock.calls[0][0]).toBe(Errors.POPOVER_WARN_UNCONTROLLED_ONINTERACTION);
localWarnSpy.mockRestore();
});
it("does apply active class to target when open", async () => {
const user = userEvent.setup();
render(
,
);
await user.click(screen.getByRole("button", { name: "target" }));
expect(screen.getByRole("button", { name: "target" })).toHaveClass(Classes.ACTIVE);
});
});
describe("when composed with ", () => {
it("shows tooltip on hover", async () => {
const user = userEvent.setup();
render(
,
);
const targetButton = screen.getByRole("button", { name: "target" });
await user.hover(targetButton);
await waitFor(() => expect(screen.getByText("tooltip content")).toBeInTheDocument());
});
it("shows popover on click", async () => {
const user = userEvent.setup();
render(
,
);
const targetButton = screen.getByRole("button", { name: "target" });
await user.click(targetButton);
await waitFor(() => expect(screen.getByText("popover content")).toBeInTheDocument());
expect(screen.queryByText("tooltip content")).not.toBeInTheDocument();
});
it("opens popover when Tooltip renderTarget props are spread after PopoverNext renderTarget props", async () => {
const user = userEvent.setup();
render(
(
(
)}
/>
)}
/>,
);
await user.click(screen.getByRole("button", { name: "target" }));
await waitFor(() => expect(screen.getByText("popover content")).toBeInTheDocument());
});
it("the target is focusable", () => {
render(
,
);
const targetButton = screen.getByRole("button", { name: "target" });
expect(targetButton).toHaveAttribute("tabindex", "0");
});
describe("when disabled=true", () => {
it("shows tooltip on hover", async () => {
const user = userEvent.setup();
render(
,
);
const targetButton = screen.getByRole("button", { name: "target" });
await user.hover(targetButton);
await waitFor(() => expect(screen.getByText("tooltip content")).toBeInTheDocument());
});
it("does not show popover on click", async () => {
const user = userEvent.setup();
render(
,
);
const targetButton = screen.getByRole("button", { name: "target" });
await user.click(targetButton);
expect(screen.queryByText("popover content")).not.toBeInTheDocument();
});
it("the target is focusable", () => {
render(
,
);
const targetButton = screen.getByRole("button", { name: "target" });
expect(targetButton).toHaveAttribute("tabindex", "0");
});
});
});
describe("when composed with a disabled ", () => {
it("does not show tooltip on hover", async () => {
const user = userEvent.setup();
render(
,
);
const targetButton = screen.getByRole("button", { name: "target" });
await user.hover(targetButton);
expect(screen.queryByText("tooltip content")).not.toBeInTheDocument();
});
it("shows popover on click", async () => {
const user = userEvent.setup();
render(
,
);
const targetButton = screen.getByRole("button", { name: "target" });
await user.click(targetButton);
await waitFor(() => expect(screen.getByText("popover content")).toBeInTheDocument());
});
it("the target is not focusable", () => {
render(
,
);
const targetButton = screen.getByRole("button", { name: "target" });
expect(targetButton).not.toHaveAttribute("tabindex");
});
describe("when disabled=true", () => {
it("does not show tooltip on hover", async () => {
const user = userEvent.setup();
render(
,
);
const targetButton = screen.getByRole("button", { name: "target" });
await user.hover(targetButton);
expect(screen.queryByText("tooltip content")).not.toBeInTheDocument();
});
it("does not show popover on click", async () => {
const user = userEvent.setup();
render(
,
);
const targetButton = screen.getByRole("button", { name: "target" });
await user.click(targetButton);
expect(screen.queryByText("popover content")).not.toBeInTheDocument();
});
it("the target is not focusable", () => {
render(
,
);
const targetButton = screen.getByRole("button", { name: "target" });
expect(targetButton).not.toHaveAttribute("tabindex");
});
});
});
describe("Popper.js integration", () => {
it("renders arrow element by default", async () => {
const { baseElement } = render(
,
);
await waitFor(() => expect(baseElement.querySelector(`.${Classes.POPOVER_ARROW}`)).toBeInTheDocument());
});
it("arrow can be disabled by setting arrow={false}", () => {
const { baseElement } = render(
,
);
expect(baseElement.querySelector(`.${Classes.POPOVER_ARROW}`)).not.toBeInTheDocument();
});
it("matches target width via custom modifier", async () => {
const { container } = render(
,
);
const targetElement = screen.getByRole("button", { name: "target" });
const popoverElement = container.querySelector(`.${Classes.POPOVER}`);
expect(popoverElement).not.toBeNull();
await waitFor(() => {
const diff = Math.abs((popoverElement?.clientWidth ?? 0) - targetElement.clientWidth);
expect(diff).toBeLessThanOrEqual(5);
});
});
});
describe("closing on click", () => {
it("Classes.POPOVER_DISMISS closes on click", async () => {
const user = userEvent.setup();
render(
dismiss}
defaultIsOpen={true}
>
,
);
await waitFor(() => expect(screen.getByRole("button", { name: "dismiss" })).toBeInTheDocument());
await user.click(screen.getByRole("button", { name: "dismiss" }));
await waitFor(() => expect(screen.queryByRole("button", { name: "dismiss" })).not.toBeInTheDocument());
});
it("Classes.POPOVER_DISMISS_OVERRIDE does not close", async () => {
const user = userEvent.setup();
render(
}
defaultIsOpen={true}
>
,
);
expect(screen.getByRole("button", { name: "dismiss" })).toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "dismiss" }));
expect(screen.getByRole("button", { name: "dismiss" })).toBeInTheDocument();
});
it(":disabled does not close", async () => {
const user = userEvent.setup();
render(
}
defaultIsOpen={true}
>
,
);
expect(screen.getByRole("button", { name: "dismiss" })).toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "dismiss" }));
expect(screen.getByRole("button", { name: "dismiss" })).toBeInTheDocument();
});
it("Classes.DISABLED does not close", async () => {
const user = userEvent.setup();
render(
}
defaultIsOpen={true}
>
,
);
expect(screen.getByRole("button", { name: "dismiss" })).toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "dismiss" }));
expect(screen.getByRole("button", { name: "dismiss" })).toBeInTheDocument();
});
it("captureDismiss={true} inner dismiss does not close outer popover", async () => {
const user = userEvent.setup();
render(
}
>
}
>
,
);
expect(screen.getByRole("button", { name: "dismiss" })).toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "dismiss" }));
await waitFor(() => expect(screen.queryByRole("button", { name: "dismiss" })).not.toBeInTheDocument());
expect(screen.getByRole("button", { name: "inner target" })).toBeInTheDocument();
});
it("captureDismiss={false} inner dismiss closes outer popover", async () => {
const user = userEvent.setup();
render(
}
>
}
>
,
);
expect(screen.getByRole("button", { name: "dismiss" })).toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "dismiss" }));
await waitFor(() => expect(screen.queryByRole("button", { name: "dismiss" })).not.toBeInTheDocument());
await waitFor(() => expect(screen.queryByRole("button", { name: "inner target" })).not.toBeInTheDocument());
});
});
describe("key interactions on InputGroup target", () => {
it("Space key inserts a space character instead of being swallowed", async () => {
const handleChange = vi.fn();
const user = userEvent.setup();
render(
,
);
const input = screen.getByPlaceholderText("Search...");
await user.click(input);
await waitFor(() => expect(screen.getByText("popover content")).toBeInTheDocument());
await user.type(input, "lorem ipsum");
expect(input).toHaveValue("lorem ipsum");
// Popover should stay open; Space must not toggle it
expect(screen.getByText("popover content")).toBeInTheDocument();
});
});
describe("key interactions on Button target", () => {
describe("Space key down opens click interaction popover", () => {
it("when autoFocus={true}", async () => {
const user = userEvent.setup();
const { container } = render(
,
);
const targetButton = screen.getByRole("button", { name: "target" });
targetButton.focus();
await user.keyboard(" ");
await waitFor(() => expect(screen.getByText("content")).toBeInTheDocument());
const overlay = container.querySelector(`.${Classes.OVERLAY}`);
expect(overlay).toContainElement(document.activeElement as HTMLElement);
});
it("when autoFocus={false}", async () => {
const user = userEvent.setup();
render(
,
);
const targetButton = screen.getByRole("button", { name: "target" });
targetButton.focus();
await user.keyboard(" ");
await waitFor(() => expect(screen.getByText("content")).toBeInTheDocument());
expect(targetButton).toBe(document.activeElement);
});
});
});
describe("compatibility", () => {
it("renderTarget type definition allows sending props to child components", () => {
render(
(
)}
/>,
);
});
});
describe("renderTarget interaction props", () => {
it.each(["click", "click-target"] as const)("injects onClick into renderTarget props for %s", kind => {
const renderTarget = vi.fn(({ isOpen: _isOpen, ref, ...props }) => (
));
render();
expect(renderTarget).toHaveBeenCalled();
expect(typeof renderTarget.mock.calls[0][0].onClick).toBe("function");
});
// Hover popovers (and Tooltip, which is always hover) open via mouseenter/focus, never click, so
// their target must not receive an onClick. Injecting one shadows a consumer's own onClick when a
// hover target is shared with an enclosing Popover via renderTarget. See usePopover's useClick and
// PopoverTarget's floatingReferenceProps.
it.each(["hover", "hover-target"] as const)("does not inject onClick into renderTarget props for %s", kind => {
const renderTarget = vi.fn(({ isOpen: _isOpen, ref, ...props }) => (
));
render();
expect(renderTarget).toHaveBeenCalled();
expect(renderTarget.mock.calls[0][0].onClick).toBeUndefined();
});
});
// Regression test for https://github.com/palantir/blueprint/issues/7857
describe("ref stability", () => {
it("passes an identity-stable ref to the target across parent re-renders", async () => {
const seenRefs = new Set();
function Wrapper() {
const [count, setCount] = useState(0);
return (
<>
{
seenRefs.add(ref);
return ;
}}
/>
>
);
}
render();
await userEvent.click(screen.getByTestId("rerender"));
expect(seenRefs.size).toBe(1);
});
});
});