From 619434ef40446706e5b0cfdc1432f1dabc43059c Mon Sep 17 00:00:00 2001 From: Andrew Date: Fri, 17 Oct 2014 12:23:20 -0600 Subject: [PATCH] feat(sidenav): add `lock-open` attribute --- docs/config/template/index.template.html | 5 +- src/components/sidenav/_sidenav.scss | 58 ++++---- .../sidenav/demoBasicUsage/index.html | 20 ++- src/components/sidenav/sidenav.js | 132 +++++++----------- src/components/whiteframe/_whiteframe.scss | 10 +- src/core/util/util.js | 31 +++- src/services/media/media.js | 54 +++++++ src/services/media/media.spec.js | 37 +++++ 8 files changed, 219 insertions(+), 128 deletions(-) create mode 100644 src/services/media/media.js create mode 100644 src/services/media/media.spec.js diff --git a/docs/config/template/index.template.html b/docs/config/template/index.template.html index a6722def21f..b991c05ce4a 100644 --- a/docs/config/template/index.template.html +++ b/docs/config/template/index.template.html @@ -14,7 +14,10 @@ - +

diff --git a/src/components/sidenav/_sidenav.scss b/src/components/sidenav/_sidenav.scss index 48ccb6c77a2..884b2a0ef2d 100644 --- a/src/components/sidenav/_sidenav.scss +++ b/src/components/sidenav/_sidenav.scss @@ -3,53 +3,55 @@ md-sidenav { width: $sidenav-default-width; bottom: 0; - + z-index: $z-index-sidenav; background-color: white; + overflow: auto; - transition: transform 0.3s ease-in-out; + display: none; + &.open, + &.open-add, + &.open-remove { + display: block; + } + &.open-add, + &.open-remove { + /* this is required as of 1.3x to properly + apply all styling in a show/hide animation */ + transition: 0s all; + } + &.open-add.open-add-active, + &.open-remove.open-remove-active { + transition: transform 0.3s ease-in-out; + } - @extend .md-sidenav-left; + &.lock-open, + &.lock-open.material-sidenav-left, + &.lock-open.material-sidenav-right { + position: static; + display: block; + transform: translate3d(0, 0, 0); + } - // &.closed { - // display: none; - // } + @extend .material-sidenav-left; +} +.md-sidenav-backdrop.lock-open { + display: none; } .md-sidenav-left { left: 0; top: 0; transform: translate3d(-100%, 0, 0); - &.open { transform: translate3d(0%, 0, 0); - z-index: $z-index-sidenav; } } .md-sidenav-right { left: 100%; top: 0; - transform: translate3d(100%, 0, 0); - + transform: translate3d(0%, 0, 0); &.open { transform: translate3d(-100%, 0, 0); - z-index: $z-index-sidenav; - } -} - -@media (min-width: $layout-breakpoint-md) { - md-sidenav { - position: static; - transform: translate3d(0,0,0) !important; - } - - .md-sidenav-backdrop { - display: none !important; - } -} - -@media (max-width: $sidenav-default-width + 2 * $sidenav-min-room) { - md-sidenav { - max-width: 75%; } } diff --git a/src/components/sidenav/demoBasicUsage/index.html b/src/components/sidenav/demoBasicUsage/index.html index f893542b11f..dc8270c0086 100644 --- a/src/components/sidenav/demoBasicUsage/index.html +++ b/src/components/sidenav/demoBasicUsage/index.html @@ -1,9 +1,9 @@ -
+
- +

Sidenav Left

@@ -12,6 +12,10 @@

Sidenav Left

Close Sidenav Left +

+ This sidenav is locked open on your device. To go back to the default behavior, + narrow your display. +

@@ -20,10 +24,7 @@

Sidenav Left

- On smaller devices, md-sidenav will be hidden by default. -

-

- On larger, it will be shown by default. + The left sidenav will 'lock open' on a medium (>=960px wide) device.

@@ -35,7 +36,7 @@

Sidenav Left

+ class="md-button-colored"> Toggle right
@@ -49,7 +50,7 @@

Sidenav Left

Sidenav Right

- + Close Sidenav Right @@ -59,6 +60,3 @@

Sidenav Right

- diff --git a/src/components/sidenav/sidenav.js b/src/components/sidenav/sidenav.js index f6fd4056443..7cfdb339f07 100644 --- a/src/components/sidenav/sidenav.js +++ b/src/components/sidenav/sidenav.js @@ -8,6 +8,7 @@ angular.module('material.components.sidenav', [ 'material.core', 'material.services.registry', + 'material.services.media', 'material.animations' ]) .factory('$mdSidenav', [ @@ -16,8 +17,9 @@ angular.module('material.components.sidenav', [ ]) .directive('mdSidenav', [ '$timeout', - '$mdEffects', - '$$rAF', + '$animate', + '$parse', + '$mdMedia', '$mdConstant', mdSidenavDirective ]) @@ -49,24 +51,12 @@ function mdSidenavController($scope, $element, $attrs, $timeout, $mdSidenav, $md this.isOpen = function() { return !!$scope.isOpen; }; - - /** - * Toggle the side menu to open or close depending on its current state. - */ this.toggle = function() { $scope.isOpen = !$scope.isOpen; }; - - /** - * Open the side menu - */ this.open = function() { $scope.isOpen = true; }; - - /** - * Close the side menu - */ this.close = function() { $scope.isOpen = false; }; @@ -104,32 +94,16 @@ function mdSidenavService($mdComponentRegistry) { return { isOpen: function() { - if (!instance) { return; } - return instance.isOpen(); + return instance && instance.isOpen(); }, - /** - * Toggle the given sidenav - * @param handle the specific sidenav to toggle - */ toggle: function() { - if(!instance) { return; } - instance.toggle(); + instance && instance.toggle(); }, - /** - * Open the given sidenav - * @param handle the specific sidenav to open - */ - open: function(handle) { - if(!instance) { return; } - instance.open(); + open: function() { + instance && instance.open(); }, - /** - * Close the given sidenav - * @param handle the specific sidenav to close - */ - close: function(handle) { - if(!instance) { return; } - instance.close(); + close: function() { + instance && instance.close(); } }; }; @@ -145,8 +119,8 @@ function mdSidenavService($mdComponentRegistry) { * * A Sidenav component that can be opened and closed programatically. * - * When used properly with a layout, it will seamleslly stay open on medium - * and larger screens, while being hidden by default on mobile devices. + * When opened, it will appear above the app's main content area, + * unless a `lock-open` attribute is provided (see below). * * @usage * @@ -176,69 +150,68 @@ function mdSidenavService($mdComponentRegistry) { * }; * }); * + * + * @param {string=} component-id componentId to use with $mdSidenav + * service. + * @param {expression=} lock-open When this expression evalutes to true, + * the sidenav 'locks open': it falls into the content's flow instead + * of appearing above it. + * + * A $media() function is exposed to the expression, which + * can be given a media query or one of the `sm`, `md` or `lg` presets. + * Examples: + * + * - `` + * - `` + * - `` */ -function mdSidenavDirective($timeout, $mdEffects, $$rAF, $mdConstant) { +function mdSidenavDirective($timeout, $animate, $parse, $mdMedia, $mdConstant) { return { restrict: 'E', scope: {}, controller: '$mdSidenavController', - compile: compile + link: postLink }; - function compile(element, attr) { - element.addClass('closed'); - - return postLink; - } function postLink(scope, element, attr, sidenavCtrl) { - var backdrop = angular.element(''); - - scope.$watch('isOpen', onShowHideSide); - element.on($mdEffects.TRANSITIONEND_EVENT, onTransitionEnd); + var lockOpenParsed = $parse(attr.lockOpen); + var backdrop = angular.element( + '' + ); + + scope.$watch('isOpen', setOpen); + scope.$watch(function() { + return lockOpenParsed(scope.$parent, { + $media: $mdMedia + }); + }, function(isLocked) { + element.toggleClass('lock-open', !!isLocked); + backdrop.toggleClass('lock-open', !!isLocked); + }); /** * Toggle the SideNav view and attach/detach listeners * @param isOpen */ - function onShowHideSide(isOpen) { + function setOpen(isOpen) { var parent = element.parent(); - if (isOpen) { - element.removeClass('closed'); + parent[isOpen ? 'on' : 'off']('keydown', onKeyDown); + $animate[isOpen ? 'addClass' : 'removeClass'](element, 'open'); - parent.append(backdrop); - backdrop.on('click', close); - parent.on('keydown', onKeyDown); - - } else { - backdrop.remove(); - backdrop.off('click', close); - parent.off('keydown', onKeyDown); - } - - // Wait until the next frame, so that if the `closed` class was just removed the - // element has a chance to 're-initialize' from being display: none. - $$rAF(function() { - element.toggleClass('open', !!scope.isOpen); - }); - } - - function onTransitionEnd(ev) { - if (ev.target === element[0] && !scope.isOpen) { - element.addClass('closed'); - } + $animate[isOpen ? 'enter' : 'leave'](backdrop, parent); + backdrop[isOpen ? 'on' : 'off']('click', close); } /** * Auto-close sideNav when the `escape` key is pressed. * @param evt */ - function onKeyDown(evt) { - if(evt.which === $mdConstant.KEY_CODE.ESCAPE){ + function onKeyDown(ev) { + if (ev.which === $mdConstant.KEY_CODE.ESCAPE) { close(); - - evt.preventDefault(); - evt.stopPropagation(); + ev.preventDefault(); + ev.stopPropagation(); } } @@ -248,9 +221,6 @@ function mdSidenavDirective($timeout, $mdEffects, $$rAF, $mdConstant) { * to close() and perform its own actions. */ function close() { - - onShowHideSide( false ); - $timeout(function(){ sidenavCtrl.close(); }); diff --git a/src/components/whiteframe/_whiteframe.scss b/src/components/whiteframe/_whiteframe.scss index cbef75194d1..204b9d77f7d 100644 --- a/src/components/whiteframe/_whiteframe.scss +++ b/src/components/whiteframe/_whiteframe.scss @@ -4,30 +4,30 @@ md-whiteframe { .md-whiteframe-z1 { @extend md-whiteframe; - z-index: $whiteframe-zindex-z1; + // z-index: $whiteframe-zindex-z1; box-shadow: $whiteframe-shadow-z1; } .md-whiteframe-z2 { @extend md-whiteframe; - z-index: $whiteframe-zindex-z2; + // z-index: $whiteframe-zindex-z2; box-shadow: $whiteframe-shadow-z2; } .md-whiteframe-z3 { @extend md-whiteframe; - z-index: $whiteframe-zindex-z3; + // z-index: $whiteframe-zindex-z3; box-shadow: $whiteframe-shadow-z3; } .md-whiteframe-z4 { @extend md-whiteframe; - z-index: $whiteframe-zindex-z4; + // z-index: $whiteframe-zindex-z4; box-shadow: $whiteframe-shadow-z4; } .md-whiteframe-z5 { @extend md-whiteframe; - z-index: $whiteframe-zindex-z5; + // z-index: $whiteframe-zindex-z5; box-shadow: $whiteframe-shadow-z5; } diff --git a/src/core/util/util.js b/src/core/util/util.js index 48c66f5e430..d7223e51cb6 100644 --- a/src/core/util/util.js +++ b/src/core/util/util.js @@ -1,5 +1,5 @@ angular.module('material.core') -.factory('$mdUtil', function() { +.factory('$mdUtil', ['$cacheFactory', function($cacheFactory) { var SPECIAL_CHARS_REGEXP = /([\:\-\_]+(.))/g; /* for nextUid() function below */ var uid = ['0','0','0']; @@ -74,6 +74,11 @@ angular.module('material.core') */ iterator: iterator, + /** + * @see cacheFactory below + */ + cacheFactory: cacheFactory, + // Returns a function, that, as long as it continues to be invoked, will not // be triggered. The function will be called after it stops being called for // N milliseconds. If `immediate` is passed, trigger the function on the @@ -415,7 +420,29 @@ angular.module('material.core') return _items.length ? _items[_items.length - 1] : null; } } -}); + + function cacheFactory(id, options) { + var cache = $cacheFactory(id, options); + + var keys = {}; + cache._put = cache.put; + cache.put = function(k,v) { + keys[k] = true; + return cache._put(k, v); + }; + cache._remove = cache.remove; + cache.remove = function(k) { + delete keys[k]; + return cache._remove(k); + }; + + cache.keys = function() { + return Object.keys(keys); + }; + + return cache; + } +}]); /* * Since removing jQuery from the demos, some code that uses `element.focus()` is broken. diff --git a/src/services/media/media.js b/src/services/media/media.js new file mode 100644 index 00000000000..fed3375ebfb --- /dev/null +++ b/src/services/media/media.js @@ -0,0 +1,54 @@ +angular.module('material.services.media', [ + 'material.core' +]) + +.factory('$mdMedia', [ + '$window', + '$mdUtil', + '$timeout', + mdMediaFactory +]); + +function mdMediaFactory($window, $mdUtil, $timeout) { + var cache = $mdUtil.cacheFactory('$mdMedia', { capacity: 15 }); + var presets = { + sm: '(min-width: 600px)', + md: '(min-width: 960px)', + lg: '(min-width: 1200px)' + }; + + angular.element($window).on('resize', updateAll); + + return $mdMedia; + + function $mdMedia(query) { + query = validate(query); + var result; + if ( !angular.isDefined(result = cache.get(query)) ) { + return add(query); + } + return result; + } + + function validate(query) { + return presets[query] || ( + query.charAt(0) != '(' ? ('(' + query + ')') : query + ); + } + + function add(query) { + return cache.put(query, !!$window.matchMedia(query).matches); + } + + function updateAll() { + var keys = cache.keys(); + if (keys.length) { + for (var i = 0, ii = keys.length; i < ii; i++) { + cache.put(keys[i], !!$window.matchMedia(keys[i]).matches); + } + // trigger an $digest() + $timeout(angular.noop); + } + } + +} diff --git a/src/services/media/media.spec.js b/src/services/media/media.spec.js new file mode 100644 index 00000000000..3a70d2d58f9 --- /dev/null +++ b/src/services/media/media.spec.js @@ -0,0 +1,37 @@ +describe('$mdMedia', function() { + + beforeEach(module('material.services.media')); + + var matchMediaResult = false; + beforeEach(inject(function($window) { + spyOn($window, 'matchMedia').andCallFake(function() { + return { matches: matchMediaResult }; + }); + })); + + it('should validate input', inject(function($window, $mdMedia) { + $mdMedia('something'); + expect($window.matchMedia).toHaveBeenCalledWith('(something)'); + })); + it('should validate input', inject(function($window, $mdMedia) { + $mdMedia('sm'); + expect($window.matchMedia).toHaveBeenCalledWith('(min-width: 600px)'); + + $window.matchMedia.reset(); + $mdMedia('md'); + expect($window.matchMedia).toHaveBeenCalledWith('(min-width: 960px)'); + + $window.matchMedia.reset(); + $mdMedia('lg'); + expect($window.matchMedia).toHaveBeenCalledWith('(min-width: 1200px)'); + })); + + it('should return result of matchMedia and recalculate on resize', inject(function($window, $mdMedia) { + matchMediaResult = true; + expect($mdMedia('foo')).toBe(true); + matchMediaResult = false; + expect($mdMedia('foo')).toBe(true); + angular.element($window).triggerHandler('resize'); + expect($mdMedia('foo')).toBe(false); + })); +});