From 1c3240a2515396b95be825252b187e93e3cefa6d Mon Sep 17 00:00:00 2001 From: Victor Vogel Date: Sat, 7 Nov 2026 23:16:21 +0000 Subject: [PATCH] fix(desktop): swiping sideways over a confirmation dialog no longer goes back or forward A horizontal trackpad swipe over an open alert dialog navigated history underneath it, because the swipe guard only skipped gestures inside role="dialog", and Base UI's AlertDialog popup is role="alertdialog" with no dialog ancestor. The guard now also skips gestures whose composed path contains role="alertdialog". Co-Authored-By: Claude Opus 5.5 --- .../src/components/HistorySwipeNavigation.browser.test.tsx | 4 +++- apps/web/src/components/HistorySwipeNavigation.tsx | 6 +++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/web/src/components/HistorySwipeNavigation.browser.test.tsx b/apps/web/src/components/HistorySwipeNavigation.browser.test.tsx index b2e6fcaaaf402..3cad5371e9bc9 100644 --- a/apps/web/src/components/HistorySwipeNavigation.browser.test.tsx +++ b/apps/web/src/components/HistorySwipeNavigation.browser.test.tsx @@ -27,17 +27,19 @@ describe("HistorySwipeNavigation", () => { ["swiping back at a shadow-root scroller's left edge navigates", "shadow", 0, -130, true], ["swiping back over a panel without overflow navigates", "none", 0, -130, true], ["a swipe that started in a closed dialog keeps the thread", "closed-dialog", 0, -130, false], + ["swiping back over an open alert dialog keeps the thread", "alertdialog", 0, -130, false], ] as const)("%s", async (_name, scroller, scrollLeft, deltaX, navigates) => { navigateHistory.mockReset(); await using screen = await render(); const panel = screen.container.appendChild(document.createElement("div")); const root = scroller.startsWith("shadow") ? panel.attachShadow({ mode: "open" }) : panel; const viewport = root.appendChild(document.createElement("div")); - const noOverflow = scroller === "none" || scroller === "closed-dialog"; + const noOverflow = scroller === "none" || scroller.endsWith("dialog"); viewport.style.cssText = `width: 200px; overflow-x: ${noOverflow ? "visible" : "auto"}`; const content = viewport.appendChild(document.createElement("div")); content.style.cssText = `height: 20px; width: ${noOverflow ? 100 : 1000}px`; viewport.scrollLeft = scrollLeft; + if (scroller === "alertdialog") panel.setAttribute("role", "alertdialog"); if (scroller === "closed-dialog") { panel.setAttribute("role", "dialog"); diff --git a/apps/web/src/components/HistorySwipeNavigation.tsx b/apps/web/src/components/HistorySwipeNavigation.tsx index 767b4285c696f..88de2402d36e0 100644 --- a/apps/web/src/components/HistorySwipeNavigation.tsx +++ b/apps/web/src/components/HistorySwipeNavigation.tsx @@ -57,7 +57,11 @@ function lockSwipeAxis( ): Exclude { const { path, sumX, sumY } = pending; if (Math.abs(sumX) <= Math.abs(sumY)) return { kind: "ignored" }; - if (path.some((node) => node instanceof Element && node.matches('[role="dialog"]'))) { + if ( + path.some( + (node) => node instanceof Element && node.matches('[role="dialog"], [role="alertdialog"]'), + ) + ) { return { kind: "ignored" }; } if (canScrollHorizontally(path, sumX)) return { kind: "ignored" };