From 9510a2bb3b3933012e38f1ca680d474d963fcd0e Mon Sep 17 00:00:00 2001 From: "Manu Mtz.-Almeida" Date: Fri, 18 Nov 2016 21:25:04 +0100 Subject: [PATCH] fix(activator): cancel remove .activated timeout --- src/components/tap-click/activator-base.ts | 36 ++++ src/components/tap-click/activator.ts | 74 ++++---- src/components/tap-click/ripple.ts | 58 +++--- src/components/tap-click/tap-click.ts | 3 +- .../tap-click/test/activator.spec.ts | 170 ++++++++++++++++++ src/util/dom.ts | 10 +- 6 files changed, 290 insertions(+), 61 deletions(-) create mode 100644 src/components/tap-click/activator-base.ts create mode 100644 src/components/tap-click/test/activator.spec.ts diff --git a/src/components/tap-click/activator-base.ts b/src/components/tap-click/activator-base.ts new file mode 100644 index 0000000000..43d8e3da47 --- /dev/null +++ b/src/components/tap-click/activator-base.ts @@ -0,0 +1,36 @@ +import { PointerCoordinates } from '../../util/dom'; + +export abstract class ActivatorBase { + + abstract clickAction(ev, activatableEle: HTMLElement, startCoord: PointerCoordinates); + + abstract downAction(ev: UIEvent, activatableEle: HTMLElement, startCoord: PointerCoordinates); + + abstract upAction(ev: UIEvent, activatableEle: HTMLElement, startCoord: PointerCoordinates); + + abstract clearState(); +} + +export function isActivatedDisabled(ev: any, activatableEle: any): boolean { + if (!activatableEle || !activatableEle.parentNode) { + return true; + } + if (!ev) { + return false; + } + if (ev.defaultPrevented) { + return true; + } + + let targetEle = ev.target; + for (let i = 0; i < 4; i++) { + if (!targetEle) { + break; + } + if (targetEle.hasAttribute('disable-activated')) { + return true; + } + targetEle = targetEle.parentElement; + } + return false; +} diff --git a/src/components/tap-click/activator.ts b/src/components/tap-click/activator.ts index 8c7c93d0ae..6a3f33c0b7 100644 --- a/src/components/tap-click/activator.ts +++ b/src/components/tap-click/activator.ts @@ -1,13 +1,17 @@ import { App } from '../app/app'; import { Config } from '../../config/config'; import { PointerCoordinates, nativeTimeout, rafFrames } from '../../util/dom'; +import { ActivatorBase, isActivatedDisabled } from './activator-base'; -export class Activator { - protected _css: string; +export class Activator implements ActivatorBase { protected _queue: HTMLElement[] = []; protected _active: HTMLElement[] = []; protected _activeRafDefer: Function; + protected _clearRafDefer: Function; + _css: string; + activatedDelay = ADD_ACTIVATED_DEFERS; + clearDelay = CLEAR_STATE_DEFERS; constructor(protected app: App, config: Config) { this._css = config.get('activatedClass') || 'activated'; @@ -15,7 +19,8 @@ export class Activator { clickAction(ev: UIEvent, activatableEle: HTMLElement, startCoord: PointerCoordinates) { // a click happened, so immediately deactive all activated elements - this._clearDeferred(); + this._scheduleClear(); + this._queue.length = 0; for (var i = 0; i < this._active.length; i++) { @@ -32,21 +37,22 @@ export class Activator { downAction(ev: UIEvent, activatableEle: HTMLElement, startCoord: PointerCoordinates) { // the user just pressed down - if (this.disableActivated(ev)) { + if (isActivatedDisabled(ev, activatableEle)) { return; } + this.unscheduleClear(); + this.deactivate(); + // queue to have this element activated this._queue.push(activatableEle); - this._activeRafDefer = rafFrames(6, () => { + this._activeRafDefer = rafFrames(this.activatedDelay, () => { let activatableEle: HTMLElement; for (let i = 0; i < this._queue.length; i++) { activatableEle = this._queue[i]; - if (activatableEle && activatableEle.parentNode) { - this._active.push(activatableEle); - activatableEle.classList.add(this._css); - } + this._active.push(activatableEle); + activatableEle.classList.add(this._css); } this._queue.length = 0; this._clearDeferred(); @@ -55,16 +61,28 @@ export class Activator { // the user was pressing down, then just let up upAction(ev: UIEvent, activatableEle: HTMLElement, startCoord: PointerCoordinates) { - this._clearDeferred(); + this._scheduleClear(); + } - rafFrames(CLEAR_STATE_DEFERS, () => { + _scheduleClear() { + if (this._clearRafDefer) { + return; + } + this._clearRafDefer = rafFrames(this.clearDelay, () => { this.clearState(); + this._clearRafDefer = null; }); } + unscheduleClear() { + if (this._clearRafDefer) { + this._clearRafDefer(); + this._clearRafDefer = null; + } + } + // all states should return to normal clearState() { - if (!this.app.isEnabled()) { // the app is actively disabled, so don't bother deactivating anything. // this makes it easier on the GPU so it doesn't have to redraw any @@ -85,12 +103,10 @@ export class Activator { this._queue.length = 0; - rafFrames(2, () => { - for (var i = 0; i < this._active.length; i++) { - this._active[i].classList.remove(this._css); - } - this._active.length = 0; - }); + for (var i = 0; i < this._active.length; i++) { + this._active[i].classList.remove(this._css); + } + this._active.length = 0; } _clearDeferred() { @@ -100,25 +116,7 @@ export class Activator { this._activeRafDefer = null; } } - - disableActivated(ev: any) { - if (ev.defaultPrevented) { - return true; - } - - let targetEle = ev.target; - for (let i = 0; i < 4; i++) { - if (!targetEle) { - break; - } - if (targetEle.hasAttribute('disable-activated')) { - return true; - } - targetEle = targetEle.parentElement; - } - return false; - } - } -const CLEAR_STATE_DEFERS = 5; +const ADD_ACTIVATED_DEFERS = 6; +const CLEAR_STATE_DEFERS = 6; diff --git a/src/components/tap-click/ripple.ts b/src/components/tap-click/ripple.ts index 7ef673d750..f13e5fed1b 100644 --- a/src/components/tap-click/ripple.ts +++ b/src/components/tap-click/ripple.ts @@ -1,3 +1,4 @@ +import { ActivatorBase, isActivatedDisabled } from './activator-base'; import { Activator } from './activator'; import { App } from '../app/app'; import { PointerCoordinates, CSS, hasPointerMoved, pointerCoord, rafFrames } from '../../util/dom'; @@ -7,19 +8,47 @@ import { Config } from '../../config/config'; /** * @private */ -export class RippleActivator extends Activator { +export class RippleActivator implements ActivatorBase { + protected _queue: HTMLElement[] = []; + protected _active: HTMLElement[] = []; + protected highlight: Activator; constructor(app: App, config: Config) { - super(app, config); + this.highlight = new Activator(app, config); + this.highlight.activatedDelay = 0; } clickAction(ev: UIEvent, activatableEle: HTMLElement, startCoord: PointerCoordinates) { - this.downAction(ev, activatableEle, startCoord); - this.upAction(ev, activatableEle, startCoord); + // Highlight + this.highlight && this.highlight.clickAction(ev, activatableEle, startCoord); + + // Ripple + this._clickAction(ev, activatableEle, startCoord); } downAction(ev: UIEvent, activatableEle: HTMLElement, startCoord: PointerCoordinates) { - if (this.disableActivated(ev) || !activatableEle || !activatableEle.parentNode) { + // Highlight + this.highlight && this.highlight.downAction(ev, activatableEle, startCoord); + + // Ripple + this._downAction(ev, activatableEle, startCoord); + } + + upAction(ev: UIEvent, activatableEle: HTMLElement, startCoord: PointerCoordinates) { + // Highlight + this.highlight && this.highlight.upAction(ev, activatableEle, startCoord); + + // Ripple + this._upAction(ev, activatableEle, startCoord); + } + + clearState() { + // Highlight + this.highlight && this.highlight.clearState(); + } + + _downAction(ev: UIEvent, activatableEle: HTMLElement, startCoord: PointerCoordinates) { + if (isActivatedDisabled(ev, activatableEle)) { return; } @@ -38,12 +67,9 @@ export class RippleActivator extends Activator { break; } } - - // DOM WRITE - activatableEle.classList.add(this._css); } - upAction(ev: UIEvent, activatableEle: HTMLElement, startCoord: PointerCoordinates) { + _upAction(ev: UIEvent, activatableEle: HTMLElement, startCoord: PointerCoordinates) { if (!hasPointerMoved(6, startCoord, pointerCoord(ev))) { let i = activatableEle.childElementCount; while (i--) { @@ -55,8 +81,10 @@ export class RippleActivator extends Activator { } } } + } - super.upAction(ev, activatableEle, startCoord); + _clickAction(ev: UIEvent, activatableEle: HTMLElement, startCoord: PointerCoordinates) { + // NOTHING } startRippleEffect(rippleEle: any, activatableEle: HTMLElement, startCoord: PointerCoordinates) { @@ -107,16 +135,6 @@ export class RippleActivator extends Activator { }); } - deactivate() { - rafFrames(2, () => { - for (var i = 0; i < this._active.length; i++) { - // DOM WRITE - this._active[i].classList.remove(this._css); - } - this._active.length = 0; - }); - } - } const TOUCH_DOWN_ACCEL = 300; diff --git a/src/components/tap-click/tap-click.ts b/src/components/tap-click/tap-click.ts index 0b81f3ed27..8ef5baea89 100644 --- a/src/components/tap-click/tap-click.ts +++ b/src/components/tap-click/tap-click.ts @@ -1,5 +1,6 @@ import { Injectable, NgZone } from '@angular/core'; +import { ActivatorBase } from './activator-base'; import { Activator } from './activator'; import { App } from '../app/app'; import { Config } from '../../config/config'; @@ -15,7 +16,7 @@ import { UIEventManager, PointerEvents, PointerEventType } from '../../util/ui-e export class TapClick { private disableClick: number = 0; private usePolyfill: boolean; - private activator: Activator; + private activator: ActivatorBase; private startCoord: any; private events: UIEventManager = new UIEventManager(false); private pointerEvents: PointerEvents; diff --git a/src/components/tap-click/test/activator.spec.ts b/src/components/tap-click/test/activator.spec.ts new file mode 100644 index 0000000000..720fd423e4 --- /dev/null +++ b/src/components/tap-click/test/activator.spec.ts @@ -0,0 +1,170 @@ +import { Activator } from '../activator'; +import { Config } from '../../../config/config'; + +describe('Activator', () => { + + it('should config css', () => { + let activator = mockActivator(true, null); + expect(activator._css).toEqual('activated'); + + activator = mockActivator(true, 'enabled'); + expect(activator._css).toEqual('enabled'); + }); + + it('should sync add/remove css class', () => { + const {ev, ele, pos} = testValues(); + + let activator = mockActivator(true, 'activo'); + activator.activatedDelay = 0; + activator.clearDelay = 0; + + activator.downAction(ev, ele, pos); + expect(ele.classList.contains('activo')).toBeTruthy(); + + activator.upAction(ev, ele, pos); + expect(ele.classList.contains('activo')).toBeFalsy(); + + activator.downAction(ev, ele, pos); + expect(ele.classList.contains('activo')).toBeTruthy(); + + activator.upAction(null, null, pos); + expect(ele.classList.contains('activo')).toBeFalsy(); + + activator.downAction(ev, ele, pos); + expect(ele.classList.contains('activo')).toBeTruthy(); + + activator.clickAction(null, null, pos); + expect(ele.classList.contains('activo')).toBeFalsy(); + }); + + it('should async down/up/click action (normal flow)', (done) => { + const {ev, ele, pos} = testValues(); + + let activator = mockActivator(true, null); + activator.activatedDelay = 6; + activator.clearDelay = 6; + + activator.downAction(ev, ele, pos); + expect(ele.classList.contains('activated')).toBeFalsy(); + + // upAction + setTimeout(() => { + expect(ele.classList.contains('activated')).toBeTruthy(); + activator.upAction(ev, ele, pos); + expect(ele.classList.contains('activated')).toBeTruthy(); + }, (6 + 2) * 16); + + // clickAction + setTimeout(() => { + expect(ele.classList.contains('activated')).toBeTruthy(); + activator.clickAction(ev, ele, pos); + expect(ele.classList.contains('activated')).toBeTruthy(); + }, (6 + 2 + 2) * 16); + + // Read final results + setTimeout(() => { + expect(ele.classList.contains('activated')).toBeFalsy(); + done(); + }, (6 + 6 + 4) * 16); + }, 10000); + + it('should async down then down', (done) => { + const {ev, ele, pos} = testValues(); + + let activator = mockActivator(true, null); + activator.activatedDelay = 6; + activator.clearDelay = 6; + + activator.downAction(ev, ele, pos); + + setTimeout(() => { + expect(ele.classList.contains('activated')).toBeTruthy(); + activator.downAction(ev, ele, pos); + expect(ele.classList.contains('activated')).toBeFalsy(); + }, (6 + 2) * 16); + + setTimeout(() => { + expect(ele.classList.contains('activated')).toBeTruthy(); + done(); + }, (6 + 6 + 4) * 16); + }, 10000); + + it('should async down then click', (done) => { + const {ev, ele, pos} = testValues(); + + let activator = mockActivator(true, null); + activator.activatedDelay = 6; + activator.clearDelay = 6; + + activator.downAction(ev, ele, pos); + + setTimeout(() => { + expect(ele.classList.contains('activated')).toBeFalsy(); + activator.clickAction(ev, ele, pos); + expect(ele.classList.contains('activated')).toBeTruthy(); + }, 16); + + setTimeout(() => { + expect(ele.classList.contains('activated')).toBeFalsy(); + }, (6 + 3) * 16); + + // Check the value is stable + setTimeout(() => { + expect(ele.classList.contains('activated')).toBeFalsy(); + done(); + }, 20 * 16); + }, 10000); + + it('should async down then click then down (fast clicking)', (done) => { + const {ev, ele, pos} = testValues(); + + let activator = mockActivator(true, null); + activator.activatedDelay = 6; + activator.clearDelay = 6; + + activator.downAction(ev, ele, pos); + + setTimeout(() => { + expect(ele.classList.contains('activated')).toBeFalsy(); + activator.clickAction(ev, ele, pos); + expect(ele.classList.contains('activated')).toBeTruthy(); + }, 16); + + setTimeout(() => { + expect(ele.classList.contains('activated')).toBeTruthy(); + activator.downAction(ev, ele, pos); + expect(ele.classList.contains('activated')).toBeFalsy(); + }, 16 * 2); + + setTimeout(() => { + expect(ele.classList.contains('activated')).toBeTruthy(); + done(); + }, 16 * 12); + + }, 10000); + + +}); + +function testValues() { + let parent = document.createElement('div'); + let ele = document.createElement('a'); + parent.appendChild(ele); + return { + ev: null, + ele: ele, + pos: { x: 0, y: 0 }, + }; +} + + +function mockActivator(appEnabled: boolean, css: string) { + let app = { + isEnabled: () => { return appEnabled; }, + }; + let config = new Config(); + if (css) { + config.set('activatedClass', css); + } + return new Activator(app, config); +} diff --git a/src/util/dom.ts b/src/util/dom.ts index 7161333534..439e50d000 100644 --- a/src/util/dom.ts +++ b/src/util/dom.ts @@ -48,7 +48,10 @@ export function rafFrames(framesToWait: number, callback: Function) { let rafId: any; let timeoutId: any; - if (framesToWait < 2) { + if (framesToWait === 0) { + callback(); + + }else if (framesToWait < 2) { rafId = nativeRaf(callback); } else { @@ -67,7 +70,10 @@ export function rafFrames(framesToWait: number, callback: Function) { export function zoneRafFrames(framesToWait: number, callback: Function) { framesToWait = Math.ceil(framesToWait); - if (framesToWait < 2) { + if (framesToWait === 0) { + callback(); + + } else if (framesToWait < 2) { raf(callback); } else {