feat(mobile): transform-based keyboard choreography on iOS
Stop animating the app shell height when the keyboard opens: per-frame reflow plus the scroll-follow chase caused visible micro-jitter on the composer and pinned chat. The shell layout now snaps exactly once per open/close at an invisible choreography point, while the composer and pinned chat content slide via compositor-only transforms in sync with the keyboard. Dismissal starts from the textarea focusout (no bridge latency), runs a shorter 0.2s leg, and the WebKit form accessory bar is disabled. Composer keeps a 12px gap above the open keyboard.
This commit is contained in:
@@ -537,31 +537,39 @@
|
||||
|
||||
/* Native (Capacitor) keyboard handling.
|
||||
The Keyboard plugin runs in `resize: 'none'` mode so the WebView keeps its full
|
||||
height; instead we shrink the app shell by the keyboard frame height, exposed as
|
||||
--oc-keyboard-inset and set once from `keyboardWillShow` (see useNativeMobileChrome).
|
||||
height; instead we shrink the app shell by the keyboard frame height. Two vars,
|
||||
both set by useNativeMobileChrome: --oc-keyboard-inset is the keyboard's target
|
||||
height from the moment `keyboardWillShow` fires (overlay surfaces below animate
|
||||
against it as before), while --oc-kb-layout is the same height applied to the
|
||||
shell's LAYOUT only at the choreography point where a resize is invisible.
|
||||
Scoped to .oc-capacitor-app so the browser PWA keeps its dvh / interactive-widget
|
||||
behaviour untouched.
|
||||
|
||||
`keyboardWillShow` fires at the start of the iOS keyboard animation, so the inset
|
||||
is set once and the transition carries the rise. The duration/curve are tuned to
|
||||
mimic the native iOS keyboard (≈0.25s, cubic-bezier(0.38, 0.7, 0.125, 1)) so our
|
||||
layout and the keyboard move together. (visualViewport live-tracking would be exact
|
||||
The shell height is NOT transitioned. Animating `height` reflows the whole shell
|
||||
every frame and forces the chat's ResizeObserver to chase the bottom on each of
|
||||
those frames — the source of the visible micro-jitter on the composer/chat while
|
||||
pinned. Instead the visual rise is transform-only (compositor) — a FLIP
|
||||
choreography driven by useNativeMobileChrome:
|
||||
|
||||
show: shell keeps its full height for the whole 0.25s; the composer (and, when
|
||||
pinned, the chat content — see useChatAutoFollow) slides up via
|
||||
--oc-kb-shift in sync with the keyboard; at the end the shell snaps to
|
||||
its final height (--oc-kb-layout, one reflow) and the shift is removed
|
||||
in the same frame — visually identical, so the swap is invisible.
|
||||
hide: the shell snaps back to full height immediately (still hidden behind the
|
||||
keyboard), the composer/content are FLIP-offset to their raised position
|
||||
and slide down with the keyboard.
|
||||
|
||||
`keyboardWillShow` fires at the start of the iOS keyboard animation with the final
|
||||
height, and the duration/curve mimic the native keyboard (≈0.25s,
|
||||
cubic-bezier(0.38, 0.7, 0.125, 1)). (visualViewport live-tracking would be exact
|
||||
but doesn't report under WKWebView's `resize: 'none'`, so this is the best signal.) */
|
||||
:root.oc-capacitor-app .oc-mobile-app-shell {
|
||||
height: calc(100dvh - var(--oc-keyboard-inset, 0px));
|
||||
height: calc(100dvh - var(--oc-kb-layout, 0px));
|
||||
/* Reserve the bottom safe area only while the keyboard is down — when it's up the
|
||||
inset cancels it out (the home indicator is hidden and the composer should sit
|
||||
flush above the keyboard). The shell keeps its own bg behind this padding. */
|
||||
padding-bottom: max(0px, calc(var(--oc-app-bottom-safe, 0px) - var(--oc-keyboard-inset, 0px)));
|
||||
transition: height 0.25s cubic-bezier(0.38, 0.7, 0.125, 1),
|
||||
padding-bottom 0.25s cubic-bezier(0.38, 0.7, 0.125, 1);
|
||||
}
|
||||
|
||||
/* Android resizes the window for the keyboard natively (no manual --oc-keyboard-inset),
|
||||
so 100dvh changes instantly. Animating height against that instant resize makes the
|
||||
header/content bounce on keyboard open — disable the transition on Android. */
|
||||
:root.oc-capacitor-app.oc-platform-android .oc-mobile-app-shell {
|
||||
transition: none;
|
||||
padding-bottom: max(0px, calc(var(--oc-app-bottom-safe, 0px) - var(--oc-kb-layout, 0px)));
|
||||
}
|
||||
|
||||
/* Portal surfaces (bottom sheets, overlay panels) render at <body> level, outside
|
||||
@@ -587,13 +595,32 @@
|
||||
transition: height 0.25s cubic-bezier(0.38, 0.7, 0.125, 1);
|
||||
}
|
||||
|
||||
/* The composer keeps its 1rem bottom padding while the keyboard is down (breathing
|
||||
room above the home indicator), but that gap looks artificial sitting above the
|
||||
keyboard's accessory bar — so tighten it while the keyboard is open. Animated to
|
||||
match the keyboard motion. */
|
||||
/* Keyboard slide choreography (see the shell comment above). --oc-kb-shift is the
|
||||
transform-only travel distance (keyboard height minus the bottom safe padding the
|
||||
shell gives up when the keyboard is open). The transition is gated on
|
||||
.oc-kb-animating so the end-of-animation swap (shift back to 0 while the layout
|
||||
height takes over) is instant and invisible. */
|
||||
:root.oc-capacitor-app .oc-mobile-composer {
|
||||
transition: padding-bottom 0.25s cubic-bezier(0.38, 0.7, 0.125, 1);
|
||||
transform: translateY(calc(-1 * var(--oc-kb-shift, 0px)));
|
||||
}
|
||||
:root.oc-capacitor-app.oc-kb-animating .oc-mobile-composer {
|
||||
transition: transform 0.25s cubic-bezier(0.38, 0.7, 0.125, 1);
|
||||
/* While sliding, the composer passes over chat content that hasn't moved (user
|
||||
scrolled up) — give it an opaque backdrop so text doesn't show through the
|
||||
form's transparent padding. */
|
||||
background: var(--background);
|
||||
}
|
||||
/* Keyboard dismissal reads faster than the rise — a full 0.25s down-slide feels
|
||||
laggy, so the hide leg runs shorter. Must match KB_HIDE_MS in useNativeMobileChrome. */
|
||||
:root.oc-capacitor-app.oc-kb-animating.oc-kb-hide .oc-mobile-composer {
|
||||
transition-duration: 0.2s;
|
||||
}
|
||||
|
||||
/* The composer keeps its 1rem bottom padding while the keyboard is down (breathing
|
||||
room above the home indicator), but that gap looks artificial sitting right above
|
||||
the keyboard — so tighten it while the keyboard is open. Snaps at the start of the
|
||||
show animation (a ~10px pre-slide change; transitioning it would reflow the chat
|
||||
every frame). */
|
||||
:root.oc-capacitor-app.oc-keyboard-open .oc-mobile-composer {
|
||||
padding-bottom: 6px;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user