Skip to content

Commit 3f06da4

Browse files
fix(scroll-assist): re-run when keyboard changes (#28174)
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]>
1 parent 5ff32b7 commit 3f06da4

File tree

2 files changed

+107
-3
lines changed

2 files changed

+107
-3
lines changed

core/src/utils/browser/index.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,26 @@
2020
* Note: Code inside of this if-block will
2121
* not run in an SSR environment.
2222
*/
23-
export const win: Window | undefined = typeof window !== 'undefined' ? window : undefined;
23+
24+
/**
25+
* Event listeners on the window typically expect
26+
* Event types for the listener parameter. If you want to listen
27+
* on the window for certain CustomEvent types you can add that definition
28+
* here as long as you are using the "win" utility below.
29+
*/
30+
type IonicWindow = Window & {
31+
addEventListener(
32+
type: 'ionKeyboardDidShow',
33+
listener: (ev: CustomEvent<{ keyboardHeight: number }>) => void,
34+
options?: boolean | AddEventListenerOptions
35+
): void;
36+
removeEventListener(
37+
type: 'ionKeyboardDidShow',
38+
listener: (ev: CustomEvent<{ keyboardHeight: number }>) => void,
39+
options?: boolean | AddEventListenerOptions
40+
): void;
41+
};
42+
43+
export const win: IonicWindow | undefined = typeof window !== 'undefined' ? window : undefined;
2444

2545
export const doc: Document | undefined = typeof document !== 'undefined' ? document : undefined;

core/src/utils/input-shims/hacks/scroll-assist.ts

Lines changed: 86 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,15 @@ export const enableScrollAssist = (
3535
const addScrollPadding =
3636
enableScrollPadding && (keyboardResize === undefined || keyboardResize.mode === KeyboardResize.None);
3737

38+
/**
39+
* This tracks whether or not the keyboard has been
40+
* presented for a single focused text field. Note
41+
* that it does not track if the keyboard is open
42+
* in general such as if the keyboard is open for
43+
* a different focused text field.
44+
*/
45+
let hasKeyboardBeenPresentedForTextField = false;
46+
3847
/**
3948
* When adding scroll padding we need to know
4049
* how much of the viewport the keyboard obscures.
@@ -50,6 +59,74 @@ export const enableScrollAssist = (
5059
*/
5160
const platformHeight = win !== undefined ? win.innerHeight : 0;
5261

62+
/**
63+
* Scroll assist is run when a text field
64+
* is focused. However, it may need to
65+
* re-run when the keyboard size changes
66+
* such that the text field is now hidden
67+
* underneath the keyboard.
68+
* This function re-runs scroll assist
69+
* when that happens.
70+
*
71+
* One limitation of this is on a web browser
72+
* where native keyboard APIs do not have cross-browser
73+
* support. `ionKeyboardDidShow` relies on the Visual Viewport API.
74+
* This means that if the keyboard changes but does not change
75+
* geometry, then scroll assist will not re-run even if
76+
* the user has scrolled the text field under the keyboard.
77+
* This is not a problem when running in Cordova/Capacitor
78+
* because `ionKeyboardDidShow` uses the native events
79+
* which fire every time the keyboard changes.
80+
*/
81+
const keyboardShow = (ev: CustomEvent<{ keyboardHeight: number }>) => {
82+
/**
83+
* If the keyboard has not yet been presented
84+
* for this text field then the text field has just
85+
* received focus. In that case, the focusin listener
86+
* will run scroll assist.
87+
*/
88+
if (hasKeyboardBeenPresentedForTextField === false) {
89+
hasKeyboardBeenPresentedForTextField = true;
90+
return;
91+
}
92+
93+
/**
94+
* Otherwise, the keyboard has already been presented
95+
* for the focused text field.
96+
* This means that the keyboard likely changed
97+
* geometry, and we need to re-run scroll assist.
98+
* This can happen when the user rotates their device
99+
* or when they switch keyboards.
100+
*
101+
* Make sure we pass in the computed keyboard height
102+
* rather than the estimated keyboard height.
103+
*
104+
* Since the keyboard is already open then we do not
105+
* need to wait for the webview to resize, so we pass
106+
* "waitForResize: false".
107+
*/
108+
jsSetFocus(
109+
componentEl,
110+
inputEl,
111+
contentEl,
112+
footerEl,
113+
ev.detail.keyboardHeight,
114+
addScrollPadding,
115+
disableClonedInput,
116+
platformHeight,
117+
false
118+
);
119+
};
120+
121+
/**
122+
* Reset the internal state when the text field loses focus.
123+
*/
124+
const focusOut = () => {
125+
hasKeyboardBeenPresentedForTextField = false;
126+
win?.removeEventListener('ionKeyboardDidShow', keyboardShow);
127+
componentEl.removeEventListener('focusout', focusOut, true);
128+
};
129+
53130
/**
54131
* When the input is about to receive
55132
* focus, we need to move it to prevent
@@ -76,11 +153,17 @@ export const enableScrollAssist = (
76153
disableClonedInput,
77154
platformHeight
78155
);
156+
157+
win?.addEventListener('ionKeyboardDidShow', keyboardShow);
158+
componentEl.addEventListener('focusout', focusOut, true);
79159
};
160+
80161
componentEl.addEventListener('focusin', focusIn, true);
81162

82163
return () => {
83164
componentEl.removeEventListener('focusin', focusIn, true);
165+
win?.removeEventListener('ionKeyboardDidShow', keyboardShow);
166+
componentEl.removeEventListener('focusout', focusOut, true);
84167
};
85168
};
86169

@@ -110,7 +193,8 @@ const jsSetFocus = async (
110193
keyboardHeight: number,
111194
enableScrollPadding: boolean,
112195
disableClonedInput = false,
113-
platformHeight = 0
196+
platformHeight = 0,
197+
waitForResize = true
114198
) => {
115199
if (!contentEl && !footerEl) {
116200
return;
@@ -217,7 +301,7 @@ const jsSetFocus = async (
217301
* bandwidth to become available.
218302
*/
219303
const totalScrollAmount = scrollEl.scrollHeight - scrollEl.clientHeight;
220-
if (scrollData.scrollAmount > totalScrollAmount - scrollEl.scrollTop) {
304+
if (waitForResize && scrollData.scrollAmount > totalScrollAmount - scrollEl.scrollTop) {
221305
/**
222306
* On iOS devices, the system will show a "Passwords" bar above the keyboard
223307
* after the initial keyboard is shown. This prevents the webview from resizing

0 commit comments

Comments
 (0)