Skip to content

Commit

Permalink
fix(input): scroll assist works in with shadow-dom (#16206)
Browse files Browse the repository at this point in the history
fixes #15888
fixes #15294
fixes #15895
  • Loading branch information
manucorporat authored Nov 3, 2018
1 parent 0abf992 commit d817cc3
Show file tree
Hide file tree
Showing 9 changed files with 45 additions and 98 deletions.
4 changes: 2 additions & 2 deletions core/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1700,7 +1700,7 @@ export namespace Components {
*/
'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';
/**
Expand Down Expand Up @@ -1818,7 +1818,7 @@ export namespace Components {
*/
'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';
/**
Expand Down
14 changes: 2 additions & 12 deletions core/src/components/input/input.scss
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
--padding-bottom: 0;
--padding-start: 0;
--background: transparent;
--color: inherit;
--color: initial;

display: flex;
position: relative;
Expand Down Expand Up @@ -108,17 +108,11 @@
// This will only show when the scroll assist is configured
// otherwise the .input-cover will not be rendered at all
// The input cover is not clickable when the input is disabled

.input-cover {
.cloned-input {
@include position(0, null, null, 0);

position: absolute;

width: 100%;
height: 100%;
}

:host([disabled]) .input-cover {
pointer-events: none;
}

Expand Down Expand Up @@ -151,10 +145,6 @@
// --------------------------------------------------
// When the input has focus, then the input cover should be hidden

:host(.has-focus) .input-cover {
display: none;
}

:host(.has-focus) {
pointer-events: none;
}
Expand Down
2 changes: 1 addition & 1 deletion core/src/components/input/input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export class Input implements ComponentInterface {
@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';

Expand Down
2 changes: 1 addition & 1 deletion core/src/components/input/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
| `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'` |
| `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` |
| `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` |
Expand Down
2 changes: 1 addition & 1 deletion core/src/components/input/test/basic/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
<ion-input clear-input value="reallylonglonglonginputtoseetheedgesreallylonglonglonginputtoseetheedges"></ion-input>
</ion-item>

<ion-item>
<ion-item color="dark">
<ion-label position="floating">Floating</ion-label>
<ion-input checked></ion-input>
</ion-item>
Expand Down
103 changes: 33 additions & 70 deletions core/src/utils/input-shims/hacks/common.ts
Original file line number Diff line number Diff line change
@@ -1,94 +1,57 @@
const RELOCATED_KEY = '$ionRelocated';
const cloneMap = new WeakMap<HTMLElement, HTMLElement>();

export function relocateInput(
componentEl: HTMLElement,
inputEl: HTMLInputElement,
shouldRelocate: boolean,
inputRelativeY = 0
) {
if ((componentEl as any)[RELOCATED_KEY] === shouldRelocate) {
if (cloneMap.has(componentEl) === shouldRelocate) {
return;
}
console.debug(`native-input, hideCaret, shouldHideCaret: ${shouldRelocate}, input value: ${inputEl.value}`);
if (shouldRelocate) {
// 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) 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';
addClone(componentEl, inputEl, inputRelativeY);
} else {
removeClone(componentEl, inputEl);
}
(componentEl as any)[RELOCATED_KEY] = shouldRelocate;
}

export function isFocused(input: HTMLInputElement): boolean {
return input === input.ownerDocument!.activeElement;
return input === (input as any).getRootNode().activeElement;
}

function removeClone(componentEl: HTMLElement, inputEl: HTMLElement) {
if (componentEl && componentEl.parentElement) {
Array.from(componentEl.parentElement.querySelectorAll('.cloned-input'))
.forEach(clon => clon.remove());
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);

componentEl.style.pointerEvents = '';
}
(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);
const tx = doc.dir === 'rtl' ? 9999 : -9999;
componentEl.style.pointerEvents = 'none';
inputEl.style.transform = `translate3d(${tx}px,${inputRelativeY}px,0) scale(0)`;
}

componentEl.style.pointerEvents = 'none';
function removeClone(componentEl: HTMLElement, inputEl: HTMLElement) {
const clone = cloneMap.get(componentEl);
if (clone) {
cloneMap.delete(componentEl);
clone.remove();
}
inputEl.style.transform = 'scale(0)';
componentEl.style.pointerEvents = '';
inputEl.style.transform = '';
}
1 change: 0 additions & 1 deletion core/src/utils/input-shims/hacks/hide-caret.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ export function enableHideCaretOnScroll(componentEl: HTMLElement, inputEl: HTMLI
console.debug('Input: enableHideCaretOnScroll');

const scrollHideCaret = (shouldHideCaret: boolean) => {
// console.log('scrollHideCaret', shouldHideCaret)
if (isFocused(inputEl)) {
relocateInput(componentEl, inputEl, shouldHideCaret);
}
Expand Down
5 changes: 0 additions & 5 deletions core/src/utils/input-shims/hacks/input-blurring.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,6 @@ export function enableInputBlurring(doc: Document) {
return;
}

// skip if div is a cover
if (tapped.classList.contains('input-cover')) {
return;
}

focused = false;
// TODO: find a better way, why 50ms?
setTimeout(() => {
Expand Down
10 changes: 5 additions & 5 deletions core/src/utils/input-shims/hacks/scroll-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,21 @@ function calcScrollData(
inputRect: ClientRect,
contentRect: ClientRect,
keyboardHeight: number,
plaformHeight: number
platformHeight: number
): ScrollData {
// compute input's Y values relative to the body
const inputTop = inputRect.top;
const inputBottom = inputRect.bottom;

// compute visible area
const visibleAreaTop = contentRect.top;
const visibleAreaBottom = Math.min(contentRect.bottom, plaformHeight - keyboardHeight);
const visibleAreaBottom = Math.min(contentRect.bottom, platformHeight - keyboardHeight);

// compute safe area
const safeAreaTop = visibleAreaTop + 10;
const safeAreaBottom = visibleAreaBottom / 2.0;
const safeAreaTop = visibleAreaTop + 15;
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 distanceToTop = safeAreaTop - inputTop;

Expand Down

0 comments on commit d817cc3

Please sign in to comment.