fix(mobile): steady overlay entrance while the keyboard dismisses

Opening a bottom-sheet overlay usually dismisses the keyboard in the same
moment, so the panel's keyboard-inset bottom anchor was transitioning down
underneath the panel's own rise animation — a jerky, offset entrance. The
anchor now snaps to its final position for the duration of the enter
animation and resumes animating afterwards (so an already-open overlay
still lifts smoothly when the keyboard reopens).
This commit is contained in:
Bohdan Triapitsyn
2026-07-05 00:17:19 +03:00
parent dbcb655e43
commit b19f65f9b6
2 changed files with 22 additions and 1 deletions
@@ -43,6 +43,12 @@ export const MobileOverlayPanel: React.FC<MobileOverlayPanelProps> = ({
}) => { }) => {
const overlayRootRef = React.useRef<HTMLElement | null>(null); const overlayRootRef = React.useRef<HTMLElement | null>(null);
const [entered, setEntered] = React.useState(false); 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) { if (typeof document !== 'undefined' && !overlayRootRef.current) {
overlayRootRef.current = ensureOverlayRoot(); overlayRootRef.current = ensureOverlayRoot();
@@ -52,10 +58,18 @@ export const MobileOverlayPanel: React.FC<MobileOverlayPanelProps> = ({
React.useEffect(() => { React.useEffect(() => {
if (!open) { if (!open) {
setEntered(false); setEntered(false);
setEnterSettled(false);
return; return;
} }
const id = window.setTimeout(() => setEntered(true), ENTER_DELAY_MS); 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]); }, [open]);
React.useEffect(() => { React.useEffect(() => {
@@ -86,6 +100,7 @@ export const MobileOverlayPanel: React.FC<MobileOverlayPanelProps> = ({
<div <div
className={cn( className={cn(
'oc-keyboard-inset-surface fixed inset-0 z-[60] flex flex-col bg-[rgb(0_0_0_/_0.45)] transition-opacity duration-200 ease-out', 'oc-keyboard-inset-surface fixed inset-0 z-[60] flex flex-col bg-[rgb(0_0_0_/_0.45)] transition-opacity duration-200 ease-out',
!enterSettled && 'oc-keyboard-inset-snap',
entered ? 'opacity-100' : 'opacity-0', entered ? 'opacity-100' : 'opacity-0',
)} )}
role="dialog" role="dialog"
+6
View File
@@ -583,6 +583,12 @@
bottom: var(--oc-keyboard-inset, 0px); bottom: var(--oc-keyboard-inset, 0px);
transition: bottom 0.25s cubic-bezier(0.38, 0.7, 0.125, 1), opacity 0.2s ease-out; transition: bottom 0.25s cubic-bezier(0.38, 0.7, 0.125, 1), opacity 0.2s ease-out;
} }
/* While a panel is playing its enter animation, its bottom anchor snaps
instead of transitioning — opening an overlay typically dismisses the
keyboard in the same moment, and animating both made the entrance jerky. */
:root.oc-capacitor-app .oc-keyboard-inset-surface.oc-keyboard-inset-snap {
transition: opacity 0.2s ease-out;
}
/* Full-screen scroll views (e.g. the connect/login screen) live outside the app /* Full-screen scroll views (e.g. the connect/login screen) live outside the app
shell, so shrink them by the keyboard height the same way the shell does. Capping shell, so shrink them by the keyboard height the same way the shell does. Capping