fix: Perfect modal centering on mobile (portrait and landscape)

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
This commit is contained in:
juanatsap
2025-11-25 05:15:23 +00:00
parent 639a99b8ea
commit 75efeb1474
2 changed files with 192 additions and 6 deletions
+41 -6
View File
@@ -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 */
+151
View File
@@ -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);
})();