SPB Git

spb/phi-conseil

Public

fix(menu mobile): panneau rendu par portail dans body (backdrop-filter de l'en-tête le confinait) + test de couverture d'écran

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Simon-Pierre Boucher committed (Nov 8, 2026) parent 25a9c5e Browse files

3 changed files +63 −46

added scripts/dbg-menu.mjs +5 −0
@@ -0,0 +1,5 @@
1 +import { webkit, devices } from "@playwright/test";
2 +const b = await webkit.launch(); const ctx = await b.newContext({ ...devices["iPhone 13"] }); const p = await ctx.newPage();
3 +await p.goto(process.argv[2] ?? "http://127.0.0.1:3100/", { waitUntil: "networkidle" });
4 +await p.getByRole("button", { name: "Ouvrir le menu" }).click(); await p.waitForTimeout(500);
5 +await p.screenshot({ path: process.argv[3] ?? "docs/screenshots/menu-mobile-open.png" }); await b.close(); console.log("shot ok");
modified src/components/layout/MobileNav.tsx +52 −46
@@ -1,6 +1,7 @@
1 1 "use client";
2 2
3 3 import { useEffect, useId, useRef, useState } from "react";
4 +import { createPortal } from "react-dom";
4 5 import { Button } from "@/components/ui/Button";
5 6 import { NavLinks } from "./NavLinks";
6 7
@@ -37,6 +38,8 @@ export function MobileNav({
37 38 };
38 39 }, [open]);
39 40
41 + // Le panneau est rendu dans <body> par un portail : l’en-tête utilise
42 + // backdrop-filter, qui ferait de lui le bloc conteneur d’un élément `fixed`.
40 43 return (
41 44 <div className="lg:hidden">
42 45 <button
@@ -61,54 +64,57 @@ export function MobileNav({
61 64 </svg>
62 65 </button>
63 66
64 − {open ? (
65 − <div
66 − id={panelId}
67 − role="dialog"
68 − aria-modal="true"
69 − aria-label={`Menu ${brandName}`}
70 − className="bg-ivory-100 fixed inset-0 z-50 flex flex-col motion-safe:animate-[fade-in_var(--duration-base)_var(--ease-out)]"
71 − >
72 − <div className="container-site flex items-center justify-between py-4">
73 − <span className="text-navy-900 font-serif text-xl">{brandName}</span>
74 − <button
75 − ref={closeRef}
76 − type="button"
77 − className="text-navy-900 hover:bg-navy-900/5 inline-flex h-11 w-11 items-center justify-center rounded-md"
78 − aria-label="Fermer le menu"
79 − onClick={() => setOpen(false)}
67 + {open
68 + ? createPortal(
69 + <div
70 + id={panelId}
71 + role="dialog"
72 + aria-modal="true"
73 + aria-label={`Menu ${brandName}`}
74 + className="bg-ivory-100 fixed inset-0 z-50 flex flex-col motion-safe:animate-[fade-in_var(--duration-base)_var(--ease-out)]"
80 75 >
81 − <svg
82 − viewBox="0 0 24 24"
83 − className="h-6 w-6"
84 − fill="none"
85 − stroke="currentColor"
86 − strokeWidth="1.6"
87 − strokeLinecap="round"
88 − aria-hidden="true"
76 + <div className="container-site flex items-center justify-between py-4">
77 + <span className="text-navy-900 font-serif text-xl">{brandName}</span>
78 + <button
79 + ref={closeRef}
80 + type="button"
81 + className="text-navy-900 hover:bg-navy-900/5 inline-flex h-11 w-11 items-center justify-center rounded-md"
82 + aria-label="Fermer le menu"
83 + onClick={() => setOpen(false)}
84 + >
85 + <svg
86 + viewBox="0 0 24 24"
87 + className="h-6 w-6"
88 + fill="none"
89 + stroke="currentColor"
90 + strokeWidth="1.6"
91 + strokeLinecap="round"
92 + aria-hidden="true"
93 + >
94 + <path d="m6 6 12 12M18 6 6 18" />
95 + </svg>
96 + </button>
97 + </div>
98 + <nav
99 + aria-label="Navigation mobile"
100 + className="container-site flex-1 overflow-y-auto py-6"
89 101 >
90 − <path d="m6 6 12 12M18 6 6 18" />
91 − </svg>
92 − </button>
93 − </div>
94 − <nav
95 − aria-label="Navigation mobile"
96 − className="container-site flex-1 overflow-y-auto py-6"
97 − >
98 − <NavLinks
99 − items={items}
100 − vertical
101 − className="flex flex-col gap-1"
102 − onNavigate={() => setOpen(false)}
103 − />
104 − </nav>
105 − <div className="container-site pb-10">
106 − <Button href={cta.href} size="lg" className="w-full" onClick={() => setOpen(false)}>
107 − {cta.label}
108 − </Button>
109 − </div>
110 − </div>
111 − ) : null}
102 + <NavLinks
103 + items={items}
104 + vertical
105 + className="flex flex-col gap-1"
106 + onNavigate={() => setOpen(false)}
107 + />
108 + </nav>
109 + <div className="container-site pb-10">
110 + <Button href={cta.href} size="lg" className="w-full" onClick={() => setOpen(false)}>
111 + {cta.label}
112 + </Button>
113 + </div>
114 + </div>,
115 + document.body,
116 + )
117 + : null}
112 118 </div>
113 119 );
114 120 }
modified tests/e2e/mobile-menu.spec.ts +6 −0
@@ -9,6 +9,12 @@ test.describe("menu mobile", () => {
9 9 await opener.click();
10 10 const dialog = page.getByRole("dialog");
11 11 await expect(dialog).toBeVisible();
12 + // Le panneau doit couvrir tout l’écran (régression : confiné à l’en-tête par backdrop-filter)
13 + const box = await dialog.boundingBox();
14 + const vp = page.viewportSize()!;
15 + expect(box?.width).toBeGreaterThanOrEqual(vp.width - 1);
16 + expect(box?.height).toBeGreaterThanOrEqual(vp.height - 1);
17 + await expect(dialog.getByRole("link", { name: "Nos services" })).toBeVisible();
12 18 await expect(page.getByRole("button", { name: "Fermer le menu" })).toBeFocused();
13 19 await page.keyboard.press("Escape");
14 20 await expect(dialog).toBeHidden();
15 21
Menu

Theme