Skip to content
This repository was archived by the owner on Sep 5, 2024. It is now read-only.

Commit 82858b3

Browse files
committed
fix(autocomplete): improve handling of touch pads and touchscreens
- open options pop-up on `touchstart` - since a `click` is often not sent on touch devices - this usually happens when the start/end point are not the same - use `touchend` on the document to close the options panel on iOS - iOS mostly does not send `click` events for taps on the backdrop - call `doBlur()`` since iOS doesn't blur in this case - combine some jQuery event handler calls - combine duplicate `onMouseup()` and `focusInputElement()` functions - don't let touchstart or touchend events bubble out of the component - focus the input for `mousedown` events - this covers an edge case on touch pads where a `click` isn't sent - move `isIos` and `isAndroid` logic out of gestures into `$mdUtil` - add and correct JSDoc Fixes #11778. Relates to #11625. Relates to #11757. Relates to #11758.
1 parent 8c159aa commit 82858b3

File tree

5 files changed

+130
-82
lines changed

5 files changed

+130
-82
lines changed

docs/app/js/app.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -144,8 +144,9 @@ function(SERVICES, COMPONENTS, DEMOS, PAGES,
144144

145145
}])
146146

147-
.config(['AngularyticsProvider', function(AngularyticsProvider) {
148-
AngularyticsProvider.setEventHandlers(['GoogleUniversal']);
147+
.config(['$mdGestureProvider', 'AngularyticsProvider', function($mdGestureProvider, AngularyticsProvider) {
148+
$mdGestureProvider.skipClickHijack();
149+
AngularyticsProvider.setEventHandlers(['GoogleUniversal']);
149150
}])
150151

151152
.run(['$rootScope', '$window', 'Angularytics', function($rootScope, $window, Angularytics) {

src/components/autocomplete/js/autocompleteController.js

+56-32
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming,
6363
ctrl.select = select;
6464
ctrl.listEnter = onListEnter;
6565
ctrl.listLeave = onListLeave;
66-
ctrl.mouseUp = onMouseup;
66+
ctrl.focusInput = focusInputElement;
6767
ctrl.getCurrentDisplayValue = getCurrentDisplayValue;
6868
ctrl.registerSelectedItemWatcher = registerSelectedItemWatcher;
6969
ctrl.unregisterSelectedItemWatcher = unregisterSelectedItemWatcher;
@@ -103,6 +103,10 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming,
103103
gatherElements();
104104
moveDropdown();
105105

106+
// Touch devices often do not send a click event on tap. We still want to focus the input
107+
// and open the options pop-up in these cases.
108+
$element.on('touchstart', focusInputElement);
109+
106110
// Forward all focus events to the input element when autofocus is enabled
107111
if ($scope.autofocus) {
108112
$element.on('focus', focusInputElement);
@@ -366,12 +370,31 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming,
366370

367371
// event/change handlers
368372

373+
/**
374+
* @param {Event} $event
375+
*/
376+
function preventDefault($event) {
377+
$event.preventDefault();
378+
}
379+
380+
/**
381+
* @param {Event} $event
382+
*/
383+
function stopPropagation($event) {
384+
$event.stopPropagation();
385+
}
386+
369387
/**
370388
* Handles changes to the `hidden` property.
371-
* @param {boolean} hidden
372-
* @param {boolean} oldHidden
389+
* @param {boolean} hidden true to hide the options pop-up, false to show it.
390+
* @param {boolean} oldHidden the previous value of hidden
373391
*/
374392
function handleHiddenChange (hidden, oldHidden) {
393+
var scrollContainerElement;
394+
395+
if (elements) {
396+
scrollContainerElement = angular.element(elements.scrollContainer);
397+
}
375398
if (!hidden && oldHidden) {
376399
positionDropdown();
377400

@@ -380,13 +403,23 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming,
380403
reportMessages(true, ReportType.Count | ReportType.Selected);
381404

382405
if (elements) {
383-
$mdUtil.disableScrollAround(elements.ul);
384-
enableWrapScroll = disableElementScrollEvents(angular.element(elements.wrap));
385-
ctrl.documentElement.on('click', handleClickOutside);
406+
$mdUtil.disableScrollAround(elements.scrollContainer);
407+
enableWrapScroll = disableElementScrollEvents(elements.wrap);
408+
if ($mdUtil.isIos) {
409+
ctrl.documentElement.on('touchend', handleTouchOutsidePanel);
410+
if (scrollContainerElement) {
411+
scrollContainerElement.on('touchstart touchmove touchend', stopPropagation);
412+
}
413+
}
386414
$mdUtil.nextTick(updateActiveOption);
387415
}
388416
} else if (hidden && !oldHidden) {
389-
ctrl.documentElement.off('click', handleClickOutside);
417+
if ($mdUtil.isIos) {
418+
ctrl.documentElement.off('touchend', handleTouchOutsidePanel);
419+
if (scrollContainerElement) {
420+
scrollContainerElement.off('touchstart touchmove touchend', stopPropagation);
421+
}
422+
}
390423
$mdUtil.enableScrolling();
391424

392425
if (enableWrapScroll) {
@@ -397,29 +430,27 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming,
397430
}
398431

399432
/**
400-
* Handling click events that bubble up to the document is required for closing the dropdown
401-
* panel on click outside of the panel on iOS.
433+
* Handling touch events that bubble up to the document is required for closing the dropdown
434+
* panel on touch outside of the options pop-up panel on iOS.
402435
* @param {Event} $event
403436
*/
404-
function handleClickOutside($event) {
437+
function handleTouchOutsidePanel($event) {
405438
ctrl.hidden = true;
439+
// iOS does not blur the pop-up for touches on the scroll mask, so we have to do it.
440+
doBlur(true);
406441
}
407442

408443
/**
409-
* Disables scrolling for a specific element
444+
* Disables scrolling for a specific element.
445+
* @param {!string|!DOMElement} element to disable scrolling
446+
* @return {Function} function to call to re-enable scrolling for the element
410447
*/
411448
function disableElementScrollEvents(element) {
412-
413-
function preventDefault(e) {
414-
e.preventDefault();
415-
}
416-
417-
element.on('wheel', preventDefault);
418-
element.on('touchmove', preventDefault);
449+
var elementToDisable = angular.element(element);
450+
elementToDisable.on('wheel touchmove', preventDefault);
419451

420452
return function() {
421-
element.off('wheel', preventDefault);
422-
element.off('touchmove', preventDefault);
453+
elementToDisable.off('wheel touchmove', preventDefault);
423454
};
424455
}
425456

@@ -439,13 +470,6 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming,
439470
ctrl.hidden = shouldHide();
440471
}
441472

442-
/**
443-
* When the mouse button is released, send focus back to the input field.
444-
*/
445-
function onMouseup () {
446-
elements.input.focus();
447-
}
448-
449473
/**
450474
* Handles changes to the selected item.
451475
* @param selectedItem
@@ -837,14 +861,14 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming,
837861
* Defines a public property with a handler and a default value.
838862
* @param {string} key
839863
* @param {Function} handler function
840-
* @param {*} value default value
864+
* @param {*} defaultValue default value
841865
*/
842-
function defineProperty (key, handler, value) {
866+
function defineProperty (key, handler, defaultValue) {
843867
Object.defineProperty(ctrl, key, {
844-
get: function () { return value; },
868+
get: function () { return defaultValue; },
845869
set: function (newValue) {
846-
var oldValue = value;
847-
value = newValue;
870+
var oldValue = defaultValue;
871+
defaultValue = newValue;
848872
handler(newValue, oldValue);
849873
}
850874
});

src/components/autocomplete/js/autocompleteDirective.js

+4-2
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,7 @@ function MdAutocomplete ($$mdSvgRegistry) {
365365

366366
// Stop click events from bubbling up to the document and triggering a flicker of the
367367
// options panel while still supporting ng-click to be placed on md-autocomplete.
368-
element.on('click', function(event) {
368+
element.on('click touchstart touchend', function(event) {
369369
event.stopPropagation();
370370
});
371371
};
@@ -402,7 +402,7 @@ function MdAutocomplete ($$mdSvgRegistry) {
402402
id="ul-{{$mdAutocompleteCtrl.id}}"\
403403
ng-mouseenter="$mdAutocompleteCtrl.listEnter()"\
404404
ng-mouseleave="$mdAutocompleteCtrl.listLeave()"\
405-
ng-mouseup="$mdAutocompleteCtrl.mouseUp()"\
405+
ng-mouseup="$mdAutocompleteCtrl.focusInput()"\
406406
role="listbox">\
407407
<li class="md-autocomplete-suggestion" ' + getRepeatType(attr.mdMode) + ' ="item in $mdAutocompleteCtrl.matches"\
408408
ng-class="{ selected: $index === $mdAutocompleteCtrl.index }"\
@@ -496,6 +496,7 @@ function MdAutocomplete ($$mdSvgRegistry) {
496496
ng-disabled="$mdAutocompleteCtrl.isDisabled"\
497497
ng-model="$mdAutocompleteCtrl.scope.searchText"\
498498
ng-model-options="{ allowInvalid: true }"\
499+
ng-mousedown="$mdAutocompleteCtrl.focusInput()"\
499500
ng-keydown="$mdAutocompleteCtrl.keydown($event)"\
500501
ng-blur="$mdAutocompleteCtrl.blur($event)"\
501502
ng-focus="$mdAutocompleteCtrl.focus($event)"\
@@ -523,6 +524,7 @@ function MdAutocomplete ($$mdSvgRegistry) {
523524
ng-minlength="inputMinlength"\
524525
ng-maxlength="inputMaxlength"\
525526
ng-model="$mdAutocompleteCtrl.scope.searchText"\
527+
ng-mousedown="$mdAutocompleteCtrl.focusInput()"\
526528
ng-keydown="$mdAutocompleteCtrl.keydown($event)"\
527529
ng-blur="$mdAutocompleteCtrl.blur($event)"\
528530
ng-focus="$mdAutocompleteCtrl.focus($event)"\

src/core/services/gesture/gesture.js

+19-21
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,18 @@ var forceSkipClickHijack = false, disableAllGestures = false;
1414
*/
1515
var lastLabelClickPos = null;
1616

17-
// Used to attach event listeners once when multiple ng-apps are running.
17+
/**
18+
* Used to attach event listeners once when multiple ng-apps are running.
19+
* @type {boolean}
20+
*/
1821
var isInitialized = false;
1922

20-
// Support material-tools builds.
21-
if (window.navigator) {
22-
var userAgent = window.navigator.userAgent || window.navigator.vendor || window.opera;
23-
var isIos = userAgent.match(/ipad|iphone|ipod/i);
24-
var isAndroid = userAgent.match(/android/i);
25-
}
26-
2723
/**
2824
* @ngdoc module
2925
* @name material.core.gestures
3026
* @description
31-
* AngularJS Material Gesture handling for touch devices. This module replaced the usage of the hammerjs library.
27+
* AngularJS Material Gesture handling for touch devices.
28+
* This module replaced the usage of the HammerJS library.
3229
*/
3330
angular
3431
.module('material.core.gestures', [])
@@ -43,10 +40,11 @@ angular
4340
*
4441
* @description
4542
* In some scenarios on mobile devices (without jQuery), the click events should NOT be hijacked.
46-
* `$mdGestureProvider` is used to configure the Gesture module to ignore or skip click hijacking on mobile
47-
* devices.
43+
* `$mdGestureProvider` is used to configure the Gesture module to ignore or skip click hijacking
44+
* on mobile devices.
4845
*
49-
* You can also change the max click distance, `6px` by default, if you have issues on some touch screens.
46+
* You can also change the max click distance, `6px` by default, if you have issues on some touch
47+
* screens.
5048
*
5149
* <hljs lang="js">
5250
* app.config(function($mdGestureProvider) {
@@ -105,8 +103,8 @@ MdGestureProvider.prototype = {
105103
* $get is used to build an instance of $mdGesture
106104
* @ngInject
107105
*/
108-
$get : function($$MdGestureHandler, $$rAF, $timeout) {
109-
return new MdGesture($$MdGestureHandler, $$rAF, $timeout);
106+
$get : function($$MdGestureHandler, $$rAF, $timeout, $mdUtil) {
107+
return new MdGesture($$MdGestureHandler, $$rAF, $timeout, $mdUtil);
110108
}
111109
};
112110

@@ -116,17 +114,17 @@ MdGestureProvider.prototype = {
116114
* MdGesture factory construction function
117115
* @ngInject
118116
*/
119-
function MdGesture($$MdGestureHandler, $$rAF, $timeout) {
117+
function MdGesture($$MdGestureHandler, $$rAF, $timeout, $mdUtil) {
120118
var touchActionProperty = getTouchAction();
121-
var hasJQuery = (typeof window.jQuery !== 'undefined') && (angular.element === window.jQuery);
119+
var hasJQuery = (typeof window.jQuery !== 'undefined') && (angular.element === window.jQuery);
122120

123121
var self = {
124122
handler: addHandler,
125123
register: register,
126-
isAndroid: isAndroid,
127-
isIos: isIos,
124+
isAndroid: $mdUtil.isAndroid,
125+
isIos: $mdUtil.isIos,
128126
// On mobile w/out jQuery, we normally intercept clicks. Should we skip that?
129-
isHijackingClicks: (isIos || isAndroid) && !hasJQuery && !forceSkipClickHijack
127+
isHijackingClicks: ($mdUtil.isIos || $mdUtil.isAndroid) && !hasJQuery && !forceSkipClickHijack
130128
};
131129

132130
if (self.isHijackingClicks) {
@@ -575,7 +573,7 @@ function MdGestureHandler() {
575573
* Attach Gestures: hook document and check shouldHijack clicks
576574
* @ngInject
577575
*/
578-
function attachToDocument($mdGesture, $$MdGestureHandler) {
576+
function attachToDocument($mdGesture, $$MdGestureHandler, $mdUtil) {
579577
if (disableAllGestures) {
580578
return;
581579
}
@@ -623,7 +621,7 @@ function attachToDocument($mdGesture, $$MdGestureHandler) {
623621
*/
624622
function clickHijacker(ev) {
625623
var isKeyClick;
626-
if (isIos) {
624+
if ($mdUtil.isIos) {
627625
isKeyClick = angular.isDefined(ev.webkitForce) && ev.webkitForce === 0;
628626
} else {
629627
isKeyClick = ev.clientX === 0 && ev.clientY === 0;

0 commit comments

Comments
 (0)