diff --git a/src/components/input/input-base.ts b/src/components/input/input-base.ts index e11d98b1acc..7b563d398be 100644 --- a/src/components/input/input-base.ts +++ b/src/components/input/input-base.ts @@ -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 = 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); - - /* - 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 - */ - - let 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) { - // 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); +/** + * @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); + + // 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; + + // 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); + + /* + 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 + */ - scrollData.inputSafeY = (safeAreaTop - inputTop) + 4; - } + const scrollData: ScrollData = { + scrollAmount: 0, + scrollTo: 0, + scrollPadding: 0, + inputSafeY: 0 + }; - // figure out where it should scroll to for the best position to the input - scrollData.scrollTo = (scrollViewDimensions.scrollTop - scrollData.scrollAmount); + 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; + } - 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 + // 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 - // figure out how many scrollable area is left to scroll up - let availablePadding = (scrollViewDimensions.scrollHeight - scrollViewDimensions.scrollTop) - scrollViewDimensions.contentHeight; + 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); - 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); - } + } 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); } - // 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 = ` - //
scrollTo: ${scrollData.scrollTo}
- //
scrollAmount: ${scrollData.scrollAmount}
- //
scrollPadding: ${scrollData.scrollPadding}
- //
inputSafeY: ${scrollData.inputSafeY}
- //
scrollHeight: ${scrollViewDimensions.scrollHeight}
- //
scrollTop: ${scrollViewDimensions.scrollTop}
- //
contentHeight: ${scrollViewDimensions.contentHeight}
- // `; + scrollData.inputSafeY = -(inputTop - safeAreaTop) + 4; - return scrollData; + 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 = (window).safeAreaEle; + // if (!safeAreaEle) { + // safeAreaEle = (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 = ` + //
scrollTo: ${scrollData.scrollTo}
+ //
scrollAmount: ${scrollData.scrollAmount}
+ //
scrollPadding: ${scrollData.scrollPadding}
+ //
inputSafeY: ${scrollData.inputSafeY}
+ //
scrollHeight: ${scrollViewDimensions.scrollHeight}
+ //
scrollTop: ${scrollViewDimensions.scrollTop}
+ //
contentHeight: ${scrollViewDimensions.contentHeight}
+ //
plaformHeight: ${plaformHeight}
+ // `; + + 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; +} diff --git a/src/components/input/input.ios.scss b/src/components/input/input.ios.scss index 3bbedcae7c1..fb7b711975d 100644 --- a/src/components/input/input.ios.scss +++ b/src/components/input/input.ios.scss @@ -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 // -------------------------------------------------- diff --git a/src/components/input/input.md.scss b/src/components/input/input.md.scss index ae46558ae17..8caebfce061 100644 --- a/src/components/input/input.md.scss +++ b/src/components/input/input.md.scss @@ -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 // -------------------------------------------------- diff --git a/src/components/input/input.scss b/src/components/input/input.scss index ef6ddf7dc27..4505ad2746f 100644 --- a/src/components/input/input.scss +++ b/src/components/input/input.scss @@ -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; -} diff --git a/src/components/input/input.wp.scss b/src/components/input/input.wp.scss index 01061ff1ab0..091f3177d97 100644 --- a/src/components/input/input.wp.scss +++ b/src/components/input/input.wp.scss @@ -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; diff --git a/src/components/input/native-input.ts b/src/components/input/native-input.ts index d34f25e9920..32db987b04a 100644 --- a/src/components/input/native-input.ts +++ b/src/components/input/native-input.ts @@ -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'); - (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,28 +175,58 @@ 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 or