mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-19 03:32:21 +08:00
fix(input): scroll assist works in with shadow-dom (#16206)
fixes #15888 fixes #15294 fixes #15895
This commit is contained in:
4
core/src/components.d.ts
vendored
4
core/src/components.d.ts
vendored
@ -1700,7 +1700,7 @@ export namespace Components {
|
|||||||
*/
|
*/
|
||||||
'autocomplete': 'on' | 'off';
|
'autocomplete': 'on' | 'off';
|
||||||
/**
|
/**
|
||||||
* Whether autocorrection should be enabled when the user is entering/editing the text value.
|
* Whether auto correction should be enabled when the user is entering/editing the text value.
|
||||||
*/
|
*/
|
||||||
'autocorrect': 'on' | 'off';
|
'autocorrect': 'on' | 'off';
|
||||||
/**
|
/**
|
||||||
@ -1818,7 +1818,7 @@ export namespace Components {
|
|||||||
*/
|
*/
|
||||||
'autocomplete'?: 'on' | 'off';
|
'autocomplete'?: 'on' | 'off';
|
||||||
/**
|
/**
|
||||||
* Whether autocorrection should be enabled when the user is entering/editing the text value.
|
* Whether auto correction should be enabled when the user is entering/editing the text value.
|
||||||
*/
|
*/
|
||||||
'autocorrect'?: 'on' | 'off';
|
'autocorrect'?: 'on' | 'off';
|
||||||
/**
|
/**
|
||||||
|
@ -25,7 +25,7 @@
|
|||||||
--padding-bottom: 0;
|
--padding-bottom: 0;
|
||||||
--padding-start: 0;
|
--padding-start: 0;
|
||||||
--background: transparent;
|
--background: transparent;
|
||||||
--color: inherit;
|
--color: initial;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
position: relative;
|
position: relative;
|
||||||
@ -108,17 +108,11 @@
|
|||||||
// This will only show when the scroll assist is configured
|
// This will only show when the scroll assist is configured
|
||||||
// otherwise the .input-cover will not be rendered at all
|
// otherwise the .input-cover will not be rendered at all
|
||||||
// The input cover is not clickable when the input is disabled
|
// The input cover is not clickable when the input is disabled
|
||||||
|
.cloned-input {
|
||||||
.input-cover {
|
|
||||||
@include position(0, null, null, 0);
|
@include position(0, null, null, 0);
|
||||||
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
:host([disabled]) .input-cover {
|
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -151,10 +145,6 @@
|
|||||||
// --------------------------------------------------
|
// --------------------------------------------------
|
||||||
// When the input has focus, then the input cover should be hidden
|
// When the input has focus, then the input cover should be hidden
|
||||||
|
|
||||||
:host(.has-focus) .input-cover {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
:host(.has-focus) {
|
:host(.has-focus) {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
@ -50,7 +50,7 @@ export class Input implements ComponentInterface {
|
|||||||
@Prop() autocomplete: 'on' | 'off' = 'off';
|
@Prop() autocomplete: 'on' | 'off' = 'off';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether autocorrection should be enabled when the user is entering/editing the text value.
|
* Whether auto correction should be enabled when the user is entering/editing the text value.
|
||||||
*/
|
*/
|
||||||
@Prop() autocorrect: 'on' | 'off' = 'off';
|
@Prop() autocorrect: 'on' | 'off' = 'off';
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ It is meant for text `type` inputs only, such as `"text"`, `"password"`, `"email
|
|||||||
| `accept` | `accept` | If the value of the type attribute is `"file"`, then this attribute will indicate the types of files that the server accepts, otherwise it will be ignored. The value must be a comma-separated list of unique content type specifiers. | `string \| undefined` | `undefined` |
|
| `accept` | `accept` | If the value of the type attribute is `"file"`, then this attribute will indicate the types of files that the server accepts, otherwise it will be ignored. The value must be a comma-separated list of unique content type specifiers. | `string \| undefined` | `undefined` |
|
||||||
| `autocapitalize` | `autocapitalize` | Indicates whether and how the text value should be automatically capitalized as it is entered/edited by the user. | `"characters" \| "off" \| "on" \| "words"` | `'off'` |
|
| `autocapitalize` | `autocapitalize` | Indicates whether and how the text value should be automatically capitalized as it is entered/edited by the user. | `"characters" \| "off" \| "on" \| "words"` | `'off'` |
|
||||||
| `autocomplete` | `autocomplete` | Indicates whether the value of the control can be automatically completed by the browser. | `"off" \| "on"` | `'off'` |
|
| `autocomplete` | `autocomplete` | Indicates whether the value of the control can be automatically completed by the browser. | `"off" \| "on"` | `'off'` |
|
||||||
| `autocorrect` | `autocorrect` | Whether autocorrection should be enabled when the user is entering/editing the text value. | `"off" \| "on"` | `'off'` |
|
| `autocorrect` | `autocorrect` | Whether auto correction should be enabled when the user is entering/editing the text value. | `"off" \| "on"` | `'off'` |
|
||||||
| `autofocus` | `autofocus` | This Boolean attribute lets you specify that a form control should have input focus when the page loads. | `boolean` | `false` |
|
| `autofocus` | `autofocus` | This Boolean attribute lets you specify that a form control should have input focus when the page loads. | `boolean` | `false` |
|
||||||
| `clearInput` | `clear-input` | If `true`, a clear icon will appear in the input when there is a value. Clicking it clears the input. | `boolean` | `false` |
|
| `clearInput` | `clear-input` | If `true`, a clear icon will appear in the input when there is a value. Clicking it clears the input. | `boolean` | `false` |
|
||||||
| `clearOnEdit` | `clear-on-edit` | If `true`, the value will be cleared after focus upon edit. Defaults to `true` when `type` is `"password"`, `false` for all other types. | `boolean \| undefined` | `undefined` |
|
| `clearOnEdit` | `clear-on-edit` | If `true`, the value will be cleared after focus upon edit. Defaults to `true` when `type` is `"password"`, `false` for all other types. | `boolean \| undefined` | `undefined` |
|
||||||
|
@ -51,7 +51,7 @@
|
|||||||
<ion-input clear-input value="reallylonglonglonginputtoseetheedgesreallylonglonglonginputtoseetheedges"></ion-input>
|
<ion-input clear-input value="reallylonglonglonginputtoseetheedgesreallylonglonglonginputtoseetheedges"></ion-input>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
|
||||||
<ion-item>
|
<ion-item color="dark">
|
||||||
<ion-label position="floating">Floating</ion-label>
|
<ion-label position="floating">Floating</ion-label>
|
||||||
<ion-input checked></ion-input>
|
<ion-input checked></ion-input>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
const RELOCATED_KEY = '$ionRelocated';
|
const cloneMap = new WeakMap<HTMLElement, HTMLElement>();
|
||||||
|
|
||||||
export function relocateInput(
|
export function relocateInput(
|
||||||
componentEl: HTMLElement,
|
componentEl: HTMLElement,
|
||||||
@ -6,89 +6,52 @@ export function relocateInput(
|
|||||||
shouldRelocate: boolean,
|
shouldRelocate: boolean,
|
||||||
inputRelativeY = 0
|
inputRelativeY = 0
|
||||||
) {
|
) {
|
||||||
if ((componentEl as any)[RELOCATED_KEY] === shouldRelocate) {
|
if (cloneMap.has(componentEl) === shouldRelocate) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.debug(`native-input, hideCaret, shouldHideCaret: ${shouldRelocate}, input value: ${inputEl.value}`);
|
console.debug(`native-input, hideCaret, shouldHideCaret: ${shouldRelocate}, input value: ${inputEl.value}`);
|
||||||
if (shouldRelocate) {
|
if (shouldRelocate) {
|
||||||
// this allows for the actual input to receive the focus from
|
addClone(componentEl, inputEl, inputRelativeY);
|
||||||
// the user's touch event, but before it receives focus, it
|
|
||||||
// moves the actual input to a location that will not screw
|
|
||||||
// up the app's layout, and does not allow the native browser
|
|
||||||
// to attempt to scroll the input into place (messing up headers/footers)
|
|
||||||
// the cloned input fills the area of where native input should be
|
|
||||||
// while the native input fakes out the browser by relocating itself
|
|
||||||
// before it receives the actual focus event
|
|
||||||
// We hide the focused input (with the visible caret) invisiable by making it scale(0),
|
|
||||||
cloneInputComponent(componentEl, inputEl);
|
|
||||||
const doc = componentEl.ownerDocument!;
|
|
||||||
const tx = doc.dir === 'rtl' ? 9999 : -9999;
|
|
||||||
inputEl.style.transform = `translate3d(${tx}px,${inputRelativeY}px,0)`;
|
|
||||||
// TODO
|
|
||||||
// inputEle.style.opacity = '0';
|
|
||||||
} else {
|
} else {
|
||||||
removeClone(componentEl, inputEl);
|
removeClone(componentEl, inputEl);
|
||||||
}
|
}
|
||||||
(componentEl as any)[RELOCATED_KEY] = shouldRelocate;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isFocused(input: HTMLInputElement): boolean {
|
export function isFocused(input: HTMLInputElement): boolean {
|
||||||
return input === input.ownerDocument!.activeElement;
|
return input === (input as any).getRootNode().activeElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addClone(componentEl: HTMLElement, inputEl: HTMLInputElement, inputRelativeY: number) {
|
||||||
|
// this allows for the actual input to receive the focus from
|
||||||
|
// the user's touch event, but before it receives focus, it
|
||||||
|
// moves the actual input to a location that will not screw
|
||||||
|
// up the app's layout, and does not allow the native browser
|
||||||
|
// to attempt to scroll the input into place (messing up headers/footers)
|
||||||
|
// the cloned input fills the area of where native input should be
|
||||||
|
// while the native input fakes out the browser by relocating itself
|
||||||
|
// before it receives the actual focus event
|
||||||
|
// We hide the focused input (with the visible caret) invisible by making it scale(0),
|
||||||
|
const parentEl = inputEl.parentNode!;
|
||||||
|
|
||||||
|
// DOM WRITES
|
||||||
|
const clonedEl = inputEl.cloneNode(false) as HTMLInputElement;
|
||||||
|
clonedEl.classList.add('cloned-input');
|
||||||
|
clonedEl.tabIndex = -1;
|
||||||
|
parentEl.appendChild(clonedEl);
|
||||||
|
cloneMap.set(componentEl, clonedEl);
|
||||||
|
|
||||||
|
const doc = componentEl.ownerDocument!;
|
||||||
|
const tx = doc.dir === 'rtl' ? 9999 : -9999;
|
||||||
|
componentEl.style.pointerEvents = 'none';
|
||||||
|
inputEl.style.transform = `translate3d(${tx}px,${inputRelativeY}px,0) scale(0)`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeClone(componentEl: HTMLElement, inputEl: HTMLElement) {
|
function removeClone(componentEl: HTMLElement, inputEl: HTMLElement) {
|
||||||
if (componentEl && componentEl.parentElement) {
|
const clone = cloneMap.get(componentEl);
|
||||||
Array.from(componentEl.parentElement.querySelectorAll('.cloned-input'))
|
if (clone) {
|
||||||
.forEach(clon => clon.remove());
|
cloneMap.delete(componentEl);
|
||||||
|
clone.remove();
|
||||||
componentEl.style.pointerEvents = '';
|
|
||||||
}
|
}
|
||||||
(inputEl.style as any)['transform'] = '';
|
componentEl.style.pointerEvents = '';
|
||||||
inputEl.style.opacity = '';
|
inputEl.style.transform = '';
|
||||||
}
|
|
||||||
|
|
||||||
function cloneInputComponent(componentEl: HTMLElement, inputEl: HTMLInputElement) {
|
|
||||||
// Make sure we kill all the clones before creating new ones
|
|
||||||
// It is a defensive, removeClone() should do nothing
|
|
||||||
// removeClone(plt, srcComponentEle, srcNativeInputEle);
|
|
||||||
// given a native <input> or <textarea> element
|
|
||||||
// find its parent wrapping component like <ion-input> or <ion-textarea>
|
|
||||||
// then clone the entire component
|
|
||||||
const parentElement = componentEl.parentElement;
|
|
||||||
const doc = componentEl.ownerDocument!;
|
|
||||||
if (componentEl && parentElement) {
|
|
||||||
// DOM READ
|
|
||||||
const srcTop = componentEl.offsetTop;
|
|
||||||
const srcLeft = componentEl.offsetLeft;
|
|
||||||
const srcWidth = componentEl.offsetWidth;
|
|
||||||
const srcHeight = componentEl.offsetHeight;
|
|
||||||
|
|
||||||
// DOM WRITE
|
|
||||||
// not using deep clone so we don't pull in unnecessary nodes
|
|
||||||
const clonedComponentEle = doc.createElement('div');
|
|
||||||
const clonedStyle = clonedComponentEle.style;
|
|
||||||
clonedComponentEle.classList.add(...Array.from(componentEl.classList));
|
|
||||||
clonedComponentEle.classList.add('cloned-input');
|
|
||||||
clonedComponentEle.setAttribute('aria-hidden', 'true');
|
|
||||||
clonedStyle.pointerEvents = 'none';
|
|
||||||
clonedStyle.position = 'absolute';
|
|
||||||
clonedStyle.top = srcTop + 'px';
|
|
||||||
clonedStyle.left = srcLeft + 'px';
|
|
||||||
clonedStyle.width = srcWidth + 'px';
|
|
||||||
clonedStyle.height = srcHeight + 'px';
|
|
||||||
|
|
||||||
const clonedInputEl = doc.createElement('input');
|
|
||||||
clonedInputEl.classList.add(...Array.from(inputEl.classList));
|
|
||||||
clonedInputEl.value = inputEl.value;
|
|
||||||
clonedInputEl.type = inputEl.type;
|
|
||||||
clonedInputEl.placeholder = inputEl.placeholder;
|
|
||||||
|
|
||||||
clonedInputEl.tabIndex = -1;
|
|
||||||
|
|
||||||
clonedComponentEle.appendChild(clonedInputEl);
|
|
||||||
parentElement.appendChild(clonedComponentEle);
|
|
||||||
|
|
||||||
componentEl.style.pointerEvents = 'none';
|
|
||||||
}
|
|
||||||
inputEl.style.transform = 'scale(0)';
|
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,6 @@ export function enableHideCaretOnScroll(componentEl: HTMLElement, inputEl: HTMLI
|
|||||||
console.debug('Input: enableHideCaretOnScroll');
|
console.debug('Input: enableHideCaretOnScroll');
|
||||||
|
|
||||||
const scrollHideCaret = (shouldHideCaret: boolean) => {
|
const scrollHideCaret = (shouldHideCaret: boolean) => {
|
||||||
// console.log('scrollHideCaret', shouldHideCaret)
|
|
||||||
if (isFocused(inputEl)) {
|
if (isFocused(inputEl)) {
|
||||||
relocateInput(componentEl, inputEl, shouldHideCaret);
|
relocateInput(componentEl, inputEl, shouldHideCaret);
|
||||||
}
|
}
|
||||||
|
@ -40,11 +40,6 @@ export function enableInputBlurring(doc: Document) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// skip if div is a cover
|
|
||||||
if (tapped.classList.contains('input-cover')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
focused = false;
|
focused = false;
|
||||||
// TODO: find a better way, why 50ms?
|
// TODO: find a better way, why 50ms?
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
@ -22,7 +22,7 @@ function calcScrollData(
|
|||||||
inputRect: ClientRect,
|
inputRect: ClientRect,
|
||||||
contentRect: ClientRect,
|
contentRect: ClientRect,
|
||||||
keyboardHeight: number,
|
keyboardHeight: number,
|
||||||
plaformHeight: number
|
platformHeight: number
|
||||||
): ScrollData {
|
): ScrollData {
|
||||||
// compute input's Y values relative to the body
|
// compute input's Y values relative to the body
|
||||||
const inputTop = inputRect.top;
|
const inputTop = inputRect.top;
|
||||||
@ -30,13 +30,13 @@ function calcScrollData(
|
|||||||
|
|
||||||
// compute visible area
|
// compute visible area
|
||||||
const visibleAreaTop = contentRect.top;
|
const visibleAreaTop = contentRect.top;
|
||||||
const visibleAreaBottom = Math.min(contentRect.bottom, plaformHeight - keyboardHeight);
|
const visibleAreaBottom = Math.min(contentRect.bottom, platformHeight - keyboardHeight);
|
||||||
|
|
||||||
// compute safe area
|
// compute safe area
|
||||||
const safeAreaTop = visibleAreaTop + 10;
|
const safeAreaTop = visibleAreaTop + 15;
|
||||||
const safeAreaBottom = visibleAreaBottom / 2.0;
|
const safeAreaBottom = visibleAreaBottom * 0.5;
|
||||||
|
|
||||||
// figure out if each edge of teh input is within the safe area
|
// figure out if each edge of the input is within the safe area
|
||||||
const distanceToBottom = safeAreaBottom - inputBottom;
|
const distanceToBottom = safeAreaBottom - inputBottom;
|
||||||
const distanceToTop = safeAreaTop - inputTop;
|
const distanceToTop = safeAreaTop - inputTop;
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user