移除 H5 软键盘打开时平台壳全局 transform 位移,避免浏览器原生避让后再次弹跳。 保留键盘打开状态、底部 dock 隐藏和浅色根背景兜底,避免短表单露出黑色宿主底色。 补充小程序 web-view 原生 page 浅色背景和对应样式测试。 更新统一创作页与平台键盘适配文档,沉淀不再全局上移平台壳的约束。
203 lines
6.2 KiB
TypeScript
203 lines
6.2 KiB
TypeScript
/* @vitest-environment jsdom */
|
|
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import {
|
|
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');
|
|
input.type = 'text';
|
|
const textarea = document.createElement('textarea');
|
|
const buttonInput = document.createElement('input');
|
|
buttonInput.type = 'file';
|
|
|
|
expect(isEditableKeyboardTarget(input)).toBe(true);
|
|
expect(isEditableKeyboardTarget(textarea)).toBe(true);
|
|
expect(isEditableKeyboardTarget(buttonInput)).toBe(false);
|
|
});
|
|
|
|
it('ignores disabled and readonly controls', () => {
|
|
const disabledInput = document.createElement('input');
|
|
disabledInput.disabled = true;
|
|
const readonlyInput = document.createElement('input');
|
|
readonlyInput.readOnly = true;
|
|
|
|
expect(isEditableKeyboardTarget(disabledInput)).toBe(false);
|
|
expect(isEditableKeyboardTarget(readonlyInput)).toBe(false);
|
|
});
|
|
});
|
|
|
|
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('falls back to the light platform fill before the shell mounts', () => {
|
|
document.body.innerHTML = '';
|
|
|
|
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)');
|
|
});
|
|
});
|