diff --git a/src/components/sidenav/demoBasicUsage/index.html b/src/components/sidenav/demoBasicUsage/index.html index e672ad2033a..5098a8ed505 100644 --- a/src/components/sidenav/demoBasicUsage/index.html +++ b/src/components/sidenav/demoBasicUsage/index.html @@ -26,6 +26,9 @@

Sidenav Left

The left sidenav will 'lock open' on a medium (>=960px wide) device.

+

+ The right sidenav will focus on a specific child element. +

Sidenav Left

Sidenav Right

+
+ + + + +
Close Sidenav Right diff --git a/src/components/sidenav/demoBasicUsage/script.js b/src/components/sidenav/demoBasicUsage/script.js index 3d4d9e4b8b7..2d0736ffc3d 100644 --- a/src/components/sidenav/demoBasicUsage/script.js +++ b/src/components/sidenav/demoBasicUsage/script.js @@ -2,13 +2,16 @@ angular.module('sidenavDemo1', ['ngMaterial']) .controller('AppCtrl', function($scope, $timeout, $mdSidenav, $log) { + $scope.toggleLeft = function() { $mdSidenav('left').toggle() .then(function(){ $log.debug("toggle left is done"); }); }; + $scope.toggleRight = function() { + $mdSidenav('right').toggle() .then(function(){ $log.debug("toggle RIGHT is done"); diff --git a/src/components/sidenav/sidenav.js b/src/components/sidenav/sidenav.js index d4a64d265b1..d1a4bf57c27 100644 --- a/src/components/sidenav/sidenav.js +++ b/src/components/sidenav/sidenav.js @@ -14,6 +14,7 @@ angular.module('material.components.sidenav', [ ]) .factory('$mdSidenav', SidenavService ) .directive('mdSidenav', SidenavDirective) + .directive('mdSidenavFocus', SidenavFocusDirective) .controller('$mdSidenavController', SidenavController); @@ -33,8 +34,6 @@ angular.module('material.components.sidenav', [ * $mdSidenav(componentId).then(function(instance) { * $log.debug( componentId + "is now ready" ); * }); - * - * * // Async toggle the given sidenav; * // when instance is known ready and lazy lookup is not needed. * $mdSidenav(componentId) @@ -42,28 +41,20 @@ angular.module('material.components.sidenav', [ * .then(function(){ * $log.debug('toggled'); * }); - * - * * // Async open the given sidenav * $mdSidenav(componentId) * .open(); * .then(function(){ * $log.debug('opened'); * }); - * - * * // Async close the given sidenav * $mdSidenav(componentId) * .close(); * .then(function(){ * $log.debug('closed'); * }); - * - * * // Sync check to see if the specified sidenav is set to be open * $mdSidenav(componentId).isOpen(); - * - * * // Sync check to whether given sidenav is locked open * // If this is true, the sidenav will be open regardless of close() * $mdSidenav(componentId).isLockedOpen(); @@ -122,7 +113,36 @@ function SidenavService($mdComponentRegistry, $q) { } }; } - +/** + * @private + * @name mdSidenavFocus + * @restrict A + * + * @description + * `$mdSidenavFocus` provides a way to specify the focused element when a sidenav opens. + * This is completely optional, as the sidenav itself is focused by default. + * + * @usage + * + * + *
+ * + * + * + * + *
+ *
+ *
+ **/ +function SidenavFocusDirective() { + return { + restrict: 'A', + require: '^mdSidenav', + link: function(scope, element, attr, sidenavCtrl) { + sidenavCtrl.focusElement(element); + } + }; +} /** * @ngdoc directive * @name mdSidenav @@ -135,6 +155,9 @@ function SidenavService($mdComponentRegistry, $q) { * * By default, upon opening it will slide out on top of the main content area. * + * For keyboard and screen reader accessibility, focus is sent to the sidenav wrapper by default. + * It can be overridden with the `md-sidenav-focus` directive on the child element you want focused. + * * @usage * *
@@ -152,7 +175,13 @@ function SidenavService($mdComponentRegistry, $q) { * - * Right Nav! + *
+ * + * + * + * + *
*
*
*
@@ -225,6 +254,7 @@ function SidenavDirective($timeout, $animate, $parse, $log, $mdMedia, $mdConstan // Publish special accessor for the Controller instance sidenavCtrl.$toggleOpen = toggleOpen; + sidenavCtrl.focusElement( sidenavCtrl.focusElement() || element ); /** * Toggle the DOM classes to indicate `locked` @@ -254,15 +284,15 @@ function SidenavDirective($timeout, $animate, $parse, $log, $mdMedia, $mdConstan // Capture upon opening.. triggeringElement = $document[0].activeElement; } + var focusEl = sidenavCtrl.focusElement(); disableParentScroll(isOpen); return promise = $q.all([ $animate[isOpen ? 'enter' : 'leave'](backdrop, parent), $animate[isOpen ? 'removeClass' : 'addClass'](element, 'md-closed').then(function() { - // If we opened, and haven't closed again before the animation finished if (scope.isOpen) { - element.focus(); + focusEl && focusEl.focus(); } }) ]); @@ -303,7 +333,7 @@ function SidenavDirective($timeout, $animate, $parse, $log, $mdMedia, $mdConstan $timeout(function() { // When the current `updateIsOpen()` animation finishes - promise.then(function(result){ + promise.then(function(result) { if ( !scope.isOpen ) { // reset focus to originating element (if available) upon close @@ -353,7 +383,8 @@ function SidenavDirective($timeout, $animate, $parse, $log, $mdMedia, $mdConstan */ function SidenavController($scope, $element, $attrs, $mdComponentRegistry, $q) { - var self = this; + var self = this, + focusElement; // Use Default internal method until overridden by directive postLink @@ -365,6 +396,12 @@ function SidenavController($scope, $element, $attrs, $mdComponentRegistry, $q) { self.open = function() { return self.$toggleOpen( true ); }; self.close = function() { return self.$toggleOpen( false ); }; self.toggle = function() { return self.$toggleOpen( !$scope.isOpen ); }; + self.focusElement = function(el) { + if ( angular.isDefined(el) ) { + focusElement = el; + } + return focusElement; + }; self.$toggleOpen = function() { return $q.when($scope.isOpen); }; diff --git a/src/components/sidenav/sidenav.spec.js b/src/components/sidenav/sidenav.spec.js index 6ff74f46609..70f4a6aab32 100644 --- a/src/components/sidenav/sidenav.spec.js +++ b/src/components/sidenav/sidenav.spec.js @@ -61,6 +61,43 @@ describe('mdSidenav', function() { expect($document.activeElement).toBe(el[0]); })); + it('should focus child directive with md-sidenav-focus', inject(function($rootScope, $animate, $document, $compile) { + TestUtil.mockElementFocus(this); + var parent = angular.element('
'); + var markup = ''+ + '' + + '' + + '' + + ''; + var sidenavEl = angular.element(markup); + parent.append(sidenavEl); + $compile(parent)($rootScope); + $rootScope.$apply('show = true'); + + var focusEl = sidenavEl.find('input'); + $animate.triggerCallbacks(); + expect($document.activeElement).toBe(focusEl[0]); + })); + + it('should focus on last md-sidenav-focus element', inject(function($rootScope, $animate, $document, $compile) { + TestUtil.mockElementFocus(this); + var parent = angular.element('
'); + var markup = ''+ + 'Button'+ + '' + + '' + + '' + + ''; + var sidenavEl = angular.element(markup); + parent.append(sidenavEl); + $compile(parent)($rootScope); + $rootScope.$apply('show = true'); + + $animate.triggerCallbacks(); + var focusEl = sidenavEl.find('input'); + expect($document.activeElement).toBe(focusEl[0]); + })); + it('should lock open when is-locked-open is true', inject(function($rootScope, $animate, $document) { var el = setup('md-is-open="show" md-is-locked-open="lock"'); expect(el.hasClass('md-locked-open')).toBe(false);