Badges: switch to transform-based print centering — fixes Firefox physical printer shift
This commit is contained in:
@@ -162,13 +162,10 @@
|
|||||||
<style>
|
<style>
|
||||||
@media print {
|
@media print {
|
||||||
/* Full-page reset.
|
/* Full-page reset.
|
||||||
app.css sets `overflow: hidden` on html + body globally. In print,
|
app.css sets `overflow: hidden` on html + body globally — override it
|
||||||
with display:contents dissolving the intermediate wrappers, body's
|
so body/html fill the full page (not just the badge width). */
|
||||||
only remaining content is the badge (~3.5in wide). overflow:hidden
|
|
||||||
makes body a BFC that shrinks-to-content — producing a body box that
|
|
||||||
is badge-width instead of page-width. Overriding with overflow:visible
|
|
||||||
restores normal block sizing: body fills 100% of the page width. */
|
|
||||||
html, body {
|
html, body {
|
||||||
|
display: block !important;
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
height: 100% !important;
|
height: 100% !important;
|
||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
@@ -176,30 +173,27 @@
|
|||||||
overflow: visible !important;
|
overflow: visible !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Body is the sole centering flex container.
|
|
||||||
.event_badge_wrapper is its only non-hidden flex child
|
|
||||||
once the intermediate wrappers are collapsed via display:contents. */
|
|
||||||
body {
|
|
||||||
display: flex !important;
|
|
||||||
align-items: center !important;
|
|
||||||
justify-content: center !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hide app chrome */
|
/* Hide app chrome */
|
||||||
.submenu { display: none !important; }
|
.submenu { display: none !important; }
|
||||||
|
|
||||||
/* Dissolve layout wrappers so .event_badge_wrapper bubbles up to a real
|
/* #ae_main_content becomes the positioning context for the badge.
|
||||||
centering flex container.
|
It fills the full page (100%×100%), and the badge is absolutely
|
||||||
|
centered within it.
|
||||||
|
|
||||||
#ae_main_content has overflow:auto — per CSS spec, display:contents
|
Why not flex: Firefox respects the printer driver's minimum hardware
|
||||||
cannot override an element that establishes overflow clipping; Firefox
|
margins even with @page { margin: 0 }. This makes the available
|
||||||
(spec-compliant) keeps it as a block container, breaking centering.
|
content area slightly smaller than 100%, throwing off flex centering.
|
||||||
Fix: reset it as an explicit 100%×100% pass-through flex container
|
CSS transform-based centering (translate -50%,-50%) is immune to
|
||||||
instead. Chrome applies this too (belt-and-suspenders). */
|
this: it centers relative to the containing block dimension, which
|
||||||
|
Firefox computes correctly even when printer margins are applied.
|
||||||
|
|
||||||
|
Why not display:contents on this element: #ae_main_content has
|
||||||
|
overflow:auto. Per spec, display:contents cannot override an element
|
||||||
|
with overflow clipping; Firefox keeps it as a block box. We reset it
|
||||||
|
to a positioned block instead. */
|
||||||
#ae_main_content {
|
#ae_main_content {
|
||||||
display: flex !important;
|
position: relative !important;
|
||||||
align-items: center !important;
|
display: block !important;
|
||||||
justify-content: center !important;
|
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
max-width: none !important;
|
max-width: none !important;
|
||||||
height: 100% !important;
|
height: 100% !important;
|
||||||
@@ -211,17 +205,20 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* .main_content and #badge_render_area have no overflow constraints,
|
/* .main_content and #badge_render_area have no overflow constraints,
|
||||||
so display:contents works on all browsers — they dissolve their boxes
|
so display:contents dissolves their boxes in all browsers.
|
||||||
and .event_badge_wrapper becomes a direct flex child of #ae_main_content. */
|
.event_badge_wrapper emerges as a direct child of #ae_main_content. */
|
||||||
.main_content,
|
.main_content,
|
||||||
#badge_render_area {
|
#badge_render_area {
|
||||||
display: contents !important;
|
display: contents !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Badge wrapper: reset screen-only mx-auto; the @page size = badge size
|
/* Center the badge using CSS transforms — the most reliable cross-browser
|
||||||
so margin: 0 fills the page exactly.
|
print centering method; unaffected by printer margin variations. */
|
||||||
gap/padding are stripped to eliminate any whitespace bleed. */
|
|
||||||
.event_badge_wrapper {
|
.event_badge_wrapper {
|
||||||
|
position: absolute !important;
|
||||||
|
top: 50% !important;
|
||||||
|
left: 50% !important;
|
||||||
|
transform: translate(-50%, -50%) !important;
|
||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
gap: 0 !important;
|
gap: 0 !important;
|
||||||
@@ -245,8 +242,7 @@
|
|||||||
outline: 4px solid blue !important;
|
outline: 4px solid blue !important;
|
||||||
outline-offset: -4px !important;
|
outline-offset: -4px !important;
|
||||||
}
|
}
|
||||||
/* Red = #ae_main_content — now a real flex container (full width/height).
|
/* Red = #ae_main_content — should be VISIBLE and fill the page (same as blue). */
|
||||||
Should be VISIBLE and same size as body. */
|
|
||||||
#ae_main_content {
|
#ae_main_content {
|
||||||
outline: 3px dashed red !important;
|
outline: 3px dashed red !important;
|
||||||
outline-offset: -6px !important;
|
outline-offset: -6px !important;
|
||||||
@@ -260,7 +256,7 @@
|
|||||||
outline: 3px dashed purple !important;
|
outline: 3px dashed purple !important;
|
||||||
outline-offset: -12px !important;
|
outline-offset: -12px !important;
|
||||||
}
|
}
|
||||||
/* Cyan = the actual badge — should be centered inside the red box */
|
/* Cyan = the actual badge — should be dead-center */
|
||||||
.event_badge_wrapper {
|
.event_badge_wrapper {
|
||||||
outline: 3px solid cyan !important;
|
outline: 3px solid cyan !important;
|
||||||
outline-offset: 2px !important;
|
outline-offset: 2px !important;
|
||||||
|
|||||||
@@ -177,33 +177,44 @@ test.describe('Badge Print Page — print layout centering', () => {
|
|||||||
).toBeLessThan(5);
|
).toBeLessThan(5);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('#ae_main_content does not clip in print mode', async ({ page }) => {
|
test('#ae_main_content is a full-page positioned block in print mode', async ({ page }) => {
|
||||||
// Verifies that overflow:visible is applied (the Firefox fix).
|
// Verifies overflow:visible (overflow:auto fix) and position:relative (centering context).
|
||||||
await page.goto(`/events/${event_id}/badges/${badge_id}/print`);
|
await page.goto(`/events/${event_id}/badges/${badge_id}/print`);
|
||||||
await page.waitForLoadState('domcontentloaded');
|
await page.waitForLoadState('domcontentloaded');
|
||||||
await page.emulateMedia({ media: 'print' });
|
await page.emulateMedia({ media: 'print' });
|
||||||
|
|
||||||
const overflow = await page.evaluate(() => {
|
const styles = await page.evaluate(() => {
|
||||||
const el = document.getElementById('ae_main_content');
|
const el = document.getElementById('ae_main_content');
|
||||||
return el ? getComputedStyle(el).overflow : null;
|
if (!el) return null;
|
||||||
|
const cs = getComputedStyle(el);
|
||||||
|
return { overflow: cs.overflow, position: cs.position };
|
||||||
});
|
});
|
||||||
expect(overflow, '#ae_main_content overflow should be visible in print').toBe('visible');
|
expect(styles, '#ae_main_content must exist').not.toBeNull();
|
||||||
|
expect(styles!.overflow, '#ae_main_content overflow should be visible in print').toBe('visible');
|
||||||
|
expect(styles!.position, '#ae_main_content should be positioned (relative) in print').toBe('relative');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('body is a flex container spanning full viewport width in print mode', async ({ page }) => {
|
test('badge wrapper is absolutely positioned and centered in print mode', async ({ page }) => {
|
||||||
await page.setViewportSize({ width: 816, height: 1056 });
|
await page.setViewportSize({ width: 816, height: 1056 });
|
||||||
|
|
||||||
await page.goto(`/events/${event_id}/badges/${badge_id}/print`);
|
await page.goto(`/events/${event_id}/badges/${badge_id}/print`);
|
||||||
await page.waitForLoadState('domcontentloaded');
|
await page.waitForLoadState('domcontentloaded');
|
||||||
|
await page.evaluate(inject_idb, [mock_badge, mock_template] as any);
|
||||||
|
await page.waitForSelector('.event_badge_wrapper', { timeout: 8000 });
|
||||||
await page.emulateMedia({ media: 'print' });
|
await page.emulateMedia({ media: 'print' });
|
||||||
|
|
||||||
const result = await page.evaluate(() => ({
|
const styles = await page.evaluate(() => {
|
||||||
display: getComputedStyle(document.body).display,
|
const el = document.querySelector('.event_badge_wrapper') as HTMLElement | null;
|
||||||
width: document.body.offsetWidth,
|
if (!el) return null;
|
||||||
}));
|
const cs = getComputedStyle(el);
|
||||||
|
return {
|
||||||
expect(result.display, 'body should be flex in print').toBe('flex');
|
position: cs.position,
|
||||||
// Body width must be close to the viewport width (no shrink-to-content)
|
transform: cs.transform,
|
||||||
expect(result.width, 'body should span full viewport width').toBeGreaterThanOrEqual(810);
|
};
|
||||||
|
});
|
||||||
|
expect(styles).not.toBeNull();
|
||||||
|
expect(styles!.position, 'badge wrapper should be absolutely positioned').toBe('absolute');
|
||||||
|
// transform should contain a matrix (translate is set)
|
||||||
|
expect(styles!.transform, 'badge wrapper should have a transform applied').not.toBe('none');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user