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