From 2f6cee4713ee950942809b21b659111b11048693 Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Wed, 27 Jan 2016 13:51:28 +0100 Subject: [PATCH] fix(ngTouch): deprecate ngClick and disable it by default This commit deprecates the ngClick directive from the ngTouch module. Additionally, it disables it by default. It can be enabled in the new $touchProvider with $touchProvider.ngClickOverrideEnabled() method. The directive was conceived to remove the 300ms delay for click events on mobile browsers, by sending a synthetic click event on touchstart. It also tried to make sure that the original click event that the browser sends after 300ms was "busted", so that no redundant "ghost-clicks" appear. There are various reasons why the directive is being deprecated. - "This is an ugly, terrible hack!" (says so in the source) - It is plagued by various bugs that are hard to fix / test for all platforms (see below) - Simply including ngTouch activates the ngClick override, which means even if you simply want to use ngSwipe, you may break parts of your app - There exist alternatives for removing the 300ms delay, that can be used very well with Angular: [FastClick](https://github.com/ftlabs/fastclick), [Tappy!](https://github.com/filamentgroup/tappy/) (There's also hammer.js for touch events / gestures) - The 300ms delay itself is on the way out - Chrome and Firefox for Android remove the 300ms delay when the usual `` is set. In IE, the `touch-action` css property can be set to `none` or `manipulation` to remove the delay. Finally, since iOs 8, Safari doesn't delay "slow" taps anymore. There are some caveats though, which can be found in this excellent article on which this summary is based: http://developer.telerik.com/featured/300-ms-click-delay-ios-8/ Note that this change does not affect the `ngSwipe` directive. Issues with interactive elements (input, a etc.) when parent element has ngClick: Closes #4030 Closes #5307 Closes #6001 Closes #6432 Closes #7231 Closes #11358 Closes #12082 Closes #12153 Closes #12392 Closes #12545 Closes #12867 Closes #13213 Closes #13558 Other issues: - incorrect event order - incorrect event propagation - ghost-clicks / failing clickbusting with corner cases - browser specific bugs - et al. Closes #3296 Closes #3347 Closes #3447 Closes #3999 Closes #4428 Closes #6251 Closes #6330 Closes #7134 Closes #7935 Closes #9724 Closes #9744 Closes #9872 Closes #10211 Closes #10366 Closes #10918 Closes #11197 Closes #11261 Closes #11342 Closes #11577 Closes #12150 Closes #12317 Closes #12455 Closes #12734 Closes #13122 Closes #13272 Closes #13447 BREAKING CHANGE: The `ngClick` override directive from the `ngTouch` module is **deprecated and disabled by default**. This means that on touch-based devices, users might now experience a 300ms delay before a click event is fired. If you rely on this directive, you can still enable it with the `$touchProvider.ngClickOverrideEnabled()`method: ```js angular.module('myApp').config(function($touchProvider) { $touchProvider.ngClickOverrideEnabled(true); }); ``` For migration, we recommend using [FastClick](https://github.com/ftlabs/fastclick). Also note that modern browsers remove the 300ms delay under some circumstances: - Chrome and Firefox for Android remove the 300ms delay when the well-known `` is set - Internet Explorer removes the delay when `touch-action` css property is set to `none` or `manipulation` - Since iOs 8, Safari removes the delay on so-called "slow taps" See this [article by Telerik](http://developer.telerik.com/featured/300-ms-click-delay-ios-8/) for more info on the topic. Note that this change does not affect the `ngSwipe` directive. --- src/ngTouch/directive/ngClick.js | 21 +- src/ngTouch/touch.js | 92 +++ test/ngTouch/directive/ngClickSpec.js | 986 ++++++++++++++------------ 3 files changed, 628 insertions(+), 471 deletions(-) diff --git a/src/ngTouch/directive/ngClick.js b/src/ngTouch/directive/ngClick.js index f352c252f443..d4c24bd28b88 100644 --- a/src/ngTouch/directive/ngClick.js +++ b/src/ngTouch/directive/ngClick.js @@ -7,8 +7,17 @@ /** * @ngdoc directive * @name ngClick + * @deprecated * * @description + *
+ * **DEPRECATION NOTICE**: Beginning with Angular 1.5, this directive is deprecated and by default **disabled**. + * The directive will receive no further support and might be removed from future releases. + * If you need the directive, you can enable it with the {@link ngTouch.$touchProvider $touchProvider#ngClickOverrideEnabled} + * function. We also recommend that you migrate to [FastClick](https://github.com/ftlabs/fastclick). + * To learn more about the 300ms delay, this [Telerik article](http://developer.telerik.com/featured/300-ms-click-delay-ios-8/) + * gives a good overview. + *
* A more powerful replacement for the default ngClick designed to be used on touchscreen * devices. Most mobile browsers wait about 300ms after a tap-and-release before sending * the click event. This version handles them immediately, and then prevents the @@ -40,15 +49,7 @@ */ -ngTouch.config(['$provide', function($provide) { - $provide.decorator('ngClickDirective', ['$delegate', function($delegate) { - // drop the default ngClick directive - $delegate.shift(); - return $delegate; - }]); -}]); - -ngTouch.directive('ngClick', ['$parse', '$timeout', '$rootElement', +var ngTouchClickDirectiveFactory = ['$parse', '$timeout', '$rootElement', function($parse, $timeout, $rootElement) { var TAP_DURATION = 750; // Shorter than 750ms is a tap, longer is a taphold or drag. var MOVE_TOLERANCE = 12; // 12px seems to work in most mobile browsers. @@ -292,5 +293,5 @@ ngTouch.directive('ngClick', ['$parse', '$timeout', '$rootElement', }); }; -}]); +}]; diff --git a/src/ngTouch/touch.js b/src/ngTouch/touch.js index 8191ff0b753a..4f4719588057 100644 --- a/src/ngTouch/touch.js +++ b/src/ngTouch/touch.js @@ -1,5 +1,8 @@ 'use strict'; +/* global ngTouchClickDirectiveFactory: false, + */ + /** * @ngdoc module * @name ngTouch @@ -22,6 +25,95 @@ /* global -ngTouch */ var ngTouch = angular.module('ngTouch', []); +ngTouch.provider('$touch', $TouchProvider); + function nodeName_(element) { return angular.lowercase(element.nodeName || (element[0] && element[0].nodeName)); } + +/** + * @ngdoc provider + * @name $touchProvider + * + * @description + * The `$touchProvider` allows enabling / disabling {@link ngTouch.ngClick ngTouch's ngClick directive}. + */ +$TouchProvider.$inject = ['$provide', '$compileProvider']; +function $TouchProvider($provide, $compileProvider) { + + /** + * @ngdoc method + * @name $touchProvider#ngClickOverrideEnabled + * + * @param {boolean=} enabled update the ngClickOverrideEnabled state if provided, otherwise just return the + * current ngClickOverrideEnabled state + * @returns {*} current value if used as getter or itself (chaining) if used as setter + * + * @kind function + * + * @description + * Call this method to enable/disable {@link ngTouch.ngClick ngTouch's ngClick directive}. If enabled, + * the default ngClick directive will be replaced by a version that eliminates the 300ms delay for + * click events on browser for touch-devices. + * + * The default is `false`. + * + */ + var ngClickOverrideEnabled = false; + var ngClickDirectiveAdded = false; + this.ngClickOverrideEnabled = function(enabled) { + if (angular.isDefined(enabled)) { + + if (enabled && !ngClickDirectiveAdded) { + ngClickDirectiveAdded = true; + + $compileProvider.directive('ngClick', ngTouchClickDirectiveFactory); + + $provide.decorator('ngClickDirective', ['$delegate', function($delegate) { + if (ngClickOverrideEnabled) { + // drop the default ngClick directive + $delegate.shift(); + } else { + // drop the ngTouch ngClick directive if the override has been re-disabled (because + // we cannot de-register added directives) + $delegate.splice(1, 1); + } + + return $delegate; + }]); + } + + ngClickOverrideEnabled = enabled; + return this; + } + + return ngClickOverrideEnabled; + }; + + /** + * @ngdoc service + * @name $touch + * @kind object + * + * @description + * Provides the {@link ngTouch.$touch#ngClickOverrideEnabled `ngClickOverrideEnabled`} method. + * + */ + this.$get = function() { + return { + /** + * @ngdoc method + * @name $touch#ngClickOverrideEnabled + * + * @returns {*} current value of `ngClickOverrideEnabled` set in the {@link ngTouch.$touchProvider $touchProvider}, + * i.e. if {@link ngTouch.ngClick ngTouch's ngClick} directive is enabled. + * + * @kind function + */ + ngClickOverrideEnabled: function() { + return ngClickOverrideEnabled; + } + }; + }; + +} diff --git a/test/ngTouch/directive/ngClickSpec.js b/test/ngTouch/directive/ngClickSpec.js index c9765b4c8df9..a9c09667ce3c 100644 --- a/test/ngTouch/directive/ngClickSpec.js +++ b/test/ngTouch/directive/ngClickSpec.js @@ -14,198 +14,140 @@ describe('ngClick (touch)', function() { return time; } + beforeEach(module('ngTouch')); - beforeEach(function() { - module('ngTouch'); - orig_now = Date.now; - time = 0; - Date.now = mockTime; - }); - - afterEach(function() { - dealoc(element); - Date.now = orig_now; - }); - - - it('should get called on a tap', inject(function($rootScope, $compile) { - element = $compile('
')($rootScope); - $rootScope.$digest(); - expect($rootScope.tapped).toBeUndefined(); - - browserTrigger(element, 'touchstart'); - browserTrigger(element, 'touchend'); - expect($rootScope.tapped).toEqual(true); - })); + describe('config', function() { + it('should expose ngClickOverrideEnabled in the $touchProvider', function() { + var _$touchProvider; - it('should pass event object', inject(function($rootScope, $compile) { - element = $compile('
')($rootScope); - $rootScope.$digest(); - - browserTrigger(element, 'touchstart'); - browserTrigger(element, 'touchend'); - expect($rootScope.event).toBeDefined(); - })); - - if (window.jQuery) { - it('should not unwrap a jQuery-wrapped event object on click', inject(function($rootScope, $compile) { - element = $compile('
')($rootScope); - $rootScope.$digest(); - - browserTrigger(element, 'click', { - keys: [], - x: 10, - y: 10 + module(function($touchProvider) { + _$touchProvider = $touchProvider; }); - expect($rootScope.event.originalEvent).toBeDefined(); - expect($rootScope.event.originalEvent.clientX).toBe(10); - expect($rootScope.event.originalEvent.clientY).toBe(10); - })); - - it('should not unwrap a jQuery-wrapped event object on touchstart/touchend', - inject(function($rootScope, $compile, $rootElement) { - element = $compile('
')($rootScope); - $rootElement.append(element); - $rootScope.$digest(); - - browserTrigger(element, 'touchstart'); - browserTrigger(element, 'touchend'); - - expect($rootScope.event.originalEvent).toBeDefined(); - })); - } + inject(function() { + expect(_$touchProvider.ngClickOverrideEnabled).toEqual(jasmine.any(Function)); + }); + }); - it('should not click if the touch is held too long', inject(function($rootScope, $compile, $rootElement) { - element = $compile('
')($rootScope); - $rootElement.append(element); - $rootScope.count = 0; - $rootScope.$digest(); - expect($rootScope.count).toBe(0); + it('should return "false" for ngClickOverrideEnabled by default', function() { + var enabled; - time = 10; - browserTrigger(element, 'touchstart',{ - keys: [], - x: 10, - y: 10 - }); + module(function($touchProvider) { + enabled = $touchProvider.ngClickOverrideEnabled(); + }); - time = 900; - browserTrigger(element, 'touchend',{ - keys: [], - x: 10, - y: 10 + inject(function() { + expect(enabled).toBe(false); + }); }); - expect($rootScope.count).toBe(0); - })); + it('should not apply the ngClick override directive by default', function() { + inject(function($rootScope, $compile) { + element = $compile('
')($rootScope); + $rootScope.$digest(); + expect($rootScope.tapped).toBeUndefined(); - it('should not click if the touchend is too far away', inject(function($rootScope, $compile, $rootElement) { - element = $compile('
')($rootScope); - $rootElement.append(element); - $rootScope.$digest(); + browserTrigger(element, 'touchstart'); + browserTrigger(element, 'touchend'); + expect($rootScope.tapped).toBeUndefined(); + }); + }); + }); - expect($rootScope.tapped).toBeUndefined(); + describe('directive', function() { - browserTrigger(element, 'touchstart',{ - keys: [], - x: 10, - y: 10 - }); - browserTrigger(element, 'touchend',{ - keys: [], - x: 400, - y: 400 + beforeEach(function() { + module(function($touchProvider) { + $touchProvider.ngClickOverrideEnabled(true); + }); + orig_now = Date.now; + time = 0; + Date.now = mockTime; }); - expect($rootScope.tapped).toBeUndefined(); - })); - + afterEach(function() { + dealoc(element); + Date.now = orig_now; + }); - it('should not prevent click if a touchmove comes before touchend', inject(function($rootScope, $compile, $rootElement) { - element = $compile('
')($rootScope); - $rootElement.append(element); - $rootScope.$digest(); + it('should not apply the ngClick override directive if ngClickOverrideEnabled has been set to false again', function() { + module(function($touchProvider) { + // beforeEach calls this with "true" + $touchProvider.ngClickOverrideEnabled(false); + }); - expect($rootScope.tapped).toBeUndefined(); + inject(function($rootScope, $compile) { + element = $compile('
')($rootScope); + $rootScope.$digest(); + expect($rootScope.tapped).toBeUndefined(); - browserTrigger(element, 'touchstart',{ - keys: [], - x: 10, - y: 10 - }); - browserTrigger(element, 'touchmove'); - browserTrigger(element, 'touchend',{ - keys: [], - x: 15, - y: 15 + browserTrigger(element, 'touchstart'); + browserTrigger(element, 'touchend'); + expect($rootScope.tapped).toBeUndefined(); + }); }); - expect($rootScope.tapped).toEqual(true); - })); - it('should add the CSS class while the element is held down, and then remove it', inject(function($rootScope, $compile, $rootElement) { - element = $compile('
')($rootScope); - $rootElement.append(element); - $rootScope.$digest(); - expect($rootScope.tapped).toBeUndefined(); + it('should get called on a tap', inject(function($rootScope, $compile) { + element = $compile('
')($rootScope); + $rootScope.$digest(); + expect($rootScope.tapped).toBeUndefined(); - var CSS_CLASS = 'ng-click-active'; + browserTrigger(element, 'touchstart'); + browserTrigger(element, 'touchend'); + expect($rootScope.tapped).toEqual(true); + })); - expect(element.hasClass(CSS_CLASS)).toBe(false); - browserTrigger(element, 'touchstart',{ - keys: [], - x: 10, - y: 10 - }); - expect(element.hasClass(CSS_CLASS)).toBe(true); - browserTrigger(element, 'touchend',{ - keys: [], - x: 10, - y: 10 - }); - expect(element.hasClass(CSS_CLASS)).toBe(false); - expect($rootScope.tapped).toBe(true); - })); - it('should click when target element is an SVG', inject( - function($rootScope, $compile, $rootElement) { - element = $compile('')($rootScope); - $rootElement.append(element); + it('should pass event object', inject(function($rootScope, $compile) { + element = $compile('
')($rootScope); $rootScope.$digest(); browserTrigger(element, 'touchstart'); browserTrigger(element, 'touchend'); - browserTrigger(element, 'click', {x:1, y:1}); + expect($rootScope.event).toBeDefined(); + })); - expect($rootScope.tapped).toEqual(true); - })); + if (window.jQuery) { + it('should not unwrap a jQuery-wrapped event object on click', inject(function($rootScope, $compile) { + element = $compile('
')($rootScope); + $rootScope.$digest(); - describe('the clickbuster', function() { - var element1, element2; + browserTrigger(element, 'click', { + keys: [], + x: 10, + y: 10 + }); + expect($rootScope.event.originalEvent).toBeDefined(); + expect($rootScope.event.originalEvent.clientX).toBe(10); + expect($rootScope.event.originalEvent.clientY).toBe(10); + })); - beforeEach(inject(function($rootElement, $document) { - $document.find('body').append($rootElement); - })); + it('should not unwrap a jQuery-wrapped event object on touchstart/touchend', + inject(function($rootScope, $compile, $rootElement) { + element = $compile('
')($rootScope); + $rootElement.append(element); + $rootScope.$digest(); - afterEach(inject(function($document) { - $document.find('body').empty(); - })); + browserTrigger(element, 'touchstart'); + browserTrigger(element, 'touchend'); + expect($rootScope.event.originalEvent).toBeDefined(); + })); + } - it('should cancel the following click event', inject(function($rootScope, $compile, $rootElement, $document) { + + it('should not click if the touch is held too long', inject(function($rootScope, $compile, $rootElement) { element = $compile('
')($rootScope); $rootElement.append(element); - $rootScope.count = 0; $rootScope.$digest(); expect($rootScope.count).toBe(0); - // Fire touchstart at 10ms, touchend at 50ms, the click at 300ms. time = 10; browserTrigger(element, 'touchstart',{ keys: [], @@ -213,443 +155,565 @@ describe('ngClick (touch)', function() { y: 10 }); - time = 50; + time = 900; browserTrigger(element, 'touchend',{ keys: [], x: 10, y: 10 }); - expect($rootScope.count).toBe(1); + expect($rootScope.count).toBe(0); + })); + - time = 100; - browserTrigger(element, 'click',{ + it('should not click if the touchend is too far away', inject(function($rootScope, $compile, $rootElement) { + element = $compile('
')($rootScope); + $rootElement.append(element); + $rootScope.$digest(); + + expect($rootScope.tapped).toBeUndefined(); + + browserTrigger(element, 'touchstart',{ keys: [], x: 10, y: 10 }); + browserTrigger(element, 'touchend',{ + keys: [], + x: 400, + y: 400 + }); - expect($rootScope.count).toBe(1); + expect($rootScope.tapped).toBeUndefined(); })); - it('should cancel the following click event even when the element has changed', inject( - function($rootScope, $compile, $rootElement) { - $rootElement.append( - '
x
' + - '
y
' - ); - $compile($rootElement)($rootScope); - - element1 = $rootElement.find('div').eq(0); - element2 = $rootElement.find('div').eq(1); - - $rootScope.count1 = 0; - $rootScope.count2 = 0; - + it('should not prevent click if a touchmove comes before touchend', inject(function($rootScope, $compile, $rootElement) { + element = $compile('
')($rootScope); + $rootElement.append(element); $rootScope.$digest(); - expect($rootScope.count1).toBe(0); - expect($rootScope.count2).toBe(0); + expect($rootScope.tapped).toBeUndefined(); - time = 10; - browserTrigger(element1, 'touchstart',{ + browserTrigger(element, 'touchstart',{ keys: [], x: 10, y: 10 }); + browserTrigger(element, 'touchmove'); + browserTrigger(element, 'touchend',{ + keys: [], + x: 15, + y: 15 + }); - time = 50; - browserTrigger(element1, 'touchend',{ + expect($rootScope.tapped).toEqual(true); + })); + + it('should add the CSS class while the element is held down, and then remove it', inject(function($rootScope, $compile, $rootElement) { + element = $compile('
')($rootScope); + $rootElement.append(element); + $rootScope.$digest(); + expect($rootScope.tapped).toBeUndefined(); + + var CSS_CLASS = 'ng-click-active'; + + expect(element.hasClass(CSS_CLASS)).toBe(false); + browserTrigger(element, 'touchstart',{ keys: [], x: 10, y: 10 }); - - expect($rootScope.count1).toBe(1); - - time = 100; - browserTrigger(element2, 'click',{ + expect(element.hasClass(CSS_CLASS)).toBe(true); + browserTrigger(element, 'touchend',{ keys: [], x: 10, y: 10 }); + expect(element.hasClass(CSS_CLASS)).toBe(false); + expect($rootScope.tapped).toBe(true); + })); + + it('should click when target element is an SVG', inject( + function($rootScope, $compile, $rootElement) { + element = $compile('')($rootScope); + $rootElement.append(element); + $rootScope.$digest(); + + browserTrigger(element, 'touchstart'); + browserTrigger(element, 'touchend'); + browserTrigger(element, 'click', {x:1, y:1}); - expect($rootScope.count1).toBe(1); - expect($rootScope.count2).toBe(0); + expect($rootScope.tapped).toEqual(true); })); + describe('the clickbuster', function() { + var element1, element2; - it('should not cancel clicks on distant elements', inject(function($rootScope, $compile, $rootElement) { - $rootElement.append( - '
x
' + - '
y
' - ); - $compile($rootElement)($rootScope); + beforeEach(inject(function($rootElement, $document) { + $document.find('body').append($rootElement); + })); - element1 = $rootElement.find('div').eq(0); - element2 = $rootElement.find('div').eq(1); + afterEach(inject(function($document) { + $document.find('body').empty(); + })); - $rootScope.count1 = 0; - $rootScope.count2 = 0; - $rootScope.$digest(); + it('should cancel the following click event', inject(function($rootScope, $compile, $rootElement, $document) { + element = $compile('
')($rootScope); + $rootElement.append(element); - expect($rootScope.count1).toBe(0); - expect($rootScope.count2).toBe(0); + $rootScope.count = 0; + $rootScope.$digest(); - time = 10; - browserTrigger(element1, 'touchstart',{ - keys: [], - x: 10, - y: 10 - }); + expect($rootScope.count).toBe(0); - time = 50; - browserTrigger(element1, 'touchend',{ - keys: [], - x: 10, - y: 10 - }); + // Fire touchstart at 10ms, touchend at 50ms, the click at 300ms. + time = 10; + browserTrigger(element, 'touchstart',{ + keys: [], + x: 10, + y: 10 + }); - expect($rootScope.count1).toBe(1); + time = 50; + browserTrigger(element, 'touchend',{ + keys: [], + x: 10, + y: 10 + }); - time = 90; - // Verify that it is blurred so we don't get soft-keyboard - element1[0].blur = jasmine.createSpy('blur'); - browserTrigger(element1, 'click',{ - keys: [], - x: 10, - y: 10 - }); - expect(element1[0].blur).toHaveBeenCalled(); + expect($rootScope.count).toBe(1); - expect($rootScope.count1).toBe(1); + time = 100; + browserTrigger(element, 'click',{ + keys: [], + x: 10, + y: 10 + }); - time = 100; - browserTrigger(element1, 'touchstart',{ - keys: [], - x: 10, - y: 10 - }); + expect($rootScope.count).toBe(1); + })); - time = 130; - browserTrigger(element1, 'touchend',{ - keys: [], - x: 10, - y: 10 - }); - expect($rootScope.count1).toBe(2); + it('should cancel the following click event even when the element has changed', inject( + function($rootScope, $compile, $rootElement) { + $rootElement.append( + '
x
' + + '
y
' + ); + $compile($rootElement)($rootScope); - // Click on other element that should go through. - time = 150; - browserTrigger(element2, 'touchstart',{ - keys: [], - x: 100, - y: 120 - }); - browserTrigger(element2, 'touchend',{ - keys: [], - x: 100, - y: 120 - }); - browserTrigger(element2, 'click',{ - keys: [], - x: 100, - y: 120 - }); + element1 = $rootElement.find('div').eq(0); + element2 = $rootElement.find('div').eq(1); - expect($rootScope.count2).toBe(1); + $rootScope.count1 = 0; + $rootScope.count2 = 0; - // Click event for the element that should be busted. - time = 200; - browserTrigger(element1, 'click',{ - keys: [], - x: 10, - y: 10 - }); + $rootScope.$digest(); - expect($rootScope.count1).toBe(2); - expect($rootScope.count2).toBe(1); - })); + expect($rootScope.count1).toBe(0); + expect($rootScope.count2).toBe(0); + time = 10; + browserTrigger(element1, 'touchstart',{ + keys: [], + x: 10, + y: 10 + }); - it('should not cancel clicks that come long after', inject(function($rootScope, $compile) { - element1 = $compile('
')($rootScope); + time = 50; + browserTrigger(element1, 'touchend',{ + keys: [], + x: 10, + y: 10 + }); - $rootScope.count = 0; + expect($rootScope.count1).toBe(1); - $rootScope.$digest(); + time = 100; + browserTrigger(element2, 'click',{ + keys: [], + x: 10, + y: 10 + }); - expect($rootScope.count).toBe(0); + expect($rootScope.count1).toBe(1); + expect($rootScope.count2).toBe(0); + })); - time = 10; - browserTrigger(element1, 'touchstart',{ - keys: [], - x: 10, - y: 10 - }); - time = 50; - browserTrigger(element1, 'touchend',{ - keys: [], - x: 10, - y: 10 - }); - expect($rootScope.count).toBe(1); + it('should not cancel clicks on distant elements', inject(function($rootScope, $compile, $rootElement) { + $rootElement.append( + '
x
' + + '
y
' + ); + $compile($rootElement)($rootScope); - time = 2700; - browserTrigger(element1, 'click',{ - keys: [], - x: 10, - y: 10 - }); + element1 = $rootElement.find('div').eq(0); + element2 = $rootElement.find('div').eq(1); - expect($rootScope.count).toBe(2); - })); + $rootScope.count1 = 0; + $rootScope.count2 = 0; + + $rootScope.$digest(); + expect($rootScope.count1).toBe(0); + expect($rootScope.count2).toBe(0); - describe('when clicking on a label immediately following a touch event', function() { - var touch = function(element, x, y) { time = 10; - browserTrigger(element, 'touchstart',{ + browserTrigger(element1, 'touchstart',{ keys: [], - x: x, - y: y + x: 10, + y: 10 }); time = 50; - browserTrigger(element, 'touchend',{ + browserTrigger(element1, 'touchend',{ keys: [], - x: x, - y: y + x: 10, + y: 10 }); - }; - var click = function(element, x, y) { - browserTrigger(element, 'click',{ + expect($rootScope.count1).toBe(1); + + time = 90; + // Verify that it is blurred so we don't get soft-keyboard + element1[0].blur = jasmine.createSpy('blur'); + browserTrigger(element1, 'click',{ + keys: [], + x: 10, + y: 10 + }); + expect(element1[0].blur).toHaveBeenCalled(); + + expect($rootScope.count1).toBe(1); + + time = 100; + browserTrigger(element1, 'touchstart',{ keys: [], - x: x, - y: y + x: 10, + y: 10 }); - }; - var $rootScope; - var container, otherElement, input, label; - beforeEach(inject(function(_$rootScope_, $compile, $rootElement) { - $rootScope = _$rootScope_; - var container = $compile('
' + - '' + - '
')($rootScope); - $rootElement.append(container); - otherElement = container.children()[0]; - input = container.children()[1]; - label = container.children()[2]; + time = 130; + browserTrigger(element1, 'touchend',{ + keys: [], + x: 10, + y: 10 + }); + + expect($rootScope.count1).toBe(2); + + // Click on other element that should go through. + time = 150; + browserTrigger(element2, 'touchstart',{ + keys: [], + x: 100, + y: 120 + }); + browserTrigger(element2, 'touchend',{ + keys: [], + x: 100, + y: 120 + }); + browserTrigger(element2, 'click',{ + keys: [], + x: 100, + y: 120 + }); + + expect($rootScope.count2).toBe(1); + + // Click event for the element that should be busted. + time = 200; + browserTrigger(element1, 'click',{ + keys: [], + x: 10, + y: 10 + }); + + expect($rootScope.count1).toBe(2); + expect($rootScope.count2).toBe(1); + })); + - $rootScope.selection = 'initial'; + it('should not cancel clicks that come long after', inject(function($rootScope, $compile) { + element1 = $compile('
')($rootScope); + + $rootScope.count = 0; $rootScope.$digest(); + + expect($rootScope.count).toBe(0); + + time = 10; + browserTrigger(element1, 'touchstart',{ + keys: [], + x: 10, + y: 10 + }); + + time = 50; + browserTrigger(element1, 'touchend',{ + keys: [], + x: 10, + y: 10 + }); + expect($rootScope.count).toBe(1); + + time = 2700; + browserTrigger(element1, 'click',{ + keys: [], + x: 10, + y: 10 + }); + + expect($rootScope.count).toBe(2); })); - afterEach(function() { - dealoc(label); - dealoc(input); - dealoc(otherElement); - dealoc(container); - }); + describe('when clicking on a label immediately following a touch event', function() { + var touch = function(element, x, y) { + time = 10; + browserTrigger(element, 'touchstart',{ + keys: [], + x: x, + y: y + }); + + time = 50; + browserTrigger(element, 'touchend',{ + keys: [], + x: x, + y: y + }); + }; + + var click = function(element, x, y) { + browserTrigger(element, 'click',{ + keys: [], + x: x, + y: y + }); + }; + + var $rootScope; + var container, otherElement, input, label; + beforeEach(inject(function(_$rootScope_, $compile, $rootElement) { + $rootScope = _$rootScope_; + var container = $compile('
' + + '' + + '
')($rootScope); + $rootElement.append(container); + otherElement = container.children()[0]; + input = container.children()[1]; + label = container.children()[2]; + + $rootScope.selection = 'initial'; + + $rootScope.$digest(); + })); + + + afterEach(function() { + dealoc(label); + dealoc(input); + dealoc(otherElement); + dealoc(container); + }); - it('should not cancel input clicks with (0,0) coordinates', function() { - touch(otherElement, 100, 100); + it('should not cancel input clicks with (0,0) coordinates', function() { + touch(otherElement, 100, 100); - time = 500; - click(label, 10, 10); - click(input, 0, 0); + time = 500; + click(label, 10, 10); + click(input, 0, 0); - expect($rootScope.selection).toBe('radio1'); - }); + expect($rootScope.selection).toBe('radio1'); + }); - it('should not cancel input clicks with negative coordinates', function() { - touch(otherElement, 100, 100); + it('should not cancel input clicks with negative coordinates', function() { + touch(otherElement, 100, 100); - time = 500; - click(label, 10, 10); - click(input, -1, -1); + time = 500; + click(label, 10, 10); + click(input, -1, -1); - expect($rootScope.selection).toBe('radio1'); - }); + expect($rootScope.selection).toBe('radio1'); + }); - it('should not cancel input clicks with positive coordinates identical to label click', function() { - touch(otherElement, 100, 100); + it('should not cancel input clicks with positive coordinates identical to label click', function() { + touch(otherElement, 100, 100); - time = 500; - click(label, 10, 10); - click(input, 10, 10); + time = 500; + click(label, 10, 10); + click(input, 10, 10); - expect($rootScope.selection).toBe('radio1'); - }); + expect($rootScope.selection).toBe('radio1'); + }); - it('should cancel input clicks with positive coordinates different than label click', function() { - touch(otherElement, 100, 100); + it('should cancel input clicks with positive coordinates different than label click', function() { + touch(otherElement, 100, 100); - time = 500; - click(label, 10, 10); - click(input, 11, 11); + time = 500; + click(label, 10, 10); + click(input, 11, 11); - expect($rootScope.selection).toBe('initial'); - }); + expect($rootScope.selection).toBe('initial'); + }); - it('should blur the other element on click', function() { - var blurSpy = spyOn(otherElement, 'blur'); - touch(otherElement, 10, 10); + it('should blur the other element on click', function() { + var blurSpy = spyOn(otherElement, 'blur'); + touch(otherElement, 10, 10); - time = 500; - click(label, 10, 10); + time = 500; + click(label, 10, 10); - expect(blurSpy).toHaveBeenCalled(); + expect(blurSpy).toHaveBeenCalled(); + }); }); }); - }); - describe('click fallback', function() { + describe('click fallback', function() { - it('should treat a click as a tap on desktop', inject(function($rootScope, $compile) { - element = $compile('
')($rootScope); - $rootScope.$digest(); - expect($rootScope.tapped).toBeFalsy(); + it('should treat a click as a tap on desktop', inject(function($rootScope, $compile) { + element = $compile('
')($rootScope); + $rootScope.$digest(); + expect($rootScope.tapped).toBeFalsy(); - browserTrigger(element, 'click'); - expect($rootScope.tapped).toEqual(true); - })); + browserTrigger(element, 'click'); + expect($rootScope.tapped).toEqual(true); + })); - it('should pass event object', inject(function($rootScope, $compile) { - element = $compile('
')($rootScope); - $rootScope.$digest(); + it('should pass event object', inject(function($rootScope, $compile) { + element = $compile('
')($rootScope); + $rootScope.$digest(); - browserTrigger(element, 'click'); - expect($rootScope.event).toBeDefined(); - })); - }); + browserTrigger(element, 'click'); + expect($rootScope.event).toBeDefined(); + })); + }); - describe('disabled state', function() { - it('should not trigger click if ngDisabled is true', inject(function($rootScope, $compile) { - element = $compile('
')($rootScope); - $rootScope.disabled = true; - $rootScope.$digest(); + describe('disabled state', function() { + it('should not trigger click if ngDisabled is true', inject(function($rootScope, $compile) { + element = $compile('
')($rootScope); + $rootScope.disabled = true; + $rootScope.$digest(); - browserTrigger(element, 'touchstart',{ - keys: [], - x: 10, - y: 10 - }); - browserTrigger(element, 'touchend',{ - keys: [], - x: 10, - y: 10 - }); + browserTrigger(element, 'touchstart',{ + keys: [], + x: 10, + y: 10 + }); + browserTrigger(element, 'touchend',{ + keys: [], + x: 10, + y: 10 + }); - expect($rootScope.event).toBeUndefined(); - })); - it('should trigger click if ngDisabled is false', inject(function($rootScope, $compile) { - element = $compile('
')($rootScope); - $rootScope.disabled = false; - $rootScope.$digest(); + expect($rootScope.event).toBeUndefined(); + })); + it('should trigger click if ngDisabled is false', inject(function($rootScope, $compile) { + element = $compile('
')($rootScope); + $rootScope.disabled = false; + $rootScope.$digest(); - browserTrigger(element, 'touchstart',{ - keys: [], - x: 10, - y: 10 - }); - browserTrigger(element, 'touchend',{ - keys: [], - x: 10, - y: 10 - }); + browserTrigger(element, 'touchstart',{ + keys: [], + x: 10, + y: 10 + }); + browserTrigger(element, 'touchend',{ + keys: [], + x: 10, + y: 10 + }); - expect($rootScope.event).toBeDefined(); - })); - it('should not trigger click if regular disabled is true', inject(function($rootScope, $compile) { - element = $compile('
')($rootScope); + expect($rootScope.event).toBeDefined(); + })); + it('should not trigger click if regular disabled is true', inject(function($rootScope, $compile) { + element = $compile('
')($rootScope); - browserTrigger(element, 'touchstart',{ - keys: [], - x: 10, - y: 10 - }); - browserTrigger(element, 'touchend',{ - keys: [], - x: 10, - y: 10 - }); + browserTrigger(element, 'touchstart',{ + keys: [], + x: 10, + y: 10 + }); + browserTrigger(element, 'touchend',{ + keys: [], + x: 10, + y: 10 + }); - expect($rootScope.event).toBeUndefined(); - })); - it('should not trigger click if regular disabled is present', inject(function($rootScope, $compile) { - element = $compile('')($rootScope); + expect($rootScope.event).toBeUndefined(); + })); + it('should not trigger click if regular disabled is present', inject(function($rootScope, $compile) { + element = $compile('')($rootScope); - browserTrigger(element, 'touchstart',{ - keys: [], - x: 10, - y: 10 - }); - browserTrigger(element, 'touchend',{ - keys: [], - x: 10, - y: 10 - }); + browserTrigger(element, 'touchstart',{ + keys: [], + x: 10, + y: 10 + }); + browserTrigger(element, 'touchend',{ + keys: [], + x: 10, + y: 10 + }); - expect($rootScope.event).toBeUndefined(); - })); - it('should trigger click if regular disabled is not present', inject(function($rootScope, $compile) { - element = $compile('
')($rootScope); + expect($rootScope.event).toBeUndefined(); + })); + it('should trigger click if regular disabled is not present', inject(function($rootScope, $compile) { + element = $compile('
')($rootScope); - browserTrigger(element, 'touchstart',{ - keys: [], - x: 10, - y: 10 - }); - browserTrigger(element, 'touchend',{ - keys: [], - x: 10, - y: 10 - }); + browserTrigger(element, 'touchstart',{ + keys: [], + x: 10, + y: 10 + }); + browserTrigger(element, 'touchend',{ + keys: [], + x: 10, + y: 10 + }); - expect($rootScope.event).toBeDefined(); - })); - }); + expect($rootScope.event).toBeDefined(); + })); + }); - describe('the normal click event', function() { - it('should be capturable by other handlers', inject(function($rootScope, $compile) { - var called = false; + describe('the normal click event', function() { + it('should be capturable by other handlers', inject(function($rootScope, $compile) { + var called = false; - element = $compile('
')($rootScope); + element = $compile('
')($rootScope); - element.on('click', function() { - called = true; - }); + element.on('click', function() { + called = true; + }); - browserTrigger(element, 'touchstart',{ - keys: [], - x: 10, - y: 10 - }); - browserTrigger(element, 'touchend',{ - keys: [], - x: 10, - y: 10 - }); + browserTrigger(element, 'touchstart',{ + keys: [], + x: 10, + y: 10 + }); + browserTrigger(element, 'touchend',{ + keys: [], + x: 10, + y: 10 + }); - expect(called).toEqual(true); - })); + expect(called).toEqual(true); + })); + }); }); });