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;
|
const footer = footerRef.current;
|
||||||
if (!page || !mascot || !cardContent || !cardBody || !footer) return undefined;
|
if (!page || !mascot || !cardContent || !cardBody || !footer) return undefined;
|
||||||
|
|
||||||
|
const rootEl = document.getElementById('root');
|
||||||
let frameId = 0;
|
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 => {
|
const update = (): void => {
|
||||||
cancelAnimationFrame(frameId);
|
cancelAnimationFrame(frameId);
|
||||||
frameId = requestAnimationFrame(() => {
|
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 s = getComputedStyle(page);
|
||||||
const footerHeight =
|
const footerHeight =
|
||||||
footer.offsetHeight || readPx(s, '--vojo-footer-space', footer.offsetHeight);
|
footer.offsetHeight || readPx(s, '--vojo-footer-space', footer.offsetHeight);
|
||||||
const bottomGap = readPx(s, '--vojo-modal-gap');
|
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({
|
const layout = calculateModalLayout({
|
||||||
pageHeight: page.clientHeight,
|
pageHeight,
|
||||||
mascotTopOffset: readPx(s, '--vojo-mascot-top'),
|
mascotTopOffset,
|
||||||
mascotHeight: mascot.offsetHeight,
|
mascotHeight,
|
||||||
modalHeight: cardBody.offsetHeight,
|
modalHeight: cardBody.offsetHeight,
|
||||||
footerHeight,
|
footerHeight,
|
||||||
anchorRatio: readNum(s, '--vojo-anchor-ratio', 0.58),
|
anchorRatio,
|
||||||
minTop: readPx(s, '--vojo-modal-min-top'),
|
minTop,
|
||||||
bottomGap,
|
bottomGap,
|
||||||
});
|
});
|
||||||
page.style.setProperty('--vojo-modal-top', `${layout.top}px`);
|
page.style.setProperty('--vojo-modal-top', `${layout.top}px`);
|
||||||
page.style.setProperty('--vojo-modal-max-h', `${layout.maxHeight}px`);
|
page.style.setProperty('--vojo-modal-max-h', `${layout.maxHeight}px`);
|
||||||
cardContent.classList.toggle(css.AuthCardContentScrollable, layout.constrained);
|
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(mascot);
|
||||||
ro.observe(cardBody);
|
ro.observe(cardBody);
|
||||||
ro.observe(footer);
|
ro.observe(footer);
|
||||||
window.addEventListener('resize', update);
|
const onWindowResize = (): void => {
|
||||||
|
recomputePadding();
|
||||||
|
update();
|
||||||
|
};
|
||||||
|
window.addEventListener('resize', onWindowResize);
|
||||||
|
window.visualViewport?.addEventListener('resize', update);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
cancelAnimationFrame(frameId);
|
cancelAnimationFrame(frameId);
|
||||||
ro.disconnect();
|
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({
|
export const AuthLayout = style({
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '100dvh',
|
height: '100dvh',
|
||||||
minHeight: '100dvh',
|
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
|
|
@ -65,6 +64,10 @@ export const AuthMascot = style({
|
||||||
transform: 'translateX(-50%)',
|
transform: 'translateX(-50%)',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const AuthMascotHidden = style({
|
||||||
|
display: 'none',
|
||||||
|
});
|
||||||
|
|
||||||
export const AuthMascotVideo = style({
|
export const AuthMascotVideo = style({
|
||||||
display: 'block',
|
display: 'block',
|
||||||
width: 'var(--vojo-mascot-size)',
|
width: 'var(--vojo-mascot-size)',
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue