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 { copyInputAttributes, PointerCoordinates, hasPointerMoved, pointerCoord } from '../../util/dom';
import { Config } from '../../config/config';
import { Content } from '../content/content';
import { Form } from '../../util/form';
import { Content, ContentDimensions } from '../content/content';
import { Form, IonicFormInput } from '../../util/form';
import { Ion } from '../ion';
import { isTrueProperty } from '../../util/util';
import { Item } from '../item/item';
@ -15,7 +15,11 @@ import { NavControllerBase } from '../../navigation/nav-controller-base';
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;
_deregScroll: Function;
_disabled: boolean = false;
@ -72,6 +76,8 @@ export class InputBase extends Ion {
scrollMove(ev: UIEvent) {
// scroll move event listener this instance can reuse
console.debug(`input-base, scrollMove`);
if (!(this._nav && this._nav.isTransitioning())) {
this.deregScrollMove();
@ -313,11 +319,11 @@ export class InputBase extends Ion {
*/
focusChange(inputHasFocus: boolean) {
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);
}
if (!inputHasFocus) {
this.deregScrollMove();
}
// 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) {
// input cover touchstart
console.debug('scroll assist pointerStart', ev.type);
if (ev.type === 'touchstart') {
this._isTouch = true;
}
@ -338,11 +342,13 @@ export class InputBase extends Ion {
// remember where the touchstart/mousedown started
this._coord = pointerCoord(ev);
}
console.debug(`input-base, pointerStart, type: ${ev.type}`);
}
pointerEnd(ev: any) {
// 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()) {
// the app is actively doing something right now
@ -361,10 +367,8 @@ export class InputBase extends Ion {
ev.stopPropagation();
// begin the input focus process
console.debug('initFocus', ev.type);
this.initFocus();
}
}
this._coord = null;
@ -375,22 +379,32 @@ export class InputBase extends Ion {
*/
initFocus() {
// 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) {
// this input is inside of a scroll 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
var ele: HTMLElement = this._elementRef.nativeElement;
let ele: HTMLElement = this._elementRef.nativeElement;
ele = <HTMLElement>ele.closest('ion-item,[ion-item]') || ele;
var scrollData = InputBase.getScrollData(ele.offsetTop, ele.offsetHeight, scrollView.getContentDimensions(), this._keyboardHeight, this._platform.height());
if (scrollData.scrollAmount > -3 && scrollData.scrollAmount < 3) {
const scrollData = getScrollData(ele.offsetTop, ele.offsetHeight, scrollView.getContentDimensions(), this._keyboardHeight, this._platform.height());
if (Math.abs(scrollData.scrollAmount) < 4) {
// the text input is in a safe position that doesn't
// require it to be scrolled into view, just set focus now
this.setFocus();
// all good, allow clicks again
this._app.setEnabled(true);
this._nav && this._nav.setTransitioning(false);
this.regScrollMove();
if (this._usePadding) {
this._scrollView.clearScrollPaddingFocusOut();
}
return;
}
@ -401,7 +415,7 @@ export class InputBase extends Ion {
// manually scroll the text input to the top
// 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._nav && this._nav.setTransitioning(true);
@ -411,7 +425,8 @@ export class InputBase extends Ion {
this._native.beginFocus(true, scrollData.inputSafeY);
// 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
// give the native text input focus
this._native.beginFocus(false, 0);
@ -491,125 +506,116 @@ export class InputBase extends Ion {
focusNext() {
this._form.tabFocus(this);
}
}
/**
* @private
*/
static getScrollData(inputOffsetTop: number, inputOffsetHeight: number, scrollViewDimensions: any, keyboardHeight: number, plaformHeight: number) {
// compute input's Y values relative to the body
let inputTop = (inputOffsetTop + scrollViewDimensions.contentTop - scrollViewDimensions.scrollTop);
let inputBottom = (inputTop + inputOffsetHeight);
// compute the safe area which is the viewable content area when the soft keyboard is up
let safeAreaTop = scrollViewDimensions.contentTop;
let safeAreaHeight = plaformHeight - keyboardHeight - safeAreaTop;
safeAreaHeight /= 2;
let safeAreaBottom = safeAreaTop + safeAreaHeight;
let inputTopWithinSafeArea = (inputTop >= safeAreaTop && inputTop <= safeAreaBottom);
let inputTopAboveSafeArea = (inputTop < safeAreaTop);
let inputTopBelowSafeArea = (inputTop > safeAreaBottom);
let inputBottomWithinSafeArea = (inputBottom >= safeAreaTop && inputBottom <= safeAreaBottom);
let inputBottomBelowSafeArea = (inputBottom > safeAreaBottom);
/**
* @private
*/
export function getScrollData(inputOffsetTop: number, inputOffsetHeight: number, scrollViewDimensions: ContentDimensions, keyboardHeight: number, plaformHeight: number) {
// compute input's Y values relative to the body
let inputTop = (inputOffsetTop + scrollViewDimensions.contentTop - scrollViewDimensions.scrollTop);
let inputBottom = (inputTop + inputOffsetHeight);
/*
Text Input Scroll To Scenarios
---------------------------------------
1) Input top within safe area, bottom within safe area
2) Input top within safe area, bottom below safe area, room to scroll
3) Input top above safe area, bottom within safe area, room to scroll
4) Input top below safe area, no room to scroll, input smaller than safe area
5) Input top within safe area, bottom below safe area, no room to scroll, input smaller than safe area
6) Input top within safe area, bottom 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
*/
// compute the safe area which is the viewable content area when the soft keyboard is up
let safeAreaTop = scrollViewDimensions.contentTop;
let safeAreaHeight = (plaformHeight - keyboardHeight - safeAreaTop) / 2;
let safeAreaBottom = safeAreaTop + safeAreaHeight;
let scrollData = {
scrollAmount: 0,
scrollTo: 0,
scrollPadding: 0,
inputSafeY: 0
};
// figure out if each edge of teh input is within the safe area
let inputTopWithinSafeArea = (inputTop >= safeAreaTop && inputTop <= safeAreaBottom);
let inputTopAboveSafeArea = (inputTop < safeAreaTop);
let inputTopBelowSafeArea = (inputTop > safeAreaBottom);
let inputBottomWithinSafeArea = (inputBottom >= safeAreaTop && inputBottom <= safeAreaBottom);
let inputBottomBelowSafeArea = (inputBottom > safeAreaBottom);
if (inputTopWithinSafeArea && inputBottomWithinSafeArea) {
// Input top within safe area, bottom within safe area
// no need to scroll to a position, it's good as-is
return scrollData;
}
/*
Text Input Scroll To Scenarios
---------------------------------------
1) Input top within safe area, bottom within safe area
2) Input top within safe area, bottom below safe area, room to scroll
3) Input top above safe area, bottom within safe area, room to scroll
4) Input top below safe area, no room to scroll, input smaller than safe area
5) Input top within safe area, bottom below safe area, no room to scroll, input smaller than safe area
6) Input top within safe area, bottom 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
*/
// looks like we'll have to do some auto-scrolling
if (inputTopBelowSafeArea || inputBottomBelowSafeArea) {
// Input top and bottom below safe area
// auto scroll the input up so at least the top of it shows
if (safeAreaHeight > inputOffsetHeight) {
// safe area height is taller than the input height, so we
// can bring it up the input just enough to show the input bottom
scrollData.scrollAmount = Math.round(safeAreaBottom - inputBottom);
} else {
// safe area height is smaller than the input height, so we can
// only scroll it up so the input top is at the top of the safe area
// however the input bottom will be below the safe area
scrollData.scrollAmount = Math.round(safeAreaTop - inputTop);
}
scrollData.inputSafeY = -(inputTop - safeAreaTop) + 4;
} else if (inputTopAboveSafeArea) {
// Input top above safe area
// auto scroll the input down so at least the top of it shows
scrollData.scrollAmount = Math.round(safeAreaTop - inputTop);
scrollData.inputSafeY = (safeAreaTop - inputTop) + 4;
}
// figure out where it should scroll to for the best position to the input
scrollData.scrollTo = (scrollViewDimensions.scrollTop - scrollData.scrollAmount);
if (scrollData.scrollAmount < 0) {
// when auto-scrolling up, there also needs to be enough
// content padding at the bottom of the scroll view
// manually add it if there isn't enough scrollable area
// figure out how many scrollable area is left to scroll up
let availablePadding = (scrollViewDimensions.scrollHeight - scrollViewDimensions.scrollTop) - scrollViewDimensions.contentHeight;
let paddingSpace = availablePadding + scrollData.scrollAmount;
if (paddingSpace < 0) {
// 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';
// window.safeAreaEle.style.height = safeAreaHeight + 'px';
// window.safeAreaEle.innerHTML = `
// <div>scrollTo: ${scrollData.scrollTo}</div>
// <div>scrollAmount: ${scrollData.scrollAmount}</div>
// <div>scrollPadding: ${scrollData.scrollPadding}</div>
// <div>inputSafeY: ${scrollData.inputSafeY}</div>
// <div>scrollHeight: ${scrollViewDimensions.scrollHeight}</div>
// <div>scrollTop: ${scrollViewDimensions.scrollTop}</div>
// <div>contentHeight: ${scrollViewDimensions.contentHeight}</div>
// `;
const scrollData: ScrollData = {
scrollAmount: 0,
scrollTo: 0,
scrollPadding: 0,
inputSafeY: 0
};
if (inputTopWithinSafeArea && inputBottomWithinSafeArea) {
// Input top within safe area, bottom within safe area
// no need to scroll to a position, it's good as-is
return scrollData;
}
// looks like we'll have to do some auto-scrolling
if (inputTopBelowSafeArea || inputBottomBelowSafeArea || inputTopAboveSafeArea) {
// Input top or bottom below safe area
// auto scroll the input up so at least the top of it shows
if (safeAreaHeight > inputOffsetHeight) {
// safe area height is taller than the input height, so we
// can bring up the input just enough to show the input bottom
scrollData.scrollAmount = Math.round(safeAreaBottom - inputBottom);
} else {
// safe area height is smaller than the input height, so we can
// only scroll it up so the input top is at the top of the safe area
// however the input bottom will be below the safe area
scrollData.scrollAmount = Math.round(safeAreaTop - inputTop);
}
scrollData.inputSafeY = -(inputTop - safeAreaTop) + 4;
if (inputTopAboveSafeArea && scrollData.scrollAmount > inputOffsetHeight) {
// the input top is above the safe area and we're already scrolling it into place
// don't let it scroll more than the height of the input
scrollData.scrollAmount = inputOffsetHeight;
}
}
// figure out where it should scroll to for the best position to the input
scrollData.scrollTo = (scrollViewDimensions.scrollTop - scrollData.scrollAmount);
// when auto-scrolling, there also needs to be enough
// content padding at the bottom of the scroll view
// 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;
// var safeAreaEle: HTMLElement = (<any>window).safeAreaEle;
// if (!safeAreaEle) {
// safeAreaEle = (<any>window).safeAreaEle = document.createElement('div');
// 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;';
// document.body.appendChild(safeAreaEle);
// }
// safeAreaEle.style.top = safeAreaTop + 'px';
// safeAreaEle.style.height = safeAreaHeight + 'px';
// safeAreaEle.innerHTML = `
// <div>scrollTo: ${scrollData.scrollTo}</div>
// <div>scrollAmount: ${scrollData.scrollAmount}</div>
// <div>scrollPadding: ${scrollData.scrollPadding}</div>
// <div>inputSafeY: ${scrollData.inputSafeY}</div>
// <div>scrollHeight: ${scrollViewDimensions.scrollHeight}</div>
// <div>scrollTop: ${scrollViewDimensions.scrollTop}</div>
// <div>contentHeight: ${scrollViewDimensions.contentHeight}</div>
// <div>plaformHeight: ${plaformHeight}</div>
// `;
return scrollData;
}
const SCROLL_ASSIST_SPEED = 0.3;
function getScrollAssistDuration(distanceToScroll: number) {
@ -617,3 +623,10 @@ function getScrollAssistDuration(distanceToScroll: number) {
let duration = distanceToScroll / SCROLL_ASSIST_SPEED;
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;
}
.item-label-floating .text-input-ios.cloned-input,
.item-label-stacked .text-input-ios.cloned-input {
top: 30px;
}
// iOS Clear Input Icon
// --------------------------------------------------

View File

@ -147,14 +147,6 @@ $text-input-md-highlight-color-invalid: $text-input-highlight-color-invalid
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
// --------------------------------------------------

View File

@ -119,7 +119,7 @@ input.text-input:-webkit-autofill {
[next-input] {
position: absolute;
bottom: 1px;
bottom: 20px;
padding: 0;
@ -153,18 +153,3 @@ input.text-input:-webkit-autofill {
.input-has-focus.input-has-value .text-input-clear-icon {
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});
}
.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-floating [item-right] {
align-self: flex-end;

View File

@ -62,11 +62,11 @@ export class NativeInput {
// automatically blur input if:
// 1) this input has focus
// 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);
self._unrefBlur = function() {
console.debug('input blurring disabled');
console.debug(`native-input, blurring disabled`);
document.removeEventListener('touchend', docTouchEnd, true);
};
}
@ -99,7 +99,7 @@ export class NativeInput {
beginFocus(shouldFocus: boolean, inputRelativeY: number) {
if (this._relocated !== shouldFocus) {
var focusedInputEle = this.element();
const focusedInputEle = this.element();
if (shouldFocus) {
// 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
// while the native input fakes out the browser by relocating itself
// before it receives the actual focus event
var clonedInputEle = cloneInput(focusedInputEle, 'cloned-focus');
focusedInputEle.parentNode.insertBefore(clonedInputEle, focusedInputEle);
cloneInputComponent(focusedInputEle);
// move the native input to a location safe to receive focus
// 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)
this.setFocus();
if (this._clone) {
focusedInputEle.classList.add('cloned-active');
}
} else {
// should remove the focus
if (this._clone) {
// should remove the cloned node
focusedInputEle.classList.remove('cloned-active');
(<any>focusedInputEle.style)[CSS.transform] = '';
focusedInputEle.style.opacity = '';
removeClone(focusedInputEle, 'cloned-focus');
removeClone(focusedInputEle);
}
}
@ -150,17 +142,14 @@ export class NativeInput {
hideFocus(shouldHideFocus: boolean) {
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) {
let clonedInputEle = cloneInput(focusedInputEle, 'cloned-move');
focusedInputEle.classList.add('cloned-active');
focusedInputEle.parentNode.insertBefore(clonedInputEle, focusedInputEle);
cloneInputComponent(focusedInputEle);
focusedInputEle.style.transform = 'scale(0)';
} else {
focusedInputEle.classList.remove('cloned-active');
removeClone(focusedInputEle, 'cloned-move');
removeClone(focusedInputEle);
}
}
@ -186,26 +175,56 @@ export class NativeInput {
}
function cloneInput(focusedInputEle: any, addCssClass: string) {
let clonedInputEle = focusedInputEle.cloneNode(true);
clonedInputEle.classList.add('cloned-input');
clonedInputEle.classList.add(addCssClass);
clonedInputEle.setAttribute('aria-hidden', true);
clonedInputEle.removeAttribute('aria-labelledby');
clonedInputEle.tabIndex = -1;
clonedInputEle.style.width = (focusedInputEle.offsetWidth + 10) + 'px';
clonedInputEle.style.height = focusedInputEle.offsetHeight + 'px';
clonedInputEle.value = focusedInputEle.value;
return clonedInputEle;
}
function cloneInputComponent(srcNativeInputEle: HTMLInputElement) {
// given a native <input> or <textarea> element
// find its parent wrapping component like <ion-input> or <ion-textarea>
// then clone the entire component
const srcComponentEle = <HTMLElement>srcNativeInputEle.closest('ion-input,ion-textarea');
if (srcComponentEle) {
// DOM READ
const srcTop = srcComponentEle.offsetTop;
const srcLeft = srcComponentEle.offsetLeft;
const srcWidth = srcComponentEle.offsetWidth;
const srcHeight = srcComponentEle.offsetHeight;
function removeClone(focusedInputEle: any, queryCssClass: string) {
let clonedInputEle = focusedInputEle.parentElement.querySelector('.' + queryCssClass);
if (clonedInputEle) {
clonedInputEle.parentNode.removeChild(clonedInputEle);
// 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';
}
srcNativeInputEle.style.transform = 'scale(0)';
}
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('input', () => {
it('should scroll, top and bottom below safe area, no room to scroll', () => {
let inputOffsetTop = 350;
let inputOffsetHeight = 35;
let scrollViewDimensions = {
contentTop: 100,
contentHeight: 700,
scrollTop: 30,
scrollHeight: 700
};
let keyboardHeight = 400;
let platformHeight = 800;
let scrollData = TextInput.getScrollData(inputOffsetTop, inputOffsetHeight, scrollViewDimensions, keyboardHeight, platformHeight);
describe('text input', () => {
describe('getScrollData', () => {
it('should scroll, top and bottom below safe area, no room to scroll', () => {
let inputOffsetTop = 350;
let inputOffsetHeight = 35;
let scrollViewDimensions: ContentDimensions = {
contentTop: 100,
contentHeight: 700,
contentBottom: 0,
contentWidth: 400,
contentLeft: 0,
scrollTop: 30,
scrollHeight: 700,
scrollWidth: 400,
scrollLeft: 0,
};
let keyboardHeight = 400;
let platformHeight = 800;
let scrollData = getScrollData(inputOffsetTop, inputOffsetHeight, scrollViewDimensions, keyboardHeight, platformHeight);
expect(scrollData.scrollAmount).toBe(-205);
expect(scrollData.scrollTo).toBe(235);
expect(scrollData.scrollPadding).toBe(400);
});
it('should scroll, top and bottom below safe area, room to scroll', () => {
let inputOffsetTop = 350;
let inputOffsetHeight = 35;
let scrollViewDimensions: ContentDimensions = {
contentTop: 100,
contentHeight: 700,
contentBottom: 0,
contentWidth: 400,
contentLeft: 0,
scrollTop: 30,
scrollHeight: 1000,
scrollWidth: 400,
scrollLeft: 0,
};
let keyboardHeight = 400;
let platformHeight = 800;
let scrollData = getScrollData(inputOffsetTop, inputOffsetHeight, scrollViewDimensions, keyboardHeight, platformHeight);
expect(scrollData.scrollAmount).toBe(-205);
expect(scrollData.scrollTo).toBe(235);
expect(scrollData.scrollPadding).toBe(400);
});
it('should scroll, top above safe', () => {
// TextInput top within safe area, bottom below safe area, room to scroll
let inputOffsetTop = 100;
let inputOffsetHeight = 33;
let scrollViewDimensions: ContentDimensions = {
contentTop: 100,
contentHeight: 700,
contentBottom: 0,
contentWidth: 400,
contentLeft: 0,
scrollTop: 250,
scrollHeight: 700,
scrollWidth: 400,
scrollLeft: 0,
};
let keyboardHeight = 400;
let platformHeight = 800;
let scrollData = getScrollData(inputOffsetTop, inputOffsetHeight, scrollViewDimensions, keyboardHeight, platformHeight);
expect(scrollData.scrollAmount).toBe(33);
expect(scrollData.scrollTo).toBe(217);
expect(scrollData.scrollPadding).toBe(400);
});
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
let inputOffsetTop = 100;
let inputOffsetHeight = 320;
let scrollViewDimensions: ContentDimensions = {
contentTop: 100,
contentHeight: 700,
contentBottom: 0,
contentWidth: 400,
contentLeft: 0,
scrollTop: 20,
scrollHeight: 700,
scrollWidth: 400,
scrollLeft: 0,
};
let keyboardHeight = 400;
let platformHeight = 800;
let scrollData = getScrollData(inputOffsetTop, inputOffsetHeight, scrollViewDimensions, keyboardHeight, platformHeight);
expect(scrollData.scrollAmount).toBe(-80);
expect(scrollData.scrollTo).toBe(100);
expect(scrollData.scrollPadding).toBe(400);
});
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
let inputOffsetTop = 20;
let inputOffsetHeight = 330;
let scrollViewDimensions: ContentDimensions = {
contentTop: 100,
contentHeight: 700,
contentBottom: 0,
contentWidth: 400,
contentLeft: 0,
scrollTop: 0,
scrollHeight: 700,
scrollWidth: 400,
scrollLeft: 0,
};
let keyboardHeight = 400;
let platformHeight = 800;
let scrollData = getScrollData(inputOffsetTop, inputOffsetHeight, scrollViewDimensions, keyboardHeight, platformHeight);
expect(scrollData.scrollAmount).toBe(-20);
expect(scrollData.scrollTo).toBe(20);
expect(scrollData.scrollPadding).toBe(400);
});
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
let inputOffsetTop = 250;
let inputOffsetHeight = 80; // goes 30px below safe area
let scrollViewDimensions: ContentDimensions = {
contentTop: 100,
contentHeight: 700,
contentBottom: 0,
contentWidth: 400,
contentLeft: 0,
scrollTop: 0,
scrollHeight: 700,
scrollWidth: 400,
scrollLeft: 0,
};
let keyboardHeight = 400;
let platformHeight = 800;
let scrollData = getScrollData(inputOffsetTop, inputOffsetHeight, scrollViewDimensions, keyboardHeight, platformHeight);
expect(scrollData.scrollAmount).toBe(-180);
expect(scrollData.scrollTo).toBe(180);
expect(scrollData.scrollPadding).toBe(400);
});
it('should not scroll, top in safe, bottom in safe', () => {
// TextInput top within safe area, bottom within safe area
let inputOffsetTop = 100;
let inputOffsetHeight = 50;
let scrollViewDimensions: ContentDimensions = {
contentTop: 100,
contentBottom: 0,
contentHeight: 700,
contentWidth: 400,
contentLeft: 0,
scrollTop: 0,
scrollHeight: 700,
scrollWidth: 400,
scrollLeft: 0,
};
let keyboardHeight = 400;
let platformHeight = 800;
let scrollData = getScrollData(inputOffsetTop, inputOffsetHeight, scrollViewDimensions, keyboardHeight, platformHeight);
expect(scrollData.scrollAmount).toBe(0);
});
expect(scrollData.scrollAmount).toBe(-205);
expect(scrollData.scrollTo).toBe(235);
expect(scrollData.scrollPadding).toBe(550);
});
it('should scroll, top and bottom below safe area, room to scroll', () => {
let inputOffsetTop = 350;
let inputOffsetHeight = 35;
let scrollViewDimensions = {
contentTop: 100,
contentHeight: 700,
scrollTop: 30,
scrollHeight: 1000
};
let keyboardHeight = 400;
let platformHeight = 800;
let scrollData = TextInput.getScrollData(inputOffsetTop, inputOffsetHeight, scrollViewDimensions, keyboardHeight, platformHeight);
expect(scrollData.scrollAmount).toBe(-205);
expect(scrollData.scrollTo).toBe(235);
expect(scrollData.scrollPadding).toBe(0);
});
it('should scroll, top above safe', () => {
// TextInput top within safe area, bottom below safe area, room to scroll
let inputOffsetTop = 100;
let inputOffsetHeight = 33;
let scrollViewDimensions = {
contentTop: 100,
contentHeight: 700,
scrollTop: 250,
scrollHeight: 700
};
let keyboardHeight = 400;
let platformHeight = 800;
let scrollData = TextInput.getScrollData(inputOffsetTop, inputOffsetHeight, scrollViewDimensions, keyboardHeight, platformHeight);
expect(scrollData.scrollAmount).toBe(150);
expect(scrollData.scrollTo).toBe(100);
expect(scrollData.scrollPadding).toBe(0);
});
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
let inputOffsetTop = 100;
let inputOffsetHeight = 320;
let scrollViewDimensions = {
contentTop: 100,
contentHeight: 700,
scrollTop: 20,
scrollHeight: 700
};
let keyboardHeight = 400;
let platformHeight = 800;
let scrollData = TextInput.getScrollData(inputOffsetTop, inputOffsetHeight, scrollViewDimensions, keyboardHeight, platformHeight);
expect(scrollData.scrollAmount).toBe(-80);
expect(scrollData.scrollTo).toBe(100);
expect(scrollData.scrollPadding).toBe(550);
});
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
let inputOffsetTop = 20;
let inputOffsetHeight = 330;
let scrollViewDimensions = {
contentTop: 100,
scrollTop: 0,
};
let keyboardHeight = 400;
let platformHeight = 800;
let scrollData = TextInput.getScrollData(inputOffsetTop, inputOffsetHeight, scrollViewDimensions, keyboardHeight, platformHeight);
expect(scrollData.scrollAmount).toBe(-20);
expect(scrollData.scrollTo).toBe(20);
expect(scrollData.scrollPadding).toBe(0);
});
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
let inputOffsetTop = 250;
let inputOffsetHeight = 80; // goes 30px below safe area
let scrollViewDimensions = {
contentTop: 100,
scrollTop: 0,
};
let keyboardHeight = 400;
let platformHeight = 800;
let scrollData = TextInput.getScrollData(inputOffsetTop, inputOffsetHeight, scrollViewDimensions, keyboardHeight, platformHeight);
expect(scrollData.scrollAmount).toBe(-180);
expect(scrollData.scrollTo).toBe(180);
expect(scrollData.scrollPadding).toBe(0);
});
it('should not scroll, top in safe, bottom in safe', () => {
// TextInput top within safe area, bottom within safe area
let inputOffsetTop = 100;
let inputOffsetHeight = 50;
let scrollViewDimensions = {
contentTop: 100,
scrollTop: 0,
keyboardTop: 400
};
let keyboardHeight = 400;
let platformHeight = 800;
let scrollData = TextInput.getScrollData(inputOffsetTop, inputOffsetHeight, scrollViewDimensions, keyboardHeight, platformHeight);
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;
}
.label-ios + ion-input .text-input,
.label-ios + ion-textarea .text-input {
.label-ios + .input .text-input,
.label-ios + .input + .cloned-input {
margin-left: $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';
},
autoFocusAssist: 'immediate',
inputCloning: true,
scrollAssist: true,
hoverCSS: false,
keyboardHeight: 300,

View File

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