From ed65f887d7aaf71f5d8f66d108955a958339b5fa Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Tue, 26 Jan 2016 17:29:55 +0100 Subject: [PATCH] fix(ngTouch): remove ngClick override This commit removes the ngClick directive from the ngTouch module. 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 this feature has been removed: - "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 has been removed. 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 behavior, consider using [FastClick](https://github.com/ftlabs/fastclick) or [Tappy!](https://github.com/filamentgroup/tappy/). 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 for more info on the topic: http://developer.telerik.com/featured/300-ms-click-delay-ios-8/ --- angularFiles.js | 1 - src/ngTouch/directive/ngClick.js | 296 ------------ src/ngTouch/touch.js | 12 +- test/ngTouch/directive/ngClickSpec.js | 655 -------------------------- 4 files changed, 8 insertions(+), 956 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 6be3f249bb2f..50b0fa47a4eb 100755 --- a/angularFiles.js +++ b/angularFiles.js @@ -138,7 +138,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/src/ngTouch/directive/ngClick.js b/src/ngTouch/directive/ngClick.js deleted file mode 100644 index f352c252f443..000000000000 --- a/src/ngTouch/directive/ngClick.js +++ /dev/null @@ -1,296 +0,0 @@ -'use strict'; - -/* global ngTouch: false, - nodeName_: false -*/ - -/** - * @ngdoc directive - * @name ngClick - * - * @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']); - - - */ - -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', - 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 Angular - // 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 - 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 8191ff0b753a..50192c36ac44 100644 --- a/src/ngTouch/touch.js +++ b/src/ngTouch/touch.js @@ -7,12 +7,16 @@ * * # ngTouch * - * The `ngTouch` module provides touch events and other helpers for touch-enabled devices. - * The implementation is based on jQuery Mobile touch event handling - * ([jquerymobile.com](http://jquerymobile.com/)). + * The `ngTouch` module provides the touch-event based {@link ngTouch.$swipe `$swipe`} directive. * + *
+ * Angular 1.5.0 has **removed** the override to the `ngClick` directive that eliminates the 300ms delay + * after a tap on mobile browsers. It was removed because the implementation was unreliable, and because + * the 300ms delay is on its way out on modern mobile browsers. + * If you need this behavior, consider using [FastClick](https://github.com/ftlabs/fastclick) or + * [Tappy!](https://github.com/filamentgroup/tappy/) + *
* - * See {@link ngTouch.$swipe `$swipe`} for usage. * *
* diff --git a/test/ngTouch/directive/ngClickSpec.js b/test/ngTouch/directive/ngClickSpec.js deleted file mode 100644 index c9765b4c8df9..000000000000 --- a/test/ngTouch/directive/ngClickSpec.js +++ /dev/null @@ -1,655 +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(navigator.userAgent)) { - return; - } - - function mockTime() { - return time; - } - - - 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); - })); - - - 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); - })); - }); -});