Skip to content
Closed
9 changes: 5 additions & 4 deletions packages/core/src/components/breadcrumbs/breadcrumbs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@ import { Boundary, Classes, DISPLAYNAME_PREFIX, type Props, removeNonHTMLProps }
import { Menu } from "../menu/menu";
import { MenuItem } from "../menu/menuItem";
import { OverflowList, type OverflowListProps } from "../overflow-list/overflowList";
import { Popover } from "../popover/popover";
import type { PopoverProps } from "../popover/popoverProps";
import { PopoverNext } from "../popover-next/popoverNext";
import { popoverPropsToNextProps } from "../popover-next/popoverNextMigrationUtils";

import { Breadcrumb, type BreadcrumbProps } from "./breadcrumb";

Expand Down Expand Up @@ -150,11 +151,11 @@ export const Breadcrumbs: React.FC<BreadcrumbsProps> = memo(props => {

return (
<li>
<Popover
<PopoverNext
content={<Menu>{orderedItems.map(renderOverflowBreadcrumb)}</Menu>}
disabled={orderedItems.length === 0}
placement={collapseFrom === Boundary.END ? "bottom-end" : "bottom-start"}
{...popoverProps}
{...popoverPropsToNextProps(popoverProps)}
>
<span
aria-label="collapsed breadcrumbs"
Expand All @@ -163,7 +164,7 @@ export const Breadcrumbs: React.FC<BreadcrumbsProps> = memo(props => {
{...overflowButtonProps}
className={classNames(Classes.BREADCRUMBS_COLLAPSED, overflowButtonProps?.className)}
/>
</Popover>
</PopoverNext>
</li>
);
},
Expand Down
6 changes: 3 additions & 3 deletions packages/core/src/components/button/ButtonGroup.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { StoryLabel } from "@storybook-common";
import { Flex } from "@blueprintjs/labs";

import { Alignment, ButtonVariant, Intent, Size } from "../../common";
import { Popover } from "../popover/popover";
import { PopoverNext } from "../popover-next/popoverNext";

import { ButtonGroup } from "./buttonGroup";
import { Button } from "./buttons";
Expand Down Expand Up @@ -273,13 +273,13 @@ export const WithPopover: Story = {
<StoryLabel title={variant} />
<ButtonGroup {...args} variant={variant}>
{BUTTONS.map(({ icon, label }) => (
<Popover
<PopoverNext
key={icon}
content={<span style={{ padding: 10 }}>{label}</span>}
placement="bottom"
>
<Button icon={icon} aria-label={label} />
</Popover>
</PopoverNext>
))}
</ButtonGroup>
</Flex>
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/components/button/button-group.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ Set it at the group level for uniform alignment or on individual buttons for spe
### Usage with popovers

**Button** elements inside a **ButtonGroup** can be wrapped with a
[**Popover**](#core/components/popover) to create complex toolbars.
[**PopoverNext**](#core/components/popover-next) to create complex toolbars.

@reactExample ButtonGroupPopoverExample

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ The APIs described on this page are lower-level and have some limitations compar

**Context Menu Popover** is a lower-level API for [**Context Menu**](#core/components/context-menu) which does
not hook up any interaction handlers for you and simply renders an opinionated
[**Popover**](#core/components/popover) at a particular target offset on the page through a
[**PopoverNext**](#core/components/popover-next) at a particular target offset on the page through a
[**Portal**](#core/components/portal).

@reactExample ContextMenuPopoverExample
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/components/context-menu/context-menu.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ title: Context Menu
# Context Menu

**Context menus** present the user with a list of actions when right-clicking on a target element.
They essentially generate an opinionated [**Popover**](#core/components/popover) instance configured
They essentially generate an opinionated [**PopoverNext**](#core/components/popover-next) instance configured
with the appropriate interaction handlers.

## Import
Expand Down
86 changes: 45 additions & 41 deletions packages/core/src/components/context-menu/contextMenu.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
* limitations under the License.
*/

import { waitFor } from "@testing-library/dom";
import classNames from "classnames";
import { mount, type ReactWrapper } from "enzyme";
import { createRef, useCallback } from "react";
Expand All @@ -24,8 +25,7 @@ import { Classes, Utils } from "../../common";
import { Drawer } from "../drawer/drawer";
import { Menu } from "../menu/menu";
import { MenuItem } from "../menu/menuItem";
import { Popover } from "../popover/popover";
import { PopoverInteractionKind } from "../popover/popoverProps";
import { PopoverNext } from "../popover-next/popoverNext";
import { Tooltip, type TooltipProps } from "../tooltip/tooltip";

import { ContextMenu, type ContextMenuContentProps, type ContextMenuProps } from "./contextMenu";
Expand All @@ -44,6 +44,7 @@ const TOOLTIP_SELECTOR = `.${Classes.TOOLTIP}`;
const COMMON_TOOLTIP_PROPS: Partial<TooltipProps> = {
hoverCloseDelay: 0,
hoverOpenDelay: 0,
transitionDuration: 0,
usePortal: false,
};

Expand Down Expand Up @@ -88,13 +89,13 @@ describe("ContextMenu", () => {
it("renders children and Popover", () => {
const ctxMenu = mountTestMenu();
expect(ctxMenu.find(`.${TARGET_CLASSNAME}`).exists()).toBe(true);
expect(ctxMenu.find(Popover).exists()).toBe(true);
expect(ctxMenu.find(PopoverNext).exists()).toBe(true);
});

it("opens popover on right click", () => {
const ctxMenu = mountTestMenu();
openCtxMenu(ctxMenu);
expect(ctxMenu.find(Popover).prop("isOpen")).toBe(true);
expect(ctxMenu.find(PopoverNext).prop("isOpen")).toBe(true);
});

it("renders custom HTML tag if specified", () => {
Expand All @@ -119,7 +120,7 @@ describe("ContextMenu", () => {
key: "Escape",
nativeEvent: new KeyboardEvent("keydown"),
});
expect(ctxMenu.find(Popover).prop("isOpen")).toBe(false);
expect(ctxMenu.find(PopoverNext).prop("isOpen")).toBe(false);
});

it("clicks inside popover don't propagate to context menu wrapper", () => {
Expand Down Expand Up @@ -166,13 +167,13 @@ describe("ContextMenu", () => {
it("renders children and Popover", () => {
const ctxMenu = mountTestMenu();
expect(ctxMenu.find(`.${TARGET_CLASSNAME}`).exists()).toBe(true);
expect(ctxMenu.find(Popover).exists()).toBe(true);
expect(ctxMenu.find(PopoverNext).exists()).toBe(true);
});

it("opens popover on right click", () => {
const ctxMenu = mountTestMenu();
openCtxMenu(ctxMenu);
expect(ctxMenu.find(Popover).prop("isOpen")).toBe(true);
expect(ctxMenu.find(PopoverNext).prop("isOpen")).toBe(true);
});

it("handles context menu event, even if content is undefined", () => {
Expand Down Expand Up @@ -339,7 +340,7 @@ describe("ContextMenu", () => {

describe("interacting with other components", () => {
describe("with one level of nesting", () => {
it("closes parent Tooltip", () => {
it("closes parent Tooltip", async () => {
const wrapper = mount(
<Tooltip content="hello" {...COMMON_TOOLTIP_PROPS}>
<ContextMenu content={MENU} popoverProps={{ transitionDuration: 0 }}>
Expand All @@ -352,10 +353,10 @@ describe("ContextMenu", () => {
openTooltip(wrapper);
openCtxMenu(wrapper);
expect(
wrapper.find(ContextMenu).find(Popover).prop("isOpen"),
wrapper.find(ContextMenu).find(PopoverNext).prop("isOpen"),
"ContextMenu popover should be open",
).toBe(true);
assertTooltipClosed(wrapper);
await assertTooltipClosed();
closeCtxMenu(wrapper);
});

Expand All @@ -372,7 +373,7 @@ describe("ContextMenu", () => {
openTooltip(wrapper);
openCtxMenu(wrapper);
expect(
wrapper.find(ContextMenu).find(Popover).first().prop("isOpen"),
wrapper.find(ContextMenu).find(PopoverNext).first().prop("isOpen"),
"ContextMenu popover should be open",
).toBe(true);
// this assertion is difficult to unit test, but we know that the tooltip closes in manual testing,
Expand All @@ -381,27 +382,28 @@ describe("ContextMenu", () => {
closeCtxMenu(wrapper);
});

function assertTooltipClosed(wrapper: ReactWrapper) {
expect(
wrapper
.find(Popover)
.find({ interactionKind: PopoverInteractionKind.HOVER_TARGET_ONLY })
.state("isOpen"),
"Tooltip should be closed",
).toBe(false);
async function assertTooltipClosed() {
// Tooltip close is driven by a `useEffect` inside PopoverNext that syncs internal
// state when `disabled` flips true, so the DOM unmount lands in a later tick.
// Query the document directly because the Enzyme wrapper can hold stale references.
await waitFor(() => {
expect(document.querySelectorAll(`.${Classes.TOOLTIP}`), "Tooltip should be closed").toHaveLength(
0,
);
});
}
});

describe("with multiple layers of Tooltip nesting", () => {
const OUTER_TARGET_CLASSNAME = "outer-target";

describe("ContextMenu > Tooltip > ContextMenu", () => {
it("closes tooltip when inner menu opens", () => {
it("closes tooltip when inner menu opens", async () => {
const wrapper = mountTestCase();
openTooltip(wrapper);
expect(wrapper.find(TOOLTIP_SELECTOR), "tooltip should be open").toHaveLength(1);
openCtxMenu(wrapper);
assertTooltipClosed(wrapper);
await assertTooltipClosed();
const ctxMenuPopover = wrapper.find(`.${Classes.CONTEXT_MENU_POPOVER}`).hostNodes();
expect(ctxMenuPopover.exists(), "ContextMenu popover should be open").toBe(true);
expect(ctxMenuPopover.text().includes("first"), "inner ContextMenu should be open").toBe(true);
Expand Down Expand Up @@ -470,14 +472,16 @@ describe("ContextMenu", () => {
return wrapper;
}

function assertTooltipClosed(wrapper: ReactWrapper) {
expect(
wrapper
.find(Popover)
.find({ interactionKind: PopoverInteractionKind.HOVER_TARGET_ONLY })
.state("isOpen"),
"Tooltip should be closed",
).toBe(false);
async function assertTooltipClosed() {
// Tooltip close is driven by a `useEffect` inside PopoverNext that syncs internal
// state when `disabled` flips true, so the DOM unmount lands in a later tick.
// Query the document directly because the Enzyme wrapper can hold stale references.
await waitFor(() => {
expect(
document.querySelectorAll(`.${Classes.TOOLTIP}`),
"Tooltip should be closed",
).toHaveLength(0);
});
}
});

Expand All @@ -486,21 +490,21 @@ describe("ContextMenu", () => {
const INNER_TOOLTIP_CONTENT = "goodbye";
const CTX_MENU_CLASSNAME = "test-ctx-menu";

it("closes inner tooltip when menu opens (after hovering inner target)", () => {
it("closes inner tooltip when menu opens (after hovering inner target)", async () => {
const wrapper = mountTestCase();
wrapper.find(`.${OUTER_TARGET_CLASSNAME}`).simulate("mouseenter");
openTooltip(wrapper);
expect(wrapper.find(`.${Classes.TOOLTIP}`), "tooltip should be open").toHaveLength(1);
expect(wrapper.find(TOOLTIP_SELECTOR), "tooltip should be open").toHaveLength(1);
openCtxMenu(wrapper);
// this assertion is difficult to test, but we know that the tooltip eventually does close in manual testing
expect(
wrapper
.find(Popover)
.find({ interactionKind: PopoverInteractionKind.HOVER_TARGET_ONLY })
.first()
.state("isOpen"),
"Tooltip should be closed",
).toBe(false);
// Tooltip close is driven by a `useEffect` inside PopoverNext that syncs internal
// state when `disabled` flips true, so the DOM unmount lands in a later tick.
// Query the document directly because the Enzyme wrapper can hold stale references.
await waitFor(() => {
expect(
document.querySelectorAll(`.${Classes.TOOLTIP}`),
"Tooltip should be closed",
).toHaveLength(0);
});
const ctxMenuPopover = wrapper.find(`.${Classes.CONTEXT_MENU_POPOVER}`).hostNodes();
expect(ctxMenuPopover.exists(), "ContextMenu popover should be open").toBe(true);
closeCtxMenu(wrapper);
Expand All @@ -515,7 +519,7 @@ describe("ContextMenu", () => {
// this assertion is difficult to test, but we know that the tooltip eventually does close in manual testing
// assert.isFalse(
// wrapper
// .find(Popover)
// .find(PopoverNext)
// .find({ interactionKind: PopoverInteractionKind.HOVER_TARGET_ONLY })
// .last()
// .state("isOpen"),
Expand Down
10 changes: 6 additions & 4 deletions packages/core/src/components/context-menu/contextMenuPopover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ import classNames from "classnames";
import { memo, useCallback } from "react";

import { Classes, DISPLAYNAME_PREFIX } from "../../common";
import { Popover } from "../popover/popover";
import type { PopoverTargetProps } from "../popover/popoverSharedProps";
import { PopoverNext } from "../popover-next/popoverNext";
import { popoverPropsToNextProps } from "../popover-next/popoverNextMigrationUtils";
import { Portal } from "../portal/portal";

import type { ContextMenuPopoverOptions, Offset } from "./contextMenuShared";
Expand Down Expand Up @@ -73,11 +74,11 @@ export const ContextMenuPopover = memo(function ContextMenuPopover(props: Contex
);

return (
<Popover
<PopoverNext
placement="right-start"
rootBoundary={rootBoundary}
transitionDuration={transitionDuration}
{...popoverProps}
{...popoverPropsToNextProps(popoverProps)}
content={
// this prevents right-clicking inside our context menu
<div onContextMenu={cancelContextMenu}>{content}</div>
Expand All @@ -88,7 +89,8 @@ export const ContextMenuPopover = memo(function ContextMenuPopover(props: Contex
key={getPopoverKey(targetOffset)}
hasBackdrop={true}
backdropProps={{ className: Classes.CONTEXT_MENU_BACKDROP }}
minimal={true}
animation="minimal"
arrow={false}
onInteraction={handleInteraction}
popoverClassName={classNames(Classes.CONTEXT_MENU_POPOVER, popoverClassName, {
[Classes.DARK]: isDarkTheme,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
* limitations under the License.
*/

import { waitFor } from "@testing-library/dom";

import { afterAll, assert, beforeAll, beforeEach, describe, it } from "@blueprintjs/test-commons/vitest";
import { dispatchMouseEvent } from "@blueprintjs/test-commons/vitest-utils";

Expand Down Expand Up @@ -70,8 +72,10 @@ describe("showContextMenu() + hideContextMenu()", () => {
document.body.appendChild(containerElement);
});

beforeEach(() => {
assertMenuState(false);
beforeEach(async () => {
// The prior test's dismissal may still be flushing through React when the next test starts;
// poll briefly so we don't fail on leftover overlay state.
await waitFor(() => assertMenuState(false));
});

afterAll(() => {
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export { Text, type TextProps } from "./text/text";
// eslint-disable-next-line @typescript-eslint/no-deprecated
export { PanelStack, type PanelStackProps, PanelStack2, type PanelStack2Props } from "./panel-stack/panelStack";
export type { Panel, PanelProps } from "./panel-stack/panelTypes";
// eslint-disable-next-line @typescript-eslint/no-deprecated
export { Popover } from "./popover/popover";
export { PopoverAnimation, PopoverInteractionKind, type PopoverProps } from "./popover/popoverProps";
export { PopoverPosition } from "./popover/popoverPosition";
Expand Down
6 changes: 3 additions & 3 deletions packages/core/src/components/menu/menu.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,12 @@ prop to define **MenuItem** content.
## Dropdowns

**Menu** only renders a static list container element. To make an interactive dropdown menu, you may leverage
[**Popover**](#core/components/popover) and specify a **Menu** as the `content` property:
[**PopoverNext**](#core/components/popover-next) and specify a **Menu** as the `content` property:

```tsx
<Popover content={<Menu>...</Menu>} placement="bottom">
<PopoverNext content={<Menu>...</Menu>} placement="bottom">
<Button alignText="start" icon="applications" endIcon="caret-down" text="Open with..." />
</Popover>
</PopoverNext>
```

Some tips for designing dropdown menus:
Expand Down
Loading