Skip to content

Commit

Permalink
fix(scroll-assist): re-run when keyboard changes (#28174)
Browse files Browse the repository at this point in the history
Issue number: resolves #22940

---------

<!-- Please do not submit updates to dependencies unless it fixes an
issue. -->

<!-- Please try to limit your pull request to one type (bugfix, feature,
etc). Submit multiple pull requests if needed. -->

## What is the current behavior?
<!-- Please describe the current behavior that you are modifying. -->

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?
<!-- Please describe the behavior or changes that are being added by
this PR. -->

- 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 |
| - | - |
| <video
src="https://github.com/ionic-team/ionic-framework/assets/2721089/715e176a-6724-4308-ae3e-15b5bea308ac"></video>
| <video
src="https://github.com/ionic-team/ionic-framework/assets/2721089/b9ccd482-720a-409b-a089-b3330c1e405c"></video>
|

Breakdown per-resize mode:

| Native | None | Ionic | Body |
| - | - | - | - |
| <video
src="https://github.com/ionic-team/ionic-framework/assets/2721089/b930ac5f-3398-4887-a8ca-a57708adc66d"></video>
| <video
src="https://github.com/ionic-team/ionic-framework/assets/2721089/68465854-94d0-4e00-940c-c4674a43b6a3"></video>
| <video
src="https://github.com/ionic-team/ionic-framework/assets/2721089/561f313a-9caf-4c9e-ab15-9c4383f0e3ee"></video>
| <video
src="https://github.com/ionic-team/ionic-framework/assets/2721089/300b8894-ad2a-43bc-8e82-ecd68afd407e"></video>
|

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!-- If this introduces a breaking change, please describe the impact
and migration path for existing applications below. -->


## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->

Dev build: `7.3.4-dev.11694706860.14b2710d`

---------

Co-authored-by: Amanda Johnston <[email protected]>
  • Loading branch information
liamdebeasi and averyjohnston committed Sep 22, 2023
1 parent 3f084de commit 7c240dc
Show file tree
Hide file tree
Showing 2 changed files with 107 additions and 3 deletions.
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

0 comments on commit 7c240dc

Please sign in to comment.