From 80c7633699edd2995726c963bb5735a58081bfba Mon Sep 17 00:00:00 2001 From: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@sprout-oss.stage.blox.sqprod.co> Date: Thu, 25 Jun 2026 09:30:38 -0700 Subject: [PATCH 1/3] fix(composer): make composer icon-button tooltips click-through MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Toolbar tooltips rendered with pointer-events:auto, so while visible they sat over the message textarea and swallowed clicks meant for the editor beneath. Fix the click-through on the composer surface only — where labels are known-short — rather than promising it app-wide for every tooltip. Add ComposerIconButton (desktop/src/features/messages/ui): a forwardRef component owning the Tooltip -> Trigger -> Button -> Content shape that bakes pointer-events-none onto the tooltip content (the floating popup) only. The trigger keeps pointer/focus so focus-to-show still works (WCAG content-on-hover-or-focus). Because the button owns its tooltip, the override can't be forgotten and future composer icon buttons inherit it. Swap the 5 main-toolbar icon buttons (formatting toggle/close, mention, attach, spoiler) to it. The formatting sub-toolbar (Bold/Italic/lists/Quote) maps over raw - + {/* pointer-events-none keeps these label tooltips click-through so + they don't block the message textarea floating beneath them, + matching ComposerIconButton. Content only — the trigger button + keeps its pointer/focus behavior (WCAG content-on-hover-or-focus). + These formatting buttons are raw - - Formatting - + onFormattingToggle(!isFormattingOpen)} + onMouseDown={onCaptureSelection} + tooltip="Formatting" + variant={isFormattingOpen ? "default" : "ghost"} + > + + - - - - - Close formatting - + onFormattingToggle(false)} + onMouseDown={onCaptureSelection} + variant="ghost" + className="shrink-0" + tooltip="Close formatting" + > + +
- - - - - Mention someone - - - - - - Attach image - + + + + + + - - - - - Spoiler - + + + - - - - - Formatting - + onFormattingToggle(!isFormattingOpen)} + onMouseDown={onCaptureSelection} + tooltip="Formatting" + variant={isFormattingOpen ? "default" : "ghost"} + > + + )} diff --git a/desktop/tests/e2e/screenshot-tooltip-pointer-events.spec.ts b/desktop/tests/e2e/screenshot-tooltip-pointer-events.spec.ts new file mode 100644 index 000000000..abf8f7859 --- /dev/null +++ b/desktop/tests/e2e/screenshot-tooltip-pointer-events.spec.ts @@ -0,0 +1,69 @@ +import { expect, test } from "@playwright/test"; +import { installMockBridge } from "../helpers/bridge"; + +test.beforeEach(async ({ page }) => { + await installMockBridge(page); +}); + +test("composer tooltip is visible but click-through (pointer-events:none)", async ({ + page, +}) => { + await page.goto("/"); + await page.getByTestId("channel-general").click(); + await expect(page.getByTestId("chat-title")).toHaveText("general"); + + // Hover the "Mention someone" toolbar trigger to surface its tooltip. + const trigger = page.getByTestId("message-insert-mention"); + await trigger.hover(); + + // Radix renders the tooltip content in a portal as role=tooltip. + const tip = page.getByRole("tooltip", { name: "Mention someone" }); + await expect(tip).toBeVisible(); + + // The money check: computed pointer-events on the visible tooltip popup. + const pe = await tip.evaluate((el) => getComputedStyle(el).pointerEvents); + expect(pe).toBe("none"); + + await page.screenshot({ + path: "test-results/tooltip-pe/composer-tooltip-visible.png", + clip: { x: 0, y: 360, width: 900, height: 360 }, + }); + + // Prove click-through: aim the click at the tooltip's own bounding box. + // pointer-events:none means the click should fall through to whatever is + // underneath rather than being swallowed by the popup. + const box = await tip.boundingBox(); + if (!box) throw new Error("no tooltip box"); + await page.mouse.click(box.x + box.width / 2, box.y + box.height / 2); + + // After clicking through, the editor should be focusable/usable: type and + // confirm the text lands in the ProseMirror editor under the toolbar. + const editor = page.locator(".ProseMirror"); + await editor.click(); + await page.keyboard.type("clickthrough-ok"); + await expect(editor).toContainText("clickthrough-ok"); +}); + +test("formatting sub-toolbar tooltip is visible but click-through (pointer-events:none)", async ({ + page, +}) => { + await page.goto("/"); + await page.getByTestId("channel-general").click(); + await expect(page.getByTestId("chat-title")).toHaveText("general"); + + // Open the formatting sub-toolbar (Bold / Italic / lists / Quote …). + await page.getByRole("button", { name: "Toggle formatting" }).first().click(); + + // Hover a formatting icon button to surface its label tooltip. + const bold = page.getByRole("button", { name: "Bold" }); + await expect(bold).toBeVisible(); + await bold.hover(); + + // Tooltip text is "