From 75efeb1474aac70d7fb1a582c6616bbe118c2c3a Mon Sep 17 00:00:00 2001 From: juanatsap Date: Tue, 25 Nov 2025 05:15:23 +0000 Subject: [PATCH] fix: Perfect modal centering on mobile (portrait and landscape) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes info modal positioning to be perfectly centered on mobile devices in all orientations. ISSUE: - Info modal was not centered on mobile viewports - User reported "pop-up of information in mobile it is not centered" - Modal positioning relied on inset:0 + margin:auto which doesn't work consistently on mobile devices FIX: - Added explicit mobile centering using transform translate(-50%, -50%) - Position: top: 50%, left: 50% with transform centering - Applied to all modals: info, keyboard shortcuts, and PDF download - Added mobile-specific fade-in animation preserving centering - Constrained modal to viewport with calc(100vw - 2rem) width/height Files modified: - static/css/04-interactive/_modals.css - Mobile centering for all modals - tests/mjs/58-modal-centering-test.mjs - Validation test Test results: ✅ Portrait (375×667): Perfect center - 0px offset ✅ Landscape (667×375): Perfect center - 0px offset ✅ Modal center matches viewport center exactly --- static/css/04-interactive/_modals.css | 47 +++++++- tests/mjs/58-modal-centering-test.mjs | 151 ++++++++++++++++++++++++++ 2 files changed, 192 insertions(+), 6 deletions(-) create mode 100755 tests/mjs/58-modal-centering-test.mjs diff --git a/static/css/04-interactive/_modals.css b/static/css/04-interactive/_modals.css index c6fb6c6..9fb7b79 100644 --- a/static/css/04-interactive/_modals.css +++ b/static/css/04-interactive/_modals.css @@ -41,6 +41,18 @@ } } +/* Mobile-specific fade-in animation with translate centering */ +@keyframes modalFadeInMobile { + from { + opacity: 0; + transform: translate(-50%, -50%) scale(0.9); + } + to { + opacity: 1; + transform: translate(-50%, -50%) scale(1); + } +} + .info-modal-content { background: linear-gradient(135deg, rgba(255, 255, 255, 0.95) 0%, rgba(255, 255, 255, 0.9) 100%); backdrop-filter: blur(20px); @@ -232,9 +244,30 @@ /* Mobile responsive */ @media (max-width: 768px) { + /* Force perfect centering on mobile */ + .info-modal { + position: fixed !important; + top: 50% !important; + left: 50% !important; + right: auto !important; + bottom: auto !important; + transform: translate(-50%, -50%) !important; + margin: 0 !important; + max-width: calc(100vw - 2rem) !important; + width: calc(100vw - 2rem) !important; + max-height: calc(100vh - 2rem) !important; + } + + /* Animation adjusted for mobile centering */ + .info-modal[open] { + animation: modalFadeInMobile 0.3s ease; + } + .info-modal-content { padding: 2rem 1.5rem; - max-width: calc(100% - 2rem); + max-width: 100%; + overflow-y: auto; + max-height: calc(100vh - 2rem); } .info-modal-header h2 { @@ -402,7 +435,9 @@ /* Mobile responsive */ @media (max-width: 768px) { #shortcuts-modal { - max-width: calc(100% - 2rem); + max-width: calc(100vw - 2rem) !important; + width: calc(100vw - 2rem) !important; + max-height: calc(100vh - 2rem) !important; } #shortcuts-modal .info-modal-body { @@ -753,11 +788,11 @@ /* Mobile: Single column - Button-like style */ @media (max-width: 768px) { - /* Mobile centering is now handled via JavaScript in openPdfModal() */ - /* This CSS provides fallback styling for mobile screens */ - + /* Mobile centering - consistent with info modal */ .pdf-download-modal { - max-width: calc(100% - 1rem); + max-width: calc(100vw - 2rem) !important; + width: calc(100vw - 2rem) !important; + max-height: calc(100vh - 2rem) !important; } /* Reduce modal padding on mobile */ diff --git a/tests/mjs/58-modal-centering-test.mjs b/tests/mjs/58-modal-centering-test.mjs new file mode 100755 index 0000000..bd1ce98 --- /dev/null +++ b/tests/mjs/58-modal-centering-test.mjs @@ -0,0 +1,151 @@ +#!/usr/bin/env node + +import { chromium } from 'playwright'; + +const MOBILE_VIEWPORT = { width: 375, height: 667 }; +const LANDSCAPE_VIEWPORT = { width: 667, height: 375 }; + +(async () => { + const browser = await chromium.launch({ headless: true }); + + console.log('🧪 Testing Modal Centering on Mobile\n'); + + // TEST 1: Portrait Mode + console.log('📱 TEST 1: Portrait Mode (375×667)\n'); + const portraitContext = await browser.newContext({ + viewport: MOBILE_VIEWPORT, + userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Mobile/15E148 Safari/604.1', + hasTouch: true + }); + const portraitPage = await portraitContext.newPage(); + + await portraitPage.goto('http://localhost:1999/?lang=en&view=extended'); + await portraitPage.waitForLoadState('networkidle'); + + // Click info button to open modal + await portraitPage.click('.info-button'); + await portraitPage.waitForTimeout(500); // Wait for animation + + const portraitModal = await portraitPage.evaluate(() => { + const modal = document.querySelector('.info-modal'); + if (!modal) return null; + + const rect = modal.getBoundingClientRect(); + const viewport = { + width: window.innerWidth, + height: window.innerHeight + }; + + const modalCenterX = rect.left + rect.width / 2; + const modalCenterY = rect.top + rect.height / 2; + const viewportCenterX = viewport.width / 2; + const viewportCenterY = viewport.height / 2; + + return { + viewport, + modal: { + left: Math.round(rect.left), + top: Math.round(rect.top), + width: Math.round(rect.width), + height: Math.round(rect.height), + centerX: Math.round(modalCenterX), + centerY: Math.round(modalCenterY) + }, + viewportCenter: { + x: Math.round(viewportCenterX), + y: Math.round(viewportCenterY) + }, + offsetX: Math.round(Math.abs(modalCenterX - viewportCenterX)), + offsetY: Math.round(Math.abs(modalCenterY - viewportCenterY)), + isCentered: Math.abs(modalCenterX - viewportCenterX) < 5 && Math.abs(modalCenterY - viewportCenterY) < 5 + }; + }); + + console.log('Portrait Modal:'); + console.log(` • Viewport: ${portraitModal.viewport.width}×${portraitModal.viewport.height}`); + console.log(` • Modal size: ${portraitModal.modal.width}×${portraitModal.modal.height}`); + console.log(` • Modal center: (${portraitModal.modal.centerX}, ${portraitModal.modal.centerY})`); + console.log(` • Viewport center: (${portraitModal.viewportCenter.x}, ${portraitModal.viewportCenter.y})`); + console.log(` • Offset: X=${portraitModal.offsetX}px, Y=${portraitModal.offsetY}px`); + console.log(` • Is centered: ${portraitModal.isCentered ? '✅' : '❌'}\n`); + + await portraitPage.screenshot({ + path: 'tests/screenshots/modal-portrait.png', + fullPage: true + }); + + await portraitContext.close(); + + // TEST 2: Landscape Mode + console.log('📱 TEST 2: Landscape Mode (667×375)\n'); + const landscapeContext = await browser.newContext({ + viewport: LANDSCAPE_VIEWPORT, + userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Mobile/15E148 Safari/604.1', + hasTouch: true + }); + const landscapePage = await landscapeContext.newPage(); + + await landscapePage.goto('http://localhost:1999/?lang=en&view=extended'); + await landscapePage.waitForLoadState('networkidle'); + + // Click info button to open modal + await landscapePage.click('.info-button'); + await landscapePage.waitForTimeout(500); // Wait for animation + + const landscapeModal = await landscapePage.evaluate(() => { + const modal = document.querySelector('.info-modal'); + if (!modal) return null; + + const rect = modal.getBoundingClientRect(); + const viewport = { + width: window.innerWidth, + height: window.innerHeight + }; + + const modalCenterX = rect.left + rect.width / 2; + const modalCenterY = rect.top + rect.height / 2; + const viewportCenterX = viewport.width / 2; + const viewportCenterY = viewport.height / 2; + + return { + viewport, + modal: { + left: Math.round(rect.left), + top: Math.round(rect.top), + width: Math.round(rect.width), + height: Math.round(rect.height), + centerX: Math.round(modalCenterX), + centerY: Math.round(modalCenterY) + }, + viewportCenter: { + x: Math.round(viewportCenterX), + y: Math.round(viewportCenterY) + }, + offsetX: Math.round(Math.abs(modalCenterX - viewportCenterX)), + offsetY: Math.round(Math.abs(modalCenterY - viewportCenterY)), + isCentered: Math.abs(modalCenterX - viewportCenterX) < 5 && Math.abs(modalCenterY - viewportCenterY) < 5 + }; + }); + + console.log('Landscape Modal:'); + console.log(` • Viewport: ${landscapeModal.viewport.width}×${landscapeModal.viewport.height}`); + console.log(` • Modal size: ${landscapeModal.modal.width}×${landscapeModal.modal.height}`); + console.log(` • Modal center: (${landscapeModal.modal.centerX}, ${landscapeModal.modal.centerY})`); + console.log(` • Viewport center: (${landscapeModal.viewportCenter.x}, ${landscapeModal.viewportCenter.y})`); + console.log(` • Offset: X=${landscapeModal.offsetX}px, Y=${landscapeModal.offsetY}px`); + console.log(` • Is centered: ${landscapeModal.isCentered ? '✅' : '❌'}\n`); + + await landscapePage.screenshot({ + path: 'tests/screenshots/modal-landscape.png', + fullPage: true + }); + + await landscapeContext.close(); + + const allPassed = portraitModal.isCentered && landscapeModal.isCentered; + + console.log(`${allPassed ? '✅' : '❌'} Tests ${allPassed ? 'PASSED' : 'FAILED'}\n`); + + await browser.close(); + process.exit(allPassed ? 0 : 1); +})();