mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
fix(input, textarea): inputs now scroll into view when tapping labels (#25848)
This commit is contained in:
@@ -59,3 +59,10 @@ const removeClone = (componentEl: HTMLElement, inputEl: HTMLElement) => {
|
||||
componentEl.style.pointerEvents = '';
|
||||
inputEl.style.transform = '';
|
||||
};
|
||||
|
||||
/**
|
||||
* Factoring in 50px gives us some room
|
||||
* in case the keyboard shows password/autofill bars
|
||||
* asynchronously.
|
||||
*/
|
||||
export const SCROLL_AMOUNT_PADDING = 50;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { getScrollElement, scrollByPoint } from '../../content';
|
||||
import { pointerCoord, raf } from '../../helpers';
|
||||
import { raf } from '../../helpers';
|
||||
|
||||
import { isFocused, relocateInput } from './common';
|
||||
import { relocateInput, SCROLL_AMOUNT_PADDING } from './common';
|
||||
import { getScrollData } from './scroll-data';
|
||||
|
||||
export const enableScrollAssist = (
|
||||
@@ -11,32 +11,18 @@ export const enableScrollAssist = (
|
||||
footerEl: HTMLIonFooterElement | null,
|
||||
keyboardHeight: number
|
||||
) => {
|
||||
let coord: any;
|
||||
const touchStart = (ev: Event) => {
|
||||
coord = pointerCoord(ev);
|
||||
/**
|
||||
* When the input is about to receive
|
||||
* focus, we need to move it to prevent
|
||||
* mobile Safari from adjusting the viewport.
|
||||
*/
|
||||
const focusIn = () => {
|
||||
jsSetFocus(componentEl, inputEl, contentEl, footerEl, keyboardHeight);
|
||||
};
|
||||
|
||||
const touchEnd = (ev: Event) => {
|
||||
// input cover touchend/mouseup
|
||||
if (!coord) {
|
||||
return;
|
||||
}
|
||||
// get where the touchend/mouseup ended
|
||||
const endCoord = pointerCoord(ev);
|
||||
|
||||
// focus this input if the pointer hasn't moved XX pixels
|
||||
// and the input doesn't already have focus
|
||||
if (!hasPointerMoved(6, coord, endCoord) && !isFocused(inputEl)) {
|
||||
// begin the input focus process
|
||||
jsSetFocus(componentEl, inputEl, contentEl, footerEl, keyboardHeight);
|
||||
}
|
||||
};
|
||||
componentEl.addEventListener('touchstart', touchStart, { capture: true, passive: true });
|
||||
componentEl.addEventListener('touchend', touchEnd, true);
|
||||
componentEl.addEventListener('focusin', focusIn, true);
|
||||
|
||||
return () => {
|
||||
componentEl.removeEventListener('touchstart', touchStart, true);
|
||||
componentEl.removeEventListener('touchend', touchEnd, true);
|
||||
componentEl.removeEventListener('focusin', focusIn, true);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -125,7 +111,7 @@ const jsSetFocus = async (
|
||||
*/
|
||||
if (inputEl.type === 'password') {
|
||||
// Add 50px to account for the "Passwords" bar
|
||||
scrollData.scrollAmount += 50;
|
||||
scrollData.scrollAmount += SCROLL_AMOUNT_PADDING;
|
||||
window.addEventListener('ionKeyboardDidShow', doubleKeyboardEventListener);
|
||||
} else {
|
||||
window.addEventListener('ionKeyboardDidShow', scrollContent);
|
||||
@@ -145,22 +131,3 @@ const jsSetFocus = async (
|
||||
scrollContent();
|
||||
}
|
||||
};
|
||||
|
||||
const hasPointerMoved = (
|
||||
threshold: number,
|
||||
startCoord: PointerCoordinates | undefined,
|
||||
endCoord: PointerCoordinates | undefined
|
||||
) => {
|
||||
if (startCoord && endCoord) {
|
||||
const deltaX = startCoord.x - endCoord.x;
|
||||
const deltaY = startCoord.y - endCoord.y;
|
||||
const distance = deltaX * deltaX + deltaY * deltaY;
|
||||
return distance > threshold * threshold;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export interface PointerCoordinates {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { SCROLL_AMOUNT_PADDING } from './common';
|
||||
|
||||
const SCROLL_ASSIST_SPEED = 0.3;
|
||||
|
||||
export interface ScrollData {
|
||||
@@ -33,7 +35,7 @@ const calcScrollData = (
|
||||
|
||||
// compute safe area
|
||||
const safeAreaTop = visibleAreaTop + 15;
|
||||
const safeAreaBottom = visibleAreaBottom * 0.75;
|
||||
const safeAreaBottom = visibleAreaBottom - SCROLL_AMOUNT_PADDING;
|
||||
|
||||
// figure out if each edge of the input is within the safe area
|
||||
const distanceToBottom = safeAreaBottom - inputBottom;
|
||||
|
||||
94
core/src/utils/input-shims/hacks/test/index.html
Normal file
94
core/src/utils/input-shims/hacks/test/index.html
Normal file
@@ -0,0 +1,94 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Scroll Padding</title>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"
|
||||
/>
|
||||
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
|
||||
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
|
||||
<script src="../../../../../scripts/testing/scripts.js"></script>
|
||||
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
|
||||
<style>
|
||||
.spacer {
|
||||
background: #eee;
|
||||
width: 100%;
|
||||
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.keyboard {
|
||||
position: fixed;
|
||||
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
|
||||
height: 250px;
|
||||
|
||||
background: rgba(0, 0, 255, 0.5);
|
||||
|
||||
z-index: 1;
|
||||
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="keyboard"></div>
|
||||
<ion-app>
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Modal - Inline</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding">
|
||||
<ion-item>
|
||||
<ion-label>Input Above Keyboard</ion-label>
|
||||
<ion-input id="input-above-keyboard"></ion-input>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-label>Textarea Above Keyboard</ion-label>
|
||||
<ion-textarea id="textarea-above-keyboard"></ion-textarea>
|
||||
</ion-item>
|
||||
|
||||
<div class="spacer" style="height: 400px"></div>
|
||||
|
||||
<ion-item id="item-below-keyboard">
|
||||
<ion-label>Input Below Keyboard</ion-label>
|
||||
<ion-input id="input-below-keyboard"></ion-input>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-label>Textarea Below Keyboard</ion-label>
|
||||
<ion-textarea id="textarea-below-keyboard"></ion-textarea>
|
||||
</ion-item>
|
||||
|
||||
<div class="spacer" style="height: 400px"></div>
|
||||
|
||||
<ion-item>
|
||||
<ion-label>Input Outside Viewport</ion-label>
|
||||
<ion-input id="input-outside-viewport"></ion-input>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-label>Textarea Outside Viewport</ion-label>
|
||||
<ion-textarea id="textarea-outside-viewport"></ion-textarea>
|
||||
</ion-item>
|
||||
</ion-content>
|
||||
|
||||
<script>
|
||||
window.Ionic = {
|
||||
config: {
|
||||
keyboardHeight: 250,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
</ion-app>
|
||||
</body>
|
||||
</html>
|
||||
60
core/src/utils/input-shims/hacks/test/scroll-assist.e2e.ts
Normal file
60
core/src/utils/input-shims/hacks/test/scroll-assist.e2e.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import type { Locator } from '@playwright/test';
|
||||
import { test } from '@utils/test/playwright';
|
||||
|
||||
test.describe('scroll-assist', () => {
|
||||
const getScrollPosition = async (contentEl: Locator) => {
|
||||
return await contentEl.evaluate(async (el: HTMLIonContentElement) => {
|
||||
const scrollEl = await el.getScrollElement();
|
||||
|
||||
return scrollEl.scrollTop;
|
||||
});
|
||||
};
|
||||
test.beforeEach(async ({ page, skip }) => {
|
||||
skip.rtl();
|
||||
skip.mode('md', 'Scroll utils are only needed on iOS mode');
|
||||
skip.browser('firefox');
|
||||
skip.browser('chromium');
|
||||
|
||||
await page.goto('/src/utils/input-shims/hacks/test');
|
||||
});
|
||||
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 }) => {
|
||||
const input = page.locator('#input-below-keyboard');
|
||||
const content = page.locator('ion-content');
|
||||
|
||||
await expect(await getScrollPosition(content)).toBe(0);
|
||||
|
||||
await input.click({ force: true });
|
||||
await page.waitForChanges();
|
||||
await expect(input.locator('input:not(.cloned-input)')).toBeFocused();
|
||||
|
||||
await expect(await getScrollPosition(content)).not.toBe(0);
|
||||
});
|
||||
|
||||
test('should activate even when not explicitly tapping input', async ({ page }) => {
|
||||
const label = page.locator('#item-below-keyboard ion-label');
|
||||
const input = page.locator('#input-below-keyboard');
|
||||
const content = page.locator('ion-content');
|
||||
|
||||
await expect(await getScrollPosition(content)).toBe(0);
|
||||
|
||||
await label.click({ force: true });
|
||||
await page.waitForChanges();
|
||||
await expect(input.locator('input:not(.cloned-input)')).toBeFocused();
|
||||
|
||||
await expect(await getScrollPosition(content)).not.toBe(0);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user