From 218c3ec321345d0c6dec9a1a4075a11296472382 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Tue, 23 Aug 2016 19:37:16 +0200 Subject: [PATCH] feat(sidenav): configurable scroll prevent target (#9338) * Adds the ability to select which element's scrolling will be disabled when a sidenav is open. This can be useful in the cases where the scrollable container isn't the direct parent of the sidenav. * Adds unit tests for the parent scroll prevention. Fixes #8634 --- src/components/sidenav/sidenav.js | 33 ++++++++++++++------- src/components/sidenav/sidenav.spec.js | 41 ++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 10 deletions(-) diff --git a/src/components/sidenav/sidenav.js b/src/components/sidenav/sidenav.js index cbd2a4ccbb3..b4670693f0b 100644 --- a/src/components/sidenav/sidenav.js +++ b/src/components/sidenav/sidenav.js @@ -232,6 +232,8 @@ function SidenavFocusDirective() { * @param {expression=} md-is-locked-open When this expression evaluates to true, * the sidenav 'locks open': it falls into the content's flow instead * of appearing over it. This overrides the `md-is-open` attribute. + * @param {string=} md-disable-scroll-target Selector, pointing to an element, whose scrolling will + * be disabled when the sidenav is opened. By default this is the sidenav's direct parent. * * The $mdMedia() service is exposed to the is-locked-open attribute, which * can be given a media query or one of the `sm`, `gt-sm`, `md`, `gt-md`, `lg` or `gt-lg` presets. @@ -261,6 +263,7 @@ function SidenavDirective($mdMedia, $mdUtil, $mdConstant, $mdTheming, $animate, function postLink(scope, element, attr, sidenavCtrl) { var lastParentOverFlow; var backdrop; + var disableScrollTarget = null; var triggeringElement = null; var previousContainerStyles; var promise = $q.when(true); @@ -275,8 +278,23 @@ function SidenavDirective($mdMedia, $mdUtil, $mdConstant, $mdTheming, $animate, }); }; + if (attr.mdDisableScrollTarget) { + disableScrollTarget = $document[0].querySelector(attr.mdDisableScrollTarget); + + if (disableScrollTarget) { + disableScrollTarget = angular.element(disableScrollTarget); + } else { + $log.warn($mdUtil.supplant('mdSidenav: couldn\'t find element matching ' + + 'selector "{selector}". Falling back to parent.', { selector: attr.mdDisableScrollTarget })); + } + } + + if (!disableScrollTarget) { + disableScrollTarget = element.parent(); + } + // Only create the backdrop if the backdrop isn't disabled. - if (!angular.isDefined(attr.mdDisableBackdrop)) { + if (!attr.hasOwnProperty('mdDisableBackdrop')) { backdrop = $mdUtil.createBackdrop(scope, "md-sidenav-backdrop md-opaque ng-enter"); } @@ -393,7 +411,7 @@ function SidenavDirective($mdMedia, $mdUtil, $mdConstant, $mdTheming, $animate, backdrop[0].style.height = null; previousContainerStyles = null; - } + }; } } @@ -401,17 +419,12 @@ function SidenavDirective($mdMedia, $mdUtil, $mdConstant, $mdTheming, $animate, * Prevent parent scrolling (when the SideNav is open) */ function disableParentScroll(disabled) { - var parent = element.parent(); if ( disabled && !lastParentOverFlow ) { - - lastParentOverFlow = parent.css('overflow'); - parent.css('overflow', 'hidden'); - + lastParentOverFlow = disableScrollTarget.css('overflow'); + disableScrollTarget.css('overflow', 'hidden'); } else if (angular.isDefined(lastParentOverFlow)) { - - parent.css('overflow', lastParentOverFlow); + disableScrollTarget.css('overflow', lastParentOverFlow); lastParentOverFlow = undefined; - } } diff --git a/src/components/sidenav/sidenav.spec.js b/src/components/sidenav/sidenav.spec.js index 3449a94e61d..57197a3f01e 100644 --- a/src/components/sidenav/sidenav.spec.js +++ b/src/components/sidenav/sidenav.spec.js @@ -164,6 +164,47 @@ describe('mdSidenav', function() { }); }); + describe('parent scroll prevention', function() { + it('should prevent scrolling on the parent element', inject(function($rootScope) { + var parent = setup('md-is-open="isOpen"').parent()[0]; + + expect(parent.style.overflow).toBeFalsy(); + $rootScope.$apply('isOpen = true'); + expect(parent.style.overflow).toBe('hidden'); + })); + + it('should prevent scrolling on a custom element', inject(function($compile, $rootScope) { + var preventScrollTarget = angular.element('
'); + var parent = angular.element( + '
' + + '' + + '
' + ); + + preventScrollTarget.append(parent); + angular.element(document.body).append(preventScrollTarget); + $compile(preventScrollTarget)($rootScope); + + expect(preventScrollTarget[0].style.overflow).toBeFalsy(); + expect(parent[0].style.overflow).toBeFalsy(); + + $rootScope.$apply('isOpen = true'); + expect(preventScrollTarget[0].style.overflow).toBe('hidden'); + expect(parent[0].style.overflow).toBeFalsy(); + preventScrollTarget.remove(); + })); + + it('should log a warning and fall back to the parent if the custom scroll target does not exist', + inject(function($rootScope, $log) { + spyOn($log, 'warn'); + var parent = setup('md-is-open="isOpen" md-disable-scroll-target="does-not-exist"').parent()[0]; + + $rootScope.$apply('isOpen = true'); + expect($log.warn).toHaveBeenCalled(); + expect(parent.style.overflow).toBe('hidden'); + })); + }); + }); describe('controller', function() {