diff --git a/core/src/utils/browser/index.ts b/core/src/utils/browser/index.ts index 5b3e914a45..5013b9f2c9 100644 --- a/core/src/utils/browser/index.ts +++ b/core/src/utils/browser/index.ts @@ -20,6 +20,26 @@ * Note: Code inside of this if-block will * not run in an SSR environment. */ -export const win: Window | undefined = typeof window !== 'undefined' ? window : undefined; + +/** + * Event listeners on the window typically expect + * Event types for the listener parameter. If you want to listen + * on the window for certain CustomEvent types you can add that definition + * here as long as you are using the "win" utility below. + */ +type IonicWindow = Window & { + addEventListener( + type: 'ionKeyboardDidShow', + listener: (ev: CustomEvent<{ keyboardHeight: number }>) => void, + options?: boolean | AddEventListenerOptions + ): void; + removeEventListener( + type: 'ionKeyboardDidShow', + listener: (ev: CustomEvent<{ keyboardHeight: number }>) => void, + options?: boolean | AddEventListenerOptions + ): void; +}; + +export const win: IonicWindow | undefined = typeof window !== 'undefined' ? window : undefined; export const doc: Document | undefined = typeof document !== 'undefined' ? document : undefined; diff --git a/core/src/utils/input-shims/hacks/scroll-assist.ts b/core/src/utils/input-shims/hacks/scroll-assist.ts index 400b201f09..01f810f329 100644 --- a/core/src/utils/input-shims/hacks/scroll-assist.ts +++ b/core/src/utils/input-shims/hacks/scroll-assist.ts @@ -35,6 +35,15 @@ export const enableScrollAssist = ( const addScrollPadding = enableScrollPadding && (keyboardResize === undefined || keyboardResize.mode === KeyboardResize.None); + /** + * This tracks whether or not the keyboard has been + * presented for a single focused text field. Note + * that it does not track if the keyboard is open + * in general such as if the keyboard is open for + * a different focused text field. + */ + let hasKeyboardBeenPresentedForTextField = false; + /** * When adding scroll padding we need to know * how much of the viewport the keyboard obscures. @@ -50,6 +59,74 @@ export const enableScrollAssist = ( */ const platformHeight = win !== undefined ? win.innerHeight : 0; + /** + * Scroll assist is run when a text field + * is focused. However, it may need to + * re-run when the keyboard size changes + * such that the text field is now hidden + * underneath the keyboard. + * This function re-runs scroll assist + * when that happens. + * + * One limitation of this is on a web browser + * where native keyboard APIs do not have cross-browser + * support. `ionKeyboardDidShow` relies on the Visual Viewport API. + * This means that if the keyboard changes but does not change + * geometry, then scroll assist will not re-run even if + * the user has scrolled the text field under the keyboard. + * This is not a problem when running in Cordova/Capacitor + * because `ionKeyboardDidShow` uses the native events + * which fire every time the keyboard changes. + */ + const keyboardShow = (ev: CustomEvent<{ keyboardHeight: number }>) => { + /** + * If the keyboard has not yet been presented + * for this text field then the text field has just + * received focus. In that case, the focusin listener + * will run scroll assist. + */ + if (hasKeyboardBeenPresentedForTextField === false) { + hasKeyboardBeenPresentedForTextField = true; + return; + } + + /** + * Otherwise, the keyboard has already been presented + * for the focused text field. + * This means that the keyboard likely changed + * geometry, and we need to re-run scroll assist. + * This can happen when the user rotates their device + * or when they switch keyboards. + * + * Make sure we pass in the computed keyboard height + * rather than the estimated keyboard height. + * + * Since the keyboard is already open then we do not + * need to wait for the webview to resize, so we pass + * "waitForResize: false". + */ + jsSetFocus( + componentEl, + inputEl, + contentEl, + footerEl, + ev.detail.keyboardHeight, + addScrollPadding, + disableClonedInput, + platformHeight, + false + ); + }; + + /** + * Reset the internal state when the text field loses focus. + */ + const focusOut = () => { + hasKeyboardBeenPresentedForTextField = false; + win?.removeEventListener('ionKeyboardDidShow', keyboardShow); + componentEl.removeEventListener('focusout', focusOut, true); + }; + /** * When the input is about to receive * focus, we need to move it to prevent @@ -76,11 +153,17 @@ export const enableScrollAssist = ( disableClonedInput, platformHeight ); + + win?.addEventListener('ionKeyboardDidShow', keyboardShow); + componentEl.addEventListener('focusout', focusOut, true); }; + componentEl.addEventListener('focusin', focusIn, true); return () => { componentEl.removeEventListener('focusin', focusIn, true); + win?.removeEventListener('ionKeyboardDidShow', keyboardShow); + componentEl.removeEventListener('focusout', focusOut, true); }; }; @@ -110,7 +193,8 @@ const jsSetFocus = async ( keyboardHeight: number, enableScrollPadding: boolean, disableClonedInput = false, - platformHeight = 0 + platformHeight = 0, + waitForResize = true ) => { if (!contentEl && !footerEl) { return; @@ -217,7 +301,7 @@ const jsSetFocus = async ( * bandwidth to become available. */ const totalScrollAmount = scrollEl.scrollHeight - scrollEl.clientHeight; - if (scrollData.scrollAmount > totalScrollAmount - scrollEl.scrollTop) { + if (waitForResize && scrollData.scrollAmount > totalScrollAmount - scrollEl.scrollTop) { /** * On iOS devices, the system will show a "Passwords" bar above the keyboard * after the initial keyboard is shown. This prevents the webview from resizing