fix(input): device support

This commit is contained in:
Manu Mtz.-Almeida
2018-02-27 13:08:33 +01:00
parent 0880ef57e7
commit fe9f8c40ea
18 changed files with 491 additions and 317 deletions

View File

@ -20,6 +20,10 @@ import {
AlertButton, AlertButton,
AlertInput, AlertInput,
} from './components/alert/alert'; } from './components/alert/alert';
import {
App,
FrameworkDelegate as FrameworkDelegate2,
} from '.';
import { import {
ElementRef, ElementRef,
Side, Side,
@ -38,9 +42,6 @@ import {
import { import {
SelectPopoverOption, SelectPopoverOption,
} from './components/select-popover/select-popover'; } from './components/select-popover/select-popover';
import {
FrameworkDelegate as FrameworkDelegate2,
} from '.';
import { import {
DomRenderFn, DomRenderFn,
HeaderFn, HeaderFn,
@ -847,6 +848,36 @@ declare global {
} }
import {
DeviceHacks as IonDeviceHacks
} from './components/device-hacks/device-hacks';
declare global {
interface HTMLIonDeviceHacksElement extends IonDeviceHacks, HTMLStencilElement {
}
var HTMLIonDeviceHacksElement: {
prototype: HTMLIonDeviceHacksElement;
new (): HTMLIonDeviceHacksElement;
};
interface HTMLElementTagNameMap {
"ion-device-hacks": HTMLIonDeviceHacksElement;
}
interface ElementTagNameMap {
"ion-device-hacks": HTMLIonDeviceHacksElement;
}
namespace JSX {
interface IntrinsicElements {
"ion-device-hacks": JSXElements.IonDeviceHacksAttributes;
}
}
namespace JSXElements {
export interface IonDeviceHacksAttributes extends HTMLAttributes {
app?: App;
}
}
}
import { import {
Events as IonEvents Events as IonEvents
} from './components/events/events'; } from './components/events/events';

View File

@ -1,4 +1,4 @@
import { Component, Element, Event, EventEmitter, Listen, Method, Prop, State } from '@stencil/core'; import { Component, Element, Event, EventEmitter, Listen, Method, Prop } from '@stencil/core';
import { Config, NavEvent, OverlayController, PublicNav, PublicViewController } from '../../index'; import { Config, NavEvent, OverlayController, PublicNav, PublicViewController } from '../../index';
import { getOrAppendElement } from '../../utils/helpers'; import { getOrAppendElement } from '../../utils/helpers';
@ -19,20 +19,23 @@ let backButtonActions: BackButtonAction[] = [];
}) })
export class App { export class App {
private isDevice = false;
private deviceHacks = false;
private scrollTime = 0; private scrollTime = 0;
@Element() element: HTMLElement;
@Event() exitApp: EventEmitter<ExitAppEventDetail>;
@State() modeCode: string;
@State() hoverCSS = false;
@Prop({ context: 'config' }) config: Config;
externalNavPromise: void | Promise<any> = null; externalNavPromise: void | Promise<any> = null;
externalNavOccuring = false; externalNavOccuring = false;
didScroll = false; didScroll = false;
@Element() element: HTMLElement;
@Event() exitApp: EventEmitter<ExitAppEventDetail>;
@Prop({ context: 'config' }) config: Config;
componentWillLoad() {
this.isDevice = this.config.getBoolean('isDevice', false);
this.deviceHacks = this.config.getBoolean('deviceHacks', false);
}
/** /**
* Returns the promise set by an external navigation system * Returns the promise set by an external navigation system
@ -74,11 +77,6 @@ export class App {
this.externalNavOccuring = status; this.externalNavOccuring = status;
} }
componentWillLoad() {
this.modeCode = this.config.get('mode');
this.hoverCSS = this.config.getBoolean('hoverCSS', false);
}
@Listen('body:navInit') @Listen('body:navInit')
protected registerRootNav(event: NavEvent) { protected registerRootNav(event: NavEvent) {
rootNavs.set(event.target.getId(), event.target); rootNavs.set(event.target.getId(), event.target);
@ -232,20 +230,23 @@ export class App {
} }
hostData() { hostData() {
const mode = this.config.get('mode');
const hoverCSS = this.config.getBoolean('hoverCSS', false);
return { return {
class: { class: {
[this.modeCode]: true, [mode]: true,
'enable-hover': this.hoverCSS 'enable-hover': hoverCSS
} }
}; };
} }
render() { render() {
const isDevice = true;
return [ return [
isDevice && <ion-tap-click />,
isDevice && <ion-status-tap />,
<ion-platform />, <ion-platform />,
this.deviceHacks && <ion-device-hacks app={this} />,
this.isDevice && <ion-tap-click />,
this.isDevice && <ion-status-tap />,
<slot></slot> <slot></slot>
]; ];
} }

View File

@ -0,0 +1,71 @@
import { Component, Listen, Prop } from "@stencil/core";
import { App, Config } from "../..";
import enableHideCaretOnScroll from "./hacks/hide-caret";
import enableInputBlurring from "./hacks/input-blurring";
@Component({
tag: 'ion-device-hacks',
})
export class DeviceHacks {
private didLoad = false;
private hideCaret = false;
private keyboardHeight = 0;
private hideCaretMap = new WeakMap<HTMLElement, Function>();
@Prop({context: 'config'}) config: Config;
@Prop() app: App;
componentDidLoad() {
this.keyboardHeight = this.config.getNumber('keyboardHeight', 200);
this.hideCaret = this.config.getBoolean('hideCaretOnScroll', true);
const inputBlurring = this.config.getBoolean('inputBlurring', true);
if (inputBlurring) {
enableInputBlurring(this.app);
}
// Input might be already loaded in the DOM before ion-device-hacks did.
// At this point we need to look for all the ion-inputs not registered yet
// and register them.
const inputs = Array.from(document.querySelectorAll('ion-input'));
for (let input of inputs) {
this.registerInput(input);
}
this.didLoad = true;
}
@Listen('body:ionInputDidLoad')
protected onInputDidLoad(event: CustomEvent<HTMLElement>) {
if (this.didLoad) {
this.registerInput(event.detail);
}
}
@Listen('body:ionInputDidUnload')
protected onInputDidUnload(event: CustomEvent<HTMLElement>) {
if (this.didLoad) {
this.unregisterInput(event.detail);
}
}
private registerInput(componentEl: HTMLElement) {
if (this.hideCaret && !this.hideCaretMap.has(componentEl)) {
const rmFn = enableHideCaretOnScroll(
componentEl,
componentEl.querySelector('input'),
componentEl.closest('ion-scroll'),
this.keyboardHeight
);
this.hideCaretMap.set(componentEl, rmFn);
}
}
private unregisterInput(componentEl: HTMLElement) {
if (this.hideCaret) {
const fn = this.hideCaretMap.get(componentEl);
fn && fn();
}
}
}

View File

@ -0,0 +1,129 @@
const RELOCATED_KEY= '$ionRelocated';
export default function enableHideCaretOnScroll(componentEl: HTMLElement, inputEl: HTMLInputElement, scrollEl: HTMLIonScrollElement, keyboardHeight: number) {
if(!scrollEl || !inputEl) {
return () => {};
}
console.debug('Input: enableHideCaretOnScroll');
const scrollHideCaret = (shouldHideCaret: boolean) => {
// console.log('scrollHideCaret', shouldHideCaret)
if (isFocused(inputEl)) {
relocateInput(componentEl, inputEl, keyboardHeight, shouldHideCaret);
}
};
const onBlur = () => relocateInput(componentEl, inputEl, keyboardHeight, false);
const hideCaret = () => scrollHideCaret(true);
const showCaret = () => scrollHideCaret(false);
scrollEl && scrollEl.addEventListener('ionScrollStart', hideCaret);
scrollEl && scrollEl.addEventListener('ionScrollEnd', showCaret);
inputEl.addEventListener('blur', onBlur);
return () => {
scrollEl.removeEventListener('ionScrollStart', hideCaret);
scrollEl.removeEventListener('ionScrollEnd', showCaret);
inputEl.addEventListener('ionBlur', onBlur);
};
}
function removeClone(componentEl: HTMLElement, inputEl: HTMLElement) {
if (componentEl && componentEl.parentElement) {
const clonedInputEles = componentEl.parentElement.querySelectorAll('.cloned-input');
for (let i = 0; i < clonedInputEles.length; i++) {
clonedInputEles[i].parentNode.removeChild(clonedInputEles[i]);
}
componentEl.style.pointerEvents = '';
}
(<any>inputEl.style)['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;
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 = document.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 = document.createElement('input');
clonedInputEl.classList.add(...Array.from(inputEl.classList));
// Object.assign(clonedInputEl, input);
//const clonedInputEl = <HTMLInputElement>inputEl.cloneNode(false);
clonedInputEl.value = inputEl.value;
clonedInputEl.type = inputEl.type;
clonedInputEl.tabIndex = -1;
clonedComponentEle.appendChild(clonedInputEl);
parentElement.appendChild(clonedComponentEle);
componentEl.style.pointerEvents = 'none';
}
inputEl.style.transform = 'scale(0)';
}
function relocateInput(
componentEl: HTMLElement,
inputEle: HTMLInputElement,
_keyboardHeight: number,
shouldRelocate: boolean
) {
console.log('relocateInput',shouldRelocate );
if ((componentEl as any)[RELOCATED_KEY] === shouldRelocate) {
return;
}
console.debug(`native-input, hideCaret, shouldHideCaret: ${shouldRelocate}, input value: ${inputEle.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, inputEle);
// TODO
// const inputRelativeY = getScrollData(componentEl, inputEle, keyboardHeight).scrollAmount;
// const inputRelativeY = 9999;
// fix for #11817
const tx = document.dir === 'rtl' ? 9999 : -9999;
inputEle.style.transform= `translate3d(${tx}px,${0}px,0)`;
// inputEle.style.opacity = '0';
} else {
removeClone(componentEl, inputEle);
}
(componentEl as any)[RELOCATED_KEY] = shouldRelocate;
}
function isFocused(input: HTMLInputElement): boolean {
return input === document.activeElement;
}

View File

@ -0,0 +1,58 @@
import { App } from "../../..";
const SKIP_BLURRING = ['INPUT', 'TEXTAREA', 'ION-INPUT', 'ION-TEXTAREA'];
export default function enableInputBlurring(app: App) {
console.debug('Input: enableInputBlurring');
let focused = true;
function onFocusin() {
focused = true;
}
document.addEventListener('focusin', onFocusin, true);
document.addEventListener('touchend', onTouchend, false);
function onTouchend(ev: any) {
// if app did scroll return early
if (app.didScroll) {
app.didScroll = false;
return;
}
const active = document.activeElement as HTMLElement;
if (!active) {
return;
}
// only blur if the active element is a text-input or a textarea
if (SKIP_BLURRING.indexOf(active.tagName) === -1) {
return;
}
// if the selected target is the active element, do not blur
const tapped = ev.target;
if (tapped === active) {
return;
}
if (SKIP_BLURRING.indexOf(tapped.tagName) >= 0) {
return;
}
// skip if div is a cover
if (tapped.classList.contains('input-cover')) {
return;
}
focused = false;
// TODO: find a better way, why 50ms?
setTimeout(() => {
if (!focused) {
active.blur();
}
}, 50);
}
return () => {
document.removeEventListener('focusin', onFocusin, true);
document.removeEventListener('touchend', onTouchend, false);
};
}

View File

@ -0,0 +1,66 @@
const SCROLL_ASSIST_SPEED = 0.3;
export interface ScrollData {
scrollAmount: number;
scrollPadding: number;
scrollDuration: number;
}
export function calcScrollData(
inputRect: ClientRect,
contentRect: ClientRect,
keyboardHeight: number,
plaformHeight: number
): ScrollData {
// compute input's Y values relative to the body
const inputTop = inputRect.top;
const inputBottom = inputRect.bottom;
// compute safe area
const safeAreaTop = contentRect.top;
const safeAreaBottom = Math.min(contentRect.bottom, plaformHeight - keyboardHeight);
// figure out if each edge of teh input is within the safe area
const distanceToBottom = safeAreaBottom - inputBottom;
const distanceToTop = safeAreaTop - inputTop;
const scrollAmount = Math.round((distanceToBottom < 0 )
? distanceToBottom
: (distanceToTop < 0 )
? distanceToTop
: 0);
const distance = Math.abs(scrollAmount);
const duration = distance / SCROLL_ASSIST_SPEED;
const scrollDuration = Math.min(400, Math.max(150, duration));
return {
scrollAmount,
scrollDuration,
scrollPadding: keyboardHeight,
};
}
export function getScrollData(componentEl: HTMLElement, contentEl: HTMLElement, keyboardHeight: number): ScrollData {
if (!contentEl) {
return {
scrollAmount: 0,
scrollPadding: 0,
scrollDuration: 0,
};
}
// const scrollData = (componentEl as any)[SCROLL_DATA_KEY];
// if (scrollData) {
// return scrollData;
// }
const itemEl = <HTMLElement>componentEl.closest('ion-item,[ion-item]') || componentEl;
const newScrollData = calcScrollData(
itemEl.getBoundingClientRect(),
contentEl.getBoundingClientRect(),
keyboardHeight,
window.innerHeight
);
// (componentEl as any)[SCROLL_DATA_KEY] = newScrollData;
return newScrollData;
}

View File

@ -0,0 +1,35 @@
// export function enableScrollPadding(componentEl: HTMLElement, inputEl: HTMLElement, contentEl: HTMLElement, keyboardHeight: number) {
// console.debug('Input: enableScrollPadding');
// debugger;
// const onFocus = () => {
// const scrollPadding = getScrollData(componentEl, contentEl, keyboardHeight).scrollPadding;
// contentEl.addScrollPadding(scrollPadding);
// content.clearScrollPaddingFocusOut();
// };
// inputEl.addEventListener('focus', onFocus);
// return () => {
// inputEl.removeEventListener('focus', onFocus);
// };
// }
// export function enableScrollMove(
// componentEl: HTMLElement,
// inputEl: HTMLElement,
// contentEl: HTMLIonContentElement,
// keyboardHeight: number
// ) {
// console.debug('Input: enableAutoScroll');
// const onFocus = () => {
// const scrollData = getScrollData(componentEl, contentEl, keyboardHeight);
// if (Math.abs(scrollData.scrollAmount) > 4) {
// contentEl.scrollBy(0, scrollData.scrollAmount);
// }
// };
// inputEl.addEventListener('focus', onFocus);
// return () => {
// inputEl.removeEventListener('focus', onFocus);
// };
// }

View File

@ -0,0 +1,25 @@
# ion-app
<!-- Auto Generated Below -->
## Properties
#### app
## Attributes
#### app
----------------------------------------------
*Built with [StencilJS](https://stenciljs.com/)*

View File

@ -1,278 +0,0 @@
import { assert } from '../../utils/helpers';
import { CSS_PROP } from '../animation-controller/constants';
import { App } from '../..';
const SCROLL_DATA_MAP = new WeakMap<HTMLElement, ScrollData>();
const SCROLL_ASSIST_SPEED = 0.3;
export interface ScrollData {
scrollAmount: number;
scrollPadding: number;
scrollDuration: number;
}
export function calcScrollData(
inputRect: ClientRect,
contentRect: ClientRect,
keyboardHeight: number,
plaformHeight: number
): ScrollData {
// compute input's Y values relative to the body
const inputTop = inputRect.top;
const inputBottom = inputRect.bottom;
// compute safe area
const safeAreaTop = contentRect.top;
const safeAreaBottom = Math.min(contentRect.bottom, plaformHeight - keyboardHeight);
// figure out if each edge of teh input is within the safe area
const distanceToBottom = safeAreaBottom - inputBottom;
const distanceToTop = safeAreaTop - inputTop;
const scrollAmount = Math.round((distanceToBottom < 0 )
? distanceToBottom
: (distanceToTop < 0 )
? distanceToTop
: 0);
const distance = Math.abs(scrollAmount);
const duration = distance / SCROLL_ASSIST_SPEED;
const scrollDuration = Math.min(400, Math.max(150, duration));
return {
scrollAmount,
scrollDuration,
scrollPadding: 0,
};
}
function getScrollData(componentEl: HTMLElement, contentEl: HTMLElement, keyboardHeight: number): ScrollData {
if (!contentEl) {
return {
scrollAmount: 0,
scrollPadding: 0,
scrollDuration: 0,
};
}
const scrollData = SCROLL_DATA_MAP.get(componentEl);
if (scrollData) {
return scrollData;
}
const ele = <HTMLElement>componentEl.closest('ion-item,[ion-item]') || componentEl;
const newScrollData = calcScrollData(
ele.getBoundingClientRect(),
contentEl.getBoundingClientRect(),
keyboardHeight,
window.innerHeight
);
SCROLL_DATA_MAP.set(componentEl, newScrollData);
return newScrollData;
}
export function enableScrollPadding(_componentEl: HTMLElement, inputEl: HTMLElement, _contentEl: HTMLElement, _keyboardHeight: number) {
console.debug('Input: enableScrollPadding');
const onFocus = () => {
// const scrollPadding = getScrollData(componentEl, contentEl, keyboardHeight).scrollPadding;
// content.addScrollPadding(scrollPadding);
// content.clearScrollPaddingFocusOut();
};
inputEl.addEventListener('focus', onFocus);
return () => {
inputEl.removeEventListener('focus', onFocus);
};
}
export function enableScrollMove(
componentEl: HTMLElement,
inputEl: HTMLElement,
contentEl: HTMLIonContentElement,
keyboardHeight: number
) {
console.debug('Input: enableAutoScroll');
const onFocus = () => {
const scrollData = getScrollData(componentEl, contentEl, keyboardHeight);
if (Math.abs(scrollData.scrollAmount) > 4) {
contentEl.scrollBy(0, scrollData.scrollAmount);
}
};
inputEl.addEventListener('focus', onFocus);
return () => {
inputEl.removeEventListener('focus', onFocus);
};
}
const SKIP_BLURRING = ['INPUT', 'TEXTAREA', 'ION-INPUT', 'ION-TEXTAREA'];
export function enableInputBlurring(app: App) {
let focused = true;
function onFocusin() {
focused = true;
}
document.addEventListener('focusin', onFocusin, true);
document.addEventListener('touchend', onTouchend, false);
function onTouchend(ev: any) {
// if app did scroll return early
if (app.didScroll) {
app.didScroll = false;
return;
}
const active = document.activeElement as HTMLElement;
if (!active) {
return;
}
// only blur if the active element is a text-input or a textarea
if (SKIP_BLURRING.indexOf(active.tagName) === -1) {
return;
}
// if the selected target is the active element, do not blur
const tapped = ev.target;
if (tapped === active) {
return;
}
if (SKIP_BLURRING.indexOf(tapped.tagName) >= 0) {
return;
}
// skip if div is a cover
if (tapped.classList.contains('input-cover')) {
return;
}
focused = false;
// TODO: find a better way, why 50ms?
setTimeout(() => {
if (!focused) {
active.blur();
}
}, 50);
}
return () => {
document.removeEventListener('focusin', onFocusin, true);
document.removeEventListener('touchend', onTouchend, false);
};
}
export function enableHideCaretOnScroll(componentEl: HTMLElement, inputEl: HTMLInputElement, scrollEl: HTMLIonScrollElement) {
console.debug('Input: enableHideCaretOnScroll');
function scrollHideCaret(shouldHideCaret: boolean) {
if (isFocused(inputEl)) {
relocateInput(componentEl, inputEl, shouldHideCaret);
}
}
const onBlur = () => relocateInput(componentEl, inputEl, false);
const hideCaret = () => scrollHideCaret(true);
const showCaret = () => scrollHideCaret(false);
scrollEl.addEventListener('ionScrollStart', hideCaret);
scrollEl.addEventListener('ionScrollEnd', showCaret);
inputEl.addEventListener('blur', onBlur);
return () => {
scrollEl.removeEventListener('ionScrollStart', hideCaret);
scrollEl.removeEventListener('ionScrollEnd', showCaret);
inputEl.addEventListener('ionBlur', onBlur);
};
}
function removeClone(componentEl: HTMLElement, nativeInputEl: HTMLElement) {
if (componentEl && componentEl.parentElement) {
const clonedInputEles = componentEl.parentElement.querySelectorAll('.cloned-input');
for (let i = 0; i < clonedInputEles.length; i++) {
clonedInputEles[i].parentNode.removeChild(clonedInputEles[i]);
}
componentEl.style.pointerEvents = '';
}
(<any>nativeInputEl.style)[CSS_PROP.transformProp] = '';
nativeInputEl.style.opacity = '';
}
function cloneInputComponent(componentEle: HTMLElement, nativeInputEle: 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 = componentEle.parentElement;
if (componentEle && parentElement) {
assert(parentElement.querySelector('.cloned-input') === null, 'leaked cloned input');
// DOM READ
const srcTop = componentEle.offsetTop;
const srcLeft = componentEle.offsetLeft;
const srcWidth = componentEle.offsetWidth;
const srcHeight = componentEle.offsetHeight;
// DOM WRITE
// not using deep clone so we don't pull in unnecessary nodes
const clonedComponentEle = <HTMLElement>componentEle.cloneNode(false);
const clonedStyle = clonedComponentEle.style;
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 clonedNativeInputEle = <HTMLInputElement>nativeInputEle.cloneNode(false);
clonedNativeInputEle.value = nativeInputEle.value;
clonedNativeInputEle.tabIndex = -1;
clonedComponentEle.appendChild(clonedNativeInputEle);
parentElement.appendChild(clonedComponentEle);
clonedComponentEle.style.pointerEvents = 'none';
}
(<any>nativeInputEle.style)[CSS_PROP.transformProp] = 'scale(0)';
}
function relocateInput(componentEl: HTMLElement, inputEle: HTMLInputElement, shouldRelocate: boolean) {
if ((componentEl as any)['$ionRelocated'] === shouldRelocate) {
return;
}
console.debug(`native-input, hideCaret, shouldHideCaret: ${shouldRelocate}, input value: ${inputEle.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, inputEle);
// TODO
// const inputRelativeY = this._getScrollData().inputSafeY;
const inputRelativeY = 0;
// fix for #11817
const tx = document.dir === 'rtl' ? 9999 : -9999;
(inputEle.style as any)[CSS_PROP.transformProp] = `translate3d(${tx}px,${inputRelativeY}px,0)`;
inputEle.style.opacity = '0';
} else {
removeClone(componentEl, inputEle);
}
(componentEl as any)['$ionRelocated'] = shouldRelocate;
}
function isFocused(input: HTMLInputElement): boolean {
return input === document.activeElement;
}

View File

@ -85,6 +85,13 @@
} }
} }
// iOS Input After Label
// --------------------------------------------------
.label-ios + .input .native-input,
.label-ios + .input + .cloned-input {
@include margin-horizontal($input-ios-by-label-margin-start, null);
}
// iOS Stacked & Floating Inputs // iOS Stacked & Floating Inputs
// -------------------------------------------------- // --------------------------------------------------
@ -102,14 +109,6 @@
} }
// iOS Input After Label
// --------------------------------------------------
.label-ios + ion-input .native-input,
.label-ios + .input + .cloned-input {
@include margin-horizontal($input-ios-by-label-margin-start, null);
}
// iOS Clear Input Icon // iOS Clear Input Icon
// -------------------------------------------------- // --------------------------------------------------

View File

@ -3,7 +3,7 @@
// Input // Input
// -------------------------------------------------- // --------------------------------------------------
ion-input { .input {
position: relative; position: relative;
display: block; display: block;
@ -15,7 +15,7 @@ ion-input {
// Input Within An Item // Input Within An Item
// -------------------------------------------------- // --------------------------------------------------
.item-input ion-input { .item-input .input {
position: static; position: static;
} }

View File

@ -1,4 +1,4 @@
import { Component, Element, Event, EventEmitter, Prop, Watch } from '@stencil/core'; import { Component, Event, EventEmitter, Prop, Watch, Element } from '@stencil/core';
import { debounceEvent } from '../../utils/helpers'; import { debounceEvent } from '../../utils/helpers';
import { createThemedClasses } from '../../utils/theme'; import { createThemedClasses } from '../../utils/theme';
@ -16,6 +16,8 @@ import { InputComponent } from './input-base';
} }
}) })
export class Input implements InputComponent { export class Input implements InputComponent {
private nativeInput: HTMLInputElement;
mode: string; mode: string;
color: string; color: string;
@ -44,6 +46,16 @@ export class Input implements InputComponent {
*/ */
@Event() ionFocus: EventEmitter; @Event() ionFocus: EventEmitter;
/**
* Emitted when the input has been created.
*/
@Event() ionInputDidLoad: EventEmitter;
/**
* Emitted when the input has been removed.
*/
@Event() ionInputDidUnload: EventEmitter;
/** /**
* 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. * 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.
*/ */
@ -200,7 +212,7 @@ export class Input implements InputComponent {
*/ */
@Watch('value') @Watch('value')
protected valueChanged() { protected valueChanged() {
const inputEl = this.el.querySelector('input'); const inputEl = this.nativeInput;
if (inputEl && inputEl.value !== this.value) { if (inputEl && inputEl.value !== this.value) {
inputEl.value = this.value; inputEl.value = this.value;
} }
@ -214,6 +226,12 @@ export class Input implements InputComponent {
if (this.type === 'password' && this.clearOnEdit !== false) { if (this.type === 'password' && this.clearOnEdit !== false) {
this.clearOnEdit = true; this.clearOnEdit = true;
} }
this.ionInputDidLoad.emit(this.el);
}
componentDidUnload() {
this.nativeInput = null;
this.ionInputDidUnload.emit(this.el);
} }
private emitStyle() { private emitStyle() {
@ -288,7 +306,7 @@ export class Input implements InputComponent {
hasFocus(): boolean { hasFocus(): boolean {
// check if an input has focus or not // check if an input has focus or not
return this.el && (this.el.querySelector(':focus') === this.el.querySelector('input')); return this.nativeInput === document.activeElement;
} }
hasValue(): boolean { hasValue(): boolean {
@ -301,6 +319,7 @@ export class Input implements InputComponent {
return [ return [
<input <input
ref={input => this.nativeInput = input as any}
aria-disabled={this.disabled ? 'true' : false} aria-disabled={this.disabled ? 'true' : false}
accept={this.accept} accept={this.accept}
autoCapitalize={this.autocapitalize} autoCapitalize={this.autocapitalize}

View File

@ -404,6 +404,16 @@ Emitted when the input has focus.
Emitted when the input value has changed. Emitted when the input value has changed.
#### ionInputDidLoad
Emitted when the input has been created.
#### ionInputDidUnload
Emitted when the input has been removed.
#### ionStyle #### ionStyle
Emitted when the styles change. Emitted when the styles change.

View File

@ -70,6 +70,11 @@
<ion-label>Toggle</ion-label> <ion-label>Toggle</ion-label>
<ion-toggle checked slot="end"></ion-toggle> <ion-toggle checked slot="end"></ion-toggle>
</ion-item> </ion-item>
<ion-item>
<ion-label fixed>Type #</ion-label>
<div type="number" value="333" class="input input-md hydrated"><!----><input aria-disabled="false" autocapitalize="none" autocomplete="off" autocorrect="off" autofocus="false" class="native-input native-input-md" spellcheck="false" type="number"><button type="button" class="input-clear-icon" hidden=""></button></div>
</ion-item>
</ion-list> </ion-list>
<div text-center> <div text-center>

View File

@ -69,7 +69,7 @@ ion-item-group {
} }
[vertical-align-top], [vertical-align-top],
ion-input.item { .input.item {
align-items: flex-start; align-items: flex-start;
} }

View File

@ -12,7 +12,7 @@
@include margin(-$list-md-margin-top, null, null, null); @include margin(-$list-md-margin-top, null, null, null);
} }
.list-md > ion-input:last-child::after { .list-md > .input:last-child::after {
@include position-horizontal(0, null); @include position-horizontal(0, null);
} }

View File

@ -33,9 +33,11 @@ export const PLATFORM_CONFIGS: PlatformConfig[] = [
{ {
name: IOS, name: IOS,
settings: { settings: {
mode: IOS, mode: "ios",
tabsHighlight: false, tabsHighlight: false,
statusbarPadding: isCordova(), statusbarPadding: isCordova(),
isDevice: true,
deviceHacks: true,
}, },
isMatch: (url, userAgent) => isPlatformMatch(url, userAgent, IOS, [IPHONE, IPAD, 'ipod'], WINDOWS_PHONE) isMatch: (url, userAgent) => isPlatformMatch(url, userAgent, IOS, [IPHONE, IPAD, 'ipod'], WINDOWS_PHONE)
}, },
@ -43,7 +45,7 @@ export const PLATFORM_CONFIGS: PlatformConfig[] = [
{ {
name: ANDROID, name: ANDROID,
settings: { settings: {
activator: 'ripple', isDevice: true,
mode: 'md', mode: 'md',
}, },
isMatch: (url, userAgent) => isPlatformMatch(url, userAgent, ANDROID, [ANDROID, 'silk'], WINDOWS_PHONE) isMatch: (url, userAgent) => isPlatformMatch(url, userAgent, ANDROID, [ANDROID, 'silk'], WINDOWS_PHONE)

View File

@ -46,6 +46,7 @@ exports.config = {
{ components: ['ion-toggle'] }, { components: ['ion-toggle'] },
{ components: ['ion-toast', 'ion-toast-controller'] }, { components: ['ion-toast', 'ion-toast-controller'] },
{ components: ['ion-tap-click', 'ion-status-tap'] }, { components: ['ion-tap-click', 'ion-status-tap'] },
{ components: ['ion-device-hacks'] },
{ components: ['ion-platform', 'ion-cordova-platform'] }, { components: ['ion-platform', 'ion-cordova-platform'] },
{ components: ['ion-nav-pop'] }, { components: ['ion-nav-pop'] },
{ components: ['ion-hide-when', 'ion-show-when'] }, { components: ['ion-hide-when', 'ion-show-when'] },