fix(input, textarea): inputs now scroll into view when tapping labels (#25848)

This commit is contained in:
Liam DeBeasi
2022-09-09 09:51:08 -05:00
committed by GitHub
parent 303e28ef56
commit cb265d6cc6
5 changed files with 176 additions and 46 deletions

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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;

View 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>

View 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);
});
});