diff --git a/packages/ui/src/components/ui/MobileOverlayPanel.tsx b/packages/ui/src/components/ui/MobileOverlayPanel.tsx index 55112d62..373bc6ae 100644 --- a/packages/ui/src/components/ui/MobileOverlayPanel.tsx +++ b/packages/ui/src/components/ui/MobileOverlayPanel.tsx @@ -43,6 +43,12 @@ export const MobileOverlayPanel: React.FC = ({ }) => { const overlayRootRef = React.useRef(null); const [entered, setEntered] = React.useState(false); + // True once the enter transition has finished. While entering, the panel's + // keyboard-inset bottom anchor must NOT animate: opening an overlay usually + // closes the keyboard at the same moment, and a transitioning `bottom` under + // the panel's own rise made the entrance jerky / offset. During the enter the + // anchor snaps to its final value and only the rise animates. + const [enterSettled, setEnterSettled] = React.useState(false); if (typeof document !== 'undefined' && !overlayRootRef.current) { overlayRootRef.current = ensureOverlayRoot(); @@ -52,10 +58,18 @@ export const MobileOverlayPanel: React.FC = ({ React.useEffect(() => { if (!open) { setEntered(false); + setEnterSettled(false); return; } const id = window.setTimeout(() => setEntered(true), ENTER_DELAY_MS); - return () => window.clearTimeout(id); + const settleId = window.setTimeout( + () => setEnterSettled(true), + ENTER_DELAY_MS + ENTER_DURATION_MS + 50, + ); + return () => { + window.clearTimeout(id); + window.clearTimeout(settleId); + }; }, [open]); React.useEffect(() => { @@ -86,6 +100,7 @@ export const MobileOverlayPanel: React.FC = ({