diff --git a/src/dropdown/docs/readme.md b/src/dropdown/docs/readme.md index 02970d0a5f..5f42e0e829 100644 --- a/src/dropdown/docs/readme.md +++ b/src/dropdown/docs/readme.md @@ -2,13 +2,8 @@ Dropdown is a simple directive which will toggle a dropdown menu on click or programmatically. You can either use `is-open` to toggle or add inside a `` element to toggle it when is clicked. There is also the `on-toggle(open)` optional expression fired when dropdown changes state. - -Add `dropdown-append-to-body` to the `dropdown` element to append to the inner `dropdown-menu` to the body. -This is useful when the dropdown button is inside a div with `overflow: hidden`, and the menu would otherwise be hidden. - By default the dropdown will automatically close if any of its elements is clicked, you can change this behavior by setting the `auto-close` option as follows: * `always` - (Default) automatically closes the dropdown when any of its elements is clicked. * `outsideClick` - closes the dropdown automatically only when the user clicks any element outside the dropdown. * `disabled` - disables the auto close. You can then control the open/close status of the dropdown manually, by using `is-open`. Please notice that the dropdown will still close if the toggle is clicked, the `esc` key is pressed or another dropdown is open. - diff --git a/src/dropdown/dropdown.js b/src/dropdown/dropdown.js index 14c255a13b..b469b0bec5 100644 --- a/src/dropdown/dropdown.js +++ b/src/dropdown/dropdown.js @@ -33,11 +33,18 @@ angular.module('ui.bootstrap.dropdown', []) // 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) { @@ -87,6 +94,14 @@ angular.module('ui.bootstrap.dropdown', []) return self.toggleElement; }; + scope.getAutoClose = function() { + return $attrs.autoClose || 'always'; //or 'outsideClick' or 'disabled' + }; + + scope.getElement = function() { + return self.$element; + }; + scope.focusToggleElement = function() { if ( self.toggleElement ) { self.toggleElement[0].focus(); diff --git a/src/dropdown/test/dropdown.spec.js b/src/dropdown/test/dropdown.spec.js index e69de29bb2..5e7c7dab9e 100644 --- a/src/dropdown/test/dropdown.spec.js +++ b/src/dropdown/test/dropdown.spec.js @@ -0,0 +1,415 @@ +describe('dropdownToggle', function() { + var $compile, $rootScope, $document, element; + + beforeEach(module('ui.bootstrap.dropdown')); + + beforeEach(inject(function(_$compile_, _$rootScope_, _$document_) { + $compile = _$compile_; + $rootScope = _$rootScope_; + $document = _$document_; + })); + + var clickDropdownToggle = function(elm) { + elm = elm || element; + elm.find('a[dropdown-toggle]').click(); + }; + + var triggerKeyDown = function (element, keyCode) { + var e = $.Event('keydown'); + e.which = keyCode; + element.trigger(e); + }; + + var isFocused = function(elm) { + return elm[0] === document.activeElement; + }; + + describe('basic', function() { + function dropdown() { + return $compile('
  • ')($rootScope); + } + + beforeEach(function() { + element = dropdown(); + }); + + it('should toggle on `a` click', function() { + expect(element.hasClass('open')).toBe(false); + clickDropdownToggle(); + expect(element.hasClass('open')).toBe(true); + clickDropdownToggle(); + expect(element.hasClass('open')).toBe(false); + }); + + it('should toggle when an option is clicked', function() { + $document.find('body').append(element); + expect(element.hasClass('open')).toBe(false); + clickDropdownToggle(); + expect(element.hasClass('open')).toBe(true); + + var optionEl = element.find('ul > li').eq(0).find('a').eq(0); + optionEl.click(); + expect(element.hasClass('open')).toBe(false); + element.remove(); + }); + + it('should close on document click', function() { + clickDropdownToggle(); + expect(element.hasClass('open')).toBe(true); + $document.click(); + 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('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('open')).toBe(true); + }); + + it('should close on $location change', function() { + clickDropdownToggle(); + expect(element.hasClass('open')).toBe(true); + $rootScope.$broadcast('$locationChangeSuccess'); + $rootScope.$apply(); + 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('open')).toBe(false); + expect(elm2.hasClass('open')).toBe(false); + + clickDropdownToggle( elm1 ); + expect(elm1.hasClass('open')).toBe(true); + expect(elm2.hasClass('open')).toBe(false); + + clickDropdownToggle( elm2 ); + 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('open')).toBe(false); + }); + + it('should not toggle if the element is disabled', function() { + var elm = $compile('
  • ')($rootScope); + elm.find('button').click(); + expect(elm.hasClass('open')).toBe(false); + }); + + it('should not toggle if the element has `ng-disabled` as true', function() { + $rootScope.isdisabled = true; + var elm = $compile('
  • ')($rootScope); + $rootScope.$digest(); + elm.find('div').click(); + expect(elm.hasClass('open')).toBe(false); + + $rootScope.isdisabled = false; + $rootScope.$digest(); + elm.find('div').click(); + expect(elm.hasClass('open')).toBe(true); + }); + + it('should unbind events on scope destroy', function() { + var $scope = $rootScope.$new(); + var elm = $compile('
  • ')($scope); + $scope.$digest(); + + var buttonEl = elm.find('button'); + buttonEl.click(); + expect(elm.hasClass('open')).toBe(true); + buttonEl.click(); + expect(elm.hasClass('open')).toBe(false); + + $scope.$destroy(); + buttonEl.click(); + expect(elm.hasClass('open')).toBe(false); + }); + + // issue 270 + it('executes other document click events normally', function() { + var checkboxEl = $compile('')($rootScope); + $rootScope.$digest(); + + expect(element.hasClass('open')).toBe(false); + expect($rootScope.clicked).toBeFalsy(); + + clickDropdownToggle(); + expect(element.hasClass('open')).toBe(true); + expect($rootScope.clicked).toBeFalsy(); + + checkboxEl.click(); + expect($rootScope.clicked).toBeTruthy(); + }); + + // WAI-ARIA + it('should aria markup to the `dropdown-toggle`', function() { + var toggleEl = element.find('a'); + expect(toggleEl.attr('aria-haspopup')).toBe('true'); + expect(toggleEl.attr('aria-expanded')).toBe('false'); + + clickDropdownToggle(); + expect(toggleEl.attr('aria-expanded')).toBe('true'); + clickDropdownToggle(); + expect(toggleEl.attr('aria-expanded')).toBe('false'); + }); + + // pr/issue 3274 + it('should not raise $digest:inprog if dismissed during a digest cycle', function () { + clickDropdownToggle(); + expect(element.hasClass('open')).toBe(true); + + $rootScope.$apply(function () { + $document.click(); + }); + + expect(element.hasClass('open')).toBe(false); + }); + }); + + describe('integration with $location URL rewriting', function() { + function dropdown() { + + // Simulate URL rewriting behavior + $document.on('click', 'a[href="#something"]', function () { + $rootScope.$broadcast('$locationChangeSuccess'); + $rootScope.$apply(); + }); + + return $compile('
  • ' + + '
  • ')($rootScope); + } + + beforeEach(function() { + element = dropdown(); + }); + + it('should close without errors on $location change', function() { + $document.find('body').append(element); + clickDropdownToggle(); + expect(element.hasClass('open')).toBe(true); + var optionEl = element.find('ul > li').eq(0).find('a').eq(0); + optionEl.click(); + expect(element.hasClass('open')).toBe(false); + }); + }); + + describe('without trigger', function() { + beforeEach(function() { + $rootScope.isopen = true; + element = $compile('
  • ')($rootScope); + $rootScope.$digest(); + }); + + it('should be open initially', function() { + expect(element.hasClass('open')).toBe(true); + }); + + it('should toggle when `is-open` changes', function() { + $rootScope.isopen = false; + $rootScope.$digest(); + expect(element.hasClass('open')).toBe(false); + }); + }); + + describe('`is-open`', function() { + beforeEach(function() { + $rootScope.isopen = true; + element = $compile('
  • ')($rootScope); + $rootScope.$digest(); + }); + + it('should be open initially', function() { + expect(element.hasClass('open')).toBe(true); + }); + + it('should change `is-open` binding when toggles', function() { + clickDropdownToggle(); + expect($rootScope.isopen).toBe(false); + }); + + it('should toggle when `is-open` changes', function() { + $rootScope.isopen = false; + $rootScope.$digest(); + expect(element.hasClass('open')).toBe(false); + }); + + it('focus toggle element when opening', function() { + $document.find('body').append(element); + clickDropdownToggle(); + $rootScope.isopen = false; + $rootScope.$digest(); + expect(isFocused(element.find('a'))).toBe(false); + $rootScope.isopen = true; + $rootScope.$digest(); + expect(isFocused(element.find('a'))).toBe(true); + element.remove(); + }); + }); + + describe('`on-toggle`', function() { + beforeEach(function() { + $rootScope.toggleHandler = jasmine.createSpy('toggleHandler'); + $rootScope.isopen = false; + element = $compile('
  • ')($rootScope); + $rootScope.$digest(); + }); + + it('should not have been called initially', function() { + expect($rootScope.toggleHandler).not.toHaveBeenCalled(); + }); + + it('should call it correctly when toggles', function() { + $rootScope.isopen = true; + $rootScope.$digest(); + expect($rootScope.toggleHandler).toHaveBeenCalledWith(true); + + clickDropdownToggle(); + expect($rootScope.toggleHandler).toHaveBeenCalledWith(false); + }); + }); + + describe('`on-toggle` with initially open', function() { + beforeEach(function() { + $rootScope.toggleHandler = jasmine.createSpy('toggleHandler'); + $rootScope.isopen = true; + element = $compile('
  • ')($rootScope); + $rootScope.$digest(); + }); + + it('should not have been called initially', function() { + expect($rootScope.toggleHandler).not.toHaveBeenCalled(); + }); + + it('should call it correctly when toggles', function() { + $rootScope.isopen = false; + $rootScope.$digest(); + expect($rootScope.toggleHandler).toHaveBeenCalledWith(false); + + $rootScope.isopen = true; + $rootScope.$digest(); + expect($rootScope.toggleHandler).toHaveBeenCalledWith(true); + }); + }); + + describe('`on-toggle` without is-open', function() { + beforeEach(function() { + $rootScope.toggleHandler = jasmine.createSpy('toggleHandler'); + element = $compile('
  • ')($rootScope); + $rootScope.$digest(); + }); + + it('should not have been called initially', function() { + expect($rootScope.toggleHandler).not.toHaveBeenCalled(); + }); + + it('should call it when clicked', function() { + clickDropdownToggle(); + expect($rootScope.toggleHandler).toHaveBeenCalledWith(true); + + clickDropdownToggle(); + 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('open')).toBe(true); + $document.click(); + expect(element.hasClass('open')).toBe(false); + }); + + it('should close on document click if empty auto-close is specified', function() { + element = dropdown(''); + clickDropdownToggle(); + expect(element.hasClass('open')).toBe(true); + $document.click(); + expect(element.hasClass('open')).toBe(false); + }); + + it('auto-close="disabled"', function() { + element = dropdown('disabled'); + clickDropdownToggle(); + expect(element.hasClass('open')).toBe(true); + $document.click(); + expect(element.hasClass('open')).toBe(true); + }); + + it('auto-close="outsideClick"', function() { + element = dropdown('outsideClick'); + clickDropdownToggle(); + expect(element.hasClass('open')).toBe(true); + element.find('ul li a').click(); + expect(element.hasClass('open')).toBe(true); + $document.click(); + expect(element.hasClass('open')).toBe(false); + }); + + it('control with is-open', function() { + $rootScope.isopen = true; + element = $compile('
  • ')($rootScope); + $rootScope.$digest(); + + expect(element.hasClass('open')).toBe(true); + //should remain open + $document.click(); + expect(element.hasClass('open')).toBe(true); + //now should close + $rootScope.isopen = false; + $rootScope.$digest(); + expect(element.hasClass('open')).toBe(false); + }); + + it('should close anyway if toggle is clicked', function() { + element = dropdown('disabled'); + clickDropdownToggle(); + expect(element.hasClass('open')).toBe(true); + clickDropdownToggle(); + expect(element.hasClass('open')).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('open')).toBe(false); + expect(isFocused(element.find('a'))).toBe(true); + element.remove(); + }); + + it('should close anyway if another dropdown is opened', function() { + var elm1 = dropdown('disabled'); + var elm2 = dropdown(); + expect(elm1.hasClass('open')).toBe(false); + expect(elm2.hasClass('open')).toBe(false); + clickDropdownToggle(elm1); + expect(elm1.hasClass('open')).toBe(true); + expect(elm2.hasClass('open')).toBe(false); + clickDropdownToggle(elm2); + expect(elm1.hasClass('open')).toBe(false); + expect(elm2.hasClass('open')).toBe(true); + }); + }); +});