From 3f06da4cfc0d59c658e17e09ccb1ea28a29339f9 Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Tue, 19 Sep 2023 15:02:20 -0400 Subject: [PATCH] fix(scroll-assist): re-run when keyboard changes (#28174) Issue number: resolves #22940 --------- ## What is the current behavior? Scroll assist does not run when changing keyboards. This means that inputs can be hidden under the keyboard if the new keyboard is larger than the previous keyboard. ## What is the new behavior? - On Browsers/PWAs scroll assist will re-run when the keyboard geometry changes. We don't have a cross-browser way of detecting keyboard changes yet, so this is the best we have for now. - On Cordova/Capacitor scroll assist will re-run when the keyboard changes, even if the overall keyboard geometry does not change. In the example below, we are changing keyboards while an input is focused: | `main` | branch | | - | - | | | | Breakdown per-resize mode: | Native | None | Ionic | Body | | - | - | - | - | | | | | | ## Does this introduce a breaking change? - [ ] Yes - [x] No ## Other information Dev build: `7.3.4-dev.11694706860.14b2710d` --------- Co-authored-by: Amanda Johnston <90629384+amandaejohnston@users.noreply.github.com> --- core/src/utils/browser/index.ts | 22 ++++- .../utils/input-shims/hacks/scroll-assist.ts | 88 ++++++++++++++++++- 2 files changed, 107 insertions(+), 3 deletions(-) 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