From f6aee8733c3e68136c866394c3e6aed99ae04a68 Mon Sep 17 00:00:00 2001 From: Pedro Flores <98963275+PedroFlores199@users.noreply.github.com> Date: Sat, 19 Sep 2026 09:45:37 +0200 Subject: [PATCH] Fix consent dialog covered by the mobile tab bar (#3398) --- app/components/MobileNav.browser.test.tsx | 88 +++++++++++++++++++ app/styles/common.css | 8 ++ ...-09-16-consent-dialog-behind-mobile-nav.md | 4 + 3 files changed, 100 insertions(+) create mode 100644 app/components/MobileNav.browser.test.tsx create mode 100644 changelog/2026-09-16-consent-dialog-behind-mobile-nav.md diff --git a/app/components/MobileNav.browser.test.tsx b/app/components/MobileNav.browser.test.tsx new file mode 100644 index 000000000..2e4df9bf0 --- /dev/null +++ b/app/components/MobileNav.browser.test.tsx @@ -0,0 +1,88 @@ +import type * as React from "react"; +import { createMemoryRouter, RouterProvider } from "react-router"; +import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; +import { page } from "vitest/browser"; +import { render } from "vitest-browser-react"; +import { MobileNav } from "./MobileNav"; + +let cleanupFns: Array<() => void> = []; + +beforeEach(async () => { + await page.viewport(375, 667); +}); + +afterEach(() => { + for (const cleanup of cleanupFns) { + cleanup(); + } + cleanupFns = []; +}); + +describe("MobileNav", () => { + test("does not cover the consent dialog's buttons", async () => { + await render(withRouter()); + const bar = mobileNav(); + await vi.waitFor(() => expect(bar.matches(":popover-open")).toBe(true)); + + const consentButton = renderConsentDialogStandIn(); + + expect(topmostElementAt(consentButton)).toBe(consentButton); + }); +}); + +function withRouter(element: React.ReactElement) { + const router = createMemoryRouter([{ path: "*", element }], { + initialEntries: ["/"], + }); + return ; +} + +function mobileNav() { + const bar = document.getElementById("mobile-nav"); + if (!bar) throw new Error("no mobile nav rendered"); + return bar; +} + +/** + * Stands in for the consent dialog: a full screen box of the consent platform's + * own making, holding the buttons at its bottom edge, right where the bar is. + */ +function renderConsentDialogStandIn() { + const sheet = document.createElement("style"); + sheet.textContent = `.qc-cmp-cleanslate.qc-cmp-cleanslate { + position: fixed; + inset: 0; + height: 100%; + z-index: 2147483647; + display: flex; + align-items: flex-end; + }`; + document.head.append(sheet); + + const container = document.createElement("div"); + container.id = "qc-cmp2-container"; + container.innerHTML = + '
' + + '' + + "
"; + document.body.prepend(container); + + cleanupFns.push(() => { + container.remove(); + sheet.remove(); + }); + + const button = container.querySelector("button"); + if (!button) throw new Error("no stand-in button rendered"); + + return button; +} + +function topmostElementAt(element: HTMLElement) { + const rect = element.getBoundingClientRect(); + + return document.elementFromPoint( + rect.left + rect.width / 2, + rect.top + rect.height / 2, + ); +} diff --git a/app/styles/common.css b/app/styles/common.css index f21a9a420..31f9d8109 100644 --- a/app/styles/common.css +++ b/app/styles/common.css @@ -689,6 +689,14 @@ } } +/* the consent dialog is drawn outside the top layer, where the tab bar would cover its buttons */ +@media screen and (max-width: 599px) { + body:not(:has(.planner)) #qc-cmp2-container .qc-cmp-cleanslate { + bottom: calc(var(--layout-nav-height) + env(safe-area-inset-bottom)); + height: auto; + } +} + .publift-widget-sticky_footer-container:not(.closed) { border-top: 1.5px solid var(--color-border); } diff --git a/changelog/2026-09-16-consent-dialog-behind-mobile-nav.md b/changelog/2026-09-16-consent-dialog-behind-mobile-nav.md new file mode 100644 index 000000000..8ace6c53c --- /dev/null +++ b/changelog/2026-09-16-consent-dialog-behind-mobile-nav.md @@ -0,0 +1,4 @@ +--- +type: bug +--- +Fixed the privacy consent dialog's buttons being covered by the bottom navigation on mobile