From 938b101fdd41cfe95380c0f3403f65d990cb8565 Mon Sep 17 00:00:00 2001 From: Ryan Silva Date: Wed, 1 Apr 2015 22:51:38 -0400 Subject: [PATCH] feat(dropdown): Dropdown append-to-body Closes #3411 Fixes #1030 --- src/dropdown/docs/demo.html | 14 +++++++++++ src/dropdown/docs/readme.md | 5 ++++ src/dropdown/dropdown.js | 38 +++++++++++++++++++++++++++--- src/dropdown/test/dropdown.spec.js | 28 ++++++++++++++++++---- 4 files changed, 78 insertions(+), 7 deletions(-) diff --git a/src/dropdown/docs/demo.html b/src/dropdown/docs/demo.html index 29ba11a43f..e542418f2c 100644 --- a/src/dropdown/docs/demo.html +++ b/src/dropdown/docs/demo.html @@ -70,6 +70,20 @@ + +
+ + +
+

diff --git a/src/dropdown/docs/readme.md b/src/dropdown/docs/readme.md index 5f42e0e829..02970d0a5f 100644 --- a/src/dropdown/docs/readme.md +++ b/src/dropdown/docs/readme.md @@ -2,8 +2,13 @@ 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 b469b0bec5..8f03186160 100644 --- a/src/dropdown/dropdown.js +++ b/src/dropdown/dropdown.js @@ -1,4 +1,4 @@ -angular.module('ui.bootstrap.dropdown', []) +angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position']) .constant('dropdownConfig', { openClass: 'open' @@ -60,13 +60,14 @@ angular.module('ui.bootstrap.dropdown', []) }; }]) -.controller('DropdownController', ['$scope', '$attrs', '$parse', 'dropdownConfig', 'dropdownService', '$animate', function($scope, $attrs, $parse, dropdownConfig, dropdownService, $animate) { +.controller('DropdownController', ['$scope', '$attrs', '$parse', 'dropdownConfig', 'dropdownService', '$animate', '$position', '$document', function($scope, $attrs, $parse, dropdownConfig, dropdownService, $animate, $position, $document) { var self = this, scope = $scope.$new(), // create a child scope so we are not polluting original one openClass = dropdownConfig.openClass, getIsOpen, setIsOpen = angular.noop, - toggleInvoker = $attrs.onToggle ? $parse($attrs.onToggle) : angular.noop; + toggleInvoker = $attrs.onToggle ? $parse($attrs.onToggle) : angular.noop, + appendToBody = false; this.init = function( element ) { self.$element = element; @@ -79,6 +80,15 @@ angular.module('ui.bootstrap.dropdown', []) scope.isOpen = !!value; }); } + + appendToBody = angular.isDefined($attrs.dropdownAppendToBody); + + if ( appendToBody && self.dropdownMenu ) { + $document.find('body').append( self.dropdownMenu ); + element.on('$destroy', function handleDestroyEvent() { + self.dropdownMenu.remove(); + }); + } }; this.toggle = function( open ) { @@ -109,6 +119,15 @@ angular.module('ui.bootstrap.dropdown', []) }; 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 ) { @@ -142,6 +161,19 @@ angular.module('ui.bootstrap.dropdown', []) }; }) +.directive('dropdownMenu', function() { + return { + restrict: 'AC', + require: '?^dropdown', + link: function(scope, element, attrs, dropdownCtrl) { + if ( !dropdownCtrl ) { + return; + } + dropdownCtrl.dropdownMenu = element; + } + }; +}) + .directive('dropdownToggle', function() { return { require: '?^dropdown', diff --git a/src/dropdown/test/dropdown.spec.js b/src/dropdown/test/dropdown.spec.js index 5e7c7dab9e..8438aca550 100644 --- a/src/dropdown/test/dropdown.spec.js +++ b/src/dropdown/test/dropdown.spec.js @@ -9,6 +9,10 @@ describe('dropdownToggle', function() { $document = _$document_; })); + afterEach(function() { + element.remove(); + }); + var clickDropdownToggle = function(elm) { elm = elm || element; elm.find('a[dropdown-toggle]').click(); @@ -50,7 +54,6 @@ describe('dropdownToggle', function() { 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() { @@ -66,7 +69,6 @@ describe('dropdownToggle', function() { 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() { @@ -180,6 +182,26 @@ describe('dropdownToggle', function() { }); }); + 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); + }); + }); + describe('integration with $location URL rewriting', function() { function dropdown() { @@ -256,7 +278,6 @@ describe('dropdownToggle', function() { $rootScope.isopen = true; $rootScope.$digest(); expect(isFocused(element.find('a'))).toBe(true); - element.remove(); }); }); @@ -396,7 +417,6 @@ describe('dropdownToggle', function() { 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() {