fix(chat): adapt input bar to soft keyboard in FB in-app browser

FB IAB on Xiaomi (and similar WebViews that don't shrink 100dvh) keeps
the chat input covered by the soft keyboard. Fix by writing the
visualViewport-derived keyboard height to a CSS variable on <html> and
consuming it as padding-bottom on the chat input bar.

- Add useKeyboardHeight hook (visualViewport + rAF batching, no re-renders)
- Apply --keyboard-height + env(safe-area-inset-bottom) to .bar padding
- Add scrollIntoView on textarea focus so the input stays visible
- Add viewportFit: cover so safe-area-inset-bottom is reported
This commit is contained in:
2026-06-24 14:13:01 +08:00
parent 42c03f8901
commit 853ae776f9
6 changed files with 109 additions and 4 deletions
@@ -1,12 +1,21 @@
/* ChatInputBar 输入栏容器样式(与 Dart chat_input_bar.dart 对齐) */
/* 外层:focused 时加 padding-top + 粉色背景 + 阴影 */
/* 键盘适配(修复 FB IAB / 小米 WebView 不收缩 100dvh 导致输入框被遮挡):
* --keyboard-height 由 src/hooks/use-keyboard-height.ts 写到 <html>,默认 0px。
* env(safe-area-inset-bottom) 处理小米全面屏手势条 / iPhone home indicator。
* 二者在原生已正确处理键盘的浏览器(iOS Safari、现代 Chrome Android)上都为 0
* 因此无视觉副作用。 */
.bar {
flex: 0 0 auto;
padding: 0 var(--spacing-lg, 16px) var(--spacing-lg, 16px);
padding: 0
var(--spacing-lg, 16px)
calc(
var(--spacing-lg, 16px) + var(--keyboard-height, 0px) +
env(safe-area-inset-bottom, 0px)
);
background: transparent;
transition: padding-top 0.2s ease, background-color 0.2s ease,
box-shadow 0.2s ease;
transition: padding-top 0.2s ease, padding-bottom 0.2s ease,
background-color 0.2s ease, box-shadow 0.2s ease;
}
.barFocused {
@@ -18,6 +18,7 @@ import {
forwardRef,
type FormEvent,
type KeyboardEvent,
useEffect,
useImperativeHandle,
useRef,
} from "react";
@@ -54,6 +55,20 @@ export const ChatInputTextField = forwardRef<
// 对外暴露与 forwardRef 同名的 ref(指向内部 textarea 节点)
useImperativeHandle(ref, () => innerRef.current as HTMLTextAreaElement, []);
// 获得焦点时把 textarea 滚到可见区域(修复小米/FB IAB 键盘弹起后
// 内容仍被遮挡的场景),rAF 等浏览器完成键盘布局。
useEffect(() => {
const el = innerRef.current;
if (!el) return;
const onFocus = () => {
window.requestAnimationFrame(() => {
el.scrollIntoView({ block: "nearest", behavior: "smooth" });
});
};
el.addEventListener("focus", onFocus);
return () => el.removeEventListener("focus", onFocus);
}, []);
const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key !== "Enter") return;
if (e.shiftKey || e.ctrlKey || e.metaKey) return; // 修饰键 → 换行