Skip to content
This repository has been archived by the owner on Sep 5, 2024. It is now read-only.

feat(sidenav): add gestures to sidenav to allow dragging the sidenav #6174

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions src/components/sidenav/demoSidenavDragging/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@

<div ng-controller="AppCtrl" layout="column" style="height:500px;" ng-cloak>

<section layout="row" flex>

<md-sidenav class="md-sidenav-left md-whiteframe-z2" md-component-id="left">

<md-toolbar class="md-theme-light">
<h1 class="md-toolbar-tools">Drag Enabled</h1>
</md-toolbar>
<md-content ng-controller="LeftCtrl" layout-padding>
<p>
This sidenav is able to be dragged. Through the drag gesture, it's possible to close the sidenav.
</p>
<md-button ng-click="close()" class="md-primary">
Close Sidenav Left
</md-button>
</md-content>

</md-sidenav>

<md-content flex layout-padding>

<div layout="column" layout-fill layout-align="top center">
<p>
You can simply close a sidenav by using a gesture. Simply drag the sidenav to close it.
</p>
<p>
The right sidenav allows dragging.
</p>
<p>
The left sidenav is disabled for dragging.
</p>

<div>
<md-button ng-click="toggleLeft()"
class="md-primary">
Toggle left
</md-button>
</div>

<div>
<md-button ng-click="toggleRight()"
ng-hide="isOpenRight()"
class="md-primary">
Toggle right
</md-button>
</div>
</div>

<div flex></div>

</md-content>

<md-sidenav class="md-sidenav-right md-whiteframe-z2" md-component-id="right" md-disable-drag="true">

<md-toolbar class="md-theme-indigo">
<h1 class="md-toolbar-tools">Drag Disabled</h1>
</md-toolbar>
<md-content layout-padding ng-controller="RightCtrl">
<md-button ng-click="close()" class="md-primary">
Close Sidenav Right
</md-button>
<p>
This sidenav disables the dragging gesture. It can be closed through the buttons.
</p>
</md-content>

</md-sidenav>

</section>

