Compare commits

...

5 Commits

Author SHA1 Message Date
Liam DeBeasi
12cae824b1 typo 2023-09-14 09:36:14 -04:00
Liam DeBeasi
504c90beae update types 2023-09-14 09:33:28 -04:00
Liam DeBeasi
f663b41644 add types 2023-09-13 17:12:47 -04:00
Liam DeBeasi
1a99036fd1 fix(scroll-assist): run when keyboard dimension changes 2023-09-13 16:58:39 -04:00
Liam DeBeasi
578981b2d0 fix(scroll-assist): improve input scroll accuracy 2023-09-12 16:00:53 -04:00
3 changed files with 142 additions and 7 deletions

View File

@@ -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;
/**
* Even 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;

View File

@@ -1,4 +1,5 @@
import type { KeyboardResizeOptions } from '@capacitor/keyboard';
import { win } from '@utils/browser';
import { getScrollElement, scrollByPoint } from '../../content';
import { raf } from '../../helpers';
@@ -34,6 +35,98 @@ 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.
* We do this by subtracting the keyboard height
* from the platform height.
*
* If we compute this value when switching between
* inputs then the webview may already be resized.
* At this point, `win.innerHeight` has already accounted
* for the keyboard meaning we would then subtract
* the keyboard height again. This will result in the input
* being scrolled more than it needs to.
*/
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
@@ -50,12 +143,27 @@ export const enableScrollAssist = (
inputEl.removeAttribute(SKIP_SCROLL_ASSIST);
return;
}
jsSetFocus(componentEl, inputEl, contentEl, footerEl, keyboardHeight, addScrollPadding, disableClonedInput);
jsSetFocus(
componentEl,
inputEl,
contentEl,
footerEl,
keyboardHeight,
addScrollPadding,
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);
};
};
@@ -84,12 +192,14 @@ const jsSetFocus = async (
footerEl: HTMLIonFooterElement | null,
keyboardHeight: number,
enableScrollPadding: boolean,
disableClonedInput = false
disableClonedInput = false,
platformHeight = 0,
waitForResize = true
) => {
if (!contentEl && !footerEl) {
return;
}
const scrollData = getScrollData(componentEl, (contentEl || footerEl)!, keyboardHeight);
const scrollData = getScrollData(componentEl, (contentEl || footerEl)!, keyboardHeight, platformHeight);
if (contentEl && Math.abs(scrollData.scrollAmount) < 4) {
// the text input is in a safe position that doesn't
@@ -191,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

View File

@@ -9,13 +9,18 @@ export interface ScrollData {
inputSafeY: number;
}
export const getScrollData = (componentEl: HTMLElement, contentEl: HTMLElement, keyboardHeight: number): ScrollData => {
export const getScrollData = (
componentEl: HTMLElement,
contentEl: HTMLElement,
keyboardHeight: number,
platformHeight: number
): ScrollData => {
const itemEl = (componentEl.closest('ion-item,[ion-item]') as HTMLElement) ?? componentEl;
return calcScrollData(
itemEl.getBoundingClientRect(),
contentEl.getBoundingClientRect(),
keyboardHeight,
(componentEl as any).ownerDocument.defaultView.innerHeight // TODO(FW-2832): type
platformHeight
);
};