mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-17 18:54:11 +08:00
fix(input, textarea): padding is now added to content so inputs scroll above keyboard (#25849)
resolves #18532
This commit is contained in:
@ -1,23 +1,42 @@
|
|||||||
import { getScrollElement, scrollByPoint } from '../../content';
|
import { getScrollElement, scrollByPoint } from '../../content';
|
||||||
import { raf } from '../../helpers';
|
import { raf } from '../../helpers';
|
||||||
|
import type { KeyboardResizeOptions } from '../../native/keyboard';
|
||||||
|
import { KeyboardResize } from '../../native/keyboard';
|
||||||
|
|
||||||
import { relocateInput, SCROLL_AMOUNT_PADDING } from './common';
|
import { relocateInput, SCROLL_AMOUNT_PADDING } from './common';
|
||||||
import { getScrollData } from './scroll-data';
|
import { getScrollData } from './scroll-data';
|
||||||
|
import { setScrollPadding, setClearScrollPaddingListener } from './scroll-padding';
|
||||||
|
|
||||||
|
let currentPadding = 0;
|
||||||
|
|
||||||
export const enableScrollAssist = (
|
export const enableScrollAssist = (
|
||||||
componentEl: HTMLElement,
|
componentEl: HTMLElement,
|
||||||
inputEl: HTMLInputElement | HTMLTextAreaElement,
|
inputEl: HTMLInputElement | HTMLTextAreaElement,
|
||||||
contentEl: HTMLElement | null,
|
contentEl: HTMLElement | null,
|
||||||
footerEl: HTMLIonFooterElement | null,
|
footerEl: HTMLIonFooterElement | null,
|
||||||
keyboardHeight: number
|
keyboardHeight: number,
|
||||||
|
enableScrollPadding: boolean,
|
||||||
|
keyboardResize: KeyboardResizeOptions | undefined
|
||||||
) => {
|
) => {
|
||||||
|
/**
|
||||||
|
* Scroll padding should only be added if:
|
||||||
|
* 1. The global scrollPadding config option
|
||||||
|
* is set to true.
|
||||||
|
* 2. The native keyboard resize mode is either "none"
|
||||||
|
* (keyboard overlays webview) or undefined (resize
|
||||||
|
* information unavailable)
|
||||||
|
* Resize info is available on Capacitor 4+
|
||||||
|
*/
|
||||||
|
const addScrollPadding =
|
||||||
|
enableScrollPadding && (keyboardResize === undefined || keyboardResize.mode === KeyboardResize.None);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When the input is about to receive
|
* When the input is about to receive
|
||||||
* focus, we need to move it to prevent
|
* focus, we need to move it to prevent
|
||||||
* mobile Safari from adjusting the viewport.
|
* mobile Safari from adjusting the viewport.
|
||||||
*/
|
*/
|
||||||
const focusIn = () => {
|
const focusIn = async () => {
|
||||||
jsSetFocus(componentEl, inputEl, contentEl, footerEl, keyboardHeight);
|
jsSetFocus(componentEl, inputEl, contentEl, footerEl, keyboardHeight, addScrollPadding);
|
||||||
};
|
};
|
||||||
componentEl.addEventListener('focusin', focusIn, true);
|
componentEl.addEventListener('focusin', focusIn, true);
|
||||||
|
|
||||||
@ -31,7 +50,8 @@ const jsSetFocus = async (
|
|||||||
inputEl: HTMLInputElement | HTMLTextAreaElement,
|
inputEl: HTMLInputElement | HTMLTextAreaElement,
|
||||||
contentEl: HTMLElement | null,
|
contentEl: HTMLElement | null,
|
||||||
footerEl: HTMLIonFooterElement | null,
|
footerEl: HTMLIonFooterElement | null,
|
||||||
keyboardHeight: number
|
keyboardHeight: number,
|
||||||
|
enableScrollPadding: boolean
|
||||||
) => {
|
) => {
|
||||||
if (!contentEl && !footerEl) {
|
if (!contentEl && !footerEl) {
|
||||||
return;
|
return;
|
||||||
@ -42,6 +62,22 @@ const jsSetFocus = async (
|
|||||||
// the text input is in a safe position that doesn't
|
// the text input is in a safe position that doesn't
|
||||||
// require it to be scrolled into view, just set focus now
|
// require it to be scrolled into view, just set focus now
|
||||||
inputEl.focus();
|
inputEl.focus();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Even though the input does not need
|
||||||
|
* scroll assist, we should preserve the
|
||||||
|
* the scroll padding as users could be moving
|
||||||
|
* focus from an input that needs scroll padding
|
||||||
|
* to an input that does not need scroll padding.
|
||||||
|
* If we remove the scroll padding now, users will
|
||||||
|
* see the page jump.
|
||||||
|
*/
|
||||||
|
if (enableScrollPadding && contentEl) {
|
||||||
|
currentPadding += scrollData.scrollAmount;
|
||||||
|
setScrollPadding(contentEl, currentPadding);
|
||||||
|
setClearScrollPaddingListener(inputEl, contentEl, () => (currentPadding = 0));
|
||||||
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,6 +94,17 @@ const jsSetFocus = async (
|
|||||||
*/
|
*/
|
||||||
raf(() => componentEl.click());
|
raf(() => componentEl.click());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If enabled, we can add scroll padding to
|
||||||
|
* the bottom of the content so that scroll assist
|
||||||
|
* has enough room to scroll the input above
|
||||||
|
* the keyboard.
|
||||||
|
*/
|
||||||
|
if (enableScrollPadding && contentEl) {
|
||||||
|
currentPadding += scrollData.scrollAmount;
|
||||||
|
setScrollPadding(contentEl, currentPadding);
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
let scrollContentTimeout: any;
|
let scrollContentTimeout: any;
|
||||||
const scrollContent = async () => {
|
const scrollContent = async () => {
|
||||||
@ -80,6 +127,15 @@ const jsSetFocus = async (
|
|||||||
|
|
||||||
// ensure this is the focused input
|
// ensure this is the focused input
|
||||||
inputEl.focus();
|
inputEl.focus();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When the input is about to be blurred
|
||||||
|
* we should set a timeout to remove
|
||||||
|
* any scroll padding.
|
||||||
|
*/
|
||||||
|
if (enableScrollPadding) {
|
||||||
|
setClearScrollPaddingListener(inputEl, contentEl, () => (currentPadding = 0));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const doubleKeyboardEventListener = () => {
|
const doubleKeyboardEventListener = () => {
|
||||||
|
@ -1,51 +1,61 @@
|
|||||||
import { findClosestIonContent } from '../../content';
|
|
||||||
|
|
||||||
const PADDING_TIMER_KEY = '$ionPaddingTimer';
|
const PADDING_TIMER_KEY = '$ionPaddingTimer';
|
||||||
|
|
||||||
export const enableScrollPadding = (keyboardHeight: number) => {
|
/**
|
||||||
const doc = document;
|
* Scroll padding adds additional padding to the bottom
|
||||||
|
* of ion-content so that there is enough scroll space
|
||||||
const onFocusin = (ev: any) => {
|
* for an input to be scrolled above the keyboard. This
|
||||||
setScrollPadding(ev.target, keyboardHeight);
|
* is needed in environments where the webview does not
|
||||||
};
|
* resize when the keyboard opens.
|
||||||
const onFocusout = (ev: any) => {
|
*
|
||||||
setScrollPadding(ev.target, 0);
|
* Example: If an input at the bottom of ion-content is
|
||||||
};
|
* focused, there is no additional scrolling space below
|
||||||
|
* it, so the input cannot be scrolled above the keyboard.
|
||||||
doc.addEventListener('focusin', onFocusin);
|
* Scroll padding fixes this by adding padding equal to the
|
||||||
doc.addEventListener('focusout', onFocusout);
|
* height of the keyboard to the bottom of the content.
|
||||||
|
*
|
||||||
return () => {
|
* Common environments where this is needed:
|
||||||
doc.removeEventListener('focusin', onFocusin);
|
* - Mobile Safari: The keyboard overlays the content
|
||||||
doc.removeEventListener('focusout', onFocusout);
|
* - Capacitor/Cordova on iOS: The keyboard overlays the content
|
||||||
};
|
* when the KeyboardResize mode is set to 'none'.
|
||||||
};
|
*/
|
||||||
|
export const setScrollPadding = (contentEl: HTMLElement, paddingAmount: number, clearCallback?: () => void) => {
|
||||||
const setScrollPadding = (input: HTMLElement, keyboardHeight: number) => {
|
const timer = (contentEl as any)[PADDING_TIMER_KEY];
|
||||||
if (input.tagName !== 'INPUT') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (input.parentElement && input.parentElement.tagName === 'ION-INPUT') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (input.parentElement?.parentElement?.tagName === 'ION-SEARCHBAR') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const el = findClosestIonContent(input);
|
|
||||||
if (el === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const timer = (el as any)[PADDING_TIMER_KEY];
|
|
||||||
if (timer) {
|
if (timer) {
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (keyboardHeight > 0) {
|
if (paddingAmount > 0) {
|
||||||
el.style.setProperty('--keyboard-offset', `${keyboardHeight}px`);
|
contentEl.style.setProperty('--keyboard-offset', `${paddingAmount}px`);
|
||||||
} else {
|
} else {
|
||||||
(el as any)[PADDING_TIMER_KEY] = setTimeout(() => {
|
(contentEl as any)[PADDING_TIMER_KEY] = setTimeout(() => {
|
||||||
el.style.setProperty('--keyboard-offset', '0px');
|
contentEl.style.setProperty('--keyboard-offset', '0px');
|
||||||
|
if (clearCallback) {
|
||||||
|
clearCallback();
|
||||||
|
}
|
||||||
}, 120);
|
}, 120);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When an input is about to be focused,
|
||||||
|
* set a timeout to clear any scroll padding
|
||||||
|
* on the content. Note: The clearing
|
||||||
|
* is done on a timeout so that if users
|
||||||
|
* are moving focus from one input to the next
|
||||||
|
* then re-adding scroll padding to the new
|
||||||
|
* input with cancel the timeout to clear the
|
||||||
|
* scroll padding.
|
||||||
|
*/
|
||||||
|
export const setClearScrollPaddingListener = (
|
||||||
|
inputEl: HTMLInputElement | HTMLTextAreaElement,
|
||||||
|
contentEl: HTMLElement | null,
|
||||||
|
doneCallback: () => void
|
||||||
|
) => {
|
||||||
|
const clearScrollPadding = () => {
|
||||||
|
if (contentEl) {
|
||||||
|
setScrollPadding(contentEl, 0, doneCallback);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
inputEl.addEventListener('focusout', clearScrollPadding, { once: true });
|
||||||
|
};
|
||||||
|
@ -88,6 +88,24 @@
|
|||||||
keyboardHeight: 250,
|
keyboardHeight: 250,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const params = new URLSearchParams(window.location.href.split('?')[1]);
|
||||||
|
const resizeMode = params.get('resizeMode');
|
||||||
|
|
||||||
|
if (resizeMode) {
|
||||||
|
window.Capacitor = {
|
||||||
|
isPluginAvailable: (plugin) => plugin === 'Keyboard',
|
||||||
|
Plugins: {
|
||||||
|
Keyboard: {
|
||||||
|
getResizeMode: () => {
|
||||||
|
return Promise.resolve({
|
||||||
|
mode: resizeMode,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
</ion-app>
|
</ion-app>
|
||||||
</body>
|
</body>
|
||||||
|
@ -1,60 +1,178 @@
|
|||||||
import { expect } from '@playwright/test';
|
import { expect } from '@playwright/test';
|
||||||
import type { Locator } from '@playwright/test';
|
import type { Locator } from '@playwright/test';
|
||||||
|
import { KeyboardResize } from '@utils/native/keyboard';
|
||||||
|
import type { E2EPage } from '@utils/test/playwright';
|
||||||
import { test } from '@utils/test/playwright';
|
import { test } from '@utils/test/playwright';
|
||||||
|
|
||||||
test.describe('scroll-assist', () => {
|
const getScrollPosition = async (contentEl: Locator) => {
|
||||||
const getScrollPosition = async (contentEl: Locator) => {
|
return await contentEl.evaluate(async (el: HTMLIonContentElement) => {
|
||||||
return await contentEl.evaluate(async (el: HTMLIonContentElement) => {
|
const scrollEl = await el.getScrollElement();
|
||||||
const scrollEl = await el.getScrollElement();
|
|
||||||
|
|
||||||
return scrollEl.scrollTop;
|
return scrollEl.scrollTop;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
test.describe('scroll-assist', () => {
|
||||||
|
let scrollAssistFixture: ScrollAssistFixture;
|
||||||
test.beforeEach(async ({ page, skip }) => {
|
test.beforeEach(async ({ page, skip }) => {
|
||||||
skip.rtl();
|
skip.rtl();
|
||||||
skip.mode('md', 'Scroll utils are only needed on iOS mode');
|
skip.mode('md', 'Scroll utils are only needed on iOS mode');
|
||||||
skip.browser('firefox');
|
skip.browser('firefox');
|
||||||
skip.browser('chromium');
|
skip.browser('chromium');
|
||||||
|
|
||||||
await page.goto('/src/utils/input-shims/hacks/test');
|
scrollAssistFixture = new ScrollAssistFixture(page);
|
||||||
});
|
|
||||||
test('should not activate when input is above the keyboard', async ({ page }) => {
|
|
||||||
const input = page.locator('#input-above-keyboard');
|
|
||||||
const content = page.locator('ion-content');
|
|
||||||
|
|
||||||
await expect(await getScrollPosition(content)).toBe(0);
|
|
||||||
|
|
||||||
await input.click();
|
|
||||||
await expect(input.locator('input')).toBeFocused();
|
|
||||||
await page.waitForChanges();
|
|
||||||
|
|
||||||
await expect(await getScrollPosition(content)).toBe(0);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should activate when input is below the keyboard', async ({ page }) => {
|
test.describe('scroll-assist: basic functionality', () => {
|
||||||
const input = page.locator('#input-below-keyboard');
|
test.beforeEach(async () => {
|
||||||
const content = page.locator('ion-content');
|
await scrollAssistFixture.goto();
|
||||||
|
});
|
||||||
|
test('should not activate when input is above the keyboard', async () => {
|
||||||
|
await scrollAssistFixture.expectNotToHaveScrollAssist(
|
||||||
|
'#input-above-keyboard',
|
||||||
|
'#input-above-keyboard input:not(.cloned-input)'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
await expect(await getScrollPosition(content)).toBe(0);
|
test('should activate when input is below the keyboard', async () => {
|
||||||
|
await scrollAssistFixture.expectToHaveScrollAssist(
|
||||||
|
'#input-below-keyboard',
|
||||||
|
'#input-below-keyboard input:not(.cloned-input)'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
await input.click({ force: true });
|
test('should activate even when not explicitly tapping input', async () => {
|
||||||
await page.waitForChanges();
|
await scrollAssistFixture.expectToHaveScrollAssist(
|
||||||
await expect(input.locator('input:not(.cloned-input)')).toBeFocused();
|
'#item-below-keyboard ion-label',
|
||||||
|
'#input-below-keyboard input:not(.cloned-input)'
|
||||||
await expect(await getScrollPosition(content)).not.toBe(0);
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
test.describe('scroll-assist: scroll-padding', () => {
|
||||||
|
test.describe('scroll-padding: browser/cordova', () => {
|
||||||
|
test.beforeEach(async () => {
|
||||||
|
await scrollAssistFixture.goto();
|
||||||
|
});
|
||||||
|
test('should add scroll padding for an input at the bottom of the scroll container', async () => {
|
||||||
|
await scrollAssistFixture.expectToHaveScrollPadding(
|
||||||
|
'#input-outside-viewport',
|
||||||
|
'#input-outside-viewport input:not(.cloned-input)'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('should activate even when not explicitly tapping input', async ({ page }) => {
|
test('should keep scroll padding even when switching between inputs', async () => {
|
||||||
const label = page.locator('#item-below-keyboard ion-label');
|
await scrollAssistFixture.expectToHaveScrollPadding(
|
||||||
const input = page.locator('#input-below-keyboard');
|
'#input-outside-viewport',
|
||||||
const content = page.locator('ion-content');
|
'#input-outside-viewport input:not(.cloned-input)'
|
||||||
|
);
|
||||||
|
|
||||||
await expect(await getScrollPosition(content)).toBe(0);
|
await scrollAssistFixture.expectToHaveScrollPadding(
|
||||||
|
'#textarea-outside-viewport',
|
||||||
|
'#textarea-outside-viewport textarea:not(.cloned-input)'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
test.describe('scroll-padding: webview resizing', () => {
|
||||||
|
test('should add scroll padding when webview resizing is "none"', async () => {
|
||||||
|
await scrollAssistFixture.goto(KeyboardResize.None);
|
||||||
|
|
||||||
await label.click({ force: true });
|
await scrollAssistFixture.expectToHaveScrollPadding(
|
||||||
await page.waitForChanges();
|
'#input-outside-viewport',
|
||||||
await expect(input.locator('input:not(.cloned-input)')).toBeFocused();
|
'#input-outside-viewport input:not(.cloned-input)'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
test('should not add scroll padding when webview resizing is "body"', async () => {
|
||||||
|
await scrollAssistFixture.goto(KeyboardResize.Body);
|
||||||
|
|
||||||
await expect(await getScrollPosition(content)).not.toBe(0);
|
await scrollAssistFixture.expectNotToHaveScrollPadding(
|
||||||
|
'#input-outside-viewport',
|
||||||
|
'#input-outside-viewport input:not(.cloned-input)'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
test('should not add scroll padding when webview resizing is "ionic"', async () => {
|
||||||
|
await scrollAssistFixture.goto(KeyboardResize.Ionic);
|
||||||
|
|
||||||
|
await scrollAssistFixture.expectNotToHaveScrollPadding(
|
||||||
|
'#input-outside-viewport',
|
||||||
|
'#input-outside-viewport input:not(.cloned-input)'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
test('should not add scroll padding when webview resizing is "native"', async () => {
|
||||||
|
await scrollAssistFixture.goto(KeyboardResize.Native);
|
||||||
|
|
||||||
|
await scrollAssistFixture.expectNotToHaveScrollPadding(
|
||||||
|
'#input-outside-viewport',
|
||||||
|
'#input-outside-viewport input:not(.cloned-input)'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
class ScrollAssistFixture {
|
||||||
|
readonly page: E2EPage;
|
||||||
|
private content!: Locator;
|
||||||
|
|
||||||
|
constructor(page: E2EPage) {
|
||||||
|
this.page = page;
|
||||||
|
}
|
||||||
|
|
||||||
|
async goto(resizeMode?: KeyboardResize) {
|
||||||
|
let url = `/src/utils/input-shims/hacks/test`;
|
||||||
|
if (resizeMode !== undefined) {
|
||||||
|
url += `?resizeMode=${resizeMode}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.page.goto(url);
|
||||||
|
|
||||||
|
this.content = this.page.locator('ion-content');
|
||||||
|
}
|
||||||
|
|
||||||
|
private async focusInput(interactiveSelector: string, inputSelector: string) {
|
||||||
|
const { page } = this;
|
||||||
|
const interactive = page.locator(interactiveSelector);
|
||||||
|
const input = page.locator(inputSelector);
|
||||||
|
|
||||||
|
await interactive.click({ force: true });
|
||||||
|
await expect(input).toBeFocused();
|
||||||
|
await page.waitForChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
private getScrollPosition() {
|
||||||
|
const { content } = this;
|
||||||
|
|
||||||
|
return getScrollPosition(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
async expectNotToHaveScrollAssist(interactiveSelector: string, inputSelector: string) {
|
||||||
|
await expect(await this.getScrollPosition()).toBe(0);
|
||||||
|
|
||||||
|
await this.focusInput(interactiveSelector, inputSelector);
|
||||||
|
|
||||||
|
await expect(await this.getScrollPosition()).toBe(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
async expectToHaveScrollAssist(interactiveSelector: string, inputSelector: string) {
|
||||||
|
await expect(await this.getScrollPosition()).toBe(0);
|
||||||
|
|
||||||
|
await this.focusInput(interactiveSelector, inputSelector);
|
||||||
|
|
||||||
|
await expect(await this.getScrollPosition()).not.toBe(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
async expectToHaveScrollPadding(interactiveSelector: string, inputSelector: string) {
|
||||||
|
const { content } = this;
|
||||||
|
|
||||||
|
await this.focusInput(interactiveSelector, inputSelector);
|
||||||
|
|
||||||
|
await expect(content).not.toHaveCSS('--keyboard-offset', '0px');
|
||||||
|
}
|
||||||
|
|
||||||
|
async expectNotToHaveScrollPadding(interactiveSelector: string, inputSelector: string) {
|
||||||
|
const { content } = this;
|
||||||
|
|
||||||
|
await this.focusInput(interactiveSelector, inputSelector);
|
||||||
|
|
||||||
|
await expect(content).toHaveCSS('--keyboard-offset', '0px');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,18 +1,17 @@
|
|||||||
import type { Config } from '../../interface';
|
import type { Config } from '../../interface';
|
||||||
import { findClosestIonContent } from '../content';
|
import { findClosestIonContent } from '../content';
|
||||||
import { componentOnReady } from '../helpers';
|
import { componentOnReady } from '../helpers';
|
||||||
|
import { Keyboard } from '../native/keyboard';
|
||||||
|
|
||||||
import { enableHideCaretOnScroll } from './hacks/hide-caret';
|
import { enableHideCaretOnScroll } from './hacks/hide-caret';
|
||||||
import { enableInputBlurring } from './hacks/input-blurring';
|
import { enableInputBlurring } from './hacks/input-blurring';
|
||||||
import { enableScrollAssist } from './hacks/scroll-assist';
|
import { enableScrollAssist } from './hacks/scroll-assist';
|
||||||
import { enableScrollPadding } from './hacks/scroll-padding';
|
|
||||||
|
|
||||||
const INPUT_BLURRING = true;
|
const INPUT_BLURRING = true;
|
||||||
const SCROLL_ASSIST = true;
|
const SCROLL_ASSIST = true;
|
||||||
const SCROLL_PADDING = true;
|
|
||||||
const HIDE_CARET = true;
|
const HIDE_CARET = true;
|
||||||
|
|
||||||
export const startInputShims = (config: Config) => {
|
export const startInputShims = async (config: Config) => {
|
||||||
const doc = document;
|
const doc = document;
|
||||||
const keyboardHeight = config.getNumber('keyboardHeight', 290);
|
const keyboardHeight = config.getNumber('keyboardHeight', 290);
|
||||||
const scrollAssist = config.getBoolean('scrollAssist', true);
|
const scrollAssist = config.getBoolean('scrollAssist', true);
|
||||||
@ -24,6 +23,16 @@ export const startInputShims = (config: Config) => {
|
|||||||
const hideCaretMap = new WeakMap<HTMLElement, () => void>();
|
const hideCaretMap = new WeakMap<HTMLElement, () => void>();
|
||||||
const scrollAssistMap = new WeakMap<HTMLElement, () => void>();
|
const scrollAssistMap = new WeakMap<HTMLElement, () => void>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Grab the native keyboard resize configuration
|
||||||
|
* and pass it to scroll assist. Scroll assist requires
|
||||||
|
* that we adjust the input right before the input
|
||||||
|
* is about to be focused. If we called `Keyboard.getResizeMode`
|
||||||
|
* on focusin in scroll assist, we could potentially adjust the
|
||||||
|
* input too late since this call is async.
|
||||||
|
*/
|
||||||
|
const keyboardResizeMode = await Keyboard.getResizeMode();
|
||||||
|
|
||||||
const registerInput = async (componentEl: HTMLElement) => {
|
const registerInput = async (componentEl: HTMLElement) => {
|
||||||
await new Promise((resolve) => componentOnReady(componentEl, resolve));
|
await new Promise((resolve) => componentOnReady(componentEl, resolve));
|
||||||
|
|
||||||
@ -55,7 +64,15 @@ export const startInputShims = (config: Config) => {
|
|||||||
scrollAssist &&
|
scrollAssist &&
|
||||||
!scrollAssistMap.has(componentEl)
|
!scrollAssistMap.has(componentEl)
|
||||||
) {
|
) {
|
||||||
const rmFn = enableScrollAssist(componentEl, inputEl, scrollEl, footerEl, keyboardHeight);
|
const rmFn = enableScrollAssist(
|
||||||
|
componentEl,
|
||||||
|
inputEl,
|
||||||
|
scrollEl,
|
||||||
|
footerEl,
|
||||||
|
keyboardHeight,
|
||||||
|
scrollPadding,
|
||||||
|
keyboardResizeMode
|
||||||
|
);
|
||||||
scrollAssistMap.set(componentEl, rmFn);
|
scrollAssistMap.set(componentEl, rmFn);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -82,10 +99,6 @@ export const startInputShims = (config: Config) => {
|
|||||||
enableInputBlurring();
|
enableInputBlurring();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (scrollPadding && SCROLL_PADDING) {
|
|
||||||
enableScrollPadding(keyboardHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Input might be already loaded in the DOM before ion-device-hacks did.
|
// Input might be already loaded in the DOM before ion-device-hacks did.
|
||||||
// At this point we need to look for all of the inputs not registered yet
|
// At this point we need to look for all of the inputs not registered yet
|
||||||
// and register them.
|
// and register them.
|
||||||
|
27
core/src/utils/native/keyboard.ts
Normal file
27
core/src/utils/native/keyboard.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { win } from '../window';
|
||||||
|
|
||||||
|
// Interfaces source: https://capacitorjs.com/docs/apis/keyboard#interfaces
|
||||||
|
export interface KeyboardResizeOptions {
|
||||||
|
mode: KeyboardResize;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum KeyboardResize {
|
||||||
|
Body = 'body',
|
||||||
|
Ionic = 'ionic',
|
||||||
|
Native = 'native',
|
||||||
|
None = 'none',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Keyboard = {
|
||||||
|
getEngine() {
|
||||||
|
return (win as any)?.Capacitor?.isPluginAvailable('Keyboard') && (win as any)?.Capacitor.Plugins.Keyboard;
|
||||||
|
},
|
||||||
|
getResizeMode(): Promise<KeyboardResizeOptions | undefined> {
|
||||||
|
const engine = this.getEngine();
|
||||||
|
if (!engine || !engine.getResizeMode) {
|
||||||
|
return Promise.resolve(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
return engine.getResizeMode();
|
||||||
|
},
|
||||||
|
};
|
@ -22,7 +22,22 @@ export const goto = async (page: Page, url: string, options: any, testInfo: Test
|
|||||||
const formattedRtl = urlToParams.get('rtl') ?? rtl;
|
const formattedRtl = urlToParams.get('rtl') ?? rtl;
|
||||||
const ionicTesting = urlToParams.get('ionic:_testing') ?? _testing;
|
const ionicTesting = urlToParams.get('ionic:_testing') ?? _testing;
|
||||||
|
|
||||||
const formattedUrl = `${splitUrl[0]}?ionic:_testing=${ionicTesting}&ionic:mode=${formattedMode}&rtl=${formattedRtl}`;
|
/**
|
||||||
|
* Pass through other custom query params
|
||||||
|
*/
|
||||||
|
urlToParams.delete('ionic:mode');
|
||||||
|
urlToParams.delete('rtl');
|
||||||
|
urlToParams.delete('ionic:_testing');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert remaining query params to a string.
|
||||||
|
* Be sure to call decodeURIComponent to decode
|
||||||
|
* characters such as &.
|
||||||
|
*/
|
||||||
|
const remainingQueryParams = decodeURIComponent(urlToParams.toString());
|
||||||
|
const remainingQueryParamsString = remainingQueryParams == '' ? '' : `&${remainingQueryParams}`;
|
||||||
|
|
||||||
|
const formattedUrl = `${splitUrl[0]}?ionic:_testing=${ionicTesting}&ionic:mode=${formattedMode}&rtl=${formattedRtl}${remainingQueryParamsString}`;
|
||||||
|
|
||||||
testInfo.annotations.push({
|
testInfo.annotations.push({
|
||||||
type: 'mode',
|
type: 'mode',
|
||||||
|
Reference in New Issue
Block a user