diff --git a/src/components/autocomplete/autocomplete.spec.js b/src/components/autocomplete/autocomplete.spec.js index af5d9ff713..68a2ba8766 100644 --- a/src/components/autocomplete/autocomplete.spec.js +++ b/src/components/autocomplete/autocomplete.spec.js @@ -58,7 +58,7 @@ describe('', function() { } describe('basic functionality', function() { - it('should update selected item and search text', inject(function($timeout, $mdConstant, $material) { + it('updates selected item and search text', inject(function($timeout, $mdConstant, $material) { var scope = createScope(); var template = '\ ', function() { element.remove(); })); - - it('should allow you to set an input id without floating label', inject(function() { + it('allows you to set an input id without floating label', inject(function() { var scope = createScope(null, {inputId: 'custom-input-id'}); var template = '\ ', function() { element.remove(); })); - it('should allow allow using ng-readonly', inject(function() { + it('allows using ng-readonly', inject(function() { var scope = createScope(null, {inputId: 'custom-input-id'}); var template = '\ ', function() { element.remove(); })); - it('should allow allow using an empty readonly attribute', inject(function() { + it('allows using an empty readonly attribute', inject(function() { var scope = createScope(null, {inputId: 'custom-input-id'}); var template = '\ ', function() { element.remove(); })); - it('should allow you to set an input id with floating label', inject(function() { + it('allows you to set an input id with floating label', inject(function() { var scope = createScope(null, {inputId: 'custom-input-id'}); var template = '\ ', function() { element.remove(); })); - it('should forward the `md-select-on-focus` attribute to the input', inject(function() { + it('forwards the `md-select-on-focus` attribute to the input', inject(function() { var scope = createScope(null, {inputId: 'custom-input-id'}); var template = '', function() { element.remove(); })); - it('should forward the tabindex to the input', inject(function() { + it('forwards the tabindex to the input', inject(function() { var scope = createScope(null, {inputId: 'custom-input-id'}); var template = '', function() { element.remove(); })); - it('should always set the tabindex of the autcomplete to `-1`', inject(function() { + it('always sets the tabindex of the autcomplete to `-1`', inject(function() { var scope = createScope(null, {inputId: 'custom-input-id'}); var template = '', function() { element.remove(); })); - it('should clear value when hitting escape', inject(function($mdConstant, $timeout) { + it('clears the value when hitting escape', inject(function($mdConstant, $timeout) { var scope = createScope(); var template = '\ ', function() { }); describe('basic functionality with template', function() { - it('should update selected item and search text', inject(function($timeout, $material, $mdConstant) { + it('updates selected item and search text', inject(function($timeout, $material, $mdConstant) { var scope = createScope(); var template = '\ ', function() { element.remove(); })); - it('should compile the template against the parent scope', inject(function($timeout, $material) { + it('properly clears values when the item ends in a space character', inject(function($timeout, $material, $mdConstant) { + var myItems = ['foo ', 'bar', 'baz'].map(function(item) { + return {display: item}; + }); + var scope = createScope(myItems); + + var template = '\ + \ + \ + {{item.display}}\ + \ + '; + var element = compile(template, scope); + var ctrl = element.controller('mdAutocomplete'); + var ul = element.find('ul'); + + expect(scope.searchText).toBe(''); + expect(scope.selectedItem).toBe(null); + + $material.flushInterimElement(); + + // Focus the input + ctrl.focus(); + + element.scope().searchText = 'fo'; + waitForVirtualRepeat(element); + + expect(scope.searchText).toBe('fo'); + expect(scope.match(scope.searchText).length).toBe(1); + expect(ul.find('li').length).toBe(1); + + ctrl.keydown(keydownEvent($mdConstant.KEY_CODE.DOWN_ARROW)); + ctrl.keydown(keydownEvent($mdConstant.KEY_CODE.ENTER)); + + $timeout.flush(); + + expect(scope.searchText).toBe('foo '); + expect(scope.selectedItem).toBe(scope.match(scope.searchText)[0]); + + ctrl.clear(); + $timeout.flush(); + + expect(scope.searchText).toBe(''); + expect(scope.selectedItem).toBe(null); + + element.remove(); + })); + + it('compiles the template against the parent scope', inject(function($timeout, $material) { var scope = createScope(null, {bang: 'boom'}); var template = '', function() { element.remove(); })); - it('should remove the md-scroll-mask on cleanup', inject(function($mdUtil, $timeout, $material) { + it('removes the md-scroll-mask on cleanup', inject(function($mdUtil, $timeout, $material) { spyOn($mdUtil, 'enableScrolling'); var scope = createScope(); @@ -513,7 +565,7 @@ describe('', function() { expect($mdUtil.enableScrolling).toHaveBeenCalled(); })); - it('should ensure the parent scope digests along with the current scope', inject(function($timeout, $material) { + it('ensures the parent scope digests along with the current scope', inject(function($timeout, $material) { var scope = createScope(null, {bang: 'boom'}); var template = '', function() { expect(ctrl2.hasNotFound).toBe(false); })); - it('should even show the md-not-found template if we have lost focus', inject(function($timeout) { + it('shows the md-not-found template even if we have lost focus', inject(function($timeout) { var scope = createScope(); var template = '', function() { describe('Async matching', function() { - it('should probably stop the loading indicator when clearing', inject(function($timeout, $material) { + it('properly stops the loading indicator when clearing', inject(function($timeout, $material) { var scope = createScope(); var template = '', function() { }); describe('API access', function() { - it('should clear the selected item', inject(function($timeout) { + it('clears the selected item', inject(function($timeout) { var scope = createScope(); var template = '\ ', function() { element.remove(); })); - it('should notify selected item watchers', inject(function($timeout) { + it('notifies selected item watchers', inject(function($timeout) { var scope = createScope(); scope.itemChanged = jasmine.createSpy('itemChanged'); @@ -919,7 +971,7 @@ describe('', function() { element.remove(); })); - it('should pass value to item watcher', inject(function($timeout) { + it('passes the value to the item watcher', inject(function($timeout) { var scope = createScope(); var itemValue = null; var template = '\ @@ -955,7 +1007,7 @@ describe('', function() { }); describe('md-select-on-match', function() { - it('should select matching item on exact match when `md-select-on-match` is toggled', inject(function($timeout) { + it('selects matching item on exact match when `md-select-on-match` is toggled', inject(function($timeout) { var scope = createScope(); var template = '\ ', function() { element.remove(); })); - it('should select matching item using case insensitive', inject(function($timeout) { + it('selects matching item using case insensitive', inject(function($timeout) { var scope = createScope(null, null, true); var template = '', function() { element.remove(); }); - it('should validate an empty `required` as true', function() { + it('validates an empty `required` as true', function() { var scope = createScope(); var template = '\ ', function() { expect(ctrl.isRequired).toBe(true); }); - it('should correctly validate an interpolated `ng-required` value', function() { + it('correctly validates an interpolated `ng-required` value', function() { var scope = createScope(); var template = '\ ', function() { expect(ctrl.isRequired).toBe(true); }); - it('should forward the md-no-asterisk attribute', function() { + it('forwards the md-no-asterisk attribute', function() { var scope = createScope(); var template = '\ ', function() { }); describe('md-highlight-text', function() { - it('should update when content is modified', inject(function() { + it('updates when content is modified', inject(function() { var template = '
{{message}}
'; var scope = createScope(null, {message: 'some text', query: 'some'}); var element = compile(template, scope); diff --git a/src/components/autocomplete/js/autocompleteController.js b/src/components/autocomplete/js/autocompleteController.js index 81de475f52..a680943089 100644 --- a/src/components/autocomplete/js/autocompleteController.js +++ b/src/components/autocomplete/js/autocompleteController.js @@ -421,7 +421,7 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming, /** * Handles input focus event, determines if the dropdown should show. */ - function focus () { + function focus($event) { hasFocus = true; //-- if searchText is null, let's force it to be a string if (!angular.isString($scope.searchText)) $scope.searchText = ''; @@ -639,9 +639,9 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming, /** * Clears the searchText value and selected item. */ - function clearValue () { + function clearValue ($event) { // Set the loading to true so we don't see flashes of content. - // The flashing will only occour when an async request is running. + // The flashing will only occur when an async request is running. // So the loading process will stop when the results had been retrieved. setLoading(true); @@ -652,9 +652,14 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming, // Per http://www.w3schools.com/jsref/event_oninput.asp var eventObj = document.createEvent('CustomEvent'); - eventObj.initCustomEvent('input', true, true, { value: $scope.searchText }); + eventObj.initCustomEvent('input', true, true, { value: '' }); elements.input.dispatchEvent(eventObj); + // For some reason, firing the above event resets the value of $scope.searchText if + // $scope.searchText has a space character at the end, so we blank it one more time and then + // focus. + elements.input.blur(); + $scope.searchText = ''; elements.input.focus(); } diff --git a/src/components/autocomplete/js/autocompleteDirective.js b/src/components/autocomplete/js/autocompleteDirective.js index 7149991e52..a3fb93947c 100644 --- a/src/components/autocomplete/js/autocompleteDirective.js +++ b/src/components/autocomplete/js/autocompleteDirective.js @@ -245,7 +245,7 @@ function MdAutocomplete () { ng-keydown="$mdAutocompleteCtrl.keydown($event)"\ ng-blur="$mdAutocompleteCtrl.blur()"\ ' + (attr.mdNoAsterisk != null ? 'md-no-asterisk="' + attr.mdNoAsterisk + '"' : '') + '\ - ng-focus="$mdAutocompleteCtrl.focus()"\ + ng-focus="$mdAutocompleteCtrl.focus($event)"\ aria-owns="ul-{{$mdAutocompleteCtrl.id}}"\ ' + (attr.mdSelectOnFocus != null ? 'md-select-on-focus=""' : '') + '\ aria-label="{{floatingLabel}}"\ @@ -270,7 +270,7 @@ function MdAutocomplete () { ng-model="$mdAutocompleteCtrl.scope.searchText"\ ng-keydown="$mdAutocompleteCtrl.keydown($event)"\ ng-blur="$mdAutocompleteCtrl.blur()"\ - ng-focus="$mdAutocompleteCtrl.focus()"\ + ng-focus="$mdAutocompleteCtrl.focus($event)"\ placeholder="{{placeholder}}"\ aria-owns="ul-{{$mdAutocompleteCtrl.id}}"\ ' + (attr.mdSelectOnFocus != null ? 'md-select-on-focus=""' : '') + '\ @@ -284,7 +284,7 @@ function MdAutocomplete () { type="button"\ tabindex="-1"\ ng-if="$mdAutocompleteCtrl.scope.searchText && !$mdAutocompleteCtrl.isDisabled"\ - ng-click="$mdAutocompleteCtrl.clear()">\ + ng-click="$mdAutocompleteCtrl.clear($event)">\ \ Clear\ \ diff --git a/src/components/showHide/showHide.js b/src/components/showHide/showHide.js index d6270cf6cc..0d5e363dc4 100644 --- a/src/components/showHide/showHide.js +++ b/src/components/showHide/showHide.js @@ -22,12 +22,19 @@ function createDirective(name, targetValue) { var unregister = $scope.$on('$md-resize-enable', function() { unregister(); + var cachedTransitionStyles = window.getComputedStyle($element[0]); + $scope.$watch($attr[name], function(value) { if (!!value === targetValue) { $mdUtil.nextTick(function() { $scope.$broadcast('$md-resize'); }); - $mdUtil.dom.animator.waitTransitionEnd($element).then(function() { + + var opts = { + cachedTransitionStyles: cachedTransitionStyles + }; + + $mdUtil.dom.animator.waitTransitionEnd($element, opts).then(function() { $scope.$broadcast('$md-resize'); }); } diff --git a/src/components/virtualRepeat/virtual-repeater.js b/src/components/virtualRepeat/virtual-repeater.js index e60b737971..70b941b508 100644 --- a/src/components/virtualRepeat/virtual-repeater.js +++ b/src/components/virtualRepeat/virtual-repeater.js @@ -612,6 +612,10 @@ VirtualRepeatController.prototype.repeatListExpression_ = function(scope) { VirtualRepeatController.prototype.containerUpdated = function() { // If itemSize is unknown, attempt to measure it. if (!this.itemSize) { + // Make sure to clean up watchers if we can (see #8178) + if(this.unwatchItemSize_ && this.unwatchItemSize_ !== angular.noop){ + this.unwatchItemSize_(); + } this.unwatchItemSize_ = this.$scope.$watchCollection( this.repeatListExpression, angular.bind(this, function(items) { diff --git a/src/core/util/animation/animate.js b/src/core/util/animation/animate.js index 63578d7197..c737e9db46 100644 --- a/src/core/util/animation/animate.js +++ b/src/core/util/animation/animate.js @@ -56,6 +56,13 @@ function AnimateDomUtils($mdUtil, $q, $timeout, $mdConstant, $animateCss) { return $q(function(resolve, reject){ opts = opts || { }; + // If there is no transition is found, resolve immediately + // + // NOTE: using $mdUtil.nextTick() causes delays/issues + if (noTransitionFound(opts.cachedTransitionStyles)) { + TIMEOUT = 0; + } + var timer = $timeout(finished, opts.timeout || TIMEOUT); element.on($mdConstant.CSS.TRANSITIONEND, finished); @@ -74,6 +81,20 @@ function AnimateDomUtils($mdUtil, $q, $timeout, $mdConstant, $animateCss) { } + /** + * Checks whether or not there is a transition. + * + * @param styles The cached styles to use for the calculation. If null, getComputedStyle() + * will be used. + * + * @returns {boolean} True if there is no transition/duration; false otherwise. + */ + function noTransitionFound(styles) { + styles = styles || window.getComputedStyle(element[0]); + + return styles.transitionDuration == '0s' || (!styles.transition && !styles.transitionProperty); + } + }); },