mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-19 19:57:22 +08:00
feat(app): keyboard open and close events (#18478)
This commit is contained in:
@ -1,6 +1,6 @@
|
|||||||
import { DOCUMENT } from '@angular/common';
|
import { DOCUMENT } from '@angular/common';
|
||||||
import { Inject, Injectable, NgZone } from '@angular/core';
|
import { Inject, Injectable, NgZone } from '@angular/core';
|
||||||
import { BackButtonEventDetail, Platforms, getPlatforms, isPlatform } from '@ionic/core';
|
import { BackButtonEventDetail, KeyboardEventDetail, Platforms, getPlatforms, isPlatform } from '@ionic/core';
|
||||||
import { Subject, Subscription } from 'rxjs';
|
import { Subject, Subscription } from 'rxjs';
|
||||||
|
|
||||||
export interface BackButtonEmitter extends Subject<BackButtonEventDetail> {
|
export interface BackButtonEmitter extends Subject<BackButtonEventDetail> {
|
||||||
@ -20,6 +20,18 @@ export class Platform {
|
|||||||
*/
|
*/
|
||||||
backButton: BackButtonEmitter = new Subject<BackButtonEventDetail>() as any;
|
backButton: BackButtonEmitter = new Subject<BackButtonEventDetail>() as any;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The keyboardDidShow event emits when the
|
||||||
|
* on-screen keyboard is presented.
|
||||||
|
*/
|
||||||
|
keyboardDidShow = new Subject<KeyboardEventDetail>() as any;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The keyboardDidHide event emits when the
|
||||||
|
* on-screen keyboard is hidden.
|
||||||
|
*/
|
||||||
|
keyboardDidHide = new Subject<void>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The pause event emits when the native platform puts the application
|
* The pause event emits when the native platform puts the application
|
||||||
* into the background, typically when the user switches to a different
|
* into the background, typically when the user switches to a different
|
||||||
@ -55,6 +67,8 @@ export class Platform {
|
|||||||
proxyEvent(this.resume, doc, 'resume');
|
proxyEvent(this.resume, doc, 'resume');
|
||||||
proxyEvent(this.backButton, doc, 'ionBackButton');
|
proxyEvent(this.backButton, doc, 'ionBackButton');
|
||||||
proxyEvent(this.resize, this.win, 'resize');
|
proxyEvent(this.resize, this.win, 'resize');
|
||||||
|
proxyEvent(this.keyboardDidShow, this.win, 'ionKeyboardDidShow');
|
||||||
|
proxyEvent(this.keyboardDidHide, this.win, 'ionKeyboardDidHide');
|
||||||
|
|
||||||
let readyResolve: (value: string) => void;
|
let readyResolve: (value: string) => void;
|
||||||
this._readyPromise = new Promise(res => { readyResolve = res; });
|
this._readyPromise = new Promise(res => { readyResolve = res; });
|
||||||
|
@ -28,6 +28,9 @@ export class App implements ComponentInterface {
|
|||||||
if (config.getBoolean('hardwareBackButton', isHybrid)) {
|
if (config.getBoolean('hardwareBackButton', isHybrid)) {
|
||||||
import('../../utils/hardware-back-button').then(module => module.startHardwareBackButton());
|
import('../../utils/hardware-back-button').then(module => module.startHardwareBackButton());
|
||||||
}
|
}
|
||||||
|
if (typeof (window as any) !== 'undefined') {
|
||||||
|
import('../../utils/keyboard').then(module => module.startKeyboardAssist(window));
|
||||||
|
}
|
||||||
import('../../utils/focus-visible').then(module => module.startFocusVisible());
|
import('../../utils/focus-visible').then(module => module.startFocusVisible());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
4
core/src/interface.d.ts
vendored
4
core/src/interface.d.ts
vendored
@ -56,6 +56,10 @@ export interface BackButtonEventDetail {
|
|||||||
register(priority: number, handler: (processNextHandler: () => void) => Promise<any> | void): void;
|
register(priority: number, handler: (processNextHandler: () => void) => Promise<any> | void): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface KeyboardEventDetail {
|
||||||
|
keyboardHeight: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface StyleEventDetail {
|
export interface StyleEventDetail {
|
||||||
[styleName: string]: boolean;
|
[styleName: string]: boolean;
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,8 @@ import { getScrollData } from './scroll-data';
|
|||||||
export const enableScrollAssist = (
|
export const enableScrollAssist = (
|
||||||
componentEl: HTMLElement,
|
componentEl: HTMLElement,
|
||||||
inputEl: HTMLInputElement | HTMLTextAreaElement,
|
inputEl: HTMLInputElement | HTMLTextAreaElement,
|
||||||
contentEl: HTMLIonContentElement,
|
contentEl: HTMLIonContentElement | null,
|
||||||
|
footerEl: HTMLIonFooterElement | null,
|
||||||
keyboardHeight: number
|
keyboardHeight: number
|
||||||
) => {
|
) => {
|
||||||
let coord: any;
|
let coord: any;
|
||||||
@ -29,7 +30,7 @@ export const enableScrollAssist = (
|
|||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
|
|
||||||
// begin the input focus process
|
// begin the input focus process
|
||||||
jsSetFocus(componentEl, inputEl, contentEl, keyboardHeight);
|
jsSetFocus(componentEl, inputEl, contentEl, footerEl, keyboardHeight);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
componentEl.addEventListener('touchstart', touchStart, true);
|
componentEl.addEventListener('touchstart', touchStart, true);
|
||||||
@ -44,11 +45,14 @@ export const enableScrollAssist = (
|
|||||||
const jsSetFocus = (
|
const jsSetFocus = (
|
||||||
componentEl: HTMLElement,
|
componentEl: HTMLElement,
|
||||||
inputEl: HTMLInputElement | HTMLTextAreaElement,
|
inputEl: HTMLInputElement | HTMLTextAreaElement,
|
||||||
contentEl: HTMLIonContentElement,
|
contentEl: HTMLIonContentElement | null,
|
||||||
|
footerEl: HTMLIonFooterElement | null,
|
||||||
keyboardHeight: number
|
keyboardHeight: number
|
||||||
) => {
|
) => {
|
||||||
const scrollData = getScrollData(componentEl, contentEl, keyboardHeight);
|
if (!contentEl && !footerEl) { return; }
|
||||||
if (Math.abs(scrollData.scrollAmount) < 4) {
|
const scrollData = getScrollData(componentEl, (contentEl || footerEl)!, keyboardHeight);
|
||||||
|
|
||||||
|
if (contentEl && Math.abs(scrollData.scrollAmount) < 4) {
|
||||||
// the text input is in a safe position that doesn't
|
// the text input is in a safe position that doesn't
|
||||||
// require it to be scrolled into view, just set focus now
|
// require it to be scrolled into view, just set focus now
|
||||||
inputEl.focus();
|
inputEl.focus();
|
||||||
@ -73,7 +77,9 @@ const jsSetFocus = (
|
|||||||
window.removeEventListener('keyboardWillShow', scrollContent);
|
window.removeEventListener('keyboardWillShow', scrollContent);
|
||||||
|
|
||||||
// scroll the input into place
|
// scroll the input into place
|
||||||
await contentEl.scrollByPoint(0, scrollData.scrollAmount, scrollData.scrollDuration);
|
if (contentEl) {
|
||||||
|
await contentEl.scrollByPoint(0, scrollData.scrollAmount, scrollData.scrollDuration);
|
||||||
|
}
|
||||||
|
|
||||||
// the scroll view is in the correct position now
|
// the scroll view is in the correct position now
|
||||||
// give the native text input focus
|
// give the native text input focus
|
||||||
|
@ -30,6 +30,7 @@ export const startInputShims = (config: Config) => {
|
|||||||
const inputRoot = componentEl.shadowRoot || componentEl;
|
const inputRoot = componentEl.shadowRoot || componentEl;
|
||||||
const inputEl = inputRoot.querySelector('input') || inputRoot.querySelector('textarea');
|
const inputEl = inputRoot.querySelector('input') || inputRoot.querySelector('textarea');
|
||||||
const scrollEl = componentEl.closest('ion-content');
|
const scrollEl = componentEl.closest('ion-content');
|
||||||
|
const footerEl = (!scrollEl) ? componentEl.closest('ion-footer') as HTMLIonFooterElement | null : null;
|
||||||
|
|
||||||
if (!inputEl) {
|
if (!inputEl) {
|
||||||
return;
|
return;
|
||||||
@ -40,8 +41,8 @@ export const startInputShims = (config: Config) => {
|
|||||||
hideCaretMap.set(componentEl, rmFn);
|
hideCaretMap.set(componentEl, rmFn);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (SCROLL_ASSIST && !!scrollEl && scrollAssist && !scrollAssistMap.has(componentEl)) {
|
if (SCROLL_ASSIST && (!!scrollEl || !!footerEl) && scrollAssist && !scrollAssistMap.has(componentEl)) {
|
||||||
const rmFn = enableScrollAssist(componentEl, inputEl, scrollEl, keyboardHeight);
|
const rmFn = enableScrollAssist(componentEl, inputEl, scrollEl, footerEl, keyboardHeight);
|
||||||
scrollAssistMap.set(componentEl, rmFn);
|
scrollAssistMap.set(componentEl, rmFn);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
176
core/src/utils/keyboard/index.ts
Normal file
176
core/src/utils/keyboard/index.ts
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
export const KEYBOARD_DID_OPEN = 'ionKeyboardDidShow';
|
||||||
|
export const KEYBOARD_DID_CLOSE = 'ionKeyboardDidHide';
|
||||||
|
const KEYBOARD_THRESHOLD = 150;
|
||||||
|
|
||||||
|
let previousVisualViewport: any = {};
|
||||||
|
let currentVisualViewport: any = {};
|
||||||
|
|
||||||
|
let previousLayoutViewport: any = {};
|
||||||
|
let currentLayoutViewport: any = {};
|
||||||
|
|
||||||
|
let keyboardOpen = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is only used for tests
|
||||||
|
*/
|
||||||
|
export const resetKeyboardAssist = () => {
|
||||||
|
previousVisualViewport = {};
|
||||||
|
currentVisualViewport = {};
|
||||||
|
previousLayoutViewport = {};
|
||||||
|
currentLayoutViewport = {};
|
||||||
|
keyboardOpen = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const startKeyboardAssist = (win: Window) => {
|
||||||
|
startNativeListeners(win);
|
||||||
|
|
||||||
|
if (!(win as any).visualViewport) { return; }
|
||||||
|
|
||||||
|
currentVisualViewport = copyVisualViewport((win as any).visualViewport);
|
||||||
|
currentLayoutViewport = copyLayoutViewport(win);
|
||||||
|
|
||||||
|
(win as any).visualViewport.onresize = () => {
|
||||||
|
trackViewportChanges(win);
|
||||||
|
|
||||||
|
if (keyboardDidOpen() || keyboardDidResize(win)) {
|
||||||
|
setKeyboardOpen(win);
|
||||||
|
} else if (keyboardDidClose(win)) {
|
||||||
|
setKeyboardClose(win);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listen for events fired by native keyboard plugin
|
||||||
|
* in Capacitor/Cordova so devs only need to listen
|
||||||
|
* in one place.
|
||||||
|
*/
|
||||||
|
const startNativeListeners = (win: Window) => {
|
||||||
|
win.addEventListener('keyboardDidShow', ev => setKeyboardOpen(win, ev));
|
||||||
|
win.addEventListener('keyboardDidHide', () => setKeyboardClose(win));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setKeyboardOpen = (win: Window, ev?: any) => {
|
||||||
|
fireKeyboardOpenEvent(win, ev);
|
||||||
|
keyboardOpen = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setKeyboardClose = (win: Window) => {
|
||||||
|
fireKeyboardCloseEvent(win);
|
||||||
|
keyboardOpen = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns `true` if the `keyboardOpen` flag is not
|
||||||
|
* set, the previous visual viewport width equal the current
|
||||||
|
* visual viewport width, and if the scaled difference
|
||||||
|
* of the previous visual viewport height minus the current
|
||||||
|
* visual viewport height is greater than KEYBOARD_THRESHOLD
|
||||||
|
*
|
||||||
|
* We need to be able to accomodate users who have zooming
|
||||||
|
* enabled in their browser (or have zoomed in manually) which
|
||||||
|
* is why we take into account the current visual viewport's
|
||||||
|
* scale value.
|
||||||
|
*/
|
||||||
|
export const keyboardDidOpen = (): boolean => {
|
||||||
|
const scaledHeightDifference = (previousVisualViewport.height - currentVisualViewport.height) * currentVisualViewport.scale;
|
||||||
|
return (
|
||||||
|
!keyboardOpen &&
|
||||||
|
previousVisualViewport.width === currentVisualViewport.width &&
|
||||||
|
scaledHeightDifference > KEYBOARD_THRESHOLD &&
|
||||||
|
!layoutViewportDidChange()
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns `true` if the keyboard is open,
|
||||||
|
* but the keyboard did not close
|
||||||
|
*/
|
||||||
|
export const keyboardDidResize = (win: Window): boolean => {
|
||||||
|
return keyboardOpen && !keyboardDidClose(win);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if the keyboard was closed
|
||||||
|
* Returns `true` if the `keyboardOpen` flag is set and
|
||||||
|
* the current visual viewport height equals the
|
||||||
|
* layout viewport height.
|
||||||
|
*/
|
||||||
|
export const keyboardDidClose = (win: Window): boolean => {
|
||||||
|
return keyboardOpen && currentVisualViewport.height === win.innerHeight;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if the layout viewport has
|
||||||
|
* changed since the last visual viewport change.
|
||||||
|
* It is rare that a layout viewport change is not
|
||||||
|
* associated with a visual viewport change so we
|
||||||
|
* want to make sure we don't get any false positives.
|
||||||
|
*/
|
||||||
|
const layoutViewportDidChange = (): boolean => {
|
||||||
|
return (
|
||||||
|
currentLayoutViewport.width !== previousLayoutViewport.width ||
|
||||||
|
currentLayoutViewport.height !== previousLayoutViewport.height
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatch a keyboard open event
|
||||||
|
*/
|
||||||
|
const fireKeyboardOpenEvent = (win: Window, nativeEv?: any): void => {
|
||||||
|
const keyboardHeight = nativeEv ? nativeEv.keyboardHeight : win.innerHeight - currentVisualViewport.height;
|
||||||
|
const ev = new CustomEvent(KEYBOARD_DID_OPEN, {
|
||||||
|
detail: { keyboardHeight }
|
||||||
|
});
|
||||||
|
|
||||||
|
win.dispatchEvent(ev);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatch a keyboard close event
|
||||||
|
*/
|
||||||
|
const fireKeyboardCloseEvent = (win: Window): void => {
|
||||||
|
const ev = new CustomEvent(KEYBOARD_DID_CLOSE);
|
||||||
|
win.dispatchEvent(ev);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a window object, create a copy of
|
||||||
|
* the current visual and layout viewport states
|
||||||
|
* while also preserving the previous visual and
|
||||||
|
* layout viewport states
|
||||||
|
*/
|
||||||
|
export const trackViewportChanges = (win: Window) => {
|
||||||
|
previousVisualViewport = { ...currentVisualViewport };
|
||||||
|
currentVisualViewport = copyVisualViewport((win as any).visualViewport);
|
||||||
|
|
||||||
|
previousLayoutViewport = { ...currentLayoutViewport };
|
||||||
|
currentLayoutViewport = copyLayoutViewport(win);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a deep copy of the visual viewport
|
||||||
|
* at a given state
|
||||||
|
*/
|
||||||
|
export const copyVisualViewport = (visualViewport: any): any => {
|
||||||
|
return {
|
||||||
|
width: Math.round(visualViewport.width),
|
||||||
|
height: Math.round(visualViewport.height),
|
||||||
|
offsetTop: visualViewport.offsetTop,
|
||||||
|
offsetLeft: visualViewport.offsetLeft,
|
||||||
|
pageTop: visualViewport.pageTop,
|
||||||
|
pageLeft: visualViewport.pageLeft,
|
||||||
|
scale: visualViewport.scale
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a deep copy of the layout viewport
|
||||||
|
* at a given state
|
||||||
|
*/
|
||||||
|
export const copyLayoutViewport = (win: Window): any => {
|
||||||
|
return {
|
||||||
|
width: win.innerWidth,
|
||||||
|
height: win.innerHeight
|
||||||
|
};
|
||||||
|
};
|
68
core/src/utils/keyboard/test/index.html
Normal file
68
core/src/utils/keyboard/test/index.html
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html dir="ltr">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>App - Keyboard</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||||
|
<link href="../../../../css/ionic.bundle.css" rel="stylesheet">
|
||||||
|
<link href="../../../../scripts/testing/styles.css" rel="stylesheet">
|
||||||
|
<script src="../../../../scripts/testing/scripts.js"></script>
|
||||||
|
<script src="../../../../dist/ionic.js"></script>
|
||||||
|
<style>
|
||||||
|
f {
|
||||||
|
display: block;
|
||||||
|
margin: 15px auto;
|
||||||
|
max-width: 150px;
|
||||||
|
height: 150px;
|
||||||
|
background: blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ion-footer {
|
||||||
|
transition: all 0.5s cubic-bezier(0.38, 0.7, 0.128, 1);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<ion-app>
|
||||||
|
<ion-header>
|
||||||
|
<ion-toolbar color="primary">
|
||||||
|
<ion-title>App - Keyboard</ion-title>
|
||||||
|
</ion-toolbar>
|
||||||
|
</ion-header>
|
||||||
|
|
||||||
|
<ion-content>
|
||||||
|
<f></f>
|
||||||
|
<f></f>
|
||||||
|
<f></f>
|
||||||
|
<f></f>
|
||||||
|
</ion-content>
|
||||||
|
|
||||||
|
<ion-footer>
|
||||||
|
<ion-toolbar>
|
||||||
|
<ion-input type="text" placeholder="Enter some text..."></ion-input>
|
||||||
|
<ion-input type="text" placeholder="Enter some text..."></ion-input>
|
||||||
|
</ion-toolbar>
|
||||||
|
</ion-footer>
|
||||||
|
</ion-app>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const input = document.querySelector('ion-footer');
|
||||||
|
|
||||||
|
window.addEventListener('ionKeyboardDidShow', (e) => {
|
||||||
|
console.log('ionKeyboardDidShow');
|
||||||
|
setInputOffset(e.detail.keyboardHeight);
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('ionKeyboardDidHide', () => {
|
||||||
|
console.log('ionKeyboardDidHide');
|
||||||
|
setInputOffset(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
function setInputOffset(offset) {
|
||||||
|
input.setAttribute('style', `transform: translate3d(0, ${-offset}px, 0)`);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
243
core/src/utils/keyboard/test/keyboard.spec.ts
Normal file
243
core/src/utils/keyboard/test/keyboard.spec.ts
Normal file
@ -0,0 +1,243 @@
|
|||||||
|
import { copyLayoutViewport, copyVisualViewport, setKeyboardClose, setKeyboardOpen, keyboardDidClose, keyboardDidOpen, keyboardDidResize, resetKeyboardAssist, startKeyboardAssist, trackViewportChanges, KEYBOARD_DID_OPEN, KEYBOARD_DID_CLOSE } from '../';
|
||||||
|
|
||||||
|
const mockVisualViewport = (win: Window, visualViewport: any = { width: 320, height: 568 }, layoutViewport = { innerWidth: 320, innerHeight: 568 }): any => {
|
||||||
|
win.visualViewport = {
|
||||||
|
width: 320,
|
||||||
|
height: 568,
|
||||||
|
offsetTop: 0,
|
||||||
|
offsetLeft: 0,
|
||||||
|
pageTop: 0,
|
||||||
|
pageLeft: 0,
|
||||||
|
scale: 1,
|
||||||
|
onresize: undefined,
|
||||||
|
onscroll: undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
win.visualViewport = Object.assign(win.visualViewport, visualViewport);
|
||||||
|
win = Object.assign(win, layoutViewport);
|
||||||
|
win.dispatchEvent = jest.fn(() => {});
|
||||||
|
|
||||||
|
trackViewportChanges(win);
|
||||||
|
|
||||||
|
return win;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resizeVisualViewport = (win: Window, visualViewport: any = {}) => {
|
||||||
|
win.visualViewport = Object.assign(win.visualViewport, visualViewport);
|
||||||
|
|
||||||
|
if (win.visualViewport.onresize) {
|
||||||
|
win.visualViewport.onresize();
|
||||||
|
} else {
|
||||||
|
trackViewportChanges(win);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Keyboard Assist Tests', () => {
|
||||||
|
describe('copyLayoutViewport()', () => {
|
||||||
|
it('should properly copy the layout viewport', () => {
|
||||||
|
const win = {
|
||||||
|
innerWidth: 100,
|
||||||
|
innerHeight: 200
|
||||||
|
};
|
||||||
|
|
||||||
|
const copiedViewport = copyLayoutViewport(win);
|
||||||
|
|
||||||
|
win.innerWidth = 400;
|
||||||
|
win.innerHeight = 800;
|
||||||
|
|
||||||
|
expect(copiedViewport.width).toEqual(100);
|
||||||
|
expect(copiedViewport.height).toEqual(200);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('copyVisualViewport()', () => {
|
||||||
|
it('should properly copy the visual viewport', () => {
|
||||||
|
const visualViewport = {
|
||||||
|
width: 100,
|
||||||
|
height: 200,
|
||||||
|
offsetTop: 5,
|
||||||
|
offsetLeft: 10,
|
||||||
|
pageTop: 0,
|
||||||
|
pageLeft: 0,
|
||||||
|
scale: 2
|
||||||
|
};
|
||||||
|
|
||||||
|
const copiedViewport = copyVisualViewport(visualViewport);
|
||||||
|
|
||||||
|
visualViewport.width = 400;
|
||||||
|
visualViewport.height = 800;
|
||||||
|
visualViewport.scale = 3;
|
||||||
|
visualViewport.offsetTop = 0;
|
||||||
|
|
||||||
|
expect(copiedViewport.width).toEqual(100);
|
||||||
|
expect(copiedViewport.height).toEqual(200);
|
||||||
|
expect(copiedViewport.scale).toEqual(2);
|
||||||
|
expect(copiedViewport.offsetTop).toEqual(5);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setKeyboardOpen()', () => {
|
||||||
|
it('should dispatch the keyboard open event on the window', () => {
|
||||||
|
window.dispatchEvent = jest.fn(() => {});
|
||||||
|
|
||||||
|
setKeyboardOpen(window);
|
||||||
|
|
||||||
|
expect(window.dispatchEvent.mock.calls.length).toEqual(1);
|
||||||
|
expect(window.dispatchEvent.mock.calls[0][0].type).toEqual(KEYBOARD_DID_OPEN);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setKeyboardClose()', () => {
|
||||||
|
it('should dispatch the keyboard close event on the window', () => {
|
||||||
|
window.dispatchEvent = jest.fn(() => {});
|
||||||
|
|
||||||
|
setKeyboardClose(window);
|
||||||
|
|
||||||
|
expect(window.dispatchEvent.mock.calls.length).toEqual(1);
|
||||||
|
expect(window.dispatchEvent.mock.calls[0][0].type).toEqual(KEYBOARD_DID_CLOSE);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('keyboardDidOpen()', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
resetKeyboardAssist(window);
|
||||||
|
mockVisualViewport(window);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true when visual viewport height < layout viewport height and meets or exceeds the keyboard threshold', () => {
|
||||||
|
resizeVisualViewport(window, { height: 200 });
|
||||||
|
|
||||||
|
expect(keyboardDidOpen(window)).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when visual viewport height < layout viewport heigh but does not meet the keyboard threshold', () => {
|
||||||
|
resizeVisualViewport(window, { height: 500 });
|
||||||
|
|
||||||
|
expect(keyboardDidOpen(window)).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false on orientation change', () => {
|
||||||
|
resizeVisualViewport(window, { width: 320, height: 250 });
|
||||||
|
resizeVisualViewport(window, { width: 250, height: 320 });
|
||||||
|
|
||||||
|
expect(keyboardDidOpen(window)).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when both the visual and layout viewports change', () => {
|
||||||
|
resizeVisualViewport(window, { width: 250, height: 320 }, { innerWidth: 250, innerHeight: 320 });
|
||||||
|
|
||||||
|
expect(keyboardDidOpen(window)).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true when the keyboard shows even if the user is zoomed in', () => {
|
||||||
|
// User zooms in
|
||||||
|
resizeVisualViewport(window, { width: 160, height: 284, scale: 2 });
|
||||||
|
|
||||||
|
// User taps input and keyboard appears
|
||||||
|
resizeVisualViewport(window, { width: 160, height: 184, scale: 2 });
|
||||||
|
|
||||||
|
expect(keyboardDidOpen(window)).toEqual(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('keyboardDidClose()', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
resetKeyboardAssist(window);
|
||||||
|
mockVisualViewport(window);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when keyboard is not open', () => {
|
||||||
|
expect(keyboardDidClose(window)).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when keyboard is open but visual viewport !== layout viewport', () => {
|
||||||
|
resizeVisualViewport(window, { width: 320, height: 250 });
|
||||||
|
|
||||||
|
setKeyboardOpen(window);
|
||||||
|
|
||||||
|
expect(keyboardDidClose(window)).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true when keyboard is open and viewport === layout viewport', () => {
|
||||||
|
resizeVisualViewport(window, { width: 320, height: 250 });
|
||||||
|
|
||||||
|
setKeyboardOpen(window);
|
||||||
|
|
||||||
|
resizeVisualViewport(window, { width: 320, height: 568 });
|
||||||
|
|
||||||
|
expect(keyboardDidClose(window)).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false on orientation change', () => {
|
||||||
|
resizeVisualViewport(window, { width: 320, height: 250 });
|
||||||
|
|
||||||
|
setKeyboardOpen(window);
|
||||||
|
|
||||||
|
resizeVisualViewport(window, { width: 250, height: 320 });
|
||||||
|
|
||||||
|
expect(keyboardDidClose(window)).toEqual(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('keyboardDidResize()', () => {
|
||||||
|
it('should return true when the keyboard is open but did not close', () => {
|
||||||
|
mockVisualViewport(window, { width: 250, height: 320 });
|
||||||
|
setKeyboardOpen(window);
|
||||||
|
|
||||||
|
mockVisualViewport(window, { width: 250, height: 300 });
|
||||||
|
|
||||||
|
expect(keyboardDidResize(window)).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when the keyboard is not open', () => {
|
||||||
|
mockVisualViewport(window);
|
||||||
|
|
||||||
|
expect(keyboardDidResize(window)).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when the keyboard has closed', () => {
|
||||||
|
mockVisualViewport(window, { width: 320, height: 250 });
|
||||||
|
setKeyboardOpen(window);
|
||||||
|
setKeyboardClose(window);
|
||||||
|
|
||||||
|
expect(keyboardDidResize(window)).toEqual(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Keyboard Assist Integration', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
resetKeyboardAssist(window);
|
||||||
|
mockVisualViewport(window);
|
||||||
|
startKeyboardAssist(window);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should properly set the keyboard to be open', () => {
|
||||||
|
resizeVisualViewport(window, { width: 320, height: 350 });
|
||||||
|
|
||||||
|
expect(window.dispatchEvent.mock.calls.length).toEqual(1);
|
||||||
|
expect(window.dispatchEvent.mock.calls[0][0].type).toEqual(KEYBOARD_DID_OPEN);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should properly set the keyboard to be closed', () => {
|
||||||
|
resizeVisualViewport(window, { width: 320, height: 350 });
|
||||||
|
resizeVisualViewport(window, { width: 320, height: 568 });
|
||||||
|
|
||||||
|
expect(window.dispatchEvent.mock.calls.length).toEqual(2);
|
||||||
|
expect(window.dispatchEvent.mock.calls[1][0].type).toEqual(KEYBOARD_DID_CLOSE);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should properly set the keyboard to be resized', () => {
|
||||||
|
resizeVisualViewport(window, { width: 320, height: 350 });
|
||||||
|
resizeVisualViewport(window, { width: 320, height: 360 });
|
||||||
|
|
||||||
|
expect(window.dispatchEvent.mock.calls.length).toEqual(2);
|
||||||
|
expect(window.dispatchEvent.mock.calls[0][0].type).toEqual(KEYBOARD_DID_OPEN);
|
||||||
|
expect(window.dispatchEvent.mock.calls[1][0].type).toEqual(KEYBOARD_DID_OPEN);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not set keyboard open on orientation change', () => {
|
||||||
|
resizeVisualViewport(window, { width: 568, height: 320 });
|
||||||
|
expect(window.dispatchEvent.mock.calls.length).toEqual(0);
|
||||||
|
});
|
||||||
|
});
|
Reference in New Issue
Block a user