Skip to content

Commit 09e9a9c

Browse files
committed
fix(ContextMenu): not opening under certain conditions
1 parent e7ead01 commit 09e9a9c

5 files changed

Lines changed: 50 additions & 28 deletions

File tree

.changeset/short-spiders-decide.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"bits-ui": patch
3+
---
4+
5+
fix(ContextMenu): not opening under certain conditions

packages/bits-ui/src/lib/bits/menu/menu.svelte.ts

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,9 @@ import { DOMTypeahead } from "$lib/internal/dom-typeahead.svelte.js";
4949
import { RovingFocusGroup } from "$lib/internal/roving-focus-group.js";
5050
import { GraceArea } from "$lib/internal/grace-area.svelte.js";
5151
import { OpenChangeComplete } from "$lib/internal/open-change-complete.js";
52-
import { getTopMostDismissableLayer } from "../utilities/dismissible-layer/use-dismissable-layer.svelte.js";
5352

5453
export const CONTEXT_MENU_TRIGGER_ATTR = "data-context-menu-trigger";
54+
export const CONTEXT_MENU_CONTENT_ATTR = "data-context-menu-content";
5555

5656
const MenuRootContext = new Context<MenuRootState>("Menu.Root");
5757
const MenuMenuContext = new Context<MenuMenuState>("Menu.Root | Menu.Sub");
@@ -1177,20 +1177,6 @@ export class ContextMenuTriggerState {
11771177
oncontextmenu(e: BitsMouseEvent) {
11781178
if (e.defaultPrevented || this.opts.disabled.current) return;
11791179

1180-
const topMostLayer = getTopMostDismissableLayer();
1181-
1182-
if (topMostLayer) {
1183-
const topLayerRef = topMostLayer[0].opts.ref.current;
1184-
const topLayerRefContainsTrigger = topLayerRef?.contains(this.opts.ref.current);
1185-
1186-
if (
1187-
!topLayerRefContainsTrigger &&
1188-
!topLayerRef?.hasAttribute?.("data-context-menu-content")
1189-
) {
1190-
return;
1191-
}
1192-
}
1193-
11941180
this.#clearLongPressTimer();
11951181
this.#handleOpen(e);
11961182
e.preventDefault();

packages/bits-ui/src/lib/bits/utilities/dismissible-layer/use-dismissable-layer.svelte.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,12 @@ import { type EventCallback } from "$lib/internal/events.js";
1414
import { debounce } from "$lib/internal/debounce.js";
1515
import { noop } from "$lib/internal/noop.js";
1616
import { getOwnerDocument, isOrContainsTarget } from "$lib/internal/elements.js";
17-
import { isElement } from "$lib/internal/is.js";
17+
import { isElementOrSVGElement } from "$lib/internal/is.js";
1818
import { isClickTrulyOutside } from "$lib/internal/dom.js";
19+
import {
20+
CONTEXT_MENU_CONTENT_ATTR,
21+
CONTEXT_MENU_TRIGGER_ATTR,
22+
} from "$lib/bits/menu/menu.svelte.js";
1923

2024
globalThis.bitsDismissableLayers ??= new Map<
2125
DismissibleLayerState,
@@ -246,9 +250,15 @@ function isResponsibleLayer(node: HTMLElement): boolean {
246250
}
247251

248252
function isValidEvent(e: PointerEvent, node: HTMLElement): boolean {
249-
if ("button" in e && e.button > 0) return false;
250253
const target = e.target;
251-
if (!isElement(target)) return false;
254+
if (!isElementOrSVGElement(target)) return false;
255+
256+
const targetIsContextMenuTrigger = Boolean(target.closest(`[${CONTEXT_MENU_TRIGGER_ATTR}]`));
257+
if ("button" in e && e.button > 0 && !targetIsContextMenuTrigger) return false;
258+
259+
const nodeIsContextMenu = Boolean(node.closest(`[${CONTEXT_MENU_CONTENT_ATTR}]`));
260+
if (targetIsContextMenuTrigger && nodeIsContextMenu) return false;
261+
252262
const ownerDocument = getOwnerDocument(target);
253263
const isValid =
254264
ownerDocument.documentElement.contains(target) &&
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<script lang="ts">
2+
import { Tooltip, ContextMenu } from "bits-ui";
3+
</script>
4+
5+
<Tooltip.Provider>
6+
<Tooltip.Root delayDuration={0}>
7+
<Tooltip.Trigger data-testid="tooltip-trigger">
8+
<ContextMenu.Root>
9+
<ContextMenu.Trigger data-testid="context-menu-trigger"
10+
>Right click me</ContextMenu.Trigger
11+
>
12+
<ContextMenu.Portal>
13+
<ContextMenu.Content data-testid="context-menu-content">
14+
<ContextMenu.Item>Item1</ContextMenu.Item>
15+
<ContextMenu.Item>Item2</ContextMenu.Item>
16+
</ContextMenu.Content>
17+
</ContextMenu.Portal>
18+
</ContextMenu.Root>
19+
</Tooltip.Trigger>
20+
<Tooltip.Content data-testid="tooltip-content">Tooltip content</Tooltip.Content>
21+
</Tooltip.Root>
22+
</Tooltip.Provider>

tests/src/tests/context-menu/context-menu.browser.test.ts

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import ContextMenuForceMountTest from "./context-menu-force-mount-test.svelte";
99
import { expectExists, expectNotExists } from "../browser-utils";
1010
import ContextMenuIntegrationTest from "./context-menu-integration-test.svelte";
1111
import ContextMenuNestedTest from "./context-menu-nested-test.svelte";
12+
import ContextMenuTooltipTest from "./context-menu-tooltip-test.svelte";
1213

1314
const kbd = getTestKbd();
1415

@@ -428,16 +429,6 @@ it("calls `onValueChange` when the value of the checkbox group changes", async (
428429
expect(onValueChange).toHaveBeenCalledWith(["2"]);
429430
});
430431

431-
it("should not open when right-clicked while another floating layer is open that is not a context menu", async () => {
432-
render(ContextMenuIntegrationTest);
433-
await page.getByTestId("dropdown-trigger").click();
434-
const dropdownContent = page.getByTestId("dropdown-content");
435-
await expectExists(dropdownContent);
436-
await page.getByTestId("context-trigger-1").click({ button: "right" });
437-
await expectNotExists(page.getByTestId("context-content-1"));
438-
await expectExists(dropdownContent);
439-
});
440-
441432
it("should allow switching between context menus via right-click", async () => {
442433
render(ContextMenuIntegrationTest);
443434
await page.getByTestId("context-trigger-1").click({ button: "right" });
@@ -476,3 +467,11 @@ it("should allow overriding the pointer events style", async () => {
476467
await trigger.click({ button: "right", force: true });
477468
await expectNotExists(page.getByTestId("content"));
478469
});
470+
471+
it("should open when right clicked inside a tooltip trigger", async () => {
472+
render(ContextMenuTooltipTest);
473+
474+
await page.getByTestId("tooltip-trigger").hover();
475+
await page.getByTestId("context-menu-trigger").click({ button: "right" });
476+
await expectExists(page.getByTestId("context-menu-content"));
477+
});

0 commit comments

Comments
 (0)