修复移动端软键盘页面弹跳黑底
移除 H5 软键盘打开时平台壳全局 transform 位移,避免浏览器原生避让后再次弹跳。 保留键盘打开状态、底部 dock 隐藏和浅色根背景兜底,避免短表单露出黑色宿主底色。 补充小程序 web-view 原生 page 浅色背景和对应样式测试。 更新统一创作页与平台键盘适配文档,沉淀不再全局上移平台壳的约束。
This commit is contained in:
@@ -1,31 +1,43 @@
|
||||
const MOBILE_POINTER_QUERY = '(pointer: coarse)';
|
||||
const KEYBOARD_OPEN_THRESHOLD_PX = 96;
|
||||
const FOCUS_MARGIN_PX = 18;
|
||||
const MIN_LAYOUT_VIEWPORT_HEIGHT_PX = 320;
|
||||
|
||||
const LAYOUT_HEIGHT_VAR = '--platform-layout-viewport-height';
|
||||
const KEYBOARD_FOCUS_OFFSET_VAR = '--platform-keyboard-focus-offset';
|
||||
const KEYBOARD_INSET_VAR = '--platform-keyboard-inset-bottom';
|
||||
const KEYBOARD_EXPOSED_FILL_VAR = '--platform-keyboard-exposed-fill';
|
||||
const KEYBOARD_EXPOSED_FILL_FALLBACK =
|
||||
'linear-gradient(180deg, #fffdf9 0%, #fdf9f5 54%, #f8efe7 100%)';
|
||||
|
||||
type KeyboardFocusShiftInput = {
|
||||
layoutHeight: number;
|
||||
visualTop: number;
|
||||
visualHeight: number;
|
||||
targetTop: number;
|
||||
targetBottom: number;
|
||||
currentShift: number;
|
||||
margin?: number;
|
||||
maxExtraShift?: number;
|
||||
hasEditableTarget: boolean;
|
||||
threshold?: number;
|
||||
};
|
||||
|
||||
function clamp(value: number, min: number, max: number) {
|
||||
return Math.min(max, Math.max(min, value));
|
||||
}
|
||||
|
||||
function readVisualViewport() {
|
||||
return typeof window !== 'undefined' ? window.visualViewport : undefined;
|
||||
}
|
||||
|
||||
export function resolveMobileKeyboardExposedFill() {
|
||||
if (typeof window === 'undefined' || typeof document === 'undefined') {
|
||||
return KEYBOARD_EXPOSED_FILL_FALLBACK;
|
||||
}
|
||||
|
||||
const platformShell = document.querySelector('.platform-viewport-shell');
|
||||
if (!(platformShell instanceof HTMLElement)) {
|
||||
return KEYBOARD_EXPOSED_FILL_FALLBACK;
|
||||
}
|
||||
|
||||
const platformFill = window
|
||||
.getComputedStyle(platformShell)
|
||||
.getPropertyValue('--platform-body-fill')
|
||||
.trim();
|
||||
return platformFill || KEYBOARD_EXPOSED_FILL_FALLBACK;
|
||||
}
|
||||
|
||||
function readLayoutViewportHeight() {
|
||||
if (typeof window === 'undefined' || typeof document === 'undefined') {
|
||||
return MIN_LAYOUT_VIEWPORT_HEIGHT_PX;
|
||||
@@ -110,34 +122,22 @@ export function isEditableKeyboardTarget(
|
||||
]).has(inputType);
|
||||
}
|
||||
|
||||
export function calculateMobileKeyboardFocusShift({
|
||||
export function resolveMobileKeyboardState({
|
||||
layoutHeight,
|
||||
visualTop,
|
||||
visualHeight,
|
||||
targetTop,
|
||||
targetBottom,
|
||||
currentShift,
|
||||
margin = FOCUS_MARGIN_PX,
|
||||
maxExtraShift = FOCUS_MARGIN_PX,
|
||||
hasEditableTarget,
|
||||
threshold = KEYBOARD_OPEN_THRESHOLD_PX,
|
||||
}: KeyboardFocusShiftInput) {
|
||||
const visualBottom = visualTop + visualHeight;
|
||||
const safeTop = visualTop + margin;
|
||||
const safeBottom = visualBottom - margin;
|
||||
const unshiftedTargetTop = targetTop + currentShift;
|
||||
const unshiftedTargetBottom = targetBottom + currentShift;
|
||||
let nextShift = currentShift;
|
||||
const insetBottom = Math.max(0, Math.round(layoutHeight - visualBottom));
|
||||
const isOpen =
|
||||
hasEditableTarget && layoutHeight - visualHeight > threshold;
|
||||
|
||||
if (unshiftedTargetBottom - nextShift > safeBottom) {
|
||||
nextShift = unshiftedTargetBottom - safeBottom;
|
||||
}
|
||||
|
||||
if (unshiftedTargetTop - nextShift < safeTop) {
|
||||
nextShift = Math.max(0, unshiftedTargetTop - safeTop);
|
||||
}
|
||||
|
||||
const keyboardInset = Math.max(0, layoutHeight - visualBottom);
|
||||
const maxShift = keyboardInset + maxExtraShift;
|
||||
return Math.round(clamp(nextShift, 0, Math.max(0, maxShift)));
|
||||
return {
|
||||
isOpen,
|
||||
insetBottom: isOpen ? insetBottom : 0,
|
||||
};
|
||||
}
|
||||
|
||||
export function stabilizeMobileViewportKeyboardFocus() {
|
||||
@@ -154,7 +154,6 @@ export function stabilizeMobileViewportKeyboardFocus() {
|
||||
const root = document.documentElement;
|
||||
const visualViewport = readVisualViewport();
|
||||
let stableLayoutHeight = readLayoutViewportHeight();
|
||||
let currentShift = 0;
|
||||
let frameId = 0;
|
||||
|
||||
const setLayoutHeight = (nextHeight: number) => {
|
||||
@@ -167,6 +166,10 @@ export function stabilizeMobileViewportKeyboardFocus() {
|
||||
|
||||
const setKeyboardState = (isOpen: boolean, insetBottom = 0) => {
|
||||
if (isOpen) {
|
||||
root.style.setProperty(
|
||||
KEYBOARD_EXPOSED_FILL_VAR,
|
||||
resolveMobileKeyboardExposedFill(),
|
||||
);
|
||||
root.dataset.mobileKeyboardOpen = 'true';
|
||||
} else {
|
||||
delete root.dataset.mobileKeyboardOpen;
|
||||
@@ -178,9 +181,8 @@ export function stabilizeMobileViewportKeyboardFocus() {
|
||||
);
|
||||
};
|
||||
|
||||
const setFocusShift = (nextShift: number) => {
|
||||
currentShift = Math.max(0, Math.round(nextShift));
|
||||
root.style.setProperty(KEYBOARD_FOCUS_OFFSET_VAR, `${currentShift}px`);
|
||||
const resetFocusShift = () => {
|
||||
root.style.setProperty(KEYBOARD_FOCUS_OFFSET_VAR, '0px');
|
||||
};
|
||||
|
||||
const readActiveTarget = () =>
|
||||
@@ -193,15 +195,16 @@ export function stabilizeMobileViewportKeyboardFocus() {
|
||||
const viewport = readVisualViewport();
|
||||
const visualTop = viewport?.offsetTop ?? 0;
|
||||
const visualHeight = viewport?.height ?? window.innerHeight;
|
||||
const visualBottom = visualTop + visualHeight;
|
||||
const keyboardInset = Math.max(0, stableLayoutHeight - visualBottom);
|
||||
const keyboardOpen =
|
||||
Boolean(activeTarget) &&
|
||||
stableLayoutHeight - visualHeight > KEYBOARD_OPEN_THRESHOLD_PX;
|
||||
const keyboardState = resolveMobileKeyboardState({
|
||||
layoutHeight: stableLayoutHeight,
|
||||
visualTop,
|
||||
visualHeight,
|
||||
hasEditableTarget: Boolean(activeTarget),
|
||||
});
|
||||
|
||||
if (!keyboardOpen || !activeTarget) {
|
||||
if (!keyboardState.isOpen || !activeTarget) {
|
||||
setKeyboardState(false);
|
||||
setFocusShift(0);
|
||||
resetFocusShift();
|
||||
|
||||
if (!activeTarget) {
|
||||
setLayoutHeight(readLayoutViewportHeight());
|
||||
@@ -209,19 +212,10 @@ export function stabilizeMobileViewportKeyboardFocus() {
|
||||
return;
|
||||
}
|
||||
|
||||
// 中文注释:先保持整页布局高度,再只移动画布,让输入框避开键盘。
|
||||
const targetRect = activeTarget.getBoundingClientRect();
|
||||
const nextShift = calculateMobileKeyboardFocusShift({
|
||||
layoutHeight: stableLayoutHeight,
|
||||
visualTop,
|
||||
visualHeight,
|
||||
targetTop: targetRect.top,
|
||||
targetBottom: targetRect.bottom,
|
||||
currentShift,
|
||||
});
|
||||
|
||||
setKeyboardState(true, keyboardInset);
|
||||
setFocusShift(nextShift);
|
||||
// 中文注释:H5 浏览器和小程序 web-view 已会自行处理输入框可见性。
|
||||
// 这里只记录键盘状态、隐藏底部 dock,并给可能露出的宿主区域补浅色背景。
|
||||
setKeyboardState(true, keyboardState.insetBottom);
|
||||
resetFocusShift();
|
||||
};
|
||||
|
||||
const scheduleSync = () => {
|
||||
@@ -243,14 +237,14 @@ export function stabilizeMobileViewportKeyboardFocus() {
|
||||
|
||||
setLayoutHeight(stableLayoutHeight);
|
||||
setKeyboardState(false);
|
||||
setFocusShift(0);
|
||||
resetFocusShift();
|
||||
|
||||
document.addEventListener('focusin', scheduleKeyboardAnimationSync, true);
|
||||
document.addEventListener('focusout', scheduleKeyboardAnimationSync, true);
|
||||
window.addEventListener('resize', scheduleKeyboardAnimationSync);
|
||||
window.addEventListener('orientationchange', () => {
|
||||
setKeyboardState(false);
|
||||
setFocusShift(0);
|
||||
resetFocusShift();
|
||||
window.setTimeout(() => {
|
||||
setLayoutHeight(readLayoutViewportHeight());
|
||||
scheduleSync();
|
||||
|
||||
Reference in New Issue
Block a user