From b107ee803ea6eafff70f55eeba5d3f90e56427b6 Mon Sep 17 00:00:00 2001 From: Paul Gschwendtner Date: Sun, 14 Aug 2016 22:12:34 +0200 Subject: [PATCH] feat(autocomplete): allow developers to specify amount of dropdown items. * Adds an attribute, which allows developer to overwrite the amount of items shown in the autocomplete dropdown. - When the dropdown doesn't fit into the possible space it will shrink accordingly (as same as before) - When the specified amount of items is higher than the current matches then it will shrink (as same as before) * Fixes the item height deviations between CSS and JavaScript. * Removed the unnecessary CSS for the dropdown height, because everything will be handled from the JS (same as before) * Added tests for the dropdown height calculation, which make sure that everything is now calculated properly. Fixes #9306. Closes #8751. --- src/components/autocomplete/autocomplete.scss | 12 +- .../autocomplete/autocomplete.spec.js | 118 ++++++++++++++++++ .../autocomplete/js/autocompleteController.js | 17 ++- .../autocomplete/js/autocompleteDirective.js | 7 +- 4 files changed, 141 insertions(+), 13 deletions(-) diff --git a/src/components/autocomplete/autocomplete.scss b/src/components/autocomplete/autocomplete.scss index e1e6dd84f0e..86118f729f7 100644 --- a/src/components/autocomplete/autocomplete.scss +++ b/src/components/autocomplete/autocomplete.scss @@ -1,4 +1,5 @@ -$autocomplete-option-height: 48px !default; +// The default item height is also specified in the JavaScript. +$md-autocomplete-item-height: 48px !default; @keyframes md-autocomplete-list-out { 0% { @@ -201,25 +202,24 @@ md-autocomplete { .md-virtual-repeat-container.md-autocomplete-suggestions-container { position: absolute; box-shadow: 0 2px 5px rgba(black, 0.25); - height: 41px * 5.5; - max-height: 41px * 5.5; z-index: $z-index-tooltip; } .md-virtual-repeat-container.md-not-found { - height: 48px; + height: $md-autocomplete-item-height; } .md-autocomplete-suggestions { margin: 0; list-style: none; padding: 0; + li { font-size: 14px; overflow: hidden; padding: 0 15px; - line-height: $autocomplete-option-height; - height: $autocomplete-option-height; + line-height: $md-autocomplete-item-height; + height: $md-autocomplete-item-height; transition: background 0.15s linear; margin: 0; white-space: nowrap; diff --git a/src/components/autocomplete/autocomplete.spec.js b/src/components/autocomplete/autocomplete.spec.js index 893202a44af..fc119f045b7 100644 --- a/src/components/autocomplete/autocomplete.spec.js +++ b/src/components/autocomplete/autocomplete.spec.js @@ -1736,6 +1736,26 @@ describe('', function() { describe('dropdown position', function() { + var DEFAULT_MAX_ITEMS = 5; + var DEFAULT_ITEM_HEIGHT = 48; + + var dropdownItems = DEFAULT_MAX_ITEMS; + + /** + * Function to create fake matches with the given dropdown items. + * Useful when running tests against the dropdown max items calculations. + * @returns {Array} Fake matches. + */ + function fakeItemMatch() { + var matches = []; + + for (var i = 0; i < dropdownItems; i++) { + matches.push('Item ' + i); + } + + return matches; + } + it('should adjust the width when the window resizes', inject(function($timeout, $window) { var scope = createScope(); @@ -1936,6 +1956,104 @@ describe('', function() { document.body.removeChild(parent[0]); })); + it('should calculate the height from the default max items', inject(function($timeout) { + var scope = createScope(); + + 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(); + expect(scrollContainer.style.maxHeight).toBe(DEFAULT_MAX_ITEMS * DEFAULT_ITEM_HEIGHT + 'px'); + + dropdownItems = 6; + + // Trigger a new query to request an update of the items and dropdown. + scope.$apply('searchText = "Query 2"'); + + // The dropdown should not increase its height because of the new extra item. + expect(scrollContainer.style.maxHeight).toBe(DEFAULT_MAX_ITEMS * DEFAULT_ITEM_HEIGHT + 'px'); + + document.body.removeChild(parent[0]); + })); + + it('should calculate its height from the specified max items', inject(function($timeout) { + var scope = createScope(); + var maxDropdownItems = 2; + + // Set the current dropdown items to the new maximum. + dropdownItems = maxDropdownItems; + 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(); + expect(scrollContainer.style.maxHeight).toBe(maxDropdownItems * DEFAULT_ITEM_HEIGHT + 'px'); + + dropdownItems = 6; + + // Trigger a new query to request an update of the items and dropdown. + scope.$apply('searchText = "Query 2"'); + + // The dropdown should not increase its height because of the new extra item. + expect(scrollContainer.style.maxHeight).toBe(maxDropdownItems * DEFAULT_ITEM_HEIGHT + 'px'); + + document.body.removeChild(parent[0]); + })); + }); describe('md-highlight-text', function() { diff --git a/src/components/autocomplete/js/autocompleteController.js b/src/components/autocomplete/js/autocompleteController.js index 574309c2a83..47058e1ce54 100644 --- a/src/components/autocomplete/js/autocompleteController.js +++ b/src/components/autocomplete/js/autocompleteController.js @@ -2,8 +2,8 @@ angular .module('material.components.autocomplete') .controller('MdAutocompleteCtrl', MdAutocompleteCtrl); -var ITEM_HEIGHT = 41, - MAX_HEIGHT = 5.5 * ITEM_HEIGHT, +var ITEM_HEIGHT = 48, + MAX_ITEMS = 5, MENU_PADDING = 8, INPUT_PADDING = 2; // Padding provided by `md-input-container` @@ -92,7 +92,12 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming, * @returns {*} */ function positionDropdown () { - if (!elements) return $mdUtil.nextTick(positionDropdown, false, $scope); + if (!elements) { + return $mdUtil.nextTick(positionDropdown, false, $scope); + } + + var dropdownHeight = ($scope.dropdownItems || MAX_ITEMS) * ITEM_HEIGHT; + var hrect = elements.wrap.getBoundingClientRect(), vrect = elements.snap.getBoundingClientRect(), root = elements.root.getBoundingClientRect(), @@ -112,14 +117,14 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming, minWidth: width + 'px', maxWidth: Math.max(hrect.right - root.left, root.right - hrect.left) - MENU_PADDING + 'px' }; - if (top > bot && root.height - hrect.bottom - MENU_PADDING < MAX_HEIGHT) { + if (top > bot && root.height - hrect.bottom - MENU_PADDING < dropdownHeight) { styles.top = 'auto'; styles.bottom = bot + 'px'; - styles.maxHeight = Math.min(MAX_HEIGHT, hrect.top - root.top - MENU_PADDING) + 'px'; + styles.maxHeight = Math.min(dropdownHeight, hrect.top - root.top - MENU_PADDING) + 'px'; } else { styles.top = (top - offset) + 'px'; styles.bottom = 'auto'; - styles.maxHeight = Math.min(MAX_HEIGHT, root.bottom + $mdUtil.scrollTop() - hrect.bottom - MENU_PADDING) + 'px'; + styles.maxHeight = Math.min(dropdownHeight, root.bottom + $mdUtil.scrollTop() - hrect.bottom - MENU_PADDING) + 'px'; } elements.$.scrollContainer.css(styles); diff --git a/src/components/autocomplete/js/autocompleteDirective.js b/src/components/autocomplete/js/autocompleteDirective.js index cd8f55e5ccf..8093d6dbd23 100644 --- a/src/components/autocomplete/js/autocompleteDirective.js +++ b/src/components/autocomplete/js/autocompleteDirective.js @@ -82,6 +82,10 @@ angular * will select on case-insensitive match * @param {string=} md-escape-options Override escape key logic. Default is `blur clear`.
* Options: `blur | clear`, `none` + * @param {string=} md-dropdown-items Specifies the maximum amount of items to be shown in + * the dropdown.

+ * When the dropdown doesn't fit into the viewport, the dropdown will shrink + * as less as possible. * * @usage * ### Basic Example @@ -165,7 +169,8 @@ function MdAutocomplete ($$mdSvgRegistry) { autoselect: '=?mdAutoselect', menuClass: '@?mdMenuClass', inputId: '@?mdInputId', - escapeOptions: '@?mdEscapeOptions' + escapeOptions: '@?mdEscapeOptions', + dropdownItems: '=?mdDropdownItems' }, link: function(scope, element, attrs, controller) { // Retrieve the state of using a md-not-found template by using our attribute, which will