From 11d9ad1eb25eaf5967195e424108207427835d50 Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Tue, 14 Mar 2017 11:48:54 +0100 Subject: [PATCH] fix(ngTouch): remove ngClick override, `$touchProvider`, and `$touch` Closes #15761 Closes #15755 BREAKING CHANGE: The `ngClick` directive from the ngTouch module has been removed, and with it the corresponding `$touchProvider` and `$touch` service. If you have included ngTouch v1.5.0 or higher in your application, and have not changed the value of `$touchProvider.ngClickOverrideEnabled()`, or injected and used the `$touch` service, then there are no migration steps for your code. Otherwise you must remove references to the provider and service. The `ngClick` override directive had been deprecated and by default disabled since v1.5.0, because of buggy behavior in edge cases, and a general trend to avoid special touch based overrides of click events. In modern browsers, it should not be necessary to use a touch override library: - Chrome, Firefox, Edge, and Safari remove the 300ms delay when `` is set. - Internet Explorer 10+, Edge, Safari, and Chrome remove the delay on elements that have the `touch-action` css property is set to `manipulation`. You can find out more in these articles: https://developers.google.com/web/updates/2013/12/300ms-tap-delay-gone-away https://developer.apple.com/library/content/releasenotes/General/WhatsNewInSafari/Articles/Safari_9_1.html#//apple_ref/doc/uid/TP40014305-CH10-SW8 https://blogs.msdn.microsoft.com/ie/2015/02/24/pointer-events-w3c-recommendation-interoperable-touch-and-removing-the-dreaded-300ms-tap-delay/ --- angularFiles.js | 1 - docs/content/misc/downloading.ngdoc | 2 +- src/ngTouch/directive/ngClick.js | 298 ---------- src/ngTouch/touch.js | 106 +--- test/ngTouch/directive/ngClickSpec.js | 750 -------------------------- 5 files changed, 3 insertions(+), 1154 deletions(-) delete mode 100644 src/ngTouch/directive/ngClick.js delete mode 100644 test/ngTouch/directive/ngClickSpec.js diff --git a/angularFiles.js b/angularFiles.js index 311a39139322..4c7b8cc361e4 100644 --- a/angularFiles.js +++ b/angularFiles.js @@ -146,7 +146,6 @@ var angularFiles = { 'ngTouch': [ 'src/ngTouch/touch.js', 'src/ngTouch/swipe.js', - 'src/ngTouch/directive/ngClick.js', 'src/ngTouch/directive/ngSwipe.js' ], 'ngAria': [ diff --git a/docs/content/misc/downloading.ngdoc b/docs/content/misc/downloading.ngdoc index 83806b34f835..8131946f2472 100644 --- a/docs/content/misc/downloading.ngdoc +++ b/docs/content/misc/downloading.ngdoc @@ -111,7 +111,7 @@ The set of files included in each version directory are: * __`angular-route.js`__ — Routing and deep-linking services and directives for AngularJS apps. ({@link module:ngRoute API docs}) * __`angular-sanitize.js`__ — Functionality to sanitize HTML. ({@link module:ngSanitize API docs}) - * __`angular-touch.js`__ — Touch events and other helpers for touch-enabled devices. + * __`angular-touch.js`__ — Touch events for touch-enabled devices. ({@link module:ngTouch API docs}) diff --git a/src/ngTouch/directive/ngClick.js b/src/ngTouch/directive/ngClick.js deleted file mode 100644 index 7436343b1273..000000000000 --- a/src/ngTouch/directive/ngClick.js +++ /dev/null @@ -1,298 +0,0 @@ -'use strict'; - -/* global ngTouch: false, - nodeName_: false -*/ - -/** - * @ngdoc directive - * @name ngClick - * @deprecated - * sinceVersion="v1.5.0" - * This directive is deprecated and **disabled** by default. - * 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. - * - * @description - * 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 - * following click event from propagating. - * - * Requires the {@link ngTouch `ngTouch`} module to be installed. - * - * This directive can fall back to using an ordinary click event, and so works on desktop - * browsers as well as mobile. - * - * This directive also sets the CSS class `ng-click-active` while the element is being held - * down (by a mouse click or touch) so you can restyle the depressed element if you wish. - * - * @element ANY - * @param {expression} ngClick {@link guide/expression Expression} to evaluate - * upon tap. (Event object is available as `$event`) - * - * @example - - - - count: {{ count }} - - - angular.module('ngClickExample', ['ngTouch']); - - - */ - -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. - var PREVENT_DURATION = 2500; // 2.5 seconds maximum from preventGhostClick call to click - var CLICKBUSTER_THRESHOLD = 25; // 25 pixels in any dimension is the limit for busting clicks. - - var ACTIVE_CLASS_NAME = 'ng-click-active'; - var lastPreventedTime; - var touchCoordinates; - var lastLabelClickCoordinates; - - - // TAP EVENTS AND GHOST CLICKS - // - // Why tap events? - // Mobile browsers detect a tap, then wait a moment (usually ~300ms) to see if you're - // double-tapping, and then fire a click event. - // - // This delay sucks and makes mobile apps feel unresponsive. - // So we detect touchstart, touchcancel and touchend ourselves and determine when - // the user has tapped on something. - // - // What happens when the browser then generates a click event? - // The browser, of course, also detects the tap and fires a click after a delay. This results in - // tapping/clicking twice. We do "clickbusting" to prevent it. - // - // How does it work? - // We attach global touchstart and click handlers, that run during the capture (early) phase. - // So the sequence for a tap is: - // - global touchstart: Sets an "allowable region" at the point touched. - // - element's touchstart: Starts a touch - // (- touchcancel ends the touch, no click follows) - // - element's touchend: Determines if the tap is valid (didn't move too far away, didn't hold - // too long) and fires the user's tap handler. The touchend also calls preventGhostClick(). - // - preventGhostClick() removes the allowable region the global touchstart created. - // - The browser generates a click event. - // - The global click handler catches the click, and checks whether it was in an allowable region. - // - If preventGhostClick was called, the region will have been removed, the click is busted. - // - If the region is still there, the click proceeds normally. Therefore clicks on links and - // other elements without ngTap on them work normally. - // - // This is an ugly, terrible hack! - // Yeah, tell me about it. The alternatives are using the slow click events, or making our users - // deal with the ghost clicks, so I consider this the least of evils. Fortunately AngularJS - // encapsulates this ugly logic away from the user. - // - // Why not just put click handlers on the element? - // We do that too, just to be sure. If the tap event caused the DOM to change, - // it is possible another element is now in that position. To take account for these possibly - // distinct elements, the handlers are global and care only about coordinates. - - // Checks if the coordinates are close enough to be within the region. - function hit(x1, y1, x2, y2) { - return Math.abs(x1 - x2) < CLICKBUSTER_THRESHOLD && Math.abs(y1 - y2) < CLICKBUSTER_THRESHOLD; - } - - // Checks a list of allowable regions against a click location. - // Returns true if the click should be allowed. - // Splices out the allowable region from the list after it has been used. - function checkAllowableRegions(touchCoordinates, x, y) { - for (var i = 0; i < touchCoordinates.length; i += 2) { - if (hit(touchCoordinates[i], touchCoordinates[i + 1], x, y)) { - touchCoordinates.splice(i, i + 2); - return true; // allowable region - } - } - return false; // No allowable region; bust it. - } - - // Global click handler that prevents the click if it's in a bustable zone and preventGhostClick - // was called recently. - function onClick(event) { - if (Date.now() - lastPreventedTime > PREVENT_DURATION) { - return; // Too old. - } - - var touches = event.touches && event.touches.length ? event.touches : [event]; - var x = touches[0].clientX; - var y = touches[0].clientY; - // Work around desktop Webkit quirk where clicking a label will fire two clicks (on the label - // and on the input element). Depending on the exact browser, this second click we don't want - // to bust has either (0,0), negative coordinates, or coordinates equal to triggering label - // click event - if (x < 1 && y < 1) { - return; // offscreen - } - if (lastLabelClickCoordinates && - lastLabelClickCoordinates[0] === x && lastLabelClickCoordinates[1] === y) { - return; // input click triggered by label click - } - // reset label click coordinates on first subsequent click - if (lastLabelClickCoordinates) { - lastLabelClickCoordinates = null; - } - // remember label click coordinates to prevent click busting of trigger click event on input - if (nodeName_(event.target) === 'label') { - lastLabelClickCoordinates = [x, y]; - } - - // Look for an allowable region containing this click. - // If we find one, that means it was created by touchstart and not removed by - // preventGhostClick, so we don't bust it. - if (checkAllowableRegions(touchCoordinates, x, y)) { - return; - } - - // If we didn't find an allowable region, bust the click. - event.stopPropagation(); - event.preventDefault(); - - // Blur focused form elements - if (event.target && event.target.blur) { - event.target.blur(); - } - } - - - // Global touchstart handler that creates an allowable region for a click event. - // This allowable region can be removed by preventGhostClick if we want to bust it. - function onTouchStart(event) { - var touches = event.touches && event.touches.length ? event.touches : [event]; - var x = touches[0].clientX; - var y = touches[0].clientY; - touchCoordinates.push(x, y); - - $timeout(function() { - // Remove the allowable region. - for (var i = 0; i < touchCoordinates.length; i += 2) { - if (touchCoordinates[i] === x && touchCoordinates[i + 1] === y) { - touchCoordinates.splice(i, i + 2); - return; - } - } - }, PREVENT_DURATION, false); - } - - // On the first call, attaches some event handlers. Then whenever it gets called, it creates a - // zone around the touchstart where clicks will get busted. - function preventGhostClick(x, y) { - if (!touchCoordinates) { - $rootElement[0].addEventListener('click', onClick, true); - $rootElement[0].addEventListener('touchstart', onTouchStart, true); - touchCoordinates = []; - } - - lastPreventedTime = Date.now(); - - checkAllowableRegions(touchCoordinates, x, y); - } - - // Actual linking function. - return function(scope, element, attr) { - var clickHandler = $parse(attr.ngClick), - tapping = false, - tapElement, // Used to blur the element after a tap. - startTime, // Used to check if the tap was held too long. - touchStartX, - touchStartY; - - function resetState() { - tapping = false; - element.removeClass(ACTIVE_CLASS_NAME); - } - - element.on('touchstart', function(event) { - tapping = true; - tapElement = event.target ? event.target : event.srcElement; // IE uses srcElement. - // Hack for Safari, which can target text nodes instead of containers. - if (tapElement.nodeType === 3) { - tapElement = tapElement.parentNode; - } - - element.addClass(ACTIVE_CLASS_NAME); - - startTime = Date.now(); - - // Use jQuery originalEvent - var originalEvent = event.originalEvent || event; - var touches = originalEvent.touches && originalEvent.touches.length ? originalEvent.touches : [originalEvent]; - var e = touches[0]; - touchStartX = e.clientX; - touchStartY = e.clientY; - }); - - element.on('touchcancel', function(event) { - resetState(); - }); - - element.on('touchend', function(event) { - var diff = Date.now() - startTime; - - // Use jQuery originalEvent - var originalEvent = event.originalEvent || event; - var touches = (originalEvent.changedTouches && originalEvent.changedTouches.length) ? - originalEvent.changedTouches : - ((originalEvent.touches && originalEvent.touches.length) ? originalEvent.touches : [originalEvent]); - var e = touches[0]; - var x = e.clientX; - var y = e.clientY; - var dist = Math.sqrt(Math.pow(x - touchStartX, 2) + Math.pow(y - touchStartY, 2)); - - if (tapping && diff < TAP_DURATION && dist < MOVE_TOLERANCE) { - // Call preventGhostClick so the clickbuster will catch the corresponding click. - preventGhostClick(x, y); - - // Blur the focused element (the button, probably) before firing the callback. - // This doesn't work perfectly on Android Chrome, but seems to work elsewhere. - // I couldn't get anything to work reliably on Android Chrome. - if (tapElement) { - tapElement.blur(); - } - - if (!angular.isDefined(attr.disabled) || attr.disabled === false) { - element.triggerHandler('click', [event]); - } - } - - resetState(); - }); - - // Hack for iOS Safari's benefit. It goes searching for onclick handlers and is liable to click - // something else nearby. - element.onclick = function(event) { }; - - // Actual click handler. - // There are three different kinds of clicks, only two of which reach this point. - // - On desktop browsers without touch events, their clicks will always come here. - // - On mobile browsers, the simulated "fast" click will call this. - // - But the browser's follow-up slow click will be "busted" before it reaches this handler. - // Therefore it's safe to use this directive on both mobile and desktop. - element.on('click', function(event, touchend) { - scope.$apply(function() { - clickHandler(scope, {$event: (touchend || event)}); - }); - }); - - element.on('mousedown', function(event) { - element.addClass(ACTIVE_CLASS_NAME); - }); - - element.on('mousemove mouseup', function(event) { - element.removeClass(ACTIVE_CLASS_NAME); - }); - - }; -}]; - diff --git a/src/ngTouch/touch.js b/src/ngTouch/touch.js index 2ab37acb4c93..bdd0fffc7c94 100644 --- a/src/ngTouch/touch.js +++ b/src/ngTouch/touch.js @@ -1,7 +1,5 @@ 'use strict'; -/* global ngTouchClickDirectiveFactory: false */ - /** * @ngdoc module * @name ngTouch @@ -9,7 +7,7 @@ * * # ngTouch * - * The `ngTouch` module provides touch events and other helpers for touch-enabled devices. + * The `ngTouch` module provides helpers for touch-enabled devices. * The implementation is based on jQuery Mobile touch event handling * ([jquerymobile.com](http://jquerymobile.com/)). * @@ -21,111 +19,11 @@ */ // define ngTouch module -/* global -ngTouch */ +/* global ngTouch */ var ngTouch = angular.module('ngTouch', []); ngTouch.info({ angularVersion: '"NG_VERSION_FULL"' }); -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; - // eslint-disable-next-line no-invalid-this - this.ngClickOverrideEnabled = function(enabled) { - if (angular.isDefined(enabled)) { - - if (enabled && !ngClickDirectiveAdded) { - ngClickDirectiveAdded = true; - - // Use this to identify the correct directive in the delegate - ngTouchClickDirectiveFactory.$$moduleName = 'ngTouch'; - $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) - var i = $delegate.length - 1; - while (i >= 0) { - if ($delegate[i].$$moduleName === 'ngTouch') { - $delegate.splice(i, 1); - break; - } - i--; - } - } - - return $delegate; - }]); - } - - ngClickOverrideEnabled = enabled; - return this; - } - - return ngClickOverrideEnabled; - }; - - /** - * @ngdoc service - * @name $touch - * @kind object - * - * @description - * Provides the {@link ngTouch.$touch#ngClickOverrideEnabled `ngClickOverrideEnabled`} method. - * - */ - // eslint-disable-next-line no-invalid-this - 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 deleted file mode 100644 index 9e11f8e0c9c8..000000000000 --- a/test/ngTouch/directive/ngClickSpec.js +++ /dev/null @@ -1,750 +0,0 @@ -'use strict'; - -describe('ngClick (touch)', function() { - var element, time, orig_now; - - // TODO(braden): Once we have other touch-friendly browsers on CI, allow them here. - // Currently Firefox and IE refuse to fire touch events. - // Enable iPhone for manual testing. - if (!/chrome|iphone/i.test(window.navigator.userAgent)) { - return; - } - - function mockTime() { - return time; - } - - - describe('config', function() { - beforeEach(module('ngTouch')); - - it('should expose ngClickOverrideEnabled in the $touchProvider', function() { - var _$touchProvider; - - module(function($touchProvider) { - _$touchProvider = $touchProvider; - }); - - inject(function() { - expect(_$touchProvider.ngClickOverrideEnabled).toEqual(jasmine.any(Function)); - }); - }); - - - it('should return "false" for ngClickOverrideEnabled by default', function() { - var enabled; - - module(function($touchProvider) { - enabled = $touchProvider.ngClickOverrideEnabled(); - }); - - inject(function() { - expect(enabled).toBe(false); - }); - }); - - - it('should not apply the ngClick override directive by default', function() { - inject(function($rootScope, $compile) { - element = $compile('
')($rootScope); - $rootScope.$digest(); - expect($rootScope.tapped).toBeUndefined(); - - browserTrigger(element, 'touchstart'); - browserTrigger(element, 'touchend'); - expect($rootScope.tapped).toBeUndefined(); - }); - }); - }); - - describe('interaction with custom ngClick directives', function() { - - it('should not remove other ngClick directives when removing ngTouch ngClick in the decorator', function() { - // Add another ngClick before ngTouch - module(function($compileProvider) { - $compileProvider.directive('ngClick', function() { - return {}; - }); - }); - - module('ngTouch'); - - module(function($touchProvider) { - $touchProvider.ngClickOverrideEnabled(true); - $touchProvider.ngClickOverrideEnabled(false); - }); - - inject(function($rootScope, $compile) { - element = $compile('
')($rootScope); - $rootScope.$digest(); - expect($rootScope.tapped).toBeUndefined(); - - browserTrigger(element, 'touchstart'); - browserTrigger(element, 'touchend'); - expect($rootScope.tapped).toBeUndefined(); - }); - }); - - }); - - describe('directive', function() { - - beforeEach(function() { - module('ngTouch'); - module(function($touchProvider) { - $touchProvider.ngClickOverrideEnabled(true); - }); - orig_now = Date.now; - time = 0; - Date.now = mockTime; - }); - - afterEach(function() { - dealoc(element); - Date.now = orig_now; - }); - - 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); - }); - - inject(function($rootScope, $compile) { - element = $compile('
')($rootScope); - $rootScope.$digest(); - expect($rootScope.tapped).toBeUndefined(); - - browserTrigger(element, 'touchstart'); - browserTrigger(element, 'touchend'); - expect($rootScope.tapped).toBeUndefined(); - }); - }); - - - 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); - })); - - - 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 - }); - 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(); - })); - } - - - 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); - - time = 10; - browserTrigger(element, 'touchstart',{ - keys: [], - x: 10, - y: 10 - }); - - time = 900; - browserTrigger(element, 'touchend',{ - keys: [], - x: 10, - y: 10 - }); - - expect($rootScope.count).toBe(0); - })); - - - 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.tapped).toBeUndefined(); - })); - - - 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.tapped).toBeUndefined(); - - browserTrigger(element, 'touchstart',{ - keys: [], - x: 10, - y: 10 - }); - browserTrigger(element, 'touchmove'); - browserTrigger(element, 'touchend',{ - keys: [], - x: 15, - y: 15 - }); - - 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(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.tapped).toEqual(true); - })); - - describe('the clickbuster', function() { - var element1, element2; - - beforeEach(inject(function($rootElement, $document) { - $document.find('body').append($rootElement); - })); - - afterEach(inject(function($document) { - $document.find('body').empty(); - })); - - - it('should cancel the following click event', inject(function($rootScope, $compile, $rootElement, $document) { - 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: [], - x: 10, - y: 10 - }); - - time = 50; - browserTrigger(element, 'touchend',{ - keys: [], - x: 10, - y: 10 - }); - - expect($rootScope.count).toBe(1); - - time = 100; - browserTrigger(element, 'click',{ - keys: [], - x: 10, - y: 10 - }); - - expect($rootScope.count).toBe(1); - })); - - - 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; - - $rootScope.$digest(); - - expect($rootScope.count1).toBe(0); - 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.count1).toBe(1); - - time = 100; - browserTrigger(element2, 'click',{ - keys: [], - x: 10, - y: 10 - }); - - expect($rootScope.count1).toBe(1); - expect($rootScope.count2).toBe(0); - })); - - - it('should not cancel clicks on distant elements', 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; - - $rootScope.$digest(); - - expect($rootScope.count1).toBe(0); - 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.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: 10, - y: 10 - }); - - 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); - })); - - - 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); - })); - - - 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); - - time = 500; - click(label, 10, 10); - click(input, 0, 0); - - expect($rootScope.selection).toBe('radio1'); - }); - - - it('should not cancel input clicks with negative coordinates', function() { - touch(otherElement, 100, 100); - - time = 500; - click(label, 10, 10); - click(input, -1, -1); - - expect($rootScope.selection).toBe('radio1'); - }); - - - 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); - - expect($rootScope.selection).toBe('radio1'); - }); - - - 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); - - expect($rootScope.selection).toBe('initial'); - }); - - - it('should blur the other element on click', function() { - var blurSpy = spyOn(otherElement, 'blur'); - touch(otherElement, 10, 10); - - time = 500; - click(label, 10, 10); - - expect(blurSpy).toHaveBeenCalled(); - }); - }); - }); - - - 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(); - - browserTrigger(element, 'click'); - expect($rootScope.tapped).toEqual(true); - })); - - - it('should pass event object', inject(function($rootScope, $compile) { - element = $compile('
')($rootScope); - $rootScope.$digest(); - - 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(); - - 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(); - - 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); - - 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); - - 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); - - browserTrigger(element, 'touchstart',{ - keys: [], - x: 10, - y: 10 - }); - browserTrigger(element, 'touchend',{ - keys: [], - x: 10, - y: 10 - }); - - expect($rootScope.event).toBeDefined(); - })); - }); - - - describe('the normal click event', function() { - it('should be capturable by other handlers', inject(function($rootScope, $compile) { - var called = false; - - element = $compile('
')($rootScope); - - element.on('click', function() { - called = true; - }); - - browserTrigger(element, 'touchstart',{ - keys: [], - x: 10, - y: 10 - }); - browserTrigger(element, 'touchend',{ - keys: [], - x: 10, - y: 10 - }); - - expect(called).toEqual(true); - })); - }); - }); -});