fix(input): scroll assist works in with shadow-dom (#16206)

fixes #15888
fixes #15294
fixes #15895
This commit is contained in:
Manu MA
2018-11-03 01:35:38 +01:00
committed by GitHub
parent 0abf992a13
commit d817cc3b30
9 changed files with 47 additions and 100 deletions

View File

@ -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';
/** /**

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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(() => {

View File

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