fix: prevent FAB button overflow on very small screens (iPhone 13 mini)
- Add media query for screens ≤400px (iPhone 13 mini = 375px) - Reduce button size to 34px and icon size to 16px - Recalculate 6-button positions with 6px gaps (234px total width) - Ensures buttons stay centered within narrow viewports
This commit is contained in:
@@ -379,6 +379,95 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Very Small Screens: iPhone 13 mini (375px) and smaller
|
||||
Tighter button spacing to prevent overflow
|
||||
======================================== */
|
||||
@media (max-width: 400px) {
|
||||
/* Smaller buttons for very small screens */
|
||||
.download-btn,
|
||||
.print-friendly-btn,
|
||||
.fixed-btn.contact-btn,
|
||||
.shortcuts-btn,
|
||||
.info-button,
|
||||
.back-to-top,
|
||||
.color-theme-switcher {
|
||||
width: 34px !important;
|
||||
height: 34px !important;
|
||||
}
|
||||
|
||||
/* Smaller icons */
|
||||
.download-btn iconify-icon,
|
||||
.print-friendly-btn iconify-icon,
|
||||
.fixed-btn.contact-btn iconify-icon,
|
||||
.shortcuts-btn iconify-icon,
|
||||
.info-button iconify-icon,
|
||||
.back-to-top iconify-icon,
|
||||
.color-theme-switcher iconify-icon {
|
||||
width: 16px !important;
|
||||
height: 16px !important;
|
||||
font-size: 16px !important;
|
||||
max-width: 16px !important;
|
||||
}
|
||||
|
||||
/* REAL MOBILE DEVICES: 6 buttons tightly spaced */
|
||||
/* 6 buttons * 34px + 5 gaps * 6px = 234px total, center offset = -117px */
|
||||
.is-mobile-device .download-btn {
|
||||
left: calc(50% - 117px) !important;
|
||||
}
|
||||
|
||||
.is-mobile-device .print-friendly-btn {
|
||||
left: calc(50% - 77px) !important;
|
||||
}
|
||||
|
||||
.is-mobile-device .fixed-btn.contact-btn {
|
||||
left: calc(50% - 37px) !important;
|
||||
}
|
||||
|
||||
/* Theme switcher - fourth position: +3px from center */
|
||||
.is-mobile-device .color-theme-switcher {
|
||||
left: calc(50% + 3px) !important;
|
||||
}
|
||||
|
||||
.is-mobile-device .info-button {
|
||||
left: calc(50% + 43px) !important;
|
||||
}
|
||||
|
||||
.is-mobile-device .back-to-top {
|
||||
left: calc(50% + 83px) !important;
|
||||
}
|
||||
|
||||
/* Non-mobile (desktop in mobile emulation): 7 buttons */
|
||||
/* 7 buttons * 34px + 6 gaps * 6px = 274px total, center offset = -137px */
|
||||
.download-btn {
|
||||
left: calc(50% - 137px) !important;
|
||||
}
|
||||
|
||||
.print-friendly-btn {
|
||||
left: calc(50% - 97px) !important;
|
||||
}
|
||||
|
||||
.fixed-btn.contact-btn {
|
||||
left: calc(50% - 57px) !important;
|
||||
}
|
||||
|
||||
.shortcuts-btn {
|
||||
left: calc(50% - 17px) !important;
|
||||
}
|
||||
|
||||
.color-theme-switcher {
|
||||
left: calc(50% + 23px) !important;
|
||||
}
|
||||
|
||||
.info-button {
|
||||
left: calc(50% + 63px) !important;
|
||||
}
|
||||
|
||||
.back-to-top {
|
||||
left: calc(50% + 103px) !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Mobile: Keep action bar visible (prevent hiding on scroll)
|
||||
======================================== */
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -0,0 +1,377 @@
|
||||
#!/usr/bin/env bun
|
||||
/**
|
||||
* MOBILE FAB OVERFLOW TEST
|
||||
* ========================
|
||||
* Tests that floating action buttons don't overflow on very small screens
|
||||
* like iPhone 13 mini (375px) and similar devices.
|
||||
*
|
||||
* The issue: FAB buttons were calculated for minimum 380px width,
|
||||
* causing overflow on 375px screens.
|
||||
*
|
||||
* The fix: Added @media (max-width: 400px) with tighter button spacing.
|
||||
*/
|
||||
|
||||
import { chromium } from 'playwright';
|
||||
|
||||
const URL = "http://localhost:1999";
|
||||
|
||||
async function testMobileFabOverflow() {
|
||||
console.log('🎯 MOBILE FAB OVERFLOW TEST\n');
|
||||
console.log('='.repeat(70));
|
||||
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const testResults = [];
|
||||
|
||||
try {
|
||||
// ========================================================================
|
||||
// TEST 1: iPhone 13 mini (375px) - buttons don't overflow
|
||||
// ========================================================================
|
||||
console.log("\n1️⃣ Testing iPhone 13 mini (375x812) - no overflow...");
|
||||
const context1 = await browser.newContext({
|
||||
viewport: { width: 375, height: 812 },
|
||||
deviceScaleFactor: 3,
|
||||
isMobile: true,
|
||||
hasTouch: true,
|
||||
});
|
||||
const page1 = await context1.newPage();
|
||||
|
||||
// Add is-mobile-device class to simulate real mobile
|
||||
await page1.goto(URL);
|
||||
await page1.evaluate(() => document.body.classList.add('is-mobile-device'));
|
||||
await page1.waitForTimeout(1500);
|
||||
|
||||
const iphone13Mini = await page1.evaluate(() => {
|
||||
const viewportWidth = window.innerWidth;
|
||||
const buttons = [
|
||||
'.download-btn',
|
||||
'.print-friendly-btn',
|
||||
'.fixed-btn.contact-btn',
|
||||
'.color-theme-switcher',
|
||||
'.info-button',
|
||||
'.back-to-top'
|
||||
];
|
||||
|
||||
let leftMost = Infinity;
|
||||
let rightMost = -Infinity;
|
||||
let visibleCount = 0;
|
||||
const buttonDetails = [];
|
||||
|
||||
buttons.forEach(selector => {
|
||||
const btn = document.querySelector(selector);
|
||||
if (btn) {
|
||||
const style = window.getComputedStyle(btn);
|
||||
const rect = btn.getBoundingClientRect();
|
||||
const isVisible = style.display !== 'none' && style.visibility !== 'hidden';
|
||||
|
||||
if (isVisible) {
|
||||
visibleCount++;
|
||||
leftMost = Math.min(leftMost, rect.left);
|
||||
rightMost = Math.max(rightMost, rect.right);
|
||||
buttonDetails.push({
|
||||
selector,
|
||||
left: Math.round(rect.left),
|
||||
right: Math.round(rect.right),
|
||||
width: Math.round(rect.width)
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Check for horizontal overflow
|
||||
const hasOverflow = rightMost > viewportWidth || leftMost < 0;
|
||||
const overflowAmount = Math.max(0, rightMost - viewportWidth, -leftMost);
|
||||
|
||||
// Check document has no horizontal scroll
|
||||
const hasHorizontalScroll = document.documentElement.scrollWidth > viewportWidth;
|
||||
|
||||
return {
|
||||
viewportWidth,
|
||||
leftMost: Math.round(leftMost),
|
||||
rightMost: Math.round(rightMost),
|
||||
visibleCount,
|
||||
hasOverflow,
|
||||
overflowAmount: Math.round(overflowAmount),
|
||||
hasHorizontalScroll,
|
||||
buttonDetails,
|
||||
passed: !hasOverflow && visibleCount === 6
|
||||
};
|
||||
});
|
||||
|
||||
console.log(` Viewport width: ${iphone13Mini.viewportWidth}px`);
|
||||
console.log(` Visible buttons: ${iphone13Mini.visibleCount}/6`);
|
||||
console.log(` Button range: ${iphone13Mini.leftMost}px - ${iphone13Mini.rightMost}px`);
|
||||
console.log(` Has overflow: ${iphone13Mini.hasOverflow ? '❌ YES (' + iphone13Mini.overflowAmount + 'px)' : '✅ NO'}`);
|
||||
console.log(` Horizontal scroll: ${iphone13Mini.hasHorizontalScroll ? '❌ YES' : '✅ NO'}`);
|
||||
|
||||
const test1Passed = iphone13Mini.passed;
|
||||
console.log(` ${test1Passed ? '✅ PASS' : '❌ FAIL'} - iPhone 13 mini no overflow`);
|
||||
testResults.push({ test: 'iPhone 13 mini (375px) no overflow', passed: test1Passed });
|
||||
|
||||
await context1.close();
|
||||
|
||||
// ========================================================================
|
||||
// TEST 2: iPhone SE (320px) - even smaller screen
|
||||
// ========================================================================
|
||||
console.log("\n2️⃣ Testing iPhone SE (320x568) - no overflow...");
|
||||
const context2 = await browser.newContext({
|
||||
viewport: { width: 320, height: 568 },
|
||||
deviceScaleFactor: 2,
|
||||
isMobile: true,
|
||||
hasTouch: true,
|
||||
});
|
||||
const page2 = await context2.newPage();
|
||||
|
||||
await page2.goto(URL);
|
||||
await page2.evaluate(() => document.body.classList.add('is-mobile-device'));
|
||||
await page2.waitForTimeout(1500);
|
||||
|
||||
const iphoneSE = await page2.evaluate(() => {
|
||||
const viewportWidth = window.innerWidth;
|
||||
const buttons = [
|
||||
'.download-btn',
|
||||
'.print-friendly-btn',
|
||||
'.fixed-btn.contact-btn',
|
||||
'.color-theme-switcher',
|
||||
'.info-button',
|
||||
'.back-to-top'
|
||||
];
|
||||
|
||||
let leftMost = Infinity;
|
||||
let rightMost = -Infinity;
|
||||
let visibleCount = 0;
|
||||
|
||||
buttons.forEach(selector => {
|
||||
const btn = document.querySelector(selector);
|
||||
if (btn) {
|
||||
const style = window.getComputedStyle(btn);
|
||||
const rect = btn.getBoundingClientRect();
|
||||
const isVisible = style.display !== 'none' && style.visibility !== 'hidden';
|
||||
|
||||
if (isVisible) {
|
||||
visibleCount++;
|
||||
leftMost = Math.min(leftMost, rect.left);
|
||||
rightMost = Math.max(rightMost, rect.right);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const hasOverflow = rightMost > viewportWidth || leftMost < 0;
|
||||
|
||||
return {
|
||||
viewportWidth,
|
||||
leftMost: Math.round(leftMost),
|
||||
rightMost: Math.round(rightMost),
|
||||
visibleCount,
|
||||
hasOverflow,
|
||||
passed: !hasOverflow && visibleCount === 6
|
||||
};
|
||||
});
|
||||
|
||||
console.log(` Viewport width: ${iphoneSE.viewportWidth}px`);
|
||||
console.log(` Visible buttons: ${iphoneSE.visibleCount}/6`);
|
||||
console.log(` Button range: ${iphoneSE.leftMost}px - ${iphoneSE.rightMost}px`);
|
||||
console.log(` Has overflow: ${iphoneSE.hasOverflow ? '❌ YES' : '✅ NO'}`);
|
||||
|
||||
const test2Passed = iphoneSE.passed;
|
||||
console.log(` ${test2Passed ? '✅ PASS' : '❌ FAIL'} - iPhone SE no overflow`);
|
||||
testResults.push({ test: 'iPhone SE (320px) no overflow', passed: test2Passed });
|
||||
|
||||
await context2.close();
|
||||
|
||||
// ========================================================================
|
||||
// TEST 3: Buttons are centered on small screens
|
||||
// ========================================================================
|
||||
console.log("\n3️⃣ Testing buttons centered on small screens (375px)...");
|
||||
const context3 = await browser.newContext({
|
||||
viewport: { width: 375, height: 812 },
|
||||
isMobile: true,
|
||||
hasTouch: true,
|
||||
});
|
||||
const page3 = await context3.newPage();
|
||||
|
||||
await page3.goto(URL);
|
||||
await page3.evaluate(() => document.body.classList.add('is-mobile-device'));
|
||||
await page3.waitForTimeout(1500);
|
||||
|
||||
const centered = await page3.evaluate(() => {
|
||||
const viewportWidth = window.innerWidth;
|
||||
const buttons = [
|
||||
'.download-btn',
|
||||
'.print-friendly-btn',
|
||||
'.fixed-btn.contact-btn',
|
||||
'.color-theme-switcher',
|
||||
'.info-button',
|
||||
'.back-to-top'
|
||||
];
|
||||
|
||||
let leftMost = Infinity;
|
||||
let rightMost = -Infinity;
|
||||
|
||||
buttons.forEach(selector => {
|
||||
const btn = document.querySelector(selector);
|
||||
if (btn) {
|
||||
const style = window.getComputedStyle(btn);
|
||||
const rect = btn.getBoundingClientRect();
|
||||
const isVisible = style.display !== 'none' && style.visibility !== 'hidden';
|
||||
|
||||
if (isVisible) {
|
||||
leftMost = Math.min(leftMost, rect.left);
|
||||
rightMost = Math.max(rightMost, rect.right);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const groupWidth = rightMost - leftMost;
|
||||
const groupCenter = leftMost + (groupWidth / 2);
|
||||
const viewportCenter = viewportWidth / 2;
|
||||
const centerOffset = Math.abs(groupCenter - viewportCenter);
|
||||
|
||||
// Allow 20px tolerance for centering
|
||||
const isCentered = centerOffset < 20;
|
||||
|
||||
return {
|
||||
viewportWidth,
|
||||
viewportCenter,
|
||||
groupCenter: Math.round(groupCenter),
|
||||
centerOffset: Math.round(centerOffset),
|
||||
isCentered,
|
||||
passed: isCentered
|
||||
};
|
||||
});
|
||||
|
||||
console.log(` Viewport center: ${centered.viewportCenter}px`);
|
||||
console.log(` Button group center: ${centered.groupCenter}px`);
|
||||
console.log(` Center offset: ${centered.centerOffset}px`);
|
||||
console.log(` Is centered: ${centered.isCentered ? '✅ YES' : '❌ NO'}`);
|
||||
|
||||
const test3Passed = centered.passed;
|
||||
console.log(` ${test3Passed ? '✅ PASS' : '❌ FAIL'} - Buttons centered`);
|
||||
testResults.push({ test: 'Buttons centered on small screens', passed: test3Passed });
|
||||
|
||||
await context3.close();
|
||||
|
||||
// ========================================================================
|
||||
// TEST 4: Button size reduced on very small screens
|
||||
// ========================================================================
|
||||
console.log("\n4️⃣ Testing button size reduction on small screens (375px)...");
|
||||
const context4 = await browser.newContext({
|
||||
viewport: { width: 375, height: 812 },
|
||||
isMobile: true,
|
||||
hasTouch: true,
|
||||
});
|
||||
const page4 = await context4.newPage();
|
||||
|
||||
await page4.goto(URL);
|
||||
await page4.evaluate(() => document.body.classList.add('is-mobile-device'));
|
||||
await page4.waitForTimeout(1500);
|
||||
|
||||
const buttonSize = await page4.evaluate(() => {
|
||||
const btn = document.querySelector('.download-btn');
|
||||
if (!btn) return { passed: false, reason: 'Button not found' };
|
||||
|
||||
const rect = btn.getBoundingClientRect();
|
||||
const width = Math.round(rect.width);
|
||||
const height = Math.round(rect.height);
|
||||
|
||||
// On screens <= 400px, buttons should be 34px
|
||||
const expectedSize = 34;
|
||||
const tolerance = 2; // Allow 2px tolerance
|
||||
|
||||
const correctSize = Math.abs(width - expectedSize) <= tolerance &&
|
||||
Math.abs(height - expectedSize) <= tolerance;
|
||||
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
expectedSize,
|
||||
correctSize,
|
||||
passed: correctSize
|
||||
};
|
||||
});
|
||||
|
||||
console.log(` Button size: ${buttonSize.width}x${buttonSize.height}px`);
|
||||
console.log(` Expected: ~${buttonSize.expectedSize}px`);
|
||||
console.log(` Correct size: ${buttonSize.correctSize ? '✅ YES' : '❌ NO'}`);
|
||||
|
||||
const test4Passed = buttonSize.passed;
|
||||
console.log(` ${test4Passed ? '✅ PASS' : '❌ FAIL'} - Button size reduced`);
|
||||
testResults.push({ test: 'Button size reduced on small screens', passed: test4Passed });
|
||||
|
||||
await context4.close();
|
||||
|
||||
// ========================================================================
|
||||
// TEST 5: Compare with larger screen (no size reduction)
|
||||
// ========================================================================
|
||||
console.log("\n5️⃣ Testing normal button size on larger mobile (500px)...");
|
||||
const context5 = await browser.newContext({
|
||||
viewport: { width: 500, height: 800 },
|
||||
isMobile: true,
|
||||
hasTouch: true,
|
||||
});
|
||||
const page5 = await context5.newPage();
|
||||
|
||||
await page5.goto(URL);
|
||||
await page5.evaluate(() => document.body.classList.add('is-mobile-device'));
|
||||
await page5.waitForTimeout(1500);
|
||||
|
||||
const largerScreen = await page5.evaluate(() => {
|
||||
const btn = document.querySelector('.download-btn');
|
||||
if (!btn) return { passed: false, reason: 'Button not found' };
|
||||
|
||||
const rect = btn.getBoundingClientRect();
|
||||
const width = Math.round(rect.width);
|
||||
|
||||
// On screens > 400px, buttons should be larger (36-50px based on clamp)
|
||||
const isLarger = width > 34;
|
||||
|
||||
return {
|
||||
width,
|
||||
isLarger,
|
||||
passed: isLarger
|
||||
};
|
||||
});
|
||||
|
||||
console.log(` Button size: ${largerScreen.width}px`);
|
||||
console.log(` Is larger than 34px: ${largerScreen.isLarger ? '✅ YES' : '❌ NO'}`);
|
||||
|
||||
const test5Passed = largerScreen.passed;
|
||||
console.log(` ${test5Passed ? '✅ PASS' : '❌ FAIL'} - Normal size on larger screens`);
|
||||
testResults.push({ test: 'Normal button size on larger screens (500px)', passed: test5Passed });
|
||||
|
||||
await context5.close();
|
||||
|
||||
// ========================================================================
|
||||
// FINAL SUMMARY
|
||||
// ========================================================================
|
||||
console.log("\n" + "=".repeat(70));
|
||||
console.log("📊 TEST SUMMARY\n");
|
||||
|
||||
const totalTests = testResults.length;
|
||||
const passedTests = testResults.filter(r => r.passed).length;
|
||||
const failedTests = totalTests - passedTests;
|
||||
|
||||
testResults.forEach(result => {
|
||||
console.log(` ${result.passed ? '✅' : '❌'} ${result.test}`);
|
||||
});
|
||||
|
||||
console.log(`\n Total: ${passedTests}/${totalTests} tests passed`);
|
||||
console.log("=".repeat(70) + "\n");
|
||||
|
||||
await browser.close();
|
||||
|
||||
if (failedTests === 0) {
|
||||
console.log("🎉 ALL MOBILE FAB OVERFLOW TESTS PASSED!");
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.log("⚠️ SOME TESTS FAILED - See details above");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Test failed:', error);
|
||||
await browser.close();
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
await testMobileFabOverflow();
|
||||
Reference in New Issue
Block a user