fix(input): improve scroll to input and focusing

Related #6228
This commit is contained in:
Adam Bradley
2016-11-21 08:56:00 -06:00
parent 1d245ec6bf
commit 3b304974ec
10 changed files with 373 additions and 324 deletions

View File

@ -4,8 +4,8 @@ import { NgControl } from '@angular/forms';
import { App } from '../app/app'; import { App } from '../app/app';
import { copyInputAttributes, PointerCoordinates, hasPointerMoved, pointerCoord } from '../../util/dom'; import { copyInputAttributes, PointerCoordinates, hasPointerMoved, pointerCoord } from '../../util/dom';
import { Config } from '../../config/config'; import { Config } from '../../config/config';
import { Content } from '../content/content'; import { Content, ContentDimensions } from '../content/content';
import { Form } from '../../util/form'; import { Form, IonicFormInput } from '../../util/form';
import { Ion } from '../ion'; import { Ion } from '../ion';
import { isTrueProperty } from '../../util/util'; import { isTrueProperty } from '../../util/util';
import { Item } from '../item/item'; import { Item } from '../item/item';
@ -15,7 +15,11 @@ import { NavControllerBase } from '../../navigation/nav-controller-base';
import { Platform } from '../../platform/platform'; import { Platform } from '../../platform/platform';
export class InputBase extends Ion { /**
* @private
* Hopefully someday a majority of the auto-scrolling tricks can get removed.
*/
export class InputBase extends Ion implements IonicFormInput {
_coord: PointerCoordinates; _coord: PointerCoordinates;
_deregScroll: Function; _deregScroll: Function;
_disabled: boolean = false; _disabled: boolean = false;
@ -72,6 +76,8 @@ export class InputBase extends Ion {
scrollMove(ev: UIEvent) { scrollMove(ev: UIEvent) {
// scroll move event listener this instance can reuse // scroll move event listener this instance can reuse
console.debug(`input-base, scrollMove`);
if (!(this._nav && this._nav.isTransitioning())) { if (!(this._nav && this._nav.isTransitioning())) {
this.deregScrollMove(); this.deregScrollMove();
@ -313,11 +319,11 @@ export class InputBase extends Ion {
*/ */
focusChange(inputHasFocus: boolean) { focusChange(inputHasFocus: boolean) {
if (this._item) { if (this._item) {
console.debug(`input-base, focusChange, inputHasFocus: ${inputHasFocus}, ${this._item.getNativeElement().nodeName}.${this._item.getNativeElement().className}`);
this._item.setElementClass('input-has-focus', inputHasFocus); this._item.setElementClass('input-has-focus', inputHasFocus);
} }
if (!inputHasFocus) { if (!inputHasFocus) {
this.deregScrollMove(); this.deregScrollMove();
} }
// If clearOnEdit is enabled and the input blurred but has a value, set a flag // If clearOnEdit is enabled and the input blurred but has a value, set a flag
@ -328,8 +334,6 @@ export class InputBase extends Ion {
pointerStart(ev: any) { pointerStart(ev: any) {
// input cover touchstart // input cover touchstart
console.debug('scroll assist pointerStart', ev.type);
if (ev.type === 'touchstart') { if (ev.type === 'touchstart') {
this._isTouch = true; this._isTouch = true;
} }
@ -338,11 +342,13 @@ export class InputBase extends Ion {
// remember where the touchstart/mousedown started // remember where the touchstart/mousedown started
this._coord = pointerCoord(ev); this._coord = pointerCoord(ev);
} }
console.debug(`input-base, pointerStart, type: ${ev.type}`);
} }
pointerEnd(ev: any) { pointerEnd(ev: any) {
// input cover touchend/mouseup // input cover touchend/mouseup
console.debug('scroll assist pointerEnd', ev.type); console.debug(`input-base, pointerEnd, type: ${ev.type}`);
if ((this._isTouch && ev.type === 'mouseup') || !this._app.isEnabled()) { if ((this._isTouch && ev.type === 'mouseup') || !this._app.isEnabled()) {
// the app is actively doing something right now // the app is actively doing something right now
@ -361,10 +367,8 @@ export class InputBase extends Ion {
ev.stopPropagation(); ev.stopPropagation();
// begin the input focus process // begin the input focus process
console.debug('initFocus', ev.type);
this.initFocus(); this.initFocus();
} }
} }
this._coord = null; this._coord = null;
@ -375,22 +379,32 @@ export class InputBase extends Ion {
*/ */
initFocus() { initFocus() {
// begin the process of setting focus to the inner input element // begin the process of setting focus to the inner input element
var scrollView = this._scrollView; const scrollView = this._scrollView;
console.debug(`input-base, initFocus(), scrollView: ${!!scrollView}`);
if (scrollView) { if (scrollView) {
// this input is inside of a scroll view // this input is inside of a scroll view
// find out if text input should be manually scrolled into view // find out if text input should be manually scrolled into view
// get container of this input, probably an ion-item a few nodes up // get container of this input, probably an ion-item a few nodes up
var ele: HTMLElement = this._elementRef.nativeElement; let ele: HTMLElement = this._elementRef.nativeElement;
ele = <HTMLElement>ele.closest('ion-item,[ion-item]') || ele; ele = <HTMLElement>ele.closest('ion-item,[ion-item]') || ele;
var scrollData = InputBase.getScrollData(ele.offsetTop, ele.offsetHeight, scrollView.getContentDimensions(), this._keyboardHeight, this._platform.height()); const scrollData = getScrollData(ele.offsetTop, ele.offsetHeight, scrollView.getContentDimensions(), this._keyboardHeight, this._platform.height());
if (scrollData.scrollAmount > -3 && scrollData.scrollAmount < 3) { if (Math.abs(scrollData.scrollAmount) < 4) {
// the text input is in a safe position that doesn't // the text input is in a safe position that doesn't
// require it to be scrolled into view, just set focus now // require it to be scrolled into view, just set focus now
this.setFocus(); this.setFocus();
// all good, allow clicks again
this._app.setEnabled(true);
this._nav && this._nav.setTransitioning(false);
this.regScrollMove(); this.regScrollMove();
if (this._usePadding) {
this._scrollView.clearScrollPaddingFocusOut();
}
return; return;
} }
@ -401,7 +415,7 @@ export class InputBase extends Ion {
// manually scroll the text input to the top // manually scroll the text input to the top
// do not allow any clicks while it's scrolling // do not allow any clicks while it's scrolling
var scrollDuration = getScrollAssistDuration(scrollData.scrollAmount); const scrollDuration = getScrollAssistDuration(scrollData.scrollAmount);
this._app.setEnabled(false, scrollDuration); this._app.setEnabled(false, scrollDuration);
this._nav && this._nav.setTransitioning(true); this._nav && this._nav.setTransitioning(true);
@ -411,7 +425,8 @@ export class InputBase extends Ion {
this._native.beginFocus(true, scrollData.inputSafeY); this._native.beginFocus(true, scrollData.inputSafeY);
// scroll the input into place // scroll the input into place
scrollView.scrollTo(0, scrollData.scrollTo, scrollDuration).then(() => { scrollView.scrollTo(0, scrollData.scrollTo, scrollDuration, () => {
console.debug(`input-base, scrollTo completed, scrollTo: ${scrollData.scrollTo}, scrollDuration: ${scrollDuration}`);
// the scroll view is in the correct position now // the scroll view is in the correct position now
// give the native text input focus // give the native text input focus
this._native.beginFocus(false, 0); this._native.beginFocus(false, 0);
@ -491,21 +506,24 @@ export class InputBase extends Ion {
focusNext() { focusNext() {
this._form.tabFocus(this); this._form.tabFocus(this);
} }
}
/** /**
* @private * @private
*/ */
static getScrollData(inputOffsetTop: number, inputOffsetHeight: number, scrollViewDimensions: any, keyboardHeight: number, plaformHeight: number) { export function getScrollData(inputOffsetTop: number, inputOffsetHeight: number, scrollViewDimensions: ContentDimensions, keyboardHeight: number, plaformHeight: number) {
// compute input's Y values relative to the body // compute input's Y values relative to the body
let inputTop = (inputOffsetTop + scrollViewDimensions.contentTop - scrollViewDimensions.scrollTop); let inputTop = (inputOffsetTop + scrollViewDimensions.contentTop - scrollViewDimensions.scrollTop);
let inputBottom = (inputTop + inputOffsetHeight); let inputBottom = (inputTop + inputOffsetHeight);
// compute the safe area which is the viewable content area when the soft keyboard is up // compute the safe area which is the viewable content area when the soft keyboard is up
let safeAreaTop = scrollViewDimensions.contentTop; let safeAreaTop = scrollViewDimensions.contentTop;
let safeAreaHeight = plaformHeight - keyboardHeight - safeAreaTop; let safeAreaHeight = (plaformHeight - keyboardHeight - safeAreaTop) / 2;
safeAreaHeight /= 2;
let safeAreaBottom = safeAreaTop + safeAreaHeight; let safeAreaBottom = safeAreaTop + safeAreaHeight;
// figure out if each edge of teh input is within the safe area
let inputTopWithinSafeArea = (inputTop >= safeAreaTop && inputTop <= safeAreaBottom); let inputTopWithinSafeArea = (inputTop >= safeAreaTop && inputTop <= safeAreaBottom);
let inputTopAboveSafeArea = (inputTop < safeAreaTop); let inputTopAboveSafeArea = (inputTop < safeAreaTop);
let inputTopBelowSafeArea = (inputTop > safeAreaBottom); let inputTopBelowSafeArea = (inputTop > safeAreaBottom);
@ -524,7 +542,7 @@ export class InputBase extends Ion {
7) Input top below safe area, no room to scroll, input larger than safe area 7) Input top below safe area, no room to scroll, input larger than safe area
*/ */
let scrollData = { const scrollData: ScrollData = {
scrollAmount: 0, scrollAmount: 0,
scrollTo: 0, scrollTo: 0,
scrollPadding: 0, scrollPadding: 0,
@ -538,13 +556,13 @@ export class InputBase extends Ion {
} }
// looks like we'll have to do some auto-scrolling // looks like we'll have to do some auto-scrolling
if (inputTopBelowSafeArea || inputBottomBelowSafeArea) { if (inputTopBelowSafeArea || inputBottomBelowSafeArea || inputTopAboveSafeArea) {
// Input top and bottom below safe area // Input top or bottom below safe area
// auto scroll the input up so at least the top of it shows // auto scroll the input up so at least the top of it shows
if (safeAreaHeight > inputOffsetHeight) { if (safeAreaHeight > inputOffsetHeight) {
// safe area height is taller than the input height, so we // safe area height is taller than the input height, so we
// can bring it up the input just enough to show the input bottom // can bring up the input just enough to show the input bottom
scrollData.scrollAmount = Math.round(safeAreaBottom - inputBottom); scrollData.scrollAmount = Math.round(safeAreaBottom - inputBottom);
} else { } else {
@ -556,47 +574,34 @@ export class InputBase extends Ion {
scrollData.inputSafeY = -(inputTop - safeAreaTop) + 4; scrollData.inputSafeY = -(inputTop - safeAreaTop) + 4;
} else if (inputTopAboveSafeArea) { if (inputTopAboveSafeArea && scrollData.scrollAmount > inputOffsetHeight) {
// Input top above safe area // the input top is above the safe area and we're already scrolling it into place
// auto scroll the input down so at least the top of it shows // don't let it scroll more than the height of the input
scrollData.scrollAmount = Math.round(safeAreaTop - inputTop); scrollData.scrollAmount = inputOffsetHeight;
}
scrollData.inputSafeY = (safeAreaTop - inputTop) + 4;
} }
// figure out where it should scroll to for the best position to the input // figure out where it should scroll to for the best position to the input
scrollData.scrollTo = (scrollViewDimensions.scrollTop - scrollData.scrollAmount); scrollData.scrollTo = (scrollViewDimensions.scrollTop - scrollData.scrollAmount);
if (scrollData.scrollAmount < 0) { // when auto-scrolling, there also needs to be enough
// when auto-scrolling up, there also needs to be enough
// content padding at the bottom of the scroll view // content padding at the bottom of the scroll view
// manually add it if there isn't enough scrollable area // always add scroll padding when a text input has focus
// this allows for the content to scroll above of the keyboard
// content behind the keyboard would be blank
// some cases may not need it, but when jumping around it's best
// to have the padding already rendered so there's no jank
scrollData.scrollPadding = keyboardHeight;
// figure out how many scrollable area is left to scroll up // var safeAreaEle: HTMLElement = (<any>window).safeAreaEle;
let availablePadding = (scrollViewDimensions.scrollHeight - scrollViewDimensions.scrollTop) - scrollViewDimensions.contentHeight; // if (!safeAreaEle) {
// safeAreaEle = (<any>window).safeAreaEle = document.createElement('div');
let paddingSpace = availablePadding + scrollData.scrollAmount; // safeAreaEle.style.cssText = 'position:absolute; padding:1px 5px; left:0; right:0; font-weight:bold; font-size:10px; font-family:Courier; text-align:right; background:rgba(0, 128, 0, 0.8); text-shadow:1px 1px white; pointer-events:none;';
if (paddingSpace < 0) { // document.body.appendChild(safeAreaEle);
// there's not enough scrollable area at the bottom, so manually add more
scrollData.scrollPadding = (scrollViewDimensions.contentHeight - safeAreaHeight);
}
}
// if (!window.safeAreaEle) {
// window.safeAreaEle = document.createElement('div');
// window.safeAreaEle.style.position = 'absolute';
// window.safeAreaEle.style.background = 'rgba(0, 128, 0, 0.7)';
// window.safeAreaEle.style.padding = '2px 5px';
// window.safeAreaEle.style.textShadow = '1px 1px white';
// window.safeAreaEle.style.left = '0px';
// window.safeAreaEle.style.right = '0px';
// window.safeAreaEle.style.fontWeight = 'bold';
// window.safeAreaEle.style.pointerEvents = 'none';
// document.body.appendChild(window.safeAreaEle);
// } // }
// window.safeAreaEle.style.top = safeAreaTop + 'px'; // safeAreaEle.style.top = safeAreaTop + 'px';
// window.safeAreaEle.style.height = safeAreaHeight + 'px'; // safeAreaEle.style.height = safeAreaHeight + 'px';
// window.safeAreaEle.innerHTML = ` // safeAreaEle.innerHTML = `
// <div>scrollTo: ${scrollData.scrollTo}</div> // <div>scrollTo: ${scrollData.scrollTo}</div>
// <div>scrollAmount: ${scrollData.scrollAmount}</div> // <div>scrollAmount: ${scrollData.scrollAmount}</div>
// <div>scrollPadding: ${scrollData.scrollPadding}</div> // <div>scrollPadding: ${scrollData.scrollPadding}</div>
@ -604,11 +609,12 @@ export class InputBase extends Ion {
// <div>scrollHeight: ${scrollViewDimensions.scrollHeight}</div> // <div>scrollHeight: ${scrollViewDimensions.scrollHeight}</div>
// <div>scrollTop: ${scrollViewDimensions.scrollTop}</div> // <div>scrollTop: ${scrollViewDimensions.scrollTop}</div>
// <div>contentHeight: ${scrollViewDimensions.contentHeight}</div> // <div>contentHeight: ${scrollViewDimensions.contentHeight}</div>
// <div>plaformHeight: ${plaformHeight}</div>
// `; // `;
return scrollData; return scrollData;
} }
}
const SCROLL_ASSIST_SPEED = 0.3; const SCROLL_ASSIST_SPEED = 0.3;
@ -617,3 +623,10 @@ function getScrollAssistDuration(distanceToScroll: number) {
let duration = distanceToScroll / SCROLL_ASSIST_SPEED; let duration = distanceToScroll / SCROLL_ASSIST_SPEED;
return Math.min(400, Math.max(150, duration)); return Math.min(400, Math.max(150, duration));
} }
export interface ScrollData {
scrollAmount: number;
scrollTo: number;
scrollPadding: number;
inputSafeY: number;
}

View File

@ -146,11 +146,6 @@ $text-input-ios-highlight-color-invalid: $text-input-highlight-color-invalid !
padding-left: 0; padding-left: 0;
} }
.item-label-floating .text-input-ios.cloned-input,
.item-label-stacked .text-input-ios.cloned-input {
top: 30px;
}
// iOS Clear Input Icon // iOS Clear Input Icon
// -------------------------------------------------- // --------------------------------------------------

View File

@ -147,14 +147,6 @@ $text-input-md-highlight-color-invalid: $text-input-highlight-color-invalid
padding-left: 0; padding-left: 0;
} }
.item-label-floating .text-input-md.cloned-input {
top: 32px;
}
.item-label-stacked .text-input-md.cloned-input {
top: 27px;
}
// Material Design Clear Input Icon // Material Design Clear Input Icon
// -------------------------------------------------- // --------------------------------------------------

View File

@ -119,7 +119,7 @@ input.text-input:-webkit-autofill {
[next-input] { [next-input] {
position: absolute; position: absolute;
bottom: 1px; bottom: 20px;
padding: 0; padding: 0;
@ -153,18 +153,3 @@ input.text-input:-webkit-autofill {
.input-has-focus.input-has-value .text-input-clear-icon { .input-has-focus.input-has-value .text-input-clear-icon {
display: block; display: block;
} }
// Cloned Input
// --------------------------------------------------
.text-input.cloned-input {
position: relative;
top: 0;
pointer-events: none;
}
.item-input:not(.item-label-floating) .text-input.cloned-active {
display: none;
}

View File

@ -126,14 +126,6 @@ $text-input-wp-highlight-color-invalid: $text-input-highlight-color-invalid
width: calc(100% - #{$text-input-wp-margin-right}); width: calc(100% - #{$text-input-wp-margin-right});
} }
.item-label-floating .text-input-wp.cloned-input {
top: 32px;
}
.item-label-stacked .text-input-wp.cloned-input {
top: 27px;
}
.item-wp.item-label-stacked [item-right], .item-wp.item-label-stacked [item-right],
.item-wp.item-label-floating [item-right] { .item-wp.item-label-floating [item-right] {
align-self: flex-end; align-self: flex-end;

View File

@ -62,11 +62,11 @@ export class NativeInput {
// automatically blur input if: // automatically blur input if:
// 1) this input has focus // 1) this input has focus
// 2) the newly tapped document element is not an input // 2) the newly tapped document element is not an input
console.debug('input blurring enabled'); console.debug(`native-input, blurring enabled`);
document.addEventListener('touchend', docTouchEnd, true); document.addEventListener('touchend', docTouchEnd, true);
self._unrefBlur = function() { self._unrefBlur = function() {
console.debug('input blurring disabled'); console.debug(`native-input, blurring disabled`);
document.removeEventListener('touchend', docTouchEnd, true); document.removeEventListener('touchend', docTouchEnd, true);
}; };
} }
@ -99,7 +99,7 @@ export class NativeInput {
beginFocus(shouldFocus: boolean, inputRelativeY: number) { beginFocus(shouldFocus: boolean, inputRelativeY: number) {
if (this._relocated !== shouldFocus) { if (this._relocated !== shouldFocus) {
var focusedInputEle = this.element(); const focusedInputEle = this.element();
if (shouldFocus) { if (shouldFocus) {
// we should focus into this element // we should focus into this element
@ -113,8 +113,7 @@ export class NativeInput {
// 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
var clonedInputEle = cloneInput(focusedInputEle, 'cloned-focus'); cloneInputComponent(focusedInputEle);
focusedInputEle.parentNode.insertBefore(clonedInputEle, focusedInputEle);
// move the native input to a location safe to receive focus // move the native input to a location safe to receive focus
// according to the browser, the native input receives focus in an // according to the browser, the native input receives focus in an
@ -128,18 +127,11 @@ export class NativeInput {
// to scroll the input into view itself (screwing up headers/footers) // to scroll the input into view itself (screwing up headers/footers)
this.setFocus(); this.setFocus();
if (this._clone) {
focusedInputEle.classList.add('cloned-active');
}
} else { } else {
// should remove the focus // should remove the focus
if (this._clone) { if (this._clone) {
// should remove the cloned node // should remove the cloned node
focusedInputEle.classList.remove('cloned-active'); removeClone(focusedInputEle);
(<any>focusedInputEle.style)[CSS.transform] = '';
focusedInputEle.style.opacity = '';
removeClone(focusedInputEle, 'cloned-focus');
} }
} }
@ -150,17 +142,14 @@ export class NativeInput {
hideFocus(shouldHideFocus: boolean) { hideFocus(shouldHideFocus: boolean) {
let focusedInputEle = this.element(); let focusedInputEle = this.element();
console.debug(`native input hideFocus, shouldHideFocus: ${shouldHideFocus}, input value: ${focusedInputEle.value}`); console.debug(`native-input, hideFocus, shouldHideFocus: ${shouldHideFocus}, input value: ${focusedInputEle.value}`);
if (shouldHideFocus) { if (shouldHideFocus) {
let clonedInputEle = cloneInput(focusedInputEle, 'cloned-move'); cloneInputComponent(focusedInputEle);
focusedInputEle.style.transform = 'scale(0)';
focusedInputEle.classList.add('cloned-active');
focusedInputEle.parentNode.insertBefore(clonedInputEle, focusedInputEle);
} else { } else {
focusedInputEle.classList.remove('cloned-active'); removeClone(focusedInputEle);
removeClone(focusedInputEle, 'cloned-move');
} }
} }
@ -186,26 +175,56 @@ export class NativeInput {
} }
function cloneInput(focusedInputEle: any, addCssClass: string) { function cloneInputComponent(srcNativeInputEle: HTMLInputElement) {
let clonedInputEle = focusedInputEle.cloneNode(true); // given a native <input> or <textarea> element
clonedInputEle.classList.add('cloned-input'); // find its parent wrapping component like <ion-input> or <ion-textarea>
clonedInputEle.classList.add(addCssClass); // then clone the entire component
clonedInputEle.setAttribute('aria-hidden', true); const srcComponentEle = <HTMLElement>srcNativeInputEle.closest('ion-input,ion-textarea');
clonedInputEle.removeAttribute('aria-labelledby'); if (srcComponentEle) {
clonedInputEle.tabIndex = -1; // DOM READ
clonedInputEle.style.width = (focusedInputEle.offsetWidth + 10) + 'px'; const srcTop = srcComponentEle.offsetTop;
clonedInputEle.style.height = focusedInputEle.offsetHeight + 'px'; const srcLeft = srcComponentEle.offsetLeft;
clonedInputEle.value = focusedInputEle.value; const srcWidth = srcComponentEle.offsetWidth;
return clonedInputEle; const srcHeight = srcComponentEle.offsetHeight;
// DOM WRITE
// not using deep clone so we don't pull in unnecessary nodes
const clonedComponentEle = <HTMLElement>srcComponentEle.cloneNode(false);
clonedComponentEle.classList.add('cloned-input');
clonedComponentEle.setAttribute('aria-hidden', 'true');
clonedComponentEle.style.pointerEvents = 'none';
clonedComponentEle.style.position = 'absolute';
clonedComponentEle.style.top = srcTop + 'px';
clonedComponentEle.style.left = srcLeft + 'px';
clonedComponentEle.style.width = srcWidth + 'px';
clonedComponentEle.style.height = srcHeight + 'px';
const clonedNativeInputEle = <HTMLInputElement>srcNativeInputEle.cloneNode(false);
clonedNativeInputEle.value = srcNativeInputEle.value;
clonedNativeInputEle.tabIndex = -1;
clonedComponentEle.appendChild(clonedNativeInputEle);
srcComponentEle.parentNode.appendChild(clonedComponentEle);
srcComponentEle.style.pointerEvents = 'none';
} }
function removeClone(focusedInputEle: any, queryCssClass: string) { srcNativeInputEle.style.transform = 'scale(0)';
let clonedInputEle = focusedInputEle.parentElement.querySelector('.' + queryCssClass);
if (clonedInputEle) {
clonedInputEle.parentNode.removeChild(clonedInputEle);
}
} }
function removeClone(srcNativeInputEle: HTMLElement) {
const srcComponentEle = <HTMLElement>srcNativeInputEle.closest('ion-input,ion-textarea');
if (srcComponentEle && srcComponentEle.parentElement) {
const clonedInputEles = srcComponentEle.parentElement.querySelectorAll('.cloned-input');
for (var i = 0; i < clonedInputEles.length; i++) {
clonedInputEles[i].parentNode.removeChild(clonedInputEles[i]);
}
srcComponentEle.style.pointerEvents = '';
}
srcNativeInputEle.style.transform = '';
srcNativeInputEle.style.opacity = '';
}
/** /**

View File

@ -1,133 +1,181 @@
import { TextInput } from '../input'; import { getScrollData } from '../input-base';
import { ContentDimensions } from '../../content/content';
describe('text input', () => {
describe('getScrollData', () => {
describe('input', () => {
it('should scroll, top and bottom below safe area, no room to scroll', () => { it('should scroll, top and bottom below safe area, no room to scroll', () => {
let inputOffsetTop = 350; let inputOffsetTop = 350;
let inputOffsetHeight = 35; let inputOffsetHeight = 35;
let scrollViewDimensions = { let scrollViewDimensions: ContentDimensions = {
contentTop: 100, contentTop: 100,
contentHeight: 700, contentHeight: 700,
contentBottom: 0,
contentWidth: 400,
contentLeft: 0,
scrollTop: 30, scrollTop: 30,
scrollHeight: 700 scrollHeight: 700,
scrollWidth: 400,
scrollLeft: 0,
}; };
let keyboardHeight = 400; let keyboardHeight = 400;
let platformHeight = 800; let platformHeight = 800;
let scrollData = TextInput.getScrollData(inputOffsetTop, inputOffsetHeight, scrollViewDimensions, keyboardHeight, platformHeight); let scrollData = getScrollData(inputOffsetTop, inputOffsetHeight, scrollViewDimensions, keyboardHeight, platformHeight);
expect(scrollData.scrollAmount).toBe(-205); expect(scrollData.scrollAmount).toBe(-205);
expect(scrollData.scrollTo).toBe(235); expect(scrollData.scrollTo).toBe(235);
expect(scrollData.scrollPadding).toBe(550); expect(scrollData.scrollPadding).toBe(400);
}); });
it('should scroll, top and bottom below safe area, room to scroll', () => { it('should scroll, top and bottom below safe area, room to scroll', () => {
let inputOffsetTop = 350; let inputOffsetTop = 350;
let inputOffsetHeight = 35; let inputOffsetHeight = 35;
let scrollViewDimensions = { let scrollViewDimensions: ContentDimensions = {
contentTop: 100, contentTop: 100,
contentHeight: 700, contentHeight: 700,
contentBottom: 0,
contentWidth: 400,
contentLeft: 0,
scrollTop: 30, scrollTop: 30,
scrollHeight: 1000 scrollHeight: 1000,
scrollWidth: 400,
scrollLeft: 0,
}; };
let keyboardHeight = 400; let keyboardHeight = 400;
let platformHeight = 800; let platformHeight = 800;
let scrollData = TextInput.getScrollData(inputOffsetTop, inputOffsetHeight, scrollViewDimensions, keyboardHeight, platformHeight); let scrollData = getScrollData(inputOffsetTop, inputOffsetHeight, scrollViewDimensions, keyboardHeight, platformHeight);
expect(scrollData.scrollAmount).toBe(-205); expect(scrollData.scrollAmount).toBe(-205);
expect(scrollData.scrollTo).toBe(235); expect(scrollData.scrollTo).toBe(235);
expect(scrollData.scrollPadding).toBe(0); expect(scrollData.scrollPadding).toBe(400);
}); });
it('should scroll, top above safe', () => { it('should scroll, top above safe', () => {
// TextInput top within safe area, bottom below safe area, room to scroll // TextInput top within safe area, bottom below safe area, room to scroll
let inputOffsetTop = 100; let inputOffsetTop = 100;
let inputOffsetHeight = 33; let inputOffsetHeight = 33;
let scrollViewDimensions = { let scrollViewDimensions: ContentDimensions = {
contentTop: 100, contentTop: 100,
contentHeight: 700, contentHeight: 700,
contentBottom: 0,
contentWidth: 400,
contentLeft: 0,
scrollTop: 250, scrollTop: 250,
scrollHeight: 700 scrollHeight: 700,
scrollWidth: 400,
scrollLeft: 0,
}; };
let keyboardHeight = 400; let keyboardHeight = 400;
let platformHeight = 800; let platformHeight = 800;
let scrollData = TextInput.getScrollData(inputOffsetTop, inputOffsetHeight, scrollViewDimensions, keyboardHeight, platformHeight); let scrollData = getScrollData(inputOffsetTop, inputOffsetHeight, scrollViewDimensions, keyboardHeight, platformHeight);
expect(scrollData.scrollAmount).toBe(150); expect(scrollData.scrollAmount).toBe(33);
expect(scrollData.scrollTo).toBe(100); expect(scrollData.scrollTo).toBe(217);
expect(scrollData.scrollPadding).toBe(0); expect(scrollData.scrollPadding).toBe(400);
}); });
it('should scroll, top in safe, bottom below safe, below more than top in, not enough padding', () => { it('should scroll, top in safe, bottom below safe, below more than top in, not enough padding', () => {
// TextInput top within safe area, bottom below safe area, room to scroll // TextInput top within safe area, bottom below safe area, room to scroll
let inputOffsetTop = 100; let inputOffsetTop = 100;
let inputOffsetHeight = 320; let inputOffsetHeight = 320;
let scrollViewDimensions = { let scrollViewDimensions: ContentDimensions = {
contentTop: 100, contentTop: 100,
contentHeight: 700, contentHeight: 700,
contentBottom: 0,
contentWidth: 400,
contentLeft: 0,
scrollTop: 20, scrollTop: 20,
scrollHeight: 700 scrollHeight: 700,
scrollWidth: 400,
scrollLeft: 0,
}; };
let keyboardHeight = 400; let keyboardHeight = 400;
let platformHeight = 800; let platformHeight = 800;
let scrollData = TextInput.getScrollData(inputOffsetTop, inputOffsetHeight, scrollViewDimensions, keyboardHeight, platformHeight); let scrollData = getScrollData(inputOffsetTop, inputOffsetHeight, scrollViewDimensions, keyboardHeight, platformHeight);
expect(scrollData.scrollAmount).toBe(-80); expect(scrollData.scrollAmount).toBe(-80);
expect(scrollData.scrollTo).toBe(100); expect(scrollData.scrollTo).toBe(100);
expect(scrollData.scrollPadding).toBe(550); expect(scrollData.scrollPadding).toBe(400);
}); });
it('should scroll, top in safe, bottom below safe, below more than top in, enough padding', () => { it('should scroll, top in safe, bottom below safe, below more than top in, enough padding', () => {
// TextInput top within safe area, bottom below safe area, room to scroll // TextInput top within safe area, bottom below safe area, room to scroll
let inputOffsetTop = 20; let inputOffsetTop = 20;
let inputOffsetHeight = 330; let inputOffsetHeight = 330;
let scrollViewDimensions = { let scrollViewDimensions: ContentDimensions = {
contentTop: 100, contentTop: 100,
contentHeight: 700,
contentBottom: 0,
contentWidth: 400,
contentLeft: 0,
scrollTop: 0, scrollTop: 0,
scrollHeight: 700,
scrollWidth: 400,
scrollLeft: 0,
}; };
let keyboardHeight = 400; let keyboardHeight = 400;
let platformHeight = 800; let platformHeight = 800;
let scrollData = TextInput.getScrollData(inputOffsetTop, inputOffsetHeight, scrollViewDimensions, keyboardHeight, platformHeight); let scrollData = getScrollData(inputOffsetTop, inputOffsetHeight, scrollViewDimensions, keyboardHeight, platformHeight);
expect(scrollData.scrollAmount).toBe(-20); expect(scrollData.scrollAmount).toBe(-20);
expect(scrollData.scrollTo).toBe(20); expect(scrollData.scrollTo).toBe(20);
expect(scrollData.scrollPadding).toBe(0); expect(scrollData.scrollPadding).toBe(400);
}); });
it('should scroll, top in safe, bottom below safe, below less than top in, enough padding', () => { it('should scroll, top in safe, bottom below safe, below less than top in, enough padding', () => {
// TextInput top within safe area, bottom below safe area, room to scroll // TextInput top within safe area, bottom below safe area, room to scroll
let inputOffsetTop = 250; let inputOffsetTop = 250;
let inputOffsetHeight = 80; // goes 30px below safe area let inputOffsetHeight = 80; // goes 30px below safe area
let scrollViewDimensions = { let scrollViewDimensions: ContentDimensions = {
contentTop: 100, contentTop: 100,
contentHeight: 700,
contentBottom: 0,
contentWidth: 400,
contentLeft: 0,
scrollTop: 0, scrollTop: 0,
scrollHeight: 700,
scrollWidth: 400,
scrollLeft: 0,
}; };
let keyboardHeight = 400; let keyboardHeight = 400;
let platformHeight = 800; let platformHeight = 800;
let scrollData = TextInput.getScrollData(inputOffsetTop, inputOffsetHeight, scrollViewDimensions, keyboardHeight, platformHeight); let scrollData = getScrollData(inputOffsetTop, inputOffsetHeight, scrollViewDimensions, keyboardHeight, platformHeight);
expect(scrollData.scrollAmount).toBe(-180); expect(scrollData.scrollAmount).toBe(-180);
expect(scrollData.scrollTo).toBe(180); expect(scrollData.scrollTo).toBe(180);
expect(scrollData.scrollPadding).toBe(0); expect(scrollData.scrollPadding).toBe(400);
}); });
it('should not scroll, top in safe, bottom in safe', () => { it('should not scroll, top in safe, bottom in safe', () => {
// TextInput top within safe area, bottom within safe area // TextInput top within safe area, bottom within safe area
let inputOffsetTop = 100; let inputOffsetTop = 100;
let inputOffsetHeight = 50; let inputOffsetHeight = 50;
let scrollViewDimensions = { let scrollViewDimensions: ContentDimensions = {
contentTop: 100, contentTop: 100,
contentBottom: 0,
contentHeight: 700,
contentWidth: 400,
contentLeft: 0,
scrollTop: 0, scrollTop: 0,
keyboardTop: 400 scrollHeight: 700,
scrollWidth: 400,
scrollLeft: 0,
}; };
let keyboardHeight = 400; let keyboardHeight = 400;
let platformHeight = 800; let platformHeight = 800;
let scrollData = TextInput.getScrollData(inputOffsetTop, inputOffsetHeight, scrollViewDimensions, keyboardHeight, platformHeight); let scrollData = getScrollData(inputOffsetTop, inputOffsetHeight, scrollViewDimensions, keyboardHeight, platformHeight);
expect(scrollData.scrollAmount).toBe(0); expect(scrollData.scrollAmount).toBe(0);
}); });
});
}); });

View File

@ -27,8 +27,8 @@ $label-ios-margin: $item-ios-padding-top ($item-ios-padding-right /
color: $label-ios-text-color; color: $label-ios-text-color;
} }
.label-ios + ion-input .text-input, .label-ios + .input .text-input,
.label-ios + ion-textarea .text-input { .label-ios + .input + .cloned-input {
margin-left: $item-ios-padding-left; margin-left: $item-ios-padding-left;
width: calc(100% - (#{$item-ios-padding-right} / 2) - #{$item-ios-padding-left}); width: calc(100% - (#{$item-ios-padding-right} / 2) - #{$item-ios-padding-left});

View File

@ -76,6 +76,7 @@ export const PLATFORM_CONFIGS: {[key: string]: PlatformConfig} = {
return 'ripple'; return 'ripple';
}, },
autoFocusAssist: 'immediate', autoFocusAssist: 'immediate',
inputCloning: true,
scrollAssist: true, scrollAssist: true,
hoverCSS: false, hoverCSS: false,
keyboardHeight: 300, keyboardHeight: 300,

View File

@ -107,6 +107,10 @@ textarea {
color: inherit; color: inherit;
} }
textarea::placeholder {
padding-left: 2px;
}
form, form,
input, input,
optgroup, optgroup,