ReviewOS

also looking at this

stacks/commshq

fix(ui): keep the phone menu inside the visible viewport

#9
Merged glennmichael123 wants to merge fix/mobile-nav-dvh into main
2 files +16 -2
resources/components/CommsHQ/MarketingNavigation.stxmodified+5-2
Changes to resources/components/CommsHQ/MarketingNavigation.stx
@@ -73,11 +73,11 @@ const featureGroups = [
7373 <div class="flex gap-2 items-center sm:gap-3">
7474 <a href="/login" class="hidden lg:block font-semibold text-sm ink">Sign in</a>
7575 <a href="/register" class="hidden sm:inline-flex items-center px-4 min-h-10 font-semibold text-sm text-[var(--coral-ink)] bg-coral rounded-md focus-visible:outline focus-visible:outline-[var(--coral)] focus-visible:outline-2 focus-visible:outline-offset-2">Start free</a>
76 <details class="md:hidden relative feature-menu">
76 <details class="md:hidden relative feature-menu mobile-menu">
7777 <summary class="inline-flex items-center justify-center h-11 w-11 list-none border border-tone rounded-md focus-visible:outline focus-visible:outline-[var(--coral)] focus-visible:outline-2 cursor-pointer feature-summary ink" aria-label="Open navigation">
7878 <span class="h-5 w-5 i-hugeicons-menu-01" aria-hidden="true"></span>
7979 </summary>
80 <div class="absolute overflow-y-auto right-0 top-[calc(100%+0.65rem)] p-4 max-h-[calc(100vh-6rem)] w-[min(92vw,380px)] border border-tone rounded-xl shadow-[0_24px_70px_rgba(32,29,26,0.18)] panel">
80 <div class="absolute overflow-y-auto overscroll-contain right-0 top-[calc(100%+0.65rem)] p-4 max-h-[calc(100dvh-6rem)] w-[min(92vw,380px)] border border-tone rounded-xl shadow-[0_24px_70px_rgba(32,29,26,0.18)] panel">
8181 <p class="pb-2 px-3 font-bold text-[11px] tracking-[0.18em] uppercase coral">Features</p>
8282 @foreach(featureGroups as group)
8383 <section class="py-3 border-t border-tone first:border-0">
@@ -102,4 +102,7 @@ const featureGroups = [
102102<style>
103103 .feature-summary::-webkit-details-marker { display: none; }
104104 .feature-menu[open] > .feature-summary .feature-chevron { transform: rotate(180deg); }
105 /* The phone menu scrolls inside the viewport, so the page behind it must
106 not: otherwise the first flick past the panel's end scrolls the page. */
107 @media (max-width: 767px) { body:has(.mobile-menu[open]) { overflow: hidden; } }
105108</style>
tests/Unit/MobileLayout.test.tsmodified+11-0
Changes to tests/Unit/MobileLayout.test.ts
@@ -3,6 +3,7 @@ import { readFileSync } from 'node:fs'
33import { join } from 'node:path'
44
55const auth = readFileSync(join(import.meta.dir, '../../resources/views/layouts/auth.stx'), 'utf8')
6const nav = readFileSync(join(import.meta.dir, '../../resources/components/CommsHQ/MarketingNavigation.stx'), 'utf8')
67
78// iOS Safari zooms the page when a focused input's font-size is under 16px,
89// and leaves it zoomed and scrolled sideways afterwards. The sign-in and
@@ -19,4 +20,14 @@ describe('mobile layout contracts', () => {
1920 expect(viewport).toContain('width=device-width')
2021 expect(viewport).not.toMatch(/user-scalable=no|maximum-scale=1(?![.\d])/)
2122 })
23
24 // 100vh is the largest the viewport ever gets on iOS, so a panel capped by it
25 // runs under the browser's bottom bar; 100dvh tracks the visible height.
26 it('bounds the phone menu by the visible viewport and locks the page behind it', () => {
27 const panel = nav.match(/<details class="[^"]*mobile-menu[^"]*">[\s\S]*?<div class="([^"]*)"/)?.[1] ?? ''
28 expect(panel).toContain('max-h-[calc(100dvh-6rem)]')
29 expect(panel).not.toContain('100vh')
30 expect(panel).toContain('overscroll-contain')
31 expect(nav).toContain('body:has(.mobile-menu[open]) { overflow: hidden; }')
32 })
2233})