From e41ef82b8566e8f34d928b4027117c371d3f64d0 Mon Sep 17 00:00:00 2001 From: Michael Prentice Date: Sun, 10 Mar 2019 02:15:49 -0500 Subject: [PATCH] fix(autocomplete): default dropdown to position bottom as default fix horizontal alignment edge cases fix $mdUtil.getViewportTop to handle when body scrolling is disabled Fixes #11656. Relates to #10859, #11629, #11575. --- .../autocomplete/autocomplete.spec.js | 54 +++++++++++++++++++ .../autocomplete/js/autocompleteController.js | 22 +++++--- src/core/util/util.js | 10 +++- 3 files changed, 79 insertions(+), 7 deletions(-) diff --git a/src/components/autocomplete/autocomplete.spec.js b/src/components/autocomplete/autocomplete.spec.js index bfa16fcf7ed..de2b962214e 100644 --- a/src/components/autocomplete/autocomplete.spec.js +++ b/src/components/autocomplete/autocomplete.spec.js @@ -3014,6 +3014,60 @@ describe('', function() { document.body.removeChild(parent[0]); })); + it('should default dropdown position to the bottom', inject(function($timeout, $window) { + var scope = createScope(); + scope.topMargin = '0px'; + + scope.match = fakeItemMatch; + + var template = '
' + + '' + + '{{item}}' + + '' + + '
'; + + var parent = compile(template, scope); + var element = parent.find('md-autocomplete'); + var ctrl = element.controller('mdAutocomplete'); + + // Add container to the DOM to be able to test the rect calculations. + document.body.appendChild(parent[0]); + + $timeout.flush(); + + // Focus the autocomplete and trigger a query to be able to open the dropdown. + ctrl.focus(); + scope.$apply('searchText = "Query 1"'); + waitForVirtualRepeat(element); + + var scrollContainer = document.body.querySelector('.md-virtual-repeat-container'); + + expect(scrollContainer).toBeTruthy(); + // Test that the dropdown displays with position = bottom automatically because there is no + // room above the element to display the dropdown using position = top. + expect(scrollContainer.style.bottom).toBe('auto'); + expect(scrollContainer.style.top).toMatch(/[0-9]+px/); + + // Change position and resize to force a DOM update. + scope.$apply('topMargin = "300px"'); + + angular.element($window).triggerHandler('resize'); + $timeout.flush(); + + expect(scrollContainer).toBeTruthy(); + // Test that the dropdown displays with position = bottom by default, even when there is room + // for it to display on the top. + expect(scrollContainer.style.bottom).toBe('auto'); + expect(scrollContainer.style.top).toMatch(/[0-9]+px/); + + parent.remove(); + })); + it('should allow dropdown position to be specified (virtual list)', inject(function($timeout, $window) { var scope = createScope(); diff --git a/src/components/autocomplete/js/autocompleteController.js b/src/components/autocomplete/js/autocompleteController.js index ecb7644523d..ab4e7626dc1 100644 --- a/src/components/autocomplete/js/autocompleteController.js +++ b/src/components/autocomplete/js/autocompleteController.js @@ -139,11 +139,21 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming, width = hrect.width, offset = getVerticalOffset(), position = $scope.dropdownPosition, - styles; + styles, enoughBottomSpace, enoughTopSpace; + var bottomSpace = root.bottom - vrect.bottom - MENU_PADDING + $mdUtil.getViewportTop(); + var topSpace = vrect.top - MENU_PADDING; // Automatically determine dropdown placement based on available space in viewport. if (!position) { - position = (vrect.top + MENU_PADDING > dropdownHeight) ? 'top' : 'bottom'; + enoughTopSpace = topSpace > dropdownHeight; + enoughBottomSpace = bottomSpace > dropdownHeight; + if (enoughBottomSpace) { + position = 'bottom'; + } else if (enoughTopSpace) { + position = 'top'; + } else { + position = topSpace > bottomSpace ? 'top' : 'bottom'; + } } // Adjust the width to account for the padding provided by `md-input-container` if ($attrs.mdFloatingLabel) { @@ -159,9 +169,9 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming, if (position === 'top') { styles.top = 'auto'; styles.bottom = bot + 'px'; - styles.maxHeight = Math.min(dropdownHeight, hrect.top - root.top - MENU_PADDING) + 'px'; + styles.maxHeight = Math.min(dropdownHeight, topSpace) + 'px'; } else { - var bottomSpace = root.bottom - hrect.bottom - MENU_PADDING + $mdUtil.getViewportTop(); + bottomSpace = root.bottom - hrect.bottom - MENU_PADDING + $mdUtil.getViewportTop(); styles.top = (top - offset) + 'px'; styles.bottom = 'auto'; @@ -169,7 +179,7 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming, } elements.$.scrollContainer.css(styles); - $mdUtil.nextTick(correctHorizontalAlignment, false); + $mdUtil.nextTick(correctHorizontalAlignment, false, $scope); /** * Calculates the vertical offset for floating label examples to account for ngMessages @@ -195,7 +205,7 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming, function correctHorizontalAlignment () { var dropdown = elements.scrollContainer.getBoundingClientRect(), styles = {}; - if (dropdown.right > root.right - MENU_PADDING) { + if (dropdown.right > root.right) { styles.left = (hrect.right - dropdown.width) + 'px'; } elements.$.scrollContainer.css(styles); diff --git a/src/core/util/util.js b/src/core/util/util.js index 804ab41b0fc..8fa058e0a27 100644 --- a/src/core/util/util.js +++ b/src/core/util/util.js @@ -157,7 +157,13 @@ function UtilFactory($document, $timeout, $compile, $rootScope, $$mdAnimate, $in * @returns {number} */ getViewportTop: function() { - return window.scrollY || window.pageYOffset || 0; + // If body scrolling is disabled, then use the cached viewport top value, otherwise get it + // fresh from the $window. + if ($mdUtil.disableScrollAround._count && $mdUtil.disableScrollAround._viewPortTop) { + return $mdUtil.disableScrollAround._viewPortTop; + } else { + return $window.scrollY || $window.pageYOffset || 0; + } }, /** @@ -232,6 +238,7 @@ function UtilFactory($document, $timeout, $compile, $rootScope, $$mdAnimate, $in return $mdUtil.disableScrollAround._restoreScroll = function() { if (--$mdUtil.disableScrollAround._count <= 0) { + delete $mdUtil.disableScrollAround._viewPortTop; restoreBody(); restoreElement(); delete $mdUtil.disableScrollAround._restoreScroll; @@ -282,6 +289,7 @@ function UtilFactory($document, $timeout, $compile, $rootScope, $$mdAnimate, $in var prevBodyStyle = body.style.cssText || ''; var viewportTop = $mdUtil.getViewportTop(); + $mdUtil.disableScrollAround._viewPortTop = viewportTop; var clientWidth = body.clientWidth; var hasVerticalScrollbar = body.scrollHeight > body.clientHeight + 1;