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

fix(autocomplete): improve handling of touch pads and touchscreens #11782

Merged
merged 1 commit into from
Aug 26, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions docs/app/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,9 @@ function(SERVICES, COMPONENTS, DEMOS, PAGES,

}])

.config(['AngularyticsProvider', function(AngularyticsProvider) {
AngularyticsProvider.setEventHandlers(['GoogleUniversal']);
.config(['$mdGestureProvider', 'AngularyticsProvider', function($mdGestureProvider, AngularyticsProvider) {
$mdGestureProvider.skipClickHijack();
AngularyticsProvider.setEventHandlers(['GoogleUniversal']);
}])

.run(['$rootScope', '$window', 'Angularytics', function($rootScope, $window, Angularytics) {
Expand Down
8 changes: 3 additions & 5 deletions src/components/autocomplete/autocomplete.scss
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ md-autocomplete {
}

.md-show-clear-button {

button {
display: block;
position: absolute;
Expand All @@ -43,10 +42,8 @@ md-autocomplete {
@include rtl-prop(padding-right, padding-left, $md-autocomplete-clear-size, 0);
}
}

}
md-autocomplete-wrap {

// Layout [layout='row']
display: flex;
flex-direction: row;
Expand All @@ -59,9 +56,10 @@ md-autocomplete {
z-index: $z-index-backdrop + 1;
}

md-input-container, input {
md-input-container,
input {
// Layout [flex]
flex: 1 1 0%;
flex: 1 1 0;
box-sizing: border-box;
min-width : 0;
}
Expand Down
98 changes: 61 additions & 37 deletions src/components/autocomplete/js/autocompleteController.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming,
ctrl.select = select;
ctrl.listEnter = onListEnter;
ctrl.listLeave = onListLeave;
ctrl.mouseUp = onMouseup;
ctrl.focusInput = focusInputElement;
ctrl.getCurrentDisplayValue = getCurrentDisplayValue;
ctrl.registerSelectedItemWatcher = registerSelectedItemWatcher;
ctrl.unregisterSelectedItemWatcher = unregisterSelectedItemWatcher;
Expand Down Expand Up @@ -103,6 +103,10 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming,
gatherElements();
moveDropdown();

// Touch devices often do not send a click event on tap. We still want to focus the input
// and open the options pop-up in these cases.
$element.on('touchstart', focusInputElement);

// Forward all focus events to the input element when autofocus is enabled
if ($scope.autofocus) {
$element.on('focus', focusInputElement);
Expand Down Expand Up @@ -366,12 +370,31 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming,

// event/change handlers

/**
* @param {Event} $event
*/
function preventDefault($event) {
$event.preventDefault();
}

/**
* @param {Event} $event
*/
function stopPropagation($event) {
$event.stopPropagation();
}

/**
* Handles changes to the `hidden` property.
* @param {boolean} hidden
* @param {boolean} oldHidden
* @param {boolean} hidden true to hide the options pop-up, false to show it.
* @param {boolean} oldHidden the previous value of hidden
*/
function handleHiddenChange (hidden, oldHidden) {
var scrollContainerElement;

if (elements) {
scrollContainerElement = angular.element(elements.scrollContainer);
}
if (!hidden && oldHidden) {
positionDropdown();

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

if (elements) {
$mdUtil.disableScrollAround(elements.ul);
enableWrapScroll = disableElementScrollEvents(angular.element(elements.wrap));
ctrl.documentElement.on('click', handleClickOutside);
$mdUtil.disableScrollAround(elements.scrollContainer);
enableWrapScroll = disableElementScrollEvents(elements.wrap);
if ($mdUtil.isIos) {
ctrl.documentElement.on('touchend', handleTouchOutsidePanel);
if (scrollContainerElement) {
scrollContainerElement.on('touchstart touchmove touchend', stopPropagation);
}
}
$mdUtil.nextTick(updateActiveOption);
}
} else if (hidden && !oldHidden) {
ctrl.documentElement.off('click', handleClickOutside);
if ($mdUtil.isIos) {
ctrl.documentElement.off('touchend', handleTouchOutsidePanel);
if (scrollContainerElement) {
scrollContainerElement.off('touchstart touchmove touchend', stopPropagation);
}
}
$mdUtil.enableScrolling();

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

/**
* Handling click events that bubble up to the document is required for closing the dropdown
* panel on click outside of the panel on iOS.
* Handling touch events that bubble up to the document is required for closing the dropdown
* panel on touch outside of the options pop-up panel on iOS.
* @param {Event} $event
*/
function handleClickOutside($event) {
function handleTouchOutsidePanel($event) {
ctrl.hidden = true;
// iOS does not blur the pop-up for touches on the scroll mask, so we have to do it.
doBlur(true);
}

/**
* Disables scrolling for a specific element
* Disables scrolling for a specific element.
* @param {!string|!DOMElement} element to disable scrolling
* @return {Function} function to call to re-enable scrolling for the element
*/
function disableElementScrollEvents(element) {

function preventDefault(e) {
e.preventDefault();
}

element.on('wheel', preventDefault);
element.on('touchmove', preventDefault);
var elementToDisable = angular.element(element);
elementToDisable.on('wheel touchmove', preventDefault);

return function() {
element.off('wheel', preventDefault);
element.off('touchmove', preventDefault);
elementToDisable.off('wheel touchmove', preventDefault);
};
}

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

/**
* When the mouse button is released, send focus back to the input field.
*/
function onMouseup () {
elements.input.focus();
}

/**
* Handles changes to the selected item.
* @param selectedItem
Expand Down Expand Up @@ -673,7 +697,7 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming,

/**
* Returns the display value for an item.
* @param item
* @param {*} item
* @returns {*}
*/
function getDisplayValue (item) {
Expand All @@ -689,7 +713,7 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming,
/**
* Getter function to invoke user-defined expression (in the directive)
* to convert your object to a single string.
* @param item
* @param {*} item
* @returns {string|null}
*/
function getItemText (item) {
Expand All @@ -699,7 +723,7 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming,

/**
* Returns the locals object for compiling item templates.
* @param item
* @param {*} item
* @returns {Object|undefined}
*/
function getItemAsNameVal (item) {
Expand Down Expand Up @@ -837,14 +861,14 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming,
* Defines a public property with a handler and a default value.
* @param {string} key
* @param {Function} handler function
* @param {*} value default value
* @param {*} defaultValue default value
*/
function defineProperty (key, handler, value) {
function defineProperty (key, handler, defaultValue) {
Object.defineProperty(ctrl, key, {
get: function () { return value; },
get: function () { return defaultValue; },
set: function (newValue) {
var oldValue = value;
value = newValue;
var oldValue = defaultValue;
defaultValue = newValue;
handler(newValue, oldValue);
}
});
Expand Down Expand Up @@ -1014,7 +1038,7 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming,
function updateVirtualScroll() {
// elements in virtual scroll have consistent heights
var optionHeight = elements.li[0].offsetHeight,
top = optionHeight * ctrl.index,
top = optionHeight * Math.max(0, ctrl.index),
bottom = top + optionHeight,
containerHeight = elements.scroller.clientHeight,
scrollTop = elements.scroller.scrollTop;
Expand All @@ -1028,7 +1052,7 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming,

function updateStandardScroll() {
// elements in standard scroll have variable heights
var selected = elements.li[ctrl.index] || elements.li[0];
var selected = elements.li[Math.max(0, ctrl.index)];
var containerHeight = elements.scrollContainer.offsetHeight,
top = selected && selected.offsetTop || 0,
bottom = top + selected.clientHeight,
Expand Down
8 changes: 5 additions & 3 deletions src/components/autocomplete/js/autocompleteDirective.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ angular
* @param {number=} md-delay Specifies the amount of time (in milliseconds) to wait before looking
* for results.
* @param {boolean=} md-clear-button Whether the clear button for the autocomplete input should show
* up or not.
* up or not. When `md-floating-label` is set, defaults to false, defaults to true otherwise.
* @param {boolean=} md-autofocus If true, the autocomplete will be automatically focused when a
* `$mdDialog`, `$mdBottomsheet` or `$mdSidenav`, which contains the autocomplete, is opening.
* <br/><br/>
Expand Down Expand Up @@ -365,7 +365,7 @@ function MdAutocomplete ($$mdSvgRegistry) {

// Stop click events from bubbling up to the document and triggering a flicker of the
// options panel while still supporting ng-click to be placed on md-autocomplete.
element.on('click', function(event) {
element.on('click touchstart touchend', function(event) {
event.stopPropagation();
});
};
Expand Down Expand Up @@ -402,7 +402,7 @@ function MdAutocomplete ($$mdSvgRegistry) {
id="ul-{{$mdAutocompleteCtrl.id}}"\
ng-mouseenter="$mdAutocompleteCtrl.listEnter()"\
ng-mouseleave="$mdAutocompleteCtrl.listLeave()"\
ng-mouseup="$mdAutocompleteCtrl.mouseUp()"\
ng-mouseup="$mdAutocompleteCtrl.focusInput()"\
role="listbox">\
<li class="md-autocomplete-suggestion" ' + getRepeatType(attr.mdMode) + ' ="item in $mdAutocompleteCtrl.matches"\
ng-class="{ selected: $index === $mdAutocompleteCtrl.index }"\
Expand Down Expand Up @@ -496,6 +496,7 @@ function MdAutocomplete ($$mdSvgRegistry) {
ng-disabled="$mdAutocompleteCtrl.isDisabled"\
ng-model="$mdAutocompleteCtrl.scope.searchText"\
ng-model-options="{ allowInvalid: true }"\
ng-mousedown="$mdAutocompleteCtrl.focusInput()"\
ng-keydown="$mdAutocompleteCtrl.keydown($event)"\
ng-blur="$mdAutocompleteCtrl.blur($event)"\
ng-focus="$mdAutocompleteCtrl.focus($event)"\
Expand Down Expand Up @@ -523,6 +524,7 @@ function MdAutocomplete ($$mdSvgRegistry) {
ng-minlength="inputMinlength"\
ng-maxlength="inputMaxlength"\
ng-model="$mdAutocompleteCtrl.scope.searchText"\
ng-mousedown="$mdAutocompleteCtrl.focusInput()"\
ng-keydown="$mdAutocompleteCtrl.keydown($event)"\
ng-blur="$mdAutocompleteCtrl.blur($event)"\
ng-focus="$mdAutocompleteCtrl.focus($event)"\
Expand Down
40 changes: 19 additions & 21 deletions src/core/services/gesture/gesture.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,18 @@ var forceSkipClickHijack = false, disableAllGestures = false;
*/
var lastLabelClickPos = null;

// Used to attach event listeners once when multiple ng-apps are running.
/**
* Used to attach event listeners once when multiple ng-apps are running.
* @type {boolean}
*/
var isInitialized = false;

// Support material-tools builds.
if (window.navigator) {
var userAgent = window.navigator.userAgent || window.navigator.vendor || window.opera;
var isIos = userAgent.match(/ipad|iphone|ipod/i);
var isAndroid = userAgent.match(/android/i);
}

/**
* @ngdoc module
* @name material.core.gestures
* @description
* AngularJS Material Gesture handling for touch devices. This module replaced the usage of the hammerjs library.
* AngularJS Material Gesture handling for touch devices.
* This module replaced the usage of the HammerJS library.
*/
angular
.module('material.core.gestures', [])
Expand All @@ -43,10 +40,11 @@ angular
*
* @description
* In some scenarios on mobile devices (without jQuery), the click events should NOT be hijacked.
* `$mdGestureProvider` is used to configure the Gesture module to ignore or skip click hijacking on mobile
* devices.
* `$mdGestureProvider` is used to configure the Gesture module to ignore or skip click hijacking
* on mobile devices.
*
* You can also change the max click distance, `6px` by default, if you have issues on some touch screens.
* You can also change the max click distance, `6px` by default, if you have issues on some touch
* screens.
*
* <hljs lang="js">
* app.config(function($mdGestureProvider) {
Expand Down Expand Up @@ -105,8 +103,8 @@ MdGestureProvider.prototype = {
* $get is used to build an instance of $mdGesture
* @ngInject
*/
$get : function($$MdGestureHandler, $$rAF, $timeout) {
return new MdGesture($$MdGestureHandler, $$rAF, $timeout);
$get : function($$MdGestureHandler, $$rAF, $timeout, $mdUtil) {
return new MdGesture($$MdGestureHandler, $$rAF, $timeout, $mdUtil);
}
};

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

var self = {
handler: addHandler,
register: register,
isAndroid: isAndroid,
isIos: isIos,
isAndroid: $mdUtil.isAndroid,
isIos: $mdUtil.isIos,
// On mobile w/out jQuery, we normally intercept clicks. Should we skip that?
isHijackingClicks: (isIos || isAndroid) && !hasJQuery && !forceSkipClickHijack
isHijackingClicks: ($mdUtil.isIos || $mdUtil.isAndroid) && !hasJQuery && !forceSkipClickHijack
};

if (self.isHijackingClicks) {
Expand Down Expand Up @@ -575,7 +573,7 @@ function MdGestureHandler() {
* Attach Gestures: hook document and check shouldHijack clicks
* @ngInject
*/
function attachToDocument($mdGesture, $$MdGestureHandler) {
function attachToDocument($mdGesture, $$MdGestureHandler, $mdUtil) {
if (disableAllGestures) {
return;
}
Expand Down Expand Up @@ -623,7 +621,7 @@ function attachToDocument($mdGesture, $$MdGestureHandler) {
*/
function clickHijacker(ev) {
var isKeyClick;
if (isIos) {
if ($mdUtil.isIos) {
isKeyClick = angular.isDefined(ev.webkitForce) && ev.webkitForce === 0;
} else {
isKeyClick = ev.clientX === 0 && ev.clientY === 0;
Expand Down
Loading