mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-23 05:58:26 +08:00
fix(input): scroll assist works in with shadow-dom (#16206)
fixes #15888 fixes #15294 fixes #15895
This commit is contained in:
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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,11 +6,22 @@ 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) {
|
||||||
|
addClone(componentEl, inputEl, inputRelativeY);
|
||||||
|
} else {
|
||||||
|
removeClone(componentEl, inputEl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isFocused(input: HTMLInputElement): boolean {
|
||||||
|
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
|
// this allows for the actual input to receive the focus from
|
||||||
// the user's touch event, but before it receives focus, it
|
// the user's touch event, but before it receives focus, it
|
||||||
// moves the actual input to a location that will not screw
|
// moves the actual input to a location that will not screw
|
||||||
@ -19,76 +30,28 @@ export function relocateInput(
|
|||||||
// the cloned input fills the area of where native input should be
|
// the cloned input fills the area of where native input should be
|
||||||
// while the native input fakes out the browser by relocating itself
|
// while the native input fakes out the browser by relocating itself
|
||||||
// before it receives the actual focus event
|
// before it receives the actual focus event
|
||||||
// We hide the focused input (with the visible caret) invisiable by making it scale(0),
|
// We hide the focused input (with the visible caret) invisible by making it scale(0),
|
||||||
cloneInputComponent(componentEl, inputEl);
|
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 doc = componentEl.ownerDocument!;
|
||||||
const tx = doc.dir === 'rtl' ? 9999 : -9999;
|
const tx = doc.dir === 'rtl' ? 9999 : -9999;
|
||||||
inputEl.style.transform = `translate3d(${tx}px,${inputRelativeY}px,0)`;
|
componentEl.style.pointerEvents = 'none';
|
||||||
// TODO
|
inputEl.style.transform = `translate3d(${tx}px,${inputRelativeY}px,0) scale(0)`;
|
||||||
// inputEle.style.opacity = '0';
|
|
||||||
} else {
|
|
||||||
removeClone(componentEl, inputEl);
|
|
||||||
}
|
|
||||||
(componentEl as any)[RELOCATED_KEY] = shouldRelocate;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isFocused(input: HTMLInputElement): boolean {
|
|
||||||
return input === input.ownerDocument!.activeElement;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 = '';
|
componentEl.style.pointerEvents = '';
|
||||||
}
|
inputEl.style.transform = '';
|
||||||
(inputEl.style as any)['transform'] = '';
|
|
||||||
inputEl.style.opacity = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
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