fix(desktop): keep closed message actions from taking edit focus

Backport the independently reviewed 646f30febd436d8a10a2f531f38a4ac9d2912064 fix and its regression to published PR1 db566eb5. Keep the closed-menu production owner and browser assertions unchanged; register the test next to mention-recipients on this base.

Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Signed-off-by: Logan Johnson <loganj@squareup.com>
This commit is contained in:
Logan Johnson
2026-09-01 21:59:44 -04:00
parent db566eb57d
commit b7f124f7fc
3 changed files with 90 additions and 0 deletions

View File

@@ -77,6 +77,7 @@ export default defineConfig({
"**/remote-owned-mentions.spec.ts",
"**/mention-spacing.spec.ts",
"**/mention-recipients.spec.ts",
"**/message-edit-focus.spec.ts",
"**/team-mentions.spec.ts",
"**/persistent-agent-audience.spec.ts",
"**/relay-reconnect.spec.ts",

View File

@@ -157,6 +157,9 @@ function MoreActionsMenu({
</Tooltip>
<DropdownMenuContent
align="end"
// Exit animation keeps Radix pointer-leave handlers mounted after
// editing takes focus. A closed menu must no longer accept focus.
inert={!open}
side="top"
sideOffset={6}
onCloseAutoFocus={(event) => {

View File

@@ -0,0 +1,86 @@
import { expect, test } from "@playwright/test";
import { installMockBridge } from "../helpers/bridge";
// Hold exit presence, not composer readiness: a closed menu can still receive a
// queued pointer-leave while its CSS exit animation is mounted.
const holdExit = `
[role="menu"][data-state="closed"] { animation-play-state: paused !important; }
`;
test("closing message menu cannot reclaim editor focus on pointer leave", async ({
page,
}) => {
await installMockBridge(page);
await page.goto("/");
await page.getByTestId("channel-general").click();
await page.addStyleTag({ content: holdExit });
const input = page.getByTestId("message-input");
await input.fill("focus ownership");
await page.getByTestId("send-message").click();
const row = page
.getByTestId("message-timeline")
.getByTestId("message-row")
.last();
await expect(row).toContainText("focus ownership");
await row.hover();
await row.getByRole("button", { name: "More actions" }).click();
const edit = page.getByRole("menuitem", { name: "Edit message" });
const editElement = await edit.elementHandle();
await edit.click();
await expect(input).toHaveText("focus ownership");
// A deliberate user click establishes focus independently of RAF timing.
await input.click();
await input.press("ControlOrMeta+End");
await expect(input).toBeFocused();
await page.keyboard.type(" edit");
await expect(input).toHaveText("focus ownership edit");
const closing = await editElement?.evaluate((element) => {
const menu = element.closest('[role="menu"]');
const state = menu?.getAttribute("data-state");
const connected = element.isConnected;
element.dispatchEvent(
new PointerEvent("pointerout", {
bubbles: true,
pointerType: "mouse",
relatedTarget: document.body,
}),
);
return { state, connected };
});
expect(closing).toEqual({ state: "closed", connected: true });
await expect(input).toBeFocused();
await page.keyboard.type("ed");
await expect(input).toHaveText("focus ownership edited");
await page.getByTestId("send-message").click();
await expect
.poll(() =>
page.evaluate(() => {
const call = window.__BUZZ_E2E_COMMAND_LOG__
?.filter((entry) => entry.command === "edit_message")
.at(-1);
return (call?.payload as { input?: { content: string } })?.input
?.content;
}),
)
.toBe("focus ownership edited");
});
test("message menu Escape still restores its keyboard trigger", async ({
page,
}) => {
await installMockBridge(page);
await page.goto("/");
await page.getByTestId("channel-general").click();
const row = page
.getByTestId("message-timeline")
.getByTestId("message-row")
.last();
await row.hover();
const trigger = row.getByRole("button", { name: "More actions" });
await trigger.focus();
await trigger.press("Enter");
await expect(page.getByRole("menu")).toBeVisible();
await page.keyboard.press("Escape");
await expect(page.getByRole("menu")).toHaveCount(0);
await expect(trigger).toBeFocused();
});