Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/shadcn/src/menu/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ export const MenuTrigger = (
} else {
return (
<ShadCNComponents.DropdownMenu.DropdownMenuTrigger
// Open on click so pressing a draggable trigger can start a drag.
// Also skip Base UI's pointer tracking: otherwise it ignores the click
// because it expects the preceding mouse-down to have opened the menu.
onPointerDown={(event) => event.preventBaseUIHandler()}
onMouseDown={(event) => event.preventBaseUIHandler()}
render={children as ReactElement}
/>
);
Expand Down
87 changes: 86 additions & 1 deletion tests/src/end-to-end/shadcn/shadcn.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import App from "@examples/01-basic/09-shadcn/src/App";
// to bootstrap Tailwind v4 + the ShadCN theme variables. Without it the
// ShadCN UI components render unstyled (no popovers, no borders, no theme).
import "@examples/01-basic/09-shadcn/tailwind.css";
import { beforeEach, describe, test } from "vite-plus/test";
import { beforeEach, describe, expect, test, vi } from "vite-plus/test";
import { render } from "vitest-browser-react";
import { userEvent } from "../../utils/context.js";
import {
Expand All @@ -26,6 +26,17 @@ import {
} from "../../utils/mouse.js";
import { executeSlashCommand } from "../../utils/slashmenu.js";

// ShadCN portals the dropdown outside .bn-side-menu.
const DRAG_HANDLE_MENU_SELECTOR = ".bn-drag-handle-menu";

async function waitForMenuUpdates() {
// Base UI schedules mouse-down opening in an animation frame. Wait through
// the following frame so absence assertions also observe deferred updates.
await new Promise<void>((resolve) => {
requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
});
}

beforeEach(async () => {
await render(<App />);
await waitForSelector(EDITOR_SELECTOR);
Expand Down Expand Up @@ -98,6 +109,80 @@ describe("Check ShadCN UI", () => {
"shadcn-drag-handle-menu",
);
});
test("Drag handle menu opens on release instead of press", async () => {
await focusOnEditor();
await waitForSelector(PARAGRAPH_SELECTOR);
await moveMouseOverElement(PARAGRAPH_SELECTOR);

const dragHandle = await waitForSelector(DRAG_HANDLE_SELECTOR);
await moveMouseOverElement(dragHandle);
await mouseSequence([{ type: "down" }]);

await waitForMenuUpdates();
expect(document.querySelector(DRAG_HANDLE_MENU_SELECTOR)).toBeNull();

await mouseSequence([{ type: "up" }]);
await waitForSelector(DRAG_HANDLE_MENU_SELECTOR);
});
test.each(["click", "Enter", "Space"] as const)(
"Dragging keeps the menu closed and allows a subsequent %s",
async (activation) => {
await focusOnEditor();
await waitForSelector(PARAGRAPH_SELECTOR);
await moveMouseOverElement(PARAGRAPH_SELECTOR);

const dragHandle = await waitForSelector(DRAG_HANDLE_SELECTOR);
const dragHandleButton = dragHandle.closest("button");
if (!dragHandleButton) {
throw new Error("Drag handle button not found");
}
const onDragStart = vi.fn();
dragHandleButton.addEventListener("dragstart", onDragStart, {
once: true,
});
const dragHandleRect = getRect(dragHandle);
const x = dragHandleRect.x + dragHandleRect.width / 2;
const y = dragHandleRect.y + dragHandleRect.height / 2;
await mouseSequence([
{ type: "move", x, y },
{ type: "down" },
{ type: "move", x: x + 20, y: y + 20, steps: 5 },
]);

expect(onDragStart).toHaveBeenCalledOnce();
await waitForMenuUpdates();
expect(document.querySelector(DRAG_HANDLE_MENU_SELECTOR)).toBeNull();

// Release over the trigger itself: a mouse-up there must not be treated
// as a click after a drag, even when the block ends up in the same place.
await mouseSequence([{ type: "move", x, y, steps: 5 }, { type: "up" }]);
await waitForMenuUpdates();
expect(document.querySelector(DRAG_HANDLE_MENU_SELECTOR)).toBeNull();

await moveMouseOverElement(PARAGRAPH_SELECTOR);
const dragHandleAfterDragging =
await waitForSelector(DRAG_HANDLE_SELECTOR);
const buttonAfterDragging = dragHandleAfterDragging.closest("button");
if (!buttonAfterDragging) {
throw new Error("Drag handle button not found");
}
switch (activation) {
case "click":
await moveMouseOverElement(buttonAfterDragging);
await mouseSequence([{ type: "down" }, { type: "up" }]);
break;
case "Enter":
buttonAfterDragging.focus();
await userEvent.keyboard("{Enter}");
break;
case "Space":
buttonAfterDragging.focus();
await userEvent.keyboard(" ");
break;
}
await waitForSelector(DRAG_HANDLE_MENU_SELECTOR);
},
);
test("Check image toolbar", async () => {
await focusOnEditor();
await executeSlashCommand("image");
Expand Down