From 9fded75502fa95ff0d1e60b672d42d5387252737 Mon Sep 17 00:00:00 2001 From: "Manu Mtz.-Almeida" Date: Tue, 20 Feb 2018 12:38:36 +0100 Subject: [PATCH] fix(gesture/tapclick): ios support --- packages/core/src/components.d.ts | 31 +++++++++++- .../gesture-controller/gesture-controller.ts | 44 +++++++++++------ .../components/gesture-controller/readme.md | 15 +++++- .../core/src/components/gesture/gesture.tsx | 49 +++++++++---------- .../infinite-scroll/infinite-scroll.tsx | 3 -- packages/core/src/components/scroll/readme.md | 10 ---- .../core/src/components/scroll/scroll.tsx | 27 +++++----- .../src/components/tap-click/tap-click.tsx | 38 +++++++------- .../core/src/components/toggle/toggle.scss | 5 ++ .../core/src/components/toggle/toggle.tsx | 7 ++- packages/core/src/index.d.ts | 6 +-- packages/core/stencil.config.js | 2 +- 12 files changed, 139 insertions(+), 98 deletions(-) diff --git a/packages/core/src/components.d.ts b/packages/core/src/components.d.ts index f509d64930..f333349148 100644 --- a/packages/core/src/components.d.ts +++ b/packages/core/src/components.d.ts @@ -1000,6 +1000,36 @@ declare global { } +import { + GestureController as IonGestureController +} from './components/gesture-controller/gesture-controller'; + +declare global { + interface HTMLIonGestureControllerElement extends IonGestureController, HTMLStencilElement { + } + var HTMLIonGestureControllerElement: { + prototype: HTMLIonGestureControllerElement; + new (): HTMLIonGestureControllerElement; + }; + interface HTMLElementTagNameMap { + "ion-gesture-controller": HTMLIonGestureControllerElement; + } + interface ElementTagNameMap { + "ion-gesture-controller": HTMLIonGestureControllerElement; + } + namespace JSX { + interface IntrinsicElements { + "ion-gesture-controller": JSXElements.IonGestureControllerAttributes; + } + } + namespace JSXElements { + export interface IonGestureControllerAttributes extends HTMLAttributes { + + } + } +} + + import { Gesture as IonGesture } from './components/gesture/gesture'; @@ -2660,7 +2690,6 @@ declare global { onionScroll?: ScrollCallback; onionScrollEnd?: ScrollCallback; onionScrollStart?: ScrollCallback; - scrollEvents?: boolean; } } } diff --git a/packages/core/src/components/gesture-controller/gesture-controller.ts b/packages/core/src/components/gesture-controller/gesture-controller.ts index bf4d3a7ad5..52ddc9b2e6 100644 --- a/packages/core/src/components/gesture-controller/gesture-controller.ts +++ b/packages/core/src/components/gesture-controller/gesture-controller.ts @@ -1,3 +1,9 @@ +import { Component, Method, EventEmitter, Event } from "@stencil/core"; + + +@Component({ + tag: 'ion-gesture-controller' +}) export class GestureController { private gestureId = 0; @@ -6,21 +12,21 @@ export class GestureController { private disabledScroll = new Set(); private capturedId: number|null = null; - createGesture(gestureName: string, gesturePriority: number, disableScroll: boolean): GestureDelegate { - return new GestureDelegate(this, this.newID(), gestureName, gesturePriority, disableScroll); + @Event() ionGestureCaptured: EventEmitter; + + @Method() + create(config: GestureConfig): Promise { + return Promise.resolve(new GestureDelegate(this, this.newID(), config.name, config.priority, config.disableScroll)); } - createBlocker(opts: BlockerOptions = {}): BlockerDelegate { + @Method() + createBlocker(opts: BlockerConfig = {}): BlockerDelegate { return new BlockerDelegate(this.newID(), this, opts.disable, !!opts.disableScroll ); } - newID(): number { - return this.gestureId++; - } - start(gestureName: string, id: number, priority: number): boolean { if (!this.canStart(gestureName)) { this.requestedStart.delete(id); @@ -43,6 +49,7 @@ export class GestureController { if (maxPriority === priority) { this.capturedId = id; this.requestedStart.clear(); + this.ionGestureCaptured.emit(gestureName); return true; } requestedStart.delete(id); @@ -121,6 +128,9 @@ export class GestureController { return false; } + private newID(): number { + return this.gestureId++; + } } @@ -180,14 +190,11 @@ export class GestureDelegate { this.release(); this.ctrl = null; } - } export class BlockerDelegate { - blocked = false; - private ctrl: GestureController|null; constructor( @@ -212,7 +219,6 @@ export class BlockerDelegate { if (this.disableScroll) { this.ctrl.disableScroll(this.blockerDelegateId); } - this.blocked = true; } unblock() { @@ -227,7 +233,6 @@ export class BlockerDelegate { if (this.disableScroll) { this.ctrl.enableScroll(this.blockerDelegateId); } - this.blocked = false; } destroy() { @@ -237,13 +242,20 @@ export class BlockerDelegate { } -export interface BlockerOptions { - disableScroll?: boolean; - disable?: string[]; +export interface GestureConfig { + name: string; + priority: number; + disableScroll: boolean; } -export const BLOCK_ALL: BlockerOptions = { +export interface BlockerConfig { + disable?: string[]; + disableScroll?: boolean; +} + + +export const BLOCK_ALL: BlockerConfig = { disable: ['menu-swipe', 'goback-swipe'], disableScroll: true }; diff --git a/packages/core/src/components/gesture-controller/readme.md b/packages/core/src/components/gesture-controller/readme.md index 947addcc38..1b3275d973 100644 --- a/packages/core/src/components/gesture-controller/readme.md +++ b/packages/core/src/components/gesture-controller/readme.md @@ -5,7 +5,20 @@ +## Events + +#### ionGestureCaptured + + +## Methods + +#### create() + + +#### createBlocker() + + ---------------------------------------------- -*Built by [StencilJS](https://stenciljs.com/)* +*Built with [StencilJS](https://stenciljs.com/)* diff --git a/packages/core/src/components/gesture/gesture.tsx b/packages/core/src/components/gesture/gesture.tsx index 0734542c94..5b750f4811 100644 --- a/packages/core/src/components/gesture/gesture.tsx +++ b/packages/core/src/components/gesture/gesture.tsx @@ -1,11 +1,9 @@ -import { Component, Element, Event, EventEmitter, EventListenerEnable, Listen, Prop, Watch } from '@stencil/core'; -import { ElementRef, applyStyles, assert, getElementReference, now, updateDetail } from '../../utils/helpers'; -import { BLOCK_ALL, BlockerDelegate, GestureController, GestureDelegate } from '../gesture-controller/gesture-controller'; +import { Component, Event, EventEmitter, EventListenerEnable, Listen, Prop, Watch } from '@stencil/core'; +import { ElementRef, assert, now, updateDetail } from '../../utils/helpers'; +import { BlockerDelegate, GestureDelegate, BlockerConfig, BLOCK_ALL } from '../gesture-controller/gesture-controller'; import { DomController } from '../../index'; import { PanRecognizer } from './recognizers'; -declare const Ionic: { gesture: GestureController }; - @Component({ tag: 'ion-gesture' @@ -14,7 +12,6 @@ export class Gesture { private detail: GestureDetail = {}; private positions: number[] = []; - private ctrl: GestureController; private gesture: GestureDelegate; private lastTouch = 0; private pan: PanRecognizer; @@ -25,8 +22,7 @@ export class Gesture { private isMoveQueued = false; private blocker: BlockerDelegate; - @Element() private el: HTMLElement; - + @Prop({ connect: 'ion-gesture-controller' }) gestureCtrl: HTMLIonGestureControllerElement; @Prop({ context: 'dom' }) dom: DomController; @Prop({ context: 'enableListener' }) enableListener: EventListenerEnable; @@ -76,13 +72,18 @@ export class Gesture { */ @Event() ionPress: EventEmitter; + componentWillLoad() { + return this.gestureCtrl.create({ + name: this.gestureName, + priority: this.gesturePriority, + disableScroll: this.disableScroll + }).then((gesture) => this.gesture = gesture); + } componentDidLoad() { // in this case, we already know the GestureController and Gesture are already // apart of the same bundle, so it's safe to load it this way // only create one instance of GestureController, and reuse the same one later - this.ctrl = Ionic.gesture = Ionic.gesture || new GestureController(); - this.gesture = this.ctrl.createGesture(this.gestureName, this.gesturePriority, this.disableScroll); const types = this.type.replace(/\s/g, '').toLowerCase().split(','); if (types.indexOf('pan') > -1) { @@ -91,15 +92,9 @@ export class Gesture { this.hasPress = (types.indexOf('press') > -1); this.disabledChanged(this.disabled); - if (this.pan || this.hasPress) { - this.dom.write(() => { - applyStyles(getElementReference(this.el, this.attachTo), GESTURE_INLINE_STYLES); - }); - } if (this.autoBlockAll) { - this.blocker = this.ctrl.createBlocker(BLOCK_ALL); - this.blocker.block(); + this.setBlocker(BLOCK_ALL).then(b => b.block()); } } @@ -116,12 +111,19 @@ export class Gesture { @Watch('block') protected blockChanged(block: string) { + this.setBlocker({ disable: block.split(',')}); + } + + private setBlocker(config: BlockerConfig) { if (this.blocker) { this.blocker.destroy(); } - if (block) { - this.blocker = this.ctrl.createBlocker({ disable: block.split(',')}); + if (config) { + return this.gestureCtrl.componentOnReady() + .then(ctrl => ctrl.createBlocker(config)) + .then(blocker => this.blocker = blocker); } + return Promise.resolve(null); } // DOWN ************************* @@ -459,19 +461,12 @@ export class Gesture { this.blocker = null; } this.gesture && this.gesture.destroy(); - this.ctrl = this.gesture = this.pan = this.detail = this.detail.event = null; + this.gesture = this.pan = this.detail = this.detail.event = null; } } -const GESTURE_INLINE_STYLES = { - 'touch-action': 'none', - 'user-select': 'none', - '-webkit-user-drag': 'none', - '-webkit-tap-highlight-color': 'rgba(0,0,0,0)' -}; - const MOUSE_WAIT = 2500; diff --git a/packages/core/src/components/infinite-scroll/infinite-scroll.tsx b/packages/core/src/components/infinite-scroll/infinite-scroll.tsx index 5f3eeeb897..e5267766eb 100644 --- a/packages/core/src/components/infinite-scroll/infinite-scroll.tsx +++ b/packages/core/src/components/infinite-scroll/infinite-scroll.tsx @@ -221,9 +221,6 @@ export class InfiniteScroll { private enableScrollEvents(shouldListen: boolean) { if (this.scrollEl) { - if (shouldListen) { - this.scrollEl.scrollEvents = true; - } this.enableListener(this, 'ionScroll', shouldListen, this.scrollEl); } } diff --git a/packages/core/src/components/scroll/readme.md b/packages/core/src/components/scroll/readme.md index 763676ac0d..431524a16a 100644 --- a/packages/core/src/components/scroll/readme.md +++ b/packages/core/src/components/scroll/readme.md @@ -22,11 +22,6 @@ -#### scrollEvents - -boolean - - ## Attributes #### onion-scroll @@ -44,11 +39,6 @@ boolean -#### scroll-events - -boolean - - ## Events #### ionScroll diff --git a/packages/core/src/components/scroll/scroll.tsx b/packages/core/src/components/scroll/scroll.tsx index 282d438cff..adcbf15228 100644 --- a/packages/core/src/components/scroll/scroll.tsx +++ b/packages/core/src/components/scroll/scroll.tsx @@ -1,8 +1,6 @@ -import { Component, Element, Event, EventEmitter, EventListenerEnable, Listen, Method, Prop, Watch } from '@stencil/core'; +import { Component, Element, Event, EventEmitter, EventListenerEnable, Listen, Method, Prop } from '@stencil/core'; import { Config, DomController, GestureDetail } from '../../index'; -import { GestureController, GestureDelegate } from '../gesture-controller/gesture-controller'; - -declare const Ionic: { gesture: GestureController }; +import { GestureDelegate } from '../gesture-controller/gesture-controller'; @Component({ @@ -20,6 +18,7 @@ export class Scroll { @Element() private el: HTMLElement; + @Prop({ connect: 'ion-gesture-controller'}) gestureCtrl: HTMLIonGestureControllerElement; @Prop({ context: 'config'}) config: Config; @Prop({ context: 'enableListener'}) enableListener: EventListenerEnable; @Prop({ context: 'dom' }) dom: DomController; @@ -44,22 +43,19 @@ export class Scroll { */ @Event() ionScrollEnd: EventEmitter; - - @Prop() scrollEvents = false; - @Watch('scrollEvents') - scrollChanged(enabled: boolean) { - this.enableListener(this, 'scroll', enabled); + componentWillLoad() { + return this.gestureCtrl.create({ + name: 'scroll', + priority: 100, + disableScroll: false, + }).then(gesture => this.gesture = gesture); } componentDidLoad() { if (this.isServer) { return; } - - const gestureCtrl = Ionic.gesture = Ionic.gesture || new GestureController(); - this.gesture = gestureCtrl.createGesture('scroll', 100, false); this.app = this.el.closest('ion-app') as HTMLIonAppElement; - this.scrollChanged(this.scrollEvents); } componentDidUnload() { @@ -69,7 +65,7 @@ export class Scroll { // Native Scroll ************************* - @Listen('scroll', { passive: true, enabled: false }) + @Listen('scroll', { passive: true }) onNativeScroll() { if (!this.queued) { this.queued = true; @@ -219,6 +215,7 @@ export class Scroll { if (this.onionScrollStart) { this.onionScrollStart(detail); } + this.gesture.capture(); this.ionScrollStart.emit(detail); } detail.deltaY = (detail.scrollTop - detail.startY); @@ -277,6 +274,8 @@ export class Scroll { detail.timeStamp = timeStamp; // emit that the scroll has ended + this.gesture.release(); + if (this.onionScrollEnd) { this.onionScrollEnd(detail); } diff --git a/packages/core/src/components/tap-click/tap-click.tsx b/packages/core/src/components/tap-click/tap-click.tsx index 1a559ce71c..8c47c16e06 100644 --- a/packages/core/src/components/tap-click/tap-click.tsx +++ b/packages/core/src/components/tap-click/tap-click.tsx @@ -1,8 +1,5 @@ import { Component, Element, EventListenerEnable, Listen, Prop } from '@stencil/core'; import { now, pointerCoordX, pointerCoordY } from '../../utils/helpers'; -import { GestureController } from '../gesture-controller/gesture-controller'; - -declare const Ionic: { gesture: GestureController }; @Component({ @@ -13,8 +10,7 @@ export class TapClick { private app: HTMLIonAppElement; private lastTouch = -MOUSE_WAIT*10; private lastActivated = 0; - - private gestureCtrl: GestureController; + private cancelled = false; private activatableEle: HTMLElement | null; private activeDefer: any; @@ -30,40 +26,37 @@ export class TapClick { if (this.isServer) { return; } - this.gestureCtrl = Ionic.gesture = Ionic.gesture || new GestureController(); - this.app = this.el.closest('ion-app') as HTMLIonAppElement; } - @Listen('document:click', {passive: false, capture: true}) + @Listen('body:click', {passive: false, capture: true}) onBodyClick(ev: Event) { - if (this.shouldCancel()) { - debugger; + if (this.cancelled || this.shouldCancel()) { ev.preventDefault(); ev.stopPropagation(); } } // Touch Events - @Listen('document:touchstart', { passive: true }) + @Listen('document:touchstart', { passive: true, capture: true }) onTouchStart(ev: TouchEvent) { this.lastTouch = now(ev); this.pointerDown(ev); } - @Listen('document:touchcancel', { passive: true }) + @Listen('document:touchcancel', { passive: true, capture: true }) onTouchCancel(ev: TouchEvent) { this.lastTouch = now(ev); this.pointerUp(ev); } - @Listen('document:touchend', { passive: true }) + @Listen('document:touchend', { passive: false, capture: true }) onTouchEnd(ev: TouchEvent) { this.lastTouch = now(ev); this.pointerUp(ev); } - @Listen('document:mousedown', { passive: true }) + @Listen('document:mousedown', { passive: true, capture: true }) onMouseDown(ev: MouseEvent) { const t = now(ev) - MOUSE_WAIT; if (this.lastTouch < t) { @@ -71,7 +64,7 @@ export class TapClick { } } - @Listen('document:mouseup', { passive: true }) + @Listen('document:mouseup', { passive: false, capture: true }) onMouseUp(ev: TouchEvent) { const t = now(ev) - MOUSE_WAIT; if (this.lastTouch < t) { @@ -80,25 +73,32 @@ export class TapClick { } @Listen('body:ionScrollStart') - scrollStarted() { + @Listen('body:ionGestureCaptured') + cancelActive() { clearTimeout(this.activeDefer); if (this.activatableEle) { this.removeActivated(false); this.activatableEle = null; } + this.cancelled = true; } private pointerDown(ev: any) { if (this.activatableEle) { return; } - if (!this.shouldCancel()) { + this.cancelled = this.shouldCancel(); + + if (!this.cancelled) { this.setActivatedElement(getActivatableTarget(ev.target), ev); } } private pointerUp(ev: UIEvent) { this.setActivatedElement(null, ev); + if (this.cancelled) { + ev.preventDefault(); + } } private setActivatedElement(el: HTMLElement | null, ev: UIEvent) { @@ -175,10 +175,6 @@ export class TapClick { console.debug('click prevent: appDisabled'); return true; } - if (this.gestureCtrl.isCaptured()) { - console.debug('click prevent: tap-click (gesture is captured)'); - return true; - } return false; } } diff --git a/packages/core/src/components/toggle/toggle.scss b/packages/core/src/components/toggle/toggle.scss index b1484c18fe..2e860e336c 100644 --- a/packages/core/src/components/toggle/toggle.scss +++ b/packages/core/src/components/toggle/toggle.scss @@ -7,6 +7,9 @@ ion-toggle { display: inline-block; contain: content; + + touch-action: none; + user-select: none; } ion-toggle ion-gesture { @@ -52,8 +55,10 @@ ion-toggle input { background: transparent; cursor: pointer; + border: 0; pointer-events: none; + // touch-action: pan-x; -webkit-appearance: none; -moz-appearance: none; diff --git a/packages/core/src/components/toggle/toggle.tsx b/packages/core/src/components/toggle/toggle.tsx index 578afa11a7..6b8f38266f 100644 --- a/packages/core/src/components/toggle/toggle.tsx +++ b/packages/core/src/components/toggle/toggle.tsx @@ -16,13 +16,13 @@ import { debounce } from '../../utils/helpers'; } }) export class Toggle implements CheckboxInput { + private didLoad: boolean; private gestureConfig: any; private inputId: string; private nativeInput: HTMLInputElement; private pivotX: number; - @State() activated = false; @State() keyFocus: boolean; @@ -88,6 +88,7 @@ export class Toggle implements CheckboxInput { 'onMove': this.onDragMove.bind(this), 'onEnd': this.onDragEnd.bind(this), 'gestureName': 'toggle', + 'passive': false, 'gesturePriority': 30, 'type': 'pan', 'direction': 'x', @@ -141,6 +142,10 @@ export class Toggle implements CheckboxInput { private onDragStart(detail: GestureDetail) { this.pivotX = detail.currentX; this.activated = true; + + // touch-action does not work in iOS + detail.event.preventDefault(); + return true; } private onDragMove(detail: GestureDetail) { diff --git a/packages/core/src/index.d.ts b/packages/core/src/index.d.ts index d1c91bfd96..dcc28951fc 100644 --- a/packages/core/src/index.d.ts +++ b/packages/core/src/index.d.ts @@ -36,9 +36,9 @@ export { PanRecognizer } from './components/gesture/recognizers'; export { BLOCK_ALL, BlockerDelegate, - BlockerOptions, - GestureController, - GestureDelegate + GestureDelegate, + BlockerConfig, + GestureConfig, } from './components/gesture-controller/gesture-controller'; export { Grid } from './components/grid/grid'; export { Header } from './components/header/header'; diff --git a/packages/core/stencil.config.js b/packages/core/stencil.config.js index 9a40e76782..df4e62265d 100644 --- a/packages/core/stencil.config.js +++ b/packages/core/stencil.config.js @@ -20,7 +20,7 @@ exports.config = { { components: ['ion-datetime', 'ion-picker', 'ion-picker-column', 'ion-picker-controller'] }, { components: ['ion-events'] }, { components: ['ion-fab', 'ion-fab-button', 'ion-fab-list'] }, - { components: ['ion-gesture'] }, + { components: ['ion-gesture', 'ion-gesture-controller'] }, { components: ['ion-grid', 'ion-row', 'ion-col'] }, { components: ['ion-item', 'ion-item-divider', 'ion-item-group', 'ion-label', 'ion-list', 'ion-list-header', 'ion-skeleton-text'] }, { components: ['ion-item-sliding', 'ion-item-options', 'ion-item-option'] },