</div>
27 changes: 27 additions & 0 deletions src/components/sidenav/demoSidenavDragging/script.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
angular
.module('sidenavDemo2', ['ngMaterial'])
.controller('AppCtrl', function ($scope, $timeout, $mdSidenav) {
$scope.toggleLeft = buildToggler('left');
$scope.toggleRight = buildToggler('right');

$scope.isOpenRight = function(){
return $mdSidenav('right').isOpen();
};

function buildToggler(navID) {
return function() {
$mdSidenav(navID).toggle()
}
}
})
.controller('LeftCtrl', function ($scope, $timeout, $mdSidenav) {
$scope.close = function () {
$mdSidenav('left').close();

};
})
.controller('RightCtrl', function ($scope, $timeout, $mdSidenav) {
$scope.close = function () {
$mdSidenav('right').close();
};
});
100 changes: 99 additions & 1 deletion src/components/sidenav/sidenav.js
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ function SidenavFocusDirective() {
* @param {expression=} md-is-open A model bound to whether the sidenav is opened.
* @param {boolean=} md-disable-backdrop When present in the markup, the sidenav will not show a backdrop.
* @param {string=} md-component-id componentId to use with $mdSidenav service.
* @param {boolean=} md-disable-drag Disables the abbility to drag the sidenav
* @param {expression=} md-is-locked-open When this expression evalutes 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.
Expand All @@ -209,7 +210,7 @@ function SidenavFocusDirective() {
* - `<md-sidenav md-is-locked-open="$mdMedia('min-width: 1000px')"></md-sidenav>`
* - `<md-sidenav md-is-locked-open="$mdMedia('sm')"></md-sidenav>` (locks open on small screens)
*/
function SidenavDirective($mdMedia, $mdUtil, $mdConstant, $mdTheming, $animate, $compile, $parse, $log, $q, $document) {
function SidenavDirective($mdMedia, $mdUtil, $mdConstant, $mdTheming, $animate, $mdGesture, $parse, $log, $q, $document, $timeout) {
return {
restrict: 'E',
scope: {
Expand Down Expand Up @@ -265,6 +266,8 @@ function SidenavDirective($mdMedia, $mdUtil, $mdConstant, $mdTheming, $animate,
scope.$watch(isLocked, updateIsLocked);
scope.$watch('isOpen', updateIsOpen);

// Enable dragging
if (!angular.isDefined(attr.mdDisableDrag) || !attr.mdDisableDrag) enableDragging();

// Publish special accessor for the Controller instance
sidenavCtrl.$toggleOpen = toggleOpen;
Expand Down Expand Up @@ -335,6 +338,101 @@ function SidenavDirective($mdMedia, $mdUtil, $mdConstant, $mdTheming, $animate,
}
}

function enableDragging() {
$mdGesture.register(element, 'drag', { horizontal: true });

element
.on('$md.dragstart', onDragStart)
.on('$md.drag', onDrag)
.on('$md.dragend', onDragEnd);

var style = getComputedStyle(element[0]);
var sidenavWidth = parseInt(style.width);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

element[0].offsetWidth won't work better?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That won't work, because the sidenav is hidden, that's why offsetWidth doesn't work

var isRightSidenav = element.hasClass('md-sidenav-right');
var accelerationBound = 6;

var dragCancelled = false;
var dragPercentage;
var lastOpenState;
var lastDistance = 0;
var isQuickDrag = false;

function onDragStart() {
if (element.hasClass('md-locked-open')) {
dragCancelled = true;
} else {
lastOpenState = scope.isOpen;
element.css($mdConstant.CSS.TRANSITION_DURATION, '0ms');
}
}

function onDrag(ev) {
if (dragCancelled) return;

if (!isQuickDrag) {
var distance = lastDistance - ev.pointer.distanceX;
// When the current partial drag distance is bigger than the acceleration bound, then we can
// identify it as a quick drag.
isQuickDrag = isRightSidenav ? distance <= -accelerationBound : distance >= accelerationBound;
} else if (isRightSidenav && lastDistance > ev.pointer.distanceX ||
!isRightSidenav && lastDistance < ev.pointer.distanceX) {
// When the users drags the sidenav backward, then we can reset the quick drag state.
isQuickDrag = false;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'd prefer

if (...) {
  if (...) {
  }
  else {
    ...
  }
}
else {
  ...
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that will look more clear.


dragPercentage = Math.round((ev.pointer.distanceX / sidenavWidth) * 100);
if (!isRightSidenav) dragPercentage = 0 - dragPercentage;

if (dragPercentage > 100) dragPercentage = 100;
else if (dragPercentage < 0) dragPercentage = 0;

element.css($mdConstant.CSS.TRANSFORM, 'translate3d(-' + (isRightSidenav ? 100 - dragPercentage : dragPercentage) + '%,0,0)');
lastDistance = ev.pointer.distanceX;
}

function onDragEnd() {
if (dragCancelled) {
dragCancelled = false;
return;
}

var remainingPercentage = 100 - dragPercentage;
var animationTime = 4 * remainingPercentage;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's because of my percentage calculation

400ms / 100% = 4ms

So every percentage needs to run for 4ms to reach a smooth transition as always.

var shouldClose = dragPercentage > 50 || isQuickDrag;

// This validates the correct translate value. The invert is here required, because a right
// aligned sidenav will transition in the other direction.
var endTranslate = shouldClose ? isRightSidenav ? 0 : -100 : isRightSidenav ? -100 : 0;

element.css($mdConstant.CSS.TRANSITION_DURATION, animationTime + "ms");
element.css($mdConstant.CSS.TRANSFORM, 'translate3d(' + endTranslate + '%,0,0)');

// Reset drag
lastDistance = 0;
isQuickDrag = false;

// Invert shouldClose here, because we need to know if the sidenav should open.
$timeout(onAnimationDone, animationTime, true, !shouldClose);
}

function onAnimationDone(isOpen) {
scope.isOpen = isOpen;
element.css($mdConstant.CSS.TRANSFORM, '');
element.css($mdConstant.CSS.TRANSITION_DURATION, '');

if (isOpen) {
if (!lastOpenState && backdrop) {
$animate.enter(backdrop, element.parent());
}

element.removeClass('_md-closed');
} else {
if (backdrop) $animate.leave(backdrop);
element.addClass('_md-closed');
}
}
}

/**
* Toggle the sideNav view and publish a promise to be resolved when
* the view animation finishes.
Expand Down
3 changes: 3 additions & 0 deletions src/components/sidenav/sidenav.scss
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ md-sidenav {
overflow: auto;
-webkit-overflow-scrolling: touch;

// This allows the most browsers to setup appropriate optimizations before the animation runs.
will-change: transform;

ul {
list-style: none;
}
Expand Down