修复移动端软键盘页面弹跳黑底

移除 H5 软键盘打开时平台壳全局 transform 位移,避免浏览器原生避让后再次弹跳。

保留键盘打开状态、底部 dock 隐藏和浅色根背景兜底,避免短表单露出黑色宿主底色。

补充小程序 web-view 原生 page 浅色背景和对应样式测试。

更新统一创作页与平台键盘适配文档,沉淀不再全局上移平台壳的约束。
This commit is contained in:
2026-06-07 18:17:23 +08:00
parent 56a9075582
commit 665f09f047
11 changed files with 304 additions and 115 deletions

View File

@@ -1,12 +1,59 @@
/* @vitest-environment jsdom */
import { describe, expect, it } from 'vitest';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import {
calculateMobileKeyboardFocusShift,
isEditableKeyboardTarget,
resolveMobileKeyboardExposedFill,
resolveMobileKeyboardState,
stabilizeMobileViewportKeyboardFocus,
} from './mobileViewportKeyboardFocus';
const originalMatchMedia = window.matchMedia;
const originalRequestAnimationFrame = window.requestAnimationFrame;
const originalCancelAnimationFrame = window.cancelAnimationFrame;
const originalInnerHeight = window.innerHeight;
const originalMaxTouchPoints = navigator.maxTouchPoints;
const originalVisualViewport = window.visualViewport;
function defineWindowValue<Key extends keyof Window>(key: Key, value: Window[Key]) {
Object.defineProperty(window, key, {
configurable: true,
value,
});
}
function defineNavigatorValue<Key extends keyof Navigator>(
key: Key,
value: Navigator[Key],
) {
Object.defineProperty(navigator, key, {
configurable: true,
value,
});
}
beforeEach(() => {
document.body.innerHTML = '';
document.documentElement.removeAttribute('data-mobile-keyboard-open');
document.documentElement.removeAttribute('data-mobile-viewport-keyboard-focus');
document.documentElement.removeAttribute('style');
});
afterEach(() => {
vi.useRealTimers();
document.body.innerHTML = '';
document.documentElement.removeAttribute('data-mobile-keyboard-open');
document.documentElement.removeAttribute('data-mobile-viewport-keyboard-focus');
document.documentElement.removeAttribute('style');
defineWindowValue('matchMedia', originalMatchMedia);
defineWindowValue('requestAnimationFrame', originalRequestAnimationFrame);
defineWindowValue('cancelAnimationFrame', originalCancelAnimationFrame);
defineWindowValue('innerHeight', originalInnerHeight);
defineNavigatorValue('maxTouchPoints', originalMaxTouchPoints);
defineWindowValue('visualViewport', originalVisualViewport);
});
describe('isEditableKeyboardTarget', () => {
it('matches controls that open the mobile keyboard', () => {
const input = document.createElement('input');
@@ -31,47 +78,125 @@ describe('isEditableKeyboardTarget', () => {
});
});
describe('calculateMobileKeyboardFocusShift', () => {
it('moves a bottom input above the visible keyboard area', () => {
expect(
calculateMobileKeyboardFocusShift({
layoutHeight: 800,
visualTop: 0,
visualHeight: 500,
targetTop: 720,
targetBottom: 770,
currentShift: 0,
margin: 20,
}),
).toBe(290);
describe('resolveMobileKeyboardExposedFill', () => {
it('uses the active platform shell fill for exposed mini-program keyboard space', () => {
document.body.innerHTML = `
<div
class="platform-viewport-shell"
style="--platform-body-fill: linear-gradient(180deg, rgb(255, 253, 249), rgb(248, 239, 231));"
></div>
`;
expect(resolveMobileKeyboardExposedFill()).toContain('rgb(255, 253, 249)');
});
it('does not move when the focused input is already visible', () => {
expect(
calculateMobileKeyboardFocusShift({
layoutHeight: 800,
visualTop: 0,
visualHeight: 500,
targetTop: 250,
targetBottom: 300,
currentShift: 0,
margin: 20,
}),
).toBe(0);
});
it('falls back to the light platform fill before the shell mounts', () => {
document.body.innerHTML = '';
it('caps movement to keyboard inset plus safety margin', () => {
expect(
calculateMobileKeyboardFocusShift({
layoutHeight: 800,
visualTop: 0,
visualHeight: 500,
targetTop: 790,
targetBottom: 860,
currentShift: 0,
margin: 20,
maxExtraShift: 20,
}),
).toBe(320);
expect(resolveMobileKeyboardExposedFill()).toContain('#fffdf9');
});
});
describe('resolveMobileKeyboardState', () => {
it('detects the keyboard inset without asking the app shell to shift', () => {
expect(
resolveMobileKeyboardState({
layoutHeight: 800,
visualTop: 0,
visualHeight: 500,
hasEditableTarget: true,
}),
).toEqual({
isOpen: true,
insetBottom: 300,
});
});
it('stays closed when no editable target is focused', () => {
expect(
resolveMobileKeyboardState({
layoutHeight: 800,
visualTop: 0,
visualHeight: 500,
hasEditableTarget: false,
}),
).toEqual({
isOpen: false,
insetBottom: 0,
});
});
it('accounts for browser visual viewport panning without returning a shell offset', () => {
expect(
resolveMobileKeyboardState({
layoutHeight: 800,
visualTop: 120,
visualHeight: 500,
hasEditableTarget: true,
}),
).toEqual({
isOpen: true,
insetBottom: 180,
});
});
});
describe('stabilizeMobileViewportKeyboardFocus', () => {
it('marks H5 keyboard state without applying a global shell transform offset', () => {
vi.useFakeTimers();
document.body.innerHTML = `
<div
class="platform-viewport-shell"
style="--platform-body-fill: linear-gradient(180deg, rgb(255, 253, 249), rgb(248, 239, 231));"
>
<input id="theme-input" />
</div>
`;
defineNavigatorValue('maxTouchPoints', 1);
defineWindowValue(
'matchMedia',
vi.fn().mockReturnValue({ matches: true }) as unknown as Window['matchMedia'],
);
defineWindowValue(
'requestAnimationFrame',
((callback: FrameRequestCallback) => {
callback(0);
return 1;
}) as Window['requestAnimationFrame'],
);
defineWindowValue(
'cancelAnimationFrame',
vi.fn() as unknown as Window['cancelAnimationFrame'],
);
defineWindowValue('innerHeight', 800);
defineWindowValue('visualViewport', {
height: 500,
offsetTop: 120,
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
} as unknown as VisualViewport);
stabilizeMobileViewportKeyboardFocus();
document.getElementById('theme-input')?.focus();
document.dispatchEvent(new FocusEvent('focusin', { bubbles: true }));
vi.runAllTimers();
expect(document.documentElement.dataset.mobileKeyboardOpen).toBe('true');
expect(
document.documentElement.style.getPropertyValue(
'--platform-keyboard-focus-offset',
),
).toBe('0px');
expect(
document.documentElement.style.getPropertyValue(
'--platform-keyboard-inset-bottom',
),
).toBe('180px');
expect(
document.documentElement.style.getPropertyValue(
'--platform-keyboard-exposed-fill',
),
).toContain('rgb(255, 253, 249)');
});
});