Skip to content
Merged
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
16 changes: 13 additions & 3 deletions desktop/src/features/sidebar/ui/CustomChannelSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,11 @@ export function ChannelGroupSection({
items.length > 0 ? (
<SidebarMenu data-testid={listTestId}>
{items.map((channel) => (
<ContextMenu key={channel.id}>
// modal={false}: menu items (e.g. Leave channel) open a modal
// AlertDialog. A modal ContextMenu would leave `pointer-events: none`
// stuck on <body> when it closes as the dialog mounts, freezing the
// whole app. Non-modal avoids installing that body guard entirely.
<ContextMenu key={channel.id} modal={false}>
<ContextMenuTrigger asChild>
<SidebarMenuItem className="content-visibility-auto-row">
{draggable ? (
Expand Down Expand Up @@ -572,7 +576,10 @@ export function CustomChannelSection({
isDragging && "opacity-30",
)}
>
<ContextMenu>
{/* modal={false}: Rename/Delete section open a modal dialog;
a modal ContextMenu would leave `pointer-events: none` stuck on
<body> after it closes, freezing the app. */}
<ContextMenu modal={false}>
<ContextMenuTrigger asChild>
<div className="relative" {...dragHandleProps}>
<SidebarGroupLabel asChild>
Expand Down Expand Up @@ -673,7 +680,10 @@ export function CustomChannelSection({
{channels.length > 0 ? (
<SidebarMenu>
{channels.map((channel) => (
<ContextMenu key={channel.id}>
// modal={false}: see note on the other channel ContextMenu
// above — avoids the pointer-events lockup when Leave
// channel's AlertDialog opens.
<ContextMenu key={channel.id} modal={false}>
<ContextMenuTrigger asChild>
<SidebarMenuItem>
<DraggableChannelRow channelId={channel.id}>
Expand Down
37 changes: 37 additions & 0 deletions desktop/tests/e2e/sidebar.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,20 @@ async function storedSidebarWidth(page: Page) {
);
}

// Regression guard for the "Leave channel" lockup: opening a modal AlertDialog
// from a modal Radix ContextMenu leaves `pointer-events: none` stuck on <body>
// after the dialog closes, freezing the whole app. The fix makes the sidebar
// context menus non-modal. This asserts the app is still interactive.
async function expectAppClickable(page: Page) {
await expect
.poll(() =>
page.evaluate(() => getComputedStyle(document.body).pointerEvents),
)
.not.toBe("none");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
}

async function dragSidebarRail(page: Page, deltaX: number) {
const sidebarRail = page.locator('[data-sidebar="rail"]');
await expect(sidebarRail).toBeVisible();
Expand All @@ -41,6 +55,29 @@ async function dragSidebarRail(page: Page, deltaX: number) {
await page.mouse.up();
}

test("leaving a channel from the context menu never freezes the app", async ({
page,
}) => {
await page.goto("/");
await expect(page.getByTestId("app-sidebar")).toBeVisible();

// Cancel path: dialog opens from the context menu, then is dismissed.
await page.getByTestId("channel-random").click({ button: "right" });
await page.getByRole("menuitem", { name: "Leave channel" }).click();
await expect(page.getByRole("alertdialog")).toBeVisible();
await page.getByRole("button", { name: "Cancel" }).click();
await expect(page.getByRole("alertdialog")).toHaveCount(0);
await expectAppClickable(page);

// Confirm path: same overlay lifecycle, plus the leave mutation.
await page.getByTestId("channel-random").click({ button: "right" });
await page.getByRole("menuitem", { name: "Leave channel" }).click();
await expect(page.getByRole("alertdialog")).toBeVisible();
await page.getByRole("button", { name: "Leave" }).click();
await expect(page.getByRole("alertdialog")).toHaveCount(0);
await expectAppClickable(page);
});

test("fades the pinned sidebar chrome edges", async ({ page }) => {
await page.goto("/");

Expand Down
Loading