Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion core/src/utils/browser/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
88 changes: 86 additions & 2 deletions core/src/utils/input-shims/hacks/scroll-assist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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);
};
};

Expand Down Expand Up @@ -110,7 +193,8 @@ const jsSetFocus = async (
keyboardHeight: number,
enableScrollPadding: boolean,
disableClonedInput = false,
platformHeight = 0
platformHeight = 0,
waitForResize = true
) => {
if (!contentEl && !footerEl) {
return;
Expand Down Expand Up @@ -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
Expand Down