From f815cd985128e458c922af2222c6362b98902182 Mon Sep 17 00:00:00 2001 From: Max Fierke Date: Tue, 24 Mar 2015 16:30:40 -0500 Subject: [PATCH] fix(dropdown): Fix $digest:inprog on dropdown dismissal Make $apply first check if $rootScope is in $digest cycle before executing Closes #3274 --- src/dropdown/dropdown.js | 320 ----------------------- src/dropdown/test/dropdown.spec.js | 406 +++-------------------------- 2 files changed, 40 insertions(+), 686 deletions(-) diff --git a/src/dropdown/dropdown.js b/src/dropdown/dropdown.js index 9957957e83..e69de29bb2 100644 --- a/src/dropdown/dropdown.js +++ b/src/dropdown/dropdown.js @@ -1,320 +0,0 @@ -angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position']) - -.constant('dropdownConfig', { - openClass: 'open' -}) - -.service('dropdownService', ['$document', '$rootScope', function($document, $rootScope) { - var openScope = null; - - this.open = function( dropdownScope ) { - if ( !openScope ) { - $document.bind('click', closeDropdown); - $document.bind('keydown', keybindFilter); - } - - if ( openScope && openScope !== dropdownScope ) { - openScope.isOpen = false; - } - - openScope = dropdownScope; - }; - - this.close = function( dropdownScope ) { - if ( openScope === dropdownScope ) { - openScope = null; - $document.unbind('click', closeDropdown); - $document.unbind('keydown', keybindFilter); - } - }; - - var closeDropdown = function( evt ) { - // This method may still be called during the same mouse event that - // unbound this event handler. So check openScope before proceeding. - if (!openScope) { return; } - - if( evt && openScope.getAutoClose() === 'disabled' ) { return ; } - - var toggleElement = openScope.getToggleElement(); - if ( evt && toggleElement && toggleElement[0].contains(evt.target) ) { - return; - } - - var $element = openScope.getElement(); - if( evt && openScope.getAutoClose() === 'outsideClick' && $element && $element[0].contains(evt.target) ) { - return; - } - - openScope.isOpen = false; - - if (!$rootScope.$$phase) { - openScope.$apply(); - } - }; - - var keybindFilter = function( evt ) { - if ( evt.which === 27 ) { - openScope.focusToggleElement(); - closeDropdown(); - } - else if ( openScope.isKeynavEnabled() && /(38|40)/.test(evt.which) && openScope.isOpen ) { - evt.preventDefault(); - evt.stopPropagation(); - openScope.focusDropdownEntry(evt.which); - } - }; -}]) - -.controller('DropdownController', ['$scope', '$attrs', '$parse', 'dropdownConfig', 'dropdownService', '$animate', '$position', '$document', '$compile', '$templateRequest', function($scope, $attrs, $parse, dropdownConfig, dropdownService, $animate, $position, $document, $compile, $templateRequest) { - var self = this, - scope = $scope.$new(), // create a child scope so we are not polluting original one - templateScope, - openClass = dropdownConfig.openClass, - getIsOpen, - setIsOpen = angular.noop, - toggleInvoker = $attrs.onToggle ? $parse($attrs.onToggle) : angular.noop, - appendToBody = false, - keynavEnabled =false, - selectedOption = null; - - this.init = function( element ) { - self.$element = element; - - if ( $attrs.isOpen ) { - getIsOpen = $parse($attrs.isOpen); - setIsOpen = getIsOpen.assign; - - $scope.$watch(getIsOpen, function(value) { - scope.isOpen = !!value; - }); - } - - appendToBody = angular.isDefined($attrs.dropdownAppendToBody); - keynavEnabled = angular.isDefined($attrs.keyboardNav); - - if ( appendToBody && self.dropdownMenu ) { - $document.find('body').append( self.dropdownMenu ); - element.on('$destroy', function handleDestroyEvent() { - self.dropdownMenu.remove(); - }); - } - }; - - this.toggle = function( open ) { - return scope.isOpen = arguments.length ? !!open : !scope.isOpen; - }; - - // Allow other directives to watch status - this.isOpen = function() { - return scope.isOpen; - }; - - scope.getToggleElement = function() { - return self.toggleElement; - }; - - scope.getAutoClose = function() { - return $attrs.autoClose || 'always'; //or 'outsideClick' or 'disabled' - }; - - scope.getElement = function() { - return self.$element; - }; - - scope.isKeynavEnabled = function() { - return keynavEnabled; - }; - - scope.focusDropdownEntry = function(keyCode) { - var elems = self.dropdownMenu ? //If append to body is used. - (angular.element(self.dropdownMenu).find('a')) : - (angular.element(self.$element).find('ul').eq(0).find('a')); - - switch (keyCode) { - case (40): { - if ( !angular.isNumber(self.selectedOption)) { - self.selectedOption = 0; - } else { - self.selectedOption = (self.selectedOption === elems.length -1 ? - self.selectedOption : - self.selectedOption + 1); - } - break; - } - case (38): { - if ( !angular.isNumber(self.selectedOption)) { - return; - } else { - self.selectedOption = (self.selectedOption === 0 ? - 0 : - self.selectedOption - 1); - } - break; - } - } - elems[self.selectedOption].focus(); - }; - - scope.focusToggleElement = function() { - if ( self.toggleElement ) { - self.toggleElement[0].focus(); - } - }; - - scope.$watch('isOpen', function( isOpen, wasOpen ) { - if ( appendToBody && self.dropdownMenu ) { - var pos = $position.positionElements(self.$element, self.dropdownMenu, 'bottom-left', true); - self.dropdownMenu.css({ - top: pos.top + 'px', - left: pos.left + 'px', - display: isOpen ? 'block' : 'none' - }); - } - - $animate[isOpen ? 'addClass' : 'removeClass'](self.$element, openClass); - - if ( isOpen ) { - if (self.dropdownMenuTemplateUrl) { - $templateRequest(self.dropdownMenuTemplateUrl).then(function(tplContent) { - templateScope = scope.$new(); - $compile(tplContent.trim())(templateScope, function(dropdownElement) { - var newEl = dropdownElement; - self.dropdownMenu.replaceWith(newEl); - self.dropdownMenu = newEl; - }); - }); - } - - scope.focusToggleElement(); - dropdownService.open( scope ); - } else { - if (self.dropdownMenuTemplateUrl) { - if (templateScope) { - templateScope.$destroy(); - } - var newEl = angular.element(''); - self.dropdownMenu.replaceWith(newEl); - self.dropdownMenu = newEl; - } - - dropdownService.close( scope ); - self.selectedOption = null; - } - - setIsOpen($scope, isOpen); - if (angular.isDefined(isOpen) && isOpen !== wasOpen) { - toggleInvoker($scope, { open: !!isOpen }); - } - }); - - $scope.$on('$locationChangeSuccess', function() { - if (scope.getAutoClose() !== 'disabled') { - scope.isOpen = false; - } - }); - - $scope.$on('$destroy', function() { - scope.$destroy(); - }); -}]) - -.directive('dropdown', function() { - return { - controller: 'DropdownController', - link: function(scope, element, attrs, dropdownCtrl) { - dropdownCtrl.init( element ); - } - }; -}) - -.directive('dropdownMenu', function() { - return { - restrict: 'AC', - require: '?^dropdown', - link: function(scope, element, attrs, dropdownCtrl) { - if (!dropdownCtrl) { - return; - } - var tplUrl = attrs.templateUrl; - if (tplUrl) { - dropdownCtrl.dropdownMenuTemplateUrl = tplUrl; - } - if (!dropdownCtrl.dropdownMenu) { - dropdownCtrl.dropdownMenu = element; - } - } - }; -}) - -.directive('keyboardNav', function() { - return { - restrict: 'A', - require: '?^dropdown', - link: function (scope, element, attrs, dropdownCtrl) { - - element.bind('keydown', function(e) { - - if ( /(38|40)/.test(e.which)) { - - e.preventDefault(); - e.stopPropagation(); - - var elems = angular.element(element).find('a'); - - switch (e.keyCode) { - case (40): { // Down - if ( !angular.isNumber(dropdownCtrl.selectedOption)) { - dropdownCtrl.selectedOption = 0; - } else { - dropdownCtrl.selectedOption = (dropdownCtrl.selectedOption === elems.length -1 ? dropdownCtrl.selectedOption : dropdownCtrl.selectedOption+1); - } - - } - break; - case (38): { // Up - dropdownCtrl.selectedOption = (dropdownCtrl.selectedOption === 0 ? 0 : dropdownCtrl.selectedOption-1); - } - break; - } - elems[dropdownCtrl.selectedOption].focus(); - } - }); - } - - }; -}) - -.directive('dropdownToggle', function() { - return { - require: '?^dropdown', - link: function(scope, element, attrs, dropdownCtrl) { - if ( !dropdownCtrl ) { - return; - } - - dropdownCtrl.toggleElement = element; - - var toggleDropdown = function(event) { - event.preventDefault(); - - if ( !element.hasClass('disabled') && !attrs.disabled ) { - scope.$apply(function() { - dropdownCtrl.toggle(); - }); - } - }; - - element.bind('click', toggleDropdown); - - // WAI-ARIA - element.attr({ 'aria-haspopup': true, 'aria-expanded': false }); - scope.$watch(dropdownCtrl.isOpen, function( isOpen ) { - element.attr('aria-expanded', !!isOpen); - }); - - scope.$on('$destroy', function() { - element.unbind('click', toggleDropdown); - }); - } - }; -}); diff --git a/src/dropdown/test/dropdown.spec.js b/src/dropdown/test/dropdown.spec.js index ee7fcf8097..c1c46a85ee 100644 --- a/src/dropdown/test/dropdown.spec.js +++ b/src/dropdown/test/dropdown.spec.js @@ -1,20 +1,14 @@ describe('dropdownToggle', function() { - var $compile, $rootScope, $document, $templateCache, dropdownConfig, element; + var $compile, $rootScope, $document, element; beforeEach(module('ui.bootstrap.dropdown')); - beforeEach(inject(function(_$compile_, _$rootScope_, _$document_, _$templateCache_, _dropdownConfig_) { + beforeEach(inject(function(_$compile_, _$rootScope_, _$document_) { $compile = _$compile_; $rootScope = _$rootScope_; $document = _$document_; - $templateCache = _$templateCache_; - dropdownConfig = _dropdownConfig_; })); - afterEach(function() { - element.remove(); - }); - var clickDropdownToggle = function(elm) { elm = elm || element; elm.find('a[dropdown-toggle]').click(); @@ -40,78 +34,80 @@ describe('dropdownToggle', function() { }); it('should toggle on `a` click', function() { - expect(element.hasClass(dropdownConfig.openClass)).toBe(false); + expect(element.hasClass('open')).toBe(false); clickDropdownToggle(); - expect(element.hasClass(dropdownConfig.openClass)).toBe(true); + expect(element.hasClass('open')).toBe(true); clickDropdownToggle(); - expect(element.hasClass(dropdownConfig.openClass)).toBe(false); + expect(element.hasClass('open')).toBe(false); }); it('should toggle when an option is clicked', function() { $document.find('body').append(element); - expect(element.hasClass(dropdownConfig.openClass)).toBe(false); + expect(element.hasClass('open')).toBe(false); clickDropdownToggle(); - expect(element.hasClass(dropdownConfig.openClass)).toBe(true); + expect(element.hasClass('open')).toBe(true); var optionEl = element.find('ul > li').eq(0).find('a').eq(0); optionEl.click(); - expect(element.hasClass(dropdownConfig.openClass)).toBe(false); + expect(element.hasClass('open')).toBe(false); + element.remove(); }); it('should close on document click', function() { clickDropdownToggle(); - expect(element.hasClass(dropdownConfig.openClass)).toBe(true); + expect(element.hasClass('open')).toBe(true); $document.click(); - expect(element.hasClass(dropdownConfig.openClass)).toBe(false); + expect(element.hasClass('open')).toBe(false); }); it('should close on escape key & focus toggle element', function() { $document.find('body').append(element); clickDropdownToggle(); triggerKeyDown($document, 27); - expect(element.hasClass(dropdownConfig.openClass)).toBe(false); + expect(element.hasClass('open')).toBe(false); expect(isFocused(element.find('a'))).toBe(true); + element.remove(); }); it('should not close on backspace key', function() { clickDropdownToggle(); triggerKeyDown($document, 8); - expect(element.hasClass(dropdownConfig.openClass)).toBe(true); + expect(element.hasClass('open')).toBe(true); }); it('should close on $location change', function() { clickDropdownToggle(); - expect(element.hasClass(dropdownConfig.openClass)).toBe(true); + expect(element.hasClass('open')).toBe(true); $rootScope.$broadcast('$locationChangeSuccess'); $rootScope.$apply(); - expect(element.hasClass(dropdownConfig.openClass)).toBe(false); + expect(element.hasClass('open')).toBe(false); }); it('should only allow one dropdown to be open at once', function() { var elm1 = dropdown(); var elm2 = dropdown(); - expect(elm1.hasClass(dropdownConfig.openClass)).toBe(false); - expect(elm2.hasClass(dropdownConfig.openClass)).toBe(false); + expect(elm1.hasClass('open')).toBe(false); + expect(elm2.hasClass('open')).toBe(false); clickDropdownToggle( elm1 ); - expect(elm1.hasClass(dropdownConfig.openClass)).toBe(true); - expect(elm2.hasClass(dropdownConfig.openClass)).toBe(false); + expect(elm1.hasClass('open')).toBe(true); + expect(elm2.hasClass('open')).toBe(false); clickDropdownToggle( elm2 ); - expect(elm1.hasClass(dropdownConfig.openClass)).toBe(false); - expect(elm2.hasClass(dropdownConfig.openClass)).toBe(true); + expect(elm1.hasClass('open')).toBe(false); + expect(elm2.hasClass('open')).toBe(true); }); it('should not toggle if the element has `disabled` class', function() { var elm = $compile('
  • ')($rootScope); clickDropdownToggle( elm ); - expect(elm.hasClass(dropdownConfig.openClass)).toBe(false); + expect(elm.hasClass('open')).toBe(false); }); it('should not toggle if the element is disabled', function() { var elm = $compile('
  • ')($rootScope); elm.find('button').click(); - expect(elm.hasClass(dropdownConfig.openClass)).toBe(false); + expect(elm.hasClass('open')).toBe(false); }); it('should not toggle if the element has `ng-disabled` as true', function() { @@ -119,12 +115,12 @@ describe('dropdownToggle', function() { var elm = $compile('
  • ')($rootScope); $rootScope.$digest(); elm.find('div').click(); - expect(elm.hasClass(dropdownConfig.openClass)).toBe(false); + expect(elm.hasClass('open')).toBe(false); $rootScope.isdisabled = false; $rootScope.$digest(); elm.find('div').click(); - expect(elm.hasClass(dropdownConfig.openClass)).toBe(true); + expect(elm.hasClass('open')).toBe(true); }); it('should unbind events on scope destroy', function() { @@ -134,13 +130,13 @@ describe('dropdownToggle', function() { var buttonEl = elm.find('button'); buttonEl.click(); - expect(elm.hasClass(dropdownConfig.openClass)).toBe(true); + expect(elm.hasClass('open')).toBe(true); buttonEl.click(); - expect(elm.hasClass(dropdownConfig.openClass)).toBe(false); + expect(elm.hasClass('open')).toBe(false); $scope.$destroy(); buttonEl.click(); - expect(elm.hasClass(dropdownConfig.openClass)).toBe(false); + expect(elm.hasClass('open')).toBe(false); }); // issue 270 @@ -148,11 +144,11 @@ describe('dropdownToggle', function() { var checkboxEl = $compile('')($rootScope); $rootScope.$digest(); - expect(element.hasClass(dropdownConfig.openClass)).toBe(false); + expect(element.hasClass('open')).toBe(false); expect($rootScope.clicked).toBeFalsy(); clickDropdownToggle(); - expect(element.hasClass(dropdownConfig.openClass)).toBe(true); + expect(element.hasClass('open')).toBe(true); expect($rootScope.clicked).toBeFalsy(); checkboxEl.click(); @@ -174,57 +170,13 @@ describe('dropdownToggle', function() { // pr/issue 3274 it('should not raise $digest:inprog if dismissed during a digest cycle', function () { clickDropdownToggle(); - expect(element.hasClass(dropdownConfig.openClass)).toBe(true); + expect(element.hasClass('open')).toBe(true); $rootScope.$apply(function () { $document.click(); }); - expect(element.hasClass(dropdownConfig.openClass)).toBe(false); - }); - }); - - describe('using dropdownMenuTemplate', function() { - function dropdown() { - $templateCache.put('custom.html', ''); - - return $compile('
  • ')($rootScope); - } - - beforeEach(function() { - element = dropdown(); - }); - - it('should apply custom template for dropdown menu', function() { - element.find('a').click(); - expect(element.find('ul.dropdown-menu').eq(0).find('li').eq(0).text()).toEqual('Item 1'); - }); - - it('should clear ul when dropdown menu is closed', function() { - element.find('a').click(); - expect(element.find('ul.dropdown-menu').eq(0).find('li').eq(0).text()).toEqual('Item 1'); - element.find('a').click(); - expect(element.find('ul.dropdown-menu').eq(0).find('li').length).toEqual(0); - }); - }); - - describe('using dropdown-append-to-body', function() { - function dropdown() { - return $compile('
  • ')($rootScope); - } - - beforeEach(function() { - element = dropdown(); - }); - - it('adds the menu to the body', function() { - expect($document.find('#dropdown-menu').parent()[0]).toBe($document.find('body')[0]); - }); - - it('removes the menu when the dropdown is removed', function() { - element.remove(); - $rootScope.$digest(); - expect($document.find('#dropdown-menu').length).toEqual(0); + expect(element.hasClass('open')).toBe(false); }); }); @@ -248,10 +200,10 @@ describe('dropdownToggle', function() { it('should close without errors on $location change', function() { $document.find('body').append(element); clickDropdownToggle(); - expect(element.hasClass(dropdownConfig.openClass)).toBe(true); + expect(element.hasClass('open')).toBe(true); var optionEl = element.find('ul > li').eq(0).find('a').eq(0); optionEl.click(); - expect(element.hasClass(dropdownConfig.openClass)).toBe(false); + expect(element.hasClass('open')).toBe(false); }); }); @@ -263,13 +215,13 @@ describe('dropdownToggle', function() { }); it('should be open initially', function() { - expect(element.hasClass(dropdownConfig.openClass)).toBe(true); + expect(element.hasClass('open')).toBe(true); }); it('should toggle when `is-open` changes', function() { $rootScope.isopen = false; $rootScope.$digest(); - expect(element.hasClass(dropdownConfig.openClass)).toBe(false); + expect(element.hasClass('open')).toBe(false); }); }); @@ -281,7 +233,7 @@ describe('dropdownToggle', function() { }); it('should be open initially', function() { - expect(element.hasClass(dropdownConfig.openClass)).toBe(true); + expect(element.hasClass('open')).toBe(true); }); it('should change `is-open` binding when toggles', function() { @@ -292,7 +244,7 @@ describe('dropdownToggle', function() { it('should toggle when `is-open` changes', function() { $rootScope.isopen = false; $rootScope.$digest(); - expect(element.hasClass(dropdownConfig.openClass)).toBe(false); + expect(element.hasClass('open')).toBe(false); }); it('focus toggle element when opening', function() { @@ -304,6 +256,7 @@ describe('dropdownToggle', function() { $rootScope.isopen = true; $rootScope.$digest(); expect(isFocused(element.find('a'))).toBe(true); + element.remove(); }); }); @@ -371,283 +324,4 @@ describe('dropdownToggle', function() { expect($rootScope.toggleHandler).toHaveBeenCalledWith(false); }); }); - - describe('`auto-close` option', function() { - function dropdown(autoClose) { - return $compile('
  • ')($rootScope); - } - - it('should close on document click if no auto-close is specified', function() { - element = dropdown(); - clickDropdownToggle(); - expect(element.hasClass(dropdownConfig.openClass)).toBe(true); - $document.click(); - expect(element.hasClass(dropdownConfig.openClass)).toBe(false); - }); - - it('should close on document click if empty auto-close is specified', function() { - element = dropdown(''); - clickDropdownToggle(); - expect(element.hasClass(dropdownConfig.openClass)).toBe(true); - $document.click(); - expect(element.hasClass(dropdownConfig.openClass)).toBe(false); - }); - - it('auto-close="disabled"', function() { - element = dropdown('disabled'); - clickDropdownToggle(); - expect(element.hasClass(dropdownConfig.openClass)).toBe(true); - $document.click(); - expect(element.hasClass(dropdownConfig.openClass)).toBe(true); - }); - - it('auto-close="outsideClick"', function() { - element = dropdown('outsideClick'); - clickDropdownToggle(); - expect(element.hasClass(dropdownConfig.openClass)).toBe(true); - element.find('ul li a').click(); - expect(element.hasClass(dropdownConfig.openClass)).toBe(true); - $document.click(); - expect(element.hasClass(dropdownConfig.openClass)).toBe(false); - }); - - it('control with is-open', function() { - $rootScope.isopen = true; - element = $compile('
  • ')($rootScope); - $rootScope.$digest(); - - expect(element.hasClass(dropdownConfig.openClass)).toBe(true); - //should remain open - $document.click(); - expect(element.hasClass(dropdownConfig.openClass)).toBe(true); - //now should close - $rootScope.isopen = false; - $rootScope.$digest(); - expect(element.hasClass(dropdownConfig.openClass)).toBe(false); - }); - - it('should close anyway if toggle is clicked', function() { - element = dropdown('disabled'); - clickDropdownToggle(); - expect(element.hasClass(dropdownConfig.openClass)).toBe(true); - clickDropdownToggle(); - expect(element.hasClass(dropdownConfig.openClass)).toBe(false); - }); - - it('should close anyway if esc is pressed', function() { - element = dropdown('disabled'); - $document.find('body').append(element); - clickDropdownToggle(); - triggerKeyDown($document, 27); - expect(element.hasClass(dropdownConfig.openClass)).toBe(false); - expect(isFocused(element.find('a'))).toBe(true); - }); - - it('should close anyway if another dropdown is opened', function() { - var elm1 = dropdown('disabled'); - var elm2 = dropdown(); - expect(elm1.hasClass(dropdownConfig.openClass)).toBe(false); - expect(elm2.hasClass(dropdownConfig.openClass)).toBe(false); - clickDropdownToggle(elm1); - expect(elm1.hasClass(dropdownConfig.openClass)).toBe(true); - expect(elm2.hasClass(dropdownConfig.openClass)).toBe(false); - clickDropdownToggle(elm2); - expect(elm1.hasClass(dropdownConfig.openClass)).toBe(false); - expect(elm2.hasClass(dropdownConfig.openClass)).toBe(true); - }); - - it('should not close on $locationChangeSuccess if auto-close="disabled"', function () { - var elm1 = dropdown('disabled'); - expect(elm1.hasClass(dropdownConfig.openClass)).toBe(false); - clickDropdownToggle(elm1); - expect(elm1.hasClass(dropdownConfig.openClass)).toBe(true); - $rootScope.$broadcast('$locationChangeSuccess'); - $rootScope.$digest(); - expect(elm1.hasClass(dropdownConfig.openClass)).toBe(true); - }); - }); - - describe('`keyboard-nav` option', function() { - function dropdown() { - return $compile('
  • ')($rootScope); - } - beforeEach(function() { - element = dropdown(); - }); - - it('should focus first list element when down arrow pressed', function() { - $document.find('body').append(element); - clickDropdownToggle(); - triggerKeyDown($document, 40); - - expect(element.hasClass(dropdownConfig.openClass)).toBe(true); - var optionEl = element.find('ul').eq(0).find('a').eq(0); - expect(isFocused(optionEl)).toBe(true); - }); - - it('should not focus first list element when down arrow pressed if closed', function() { - $document.find('body').append(element); - triggerKeyDown($document, 40); - - expect(element.hasClass(dropdownConfig.openClass)).toBe(false); - var focusEl = element.find('ul').eq(0).find('a').eq(0); - expect(isFocused(focusEl)).toBe(false); - }); - - it('should focus second list element when down arrow pressed twice', function() { - $document.find('body').append(element); - clickDropdownToggle(); - triggerKeyDown($document, 40); - triggerKeyDown($document, 40); - - expect(element.hasClass(dropdownConfig.openClass)).toBe(true); - var focusEl = element.find('ul').eq(0).find('a').eq(1); - expect(isFocused(focusEl)).toBe(true); - }); - }); - - describe('`keyboard-nav` option', function() { - function dropdown() { - return $compile('
  • ')($rootScope); - } - beforeEach(function() { - element = dropdown(); - }); - - it('should focus first list element when down arrow pressed', function() { - $document.find('body').append(element); - clickDropdownToggle(); - triggerKeyDown($document, 40); - - expect(element.hasClass(dropdownConfig.openClass)).toBe(true); - var focusEl = element.find('ul').eq(0).find('a').eq(0); - expect(isFocused(focusEl)).toBe(true); - }); - - it('should not focus first list element when up arrow pressed after dropdown toggled', function() { - $document.find('body').append(element); - clickDropdownToggle(); - expect(element.hasClass(dropdownConfig.openClass)).toBe(true); - - triggerKeyDown($document, 38); - var focusEl = element.find('ul').eq(0).find('a').eq(0); - expect(isFocused(focusEl)).toBe(false); - }); - - it('should not focus any list element when down arrow pressed if closed', function() { - $document.find('body').append(element); - triggerKeyDown($document, 40); - - expect(element.hasClass(dropdownConfig.openClass)).toBe(false); - var focusEl = element.find('ul').eq(0).find('a'); - expect(isFocused(focusEl[0])).toBe(false); - expect(isFocused(focusEl[1])).toBe(false); - }); - - it('should not change focus when other keys are pressed', function() { - $document.find('body').append(element); - clickDropdownToggle(); - triggerKeyDown($document, 37); - - expect(element.hasClass(dropdownConfig.openClass)).toBe(true); - var focusEl = element.find('ul').eq(0).find('a'); - expect(isFocused(focusEl[0])).toBe(false); - expect(isFocused(focusEl[1])).toBe(false); - }); - - it('should focus second list element when down arrow pressed twice', function() { - $document.find('body').append(element); - clickDropdownToggle(); - triggerKeyDown($document, 40); - triggerKeyDown($document, 40); - - expect(element.hasClass(dropdownConfig.openClass)).toBe(true); - var focusEl = element.find('ul').eq(0).find('a').eq(1); - expect(isFocused(focusEl)).toBe(true); - }); - - it('should focus first list element when down arrow pressed 2x and up pressed 1x', function() { - $document.find('body').append(element); - clickDropdownToggle(); - triggerKeyDown($document, 40); - triggerKeyDown($document, 40); - - triggerKeyDown($document, 38); - - expect(element.hasClass(dropdownConfig.openClass)).toBe(true); - var focusEl = element.find('ul').eq(0).find('a').eq(0); - expect(isFocused(focusEl)).toBe(true); - }); - - it('should stay focused on final list element if down pressed at list end', function() { - $document.find('body').append(element); - clickDropdownToggle(); - triggerKeyDown($document, 40); - triggerKeyDown($document, 40); - - expect(element.hasClass(dropdownConfig.openClass)).toBe(true); - var focusEl = element.find('ul').eq(0).find('a').eq(1); - expect(isFocused(focusEl)).toBe(true); - - triggerKeyDown($document, 40); - expect(isFocused(focusEl)).toBe(true); - }); - - it('should close if esc is pressed while focused', function() { - element = dropdown('disabled'); - $document.find('body').append(element); - clickDropdownToggle(); - - triggerKeyDown($document, 40); - - expect(element.hasClass(dropdownConfig.openClass)).toBe(true); - var focusEl = element.find('ul').eq(0).find('a').eq(0); - expect(isFocused(focusEl)).toBe(true); - - triggerKeyDown($document, 27); - expect(element.hasClass(dropdownConfig.openClass)).toBe(false); - }); - }); - - describe('`keyboard-nav` option with `dropdown-append-to-body` option', function() { - function dropdown() { - return $compile('
  • ')($rootScope); - } - - beforeEach(function() { - element = dropdown(); - }); - - it('should focus first list element when down arrow pressed', function() { - clickDropdownToggle(); - - triggerKeyDown($document, 40); - - expect(element.hasClass(dropdownConfig.openClass)).toBe(true); - var focusEl = $document.find('ul').eq(0).find('a'); - expect(isFocused(focusEl)).toBe(true); - }); - - it('should not focus first list element when down arrow pressed if closed', function() { - triggerKeyDown($document, 40); - - expect(element.hasClass(dropdownConfig.openClass)).toBe(false); - var focusEl = $document.find('ul').eq(0).find('a'); - expect(isFocused(focusEl)).toBe(false); - }); - - it('should focus second list element when down arrow pressed twice', function() { - clickDropdownToggle(); - triggerKeyDown($document, 40); - triggerKeyDown($document, 40); - - expect(element.hasClass(dropdownConfig.openClass)).toBe(true); - var elem1 = $document.find('ul'); - var elem2 = elem1.find('a'); - var focusEl = $document.find('ul').eq(0).find('a').eq(1); - expect(isFocused(focusEl)).toBe(true); - }); - }); });