fix(auth): keep form within visible band on small viewports
This commit is contained in:
parent
e6623b2784
commit
c46684800c
2 changed files with 55 additions and 8 deletions
|
|
@ -249,27 +249,65 @@ export function AuthLayout() {
|
|||
const footer = footerRef.current;
|
||||
if (!page || !mascot || !cardContent || !cardBody || !footer) return undefined;
|
||||
|
||||
const rootEl = document.getElementById('root');
|
||||
let frameId = 0;
|
||||
let padTop = 0;
|
||||
let padBottom = 0;
|
||||
|
||||
// env(safe-area-inset-*) only changes on rotate / fullscreen toggle; both
|
||||
// fire window.resize. Recompute lazily, not on every RAF.
|
||||
const recomputePadding = (): void => {
|
||||
if (!rootEl) return;
|
||||
const s = getComputedStyle(rootEl);
|
||||
padTop = parseFloat(s.paddingTop) || 0;
|
||||
padBottom = parseFloat(s.paddingBottom) || 0;
|
||||
};
|
||||
recomputePadding();
|
||||
|
||||
const update = (): void => {
|
||||
cancelAnimationFrame(frameId);
|
||||
frameId = requestAnimationFrame(() => {
|
||||
// Capacitor WebView can publish env(safe-area-inset-*) one frame after
|
||||
// mount; cheaper to refresh per RAF than to miss the first cold paint.
|
||||
recomputePadding();
|
||||
const s = getComputedStyle(page);
|
||||
const footerHeight =
|
||||
footer.offsetHeight || readPx(s, '--vojo-footer-space', footer.offsetHeight);
|
||||
const bottomGap = readPx(s, '--vojo-modal-gap');
|
||||
// AuthLayout is height: 100dvh inside #root which has env(safe-area-inset-*)
|
||||
// padding. AuthLayout overflows #root's content area by the safe-area
|
||||
// amounts and gets clipped by body { overflow: hidden }. Subtract #root's
|
||||
// vertical padding from pageHeight so the form fits the visible band.
|
||||
const rawPageHeight = window.visualViewport?.height ?? page.clientHeight;
|
||||
const pageHeight = Math.max(0, rawPageHeight - padTop - padBottom);
|
||||
|
||||
const anchorRatio = readNum(s, '--vojo-anchor-ratio', 0.58);
|
||||
|
||||
// Compact mode: form + footer don't fit in the viewport at all.
|
||||
// When they do fit, the form naturally docks at the mascot anchor and
|
||||
// the mascot peeks above through the glassmorphism — that's the design,
|
||||
// not a bug. We only drop the mascot when the math has nowhere to put
|
||||
// both. Triggers on keyboard up, very small windows, landscape, etc.
|
||||
const compact = cardBody.offsetHeight + footerHeight + bottomGap > pageHeight;
|
||||
|
||||
const mascotTopOffset = compact ? 0 : readPx(s, '--vojo-mascot-top');
|
||||
const mascotHeight = compact ? 0 : mascot.offsetHeight;
|
||||
const minTop = compact ? 0 : readPx(s, '--vojo-modal-min-top');
|
||||
|
||||
const layout = calculateModalLayout({
|
||||
pageHeight: page.clientHeight,
|
||||
mascotTopOffset: readPx(s, '--vojo-mascot-top'),
|
||||
mascotHeight: mascot.offsetHeight,
|
||||
pageHeight,
|
||||
mascotTopOffset,
|
||||
mascotHeight,
|
||||
modalHeight: cardBody.offsetHeight,
|
||||
footerHeight,
|
||||
anchorRatio: readNum(s, '--vojo-anchor-ratio', 0.58),
|
||||
minTop: readPx(s, '--vojo-modal-min-top'),
|
||||
anchorRatio,
|
||||
minTop,
|
||||
bottomGap,
|
||||
});
|
||||
page.style.setProperty('--vojo-modal-top', `${layout.top}px`);
|
||||
page.style.setProperty('--vojo-modal-max-h', `${layout.maxHeight}px`);
|
||||
cardContent.classList.toggle(css.AuthCardContentScrollable, layout.constrained);
|
||||
mascot.classList.toggle(css.AuthMascotHidden, compact);
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -279,12 +317,18 @@ export function AuthLayout() {
|
|||
ro.observe(mascot);
|
||||
ro.observe(cardBody);
|
||||
ro.observe(footer);
|
||||
window.addEventListener('resize', update);
|
||||
const onWindowResize = (): void => {
|
||||
recomputePadding();
|
||||
update();
|
||||
};
|
||||
window.addEventListener('resize', onWindowResize);
|
||||
window.visualViewport?.addEventListener('resize', update);
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(frameId);
|
||||
ro.disconnect();
|
||||
window.removeEventListener('resize', update);
|
||||
window.removeEventListener('resize', onWindowResize);
|
||||
window.visualViewport?.removeEventListener('resize', update);
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import { DefaultReset, toRem, color } from 'folds';
|
|||
export const AuthLayout = style({
|
||||
width: '100%',
|
||||
height: '100dvh',
|
||||
minHeight: '100dvh',
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
|
|
@ -65,6 +64,10 @@ export const AuthMascot = style({
|
|||
transform: 'translateX(-50%)',
|
||||
});
|
||||
|
||||
export const AuthMascotHidden = style({
|
||||
display: 'none',
|
||||
});
|
||||
|
||||
export const AuthMascotVideo = style({
|
||||
display: 'block',
|
||||
width: 'var(--vojo-mascot-size)',
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue