修复移动端软键盘页面弹跳黑底
移除 H5 软键盘打开时平台壳全局 transform 位移,避免浏览器原生避让后再次弹跳。 保留键盘打开状态、底部 dock 隐藏和浅色根背景兜底,避免短表单露出黑色宿主底色。 补充小程序 web-view 原生 page 浅色背景和对应样式测试。 更新统一创作页与平台键盘适配文档,沉淀不再全局上移平台壳的约束。
This commit is contained in:
@@ -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)');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user