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

Commit 20c4d3f

Browse files
Splaktarjelbourn
authored andcommitted
fix(autocomplete): improve handling of touch pads and touchscreens (#11782)
- 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 7eb418b commit 20c4d3f

File tree

6 files changed

+139
-93
lines changed

6 files changed

+139
-93
lines changed

docs/app/js/app.js

Lines changed: 3 additions & 2 deletions
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/autocomplete.scss

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ md-autocomplete {
2828
}
2929

3030
.md-show-clear-button {
31-
3231
button {
3332
display: block;
3433
position: absolute;
@@ -43,10 +42,8 @@ md-autocomplete {
4342
@include rtl-prop(padding-right, padding-left, $md-autocomplete-clear-size, 0);
4443
}
4544
}
46-
4745
}
4846
md-autocomplete-wrap {
49-
5047
// Layout [layout='row']
5148
display: flex;
5249
flex-direction: row;
@@ -59,9 +56,10 @@ md-autocomplete {
5956
z-index: $z-index-backdrop + 1;
6057
}
6158

62-
md-input-container, input {
59+
md-input-container,
60+
input {
6361
// Layout [flex]
64-
flex: 1 1 0%;
62+
flex: 1 1 0;
6563
box-sizing: border-box;
6664
min-width : 0;
6765
}

src/components/autocomplete/js/autocompleteController.js

Lines changed: 61 additions & 37 deletions
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
@@ -673,7 +697,7 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming,
673697

674698
/**
675699
* Returns the display value for an item.
676-
* @param item
700+
* @param {*} item
677701
* @returns {*}
678702
*/
679703
function getDisplayValue (item) {
@@ -689,7 +713,7 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming,
689713
/**
690714
* Getter function to invoke user-defined expression (in the directive)
691715
* to convert your object to a single string.
692-
* @param item
716+
* @param {*} item
693717
* @returns {string|null}
694718
*/
695719
function getItemText (item) {
@@ -699,7 +723,7 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming,
699723

700724
/**
701725
* Returns the locals object for compiling item templates.
702-
* @param item
726+
* @param {*} item
703727
* @returns {Object|undefined}
704728
*/
705729
function getItemAsNameVal (item) {
@@ -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
});
@@ -1014,7 +1038,7 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming,
10141038
function updateVirtualScroll() {
10151039
// elements in virtual scroll have consistent heights
10161040
var optionHeight = elements.li[0].offsetHeight,
1017-
top = optionHeight * ctrl.index,
1041+
top = optionHeight * Math.max(0, ctrl.index),
10181042
bottom = top + optionHeight,
10191043
containerHeight = elements.scroller.clientHeight,
10201044
scrollTop = elements.scroller.scrollTop;
@@ -1028,7 +1052,7 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming,
10281052

10291053
function updateStandardScroll() {
10301054
// elements in standard scroll have variable heights
1031-
var selected = elements.li[ctrl.index] || elements.li[0];
1055+
var selected = elements.li[Math.max(0, ctrl.index)];
10321056
var containerHeight = elements.scrollContainer.offsetHeight,
10331057
top = selected && selected.offsetTop || 0,
10341058
bottom = top + selected.clientHeight,

src/components/autocomplete/js/autocompleteDirective.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ angular
9898
* @param {number=} md-delay Specifies the amount of time (in milliseconds) to wait before looking
9999
* for results.
100100
* @param {boolean=} md-clear-button Whether the clear button for the autocomplete input should show
101-
* up or not.
101+
* up or not. When `md-floating-label` is set, defaults to false, defaults to true otherwise.
102102
* @param {boolean=} md-autofocus If true, the autocomplete will be automatically focused when a
103103
* `$mdDialog`, `$mdBottomsheet` or `$mdSidenav`, which contains the autocomplete, is opening.
104104
* <br/><br/>
@@ -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

Lines changed: 19 additions & 21 deletions
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)