fix(desktop): stop Leave-channel dialog from freezing the app (#1482)

Signed-off-by: Mathieu Balez <mbalez@squareup.com>
This commit is contained in:
Mat Balez
2026-07-06 14:29:31 +00:00
committed by GitHub
parent d09d7dea55
commit 668d30b0cf
2 changed files with 50 additions and 3 deletions
@@ -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 ? (
@@ -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>
@@ -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}>
+37
View File
@@ -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();
@@ -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("/");