angular/components

feat(sidenav): add swipe functionality

rafaelss95 opened this issue Β· 40 comments

Bug, feature request, or proposal:

Feature request

What is the expected behavior?

It'd be great to have the functionality of swipe in md-sidenav, like in https://material.io/guidelines/.

What are the steps to reproduce?

Open https://material.io/guidelines/ in mobile and see how the swipe on menu works.

Is there anything else we should know?

The same feature request was requested in material(1) here and at some point it was implemented, but not merged.

We do plan on doing this.

Hi, I have only recently jumped on the angular wagon and find all of the different versions to be completely confusing. At the moment I have an app underway using Angular2 and the material web components which seems to be a fairly reliable stack, however following the evolution of this feature I'm beginning to question my choice.

This is absolutely basic functionality for a sidenav, which I have first encountered when using a similar component in Nativescript. The fact that this was first suggested in 2014 and yet 3 years later is only in the plans is extremely frustrating! Apparently someone has implemented the feature, but because of the management of the project and "surge focus on material 2" it never got merged. This is enough to make me dump the whole Angular2 project and take Polymer web components into use since they have implemented drag on their sidenav component. This is needless to say disappointing since otherwise the material library has worked reasonably well. There were a lot of plus ones originally on this feature, I suspect it should have been given higher priority.

@jelbourn, is there an ETA for when this will be implemented?

I don't see any reference to this in the "In progress, planned, and non-planned features" section of the readme.

Is there any workaround?
thx

@RafaelSS99, in fact there's a better example from the rival library:
http://www.material-ui.com/#/components/drawer

Facebook-backed JS library has better material drawer (yes, they also use the official term) than Google-backed TS framework!

@AjawadMahmoud material-ui is for React.

Wasn't my comment clear enough, @Airblader?

Well you are presenting it as an alternative to Angular Material. But it works with an entirely different technology.

Polymer's swipeable sidenav is the only implementation I know of that enhances UX and it has been like that for at least two years:
http://polymerelements.github.io/app-layout/templates/shrine
A poor implementation (material-ui for instance) is worse than no implementation.

Swoox commented

This is maybe a workaround but it works for the dialog should work for the sidenav to. This will require hammerjs.

Place a div around your content in the html:

<div style="width: 100%; height: 100%; position: absolute;" (swipeleft)="closeDialog()" (click)="closeDialog()">

Nothing new on this?

Currently using a forked ionic-split-pane to get a swipe/slideable sidenav: https://github.com/xcaliber-tech/ionic-split-pane

Which involved a lot of hacking and ripping out random ion dependencies on other unrelated things like ion-nav, ion-button and ion-content. Would be awesome to remove that monstrosity when swipe behaviour lands here :)

@Swoox's solution didn't work for me, but brought me to hammerjs and this solution:

app-container.component.scss

.app-container {
    height: 100%;
    width: 100%;
    position: absolute;
}

app-container.component.html

<mat-sidenav-container class="app-container">
  <mat-sidenav #sidenav>Sidenav content</mat-sidenav>
  <mat-sidenav-content>
    Main Content
  </mat-sidenav-content>
</mat-sidenav-container>

app-container.component.ts

import { Component, Input, ElementRef, ViewChild } from '@angular/core';
import * as Hammer from 'hammerjs';
import { MatSidenav } from '@angular/material';

@Component({
    selector: 'app-container',
    templateUrl: './app-container.component.html',
    styleUrls: ['./app-container.component.scss']
})
export class AppContainerComponent {

    @ViewChild(MatSidenav)
    public sidenav: MatSidenav;

    constructor(elementRef: ElementRef) {
        const hammertime = new Hammer(elementRef.nativeElement, {});
        hammertime.on('panright', (ev) => {
            this.sidenav.open();
        });
        hammertime.on('panleft', (ev) => {
            this.sidenav.close();
        });
    }
}

It works as long as the content doesn't exceed the width of 100%, otherwise it will scroll the page and open the menu.

Thanks @maxfriedmann works like a charm!!! πŸ‘ πŸ‘ πŸ‘ πŸ‘ πŸ‘ πŸ‘ πŸ‘ πŸ‘ πŸ‘

Thanks @maxfriedmann, If you need to prevent closing when you want to scroll up/down when your sidenav is more thant 100% height then add the following to your code :

const hammertime = new Hammer(elementRef.nativeElement, {});
hammertime.get('pan').set({direction:` Hammer.DIRECTION_ALL});
hammertime.on('panright', (ev) => {
            this.sidenav.open();
        });
        hammertime.on('panleft', (ev) => {
            this.sidenav.close();
        });
hammertime.on('panup', (ev) => false);
hammertime.on('pandown', (ev) => false);

anyway...isn't this deadly to performance?
I removed it since on mobile I'm wrapping the app in Ionic 3.9..

@maxfriedmann Like you solution but what if you add another drawer on the right side? How would you solve that?

I haven't had that problem, only used a single drawer.

Please have a look at https://hammerjs.github.io/recognizer-pan, you actually get the start and stop positions of the pan action. You could e.g. split the screen in 1/3 and 2/3 and decide programmatically, which drawer to open...

@maxfriedmann thx for pointing me in the right direction... but at this time it needs to much work to fine tune so I need to wait for a better and proper implementation.

@jelbourn is there any work started on you end for this feature?

If you only want to check for touch device swipes (and not mouse drags on desktop), you can check the event.pointerType property.

		hammertime.on('panright', (event) => {
			if (event.pointerType !== 'mouse') {
				this.sidenav.open();
			}
		});
		hammertime.on('panleft', (event) => {
			if (event.pointerType !== 'mouse') {
				this.sidenav.close();
			}
		});

@PascalTemel Didn't try this, since we decided to remove this snippet from the desktop and use the native menu in ionic...but, nice approach.

A slightly better option is to use the HostListener decorator. Then there is no need to create a new Hammer instance. For example, place this into the root AppComponent:

    @HostListener('panright')
    openSidenav() {
        // open the sidenav
    }

    @HostListener('panleft')
    closeSidenav() {
       // close the sidenav
    }
gxg10 commented

Does this implementation have any drawbacks ? (memory leaks, performance issues) Kind regards

Does this functionality not present issues in mobile browsers (like Chrome iOS) where a similar gesture is used for navigate forward and back in your history?

Hello everyone and thank you for those spectacular contributions. In my case they have helped me a lot. But I have a small inconvenience:

  1. Is it possible through Hammer to improve the animation performance of the sidenav?

When I open and close the sidenav, it has a lot of lag in the mobile devices, I have looked for how to change that for CSS and I have tried many solutions, but none has served me. I'm using Angular Material 7.3.1

  1. Is there any solution through Hammer or any other way that really works to improve the animation performance of sidenav?

And an even bolder question.

  1. Is it possible any solution through Hammer to move the sidenav according to touch of the finger? there is a very old library that does it and I have it for reference. http://jakiestfu.github.io/Snap.js/demo/apps/default.html

Thank you so much for the help you can give me.

This is my contribution for the sidenav to react on the left side of the screen to open.

    const hammertime = new Hammer(elementRef.nativeElement, {});
    hammertime.get('pan').set({ direction: Hammer.DIRECTION_ALL });
    hammertime.on('panright', event => {
      if (
        event.pointerType !== 'mouse' &&
        event.center.x >= 1 && event.center.x <= 50
      ) {
        this.sidenav.open();
      }
    });
    hammertime.on('panleft', event => {
      if (event.pointerType !== 'mouse') {
        this.sidenav.close();
      }
    });
    hammertime.on('panup', event => false);
    hammertime.on('pandown', event => false);

We really need a clean solution for that. Simply triggering the open is not an option.

Users are used to deferring the SideNav slowly. Depending on the slide progress, the cdk background layer must appear.

For example, with mode = "push", the content is moved slowly. Once more than 50% of the Sidenav wide is swiped, let the sidenav slide on. Less than 50% let go again.

Everything else is not a viable solution :)

We really need a clean solution for that. Simply triggering the open is not an option.

Users are used to deferring the SideNav slowly. Depending on the slide progress, the cdk background layer must appear.

For example, with mode = "push", the content is moved slowly. Once more than 50% of the Sidenav wide is swiped, let the sidenav slide on. Less than 50% let go again.

Everything else is not a viable solution :)

yes it should work exactly like it would in android studio, thinks like this is what makes your pwa feel native

Any update on this?

Any updates on this?
The home assistant project has a very well working swiping functionality for it's sidebar. You can try it here:
https://demo.home-assistant.io

Maybe it is possible to use the same library for that? (Polymer?)

Can you share the code @lud-hu ?

i have the auto hide toolbar if someone needs this: #16659

As of Angular v9 we're removing our dependency on HammerJS and officially deciding that Angular Material will be agnostic to any gesture recognition framework. I would still like to add documentation that gives some guidance on how to do this, but we won't be adding support directly into the sidenav.

@jelbourn does that mean that also other components such as mat-slide-toggle, mat-slider etc will not have swipe support?

Slider will still slide since that's inherent to its use, but for now that does mean the sliding/dragging behavior on slide-toggle is going away too. The main reason for that, though, isn't HammerJS. We working on new versions of the components based on top of MDC Web, and MDC's slide-toggle (they call it switch) doesn't support dragging either.

This is an incredibly disappointing decision. The sidenav should ship with basic common sense gesture support.

I built a working prototype in stackblitz that you can slide open or closed with native touch events that are passive.
Because it uses touch you'll need to open in new window and use the mobile emulator to test it.
https://stackblitz.com/edit/angular-b5gmd8

This ONLY works with the sidenav in over mode.
Unfortunately iOS still requires preventDefault for touchmove to prevent scrolling so we can't use passive. It assumes if the user has moved the touch 5px horizontally they are attempting to manipulate the drawer and disables scrolling, otherwise ignores it.

This isn't the usual, swipe and it fully opens/closes solution that I've seen in most places. You can actually pull the drawer out and let go and have it animate. It was difficult getting it to behave with the baked in animations and had to disable them while the drawer is being dragged.

There are probably a lot better ways of doing much of this and it might be brittle due to future changes in the material library but I was just interested in getting something functional that maintained the material feel and a pleasant UX, while still allowing the button toggle functionality to continue to work

HTH

Any updates on this?
The home assistant project has a very well working swiping functionality for it's sidebar. You can try it here:
https://demo.home-assistant.io

Maybe it is possible to use the same library for that? (Polymer?)

Respect for the hui prefix

The "funny" part is, I think Google is slowly getting rid of the sidenav in native apps (Store, Photos, Map). Now that Android has gesture navigation, these two features (go back & open sidenav) collide, and I think this is their solution, get rid of usages of sidenav. (I switched back to 3 dot navigation because of this collision) But this is just my theory, and I think we won't see this inplemented :)

@Swoox's solution didn't work for me, but brought me to hammerjs and this solution:

app-container.component.scss

.app-container {
    height: 100%;
    width: 100%;
    position: absolute;
}

app-container.component.html

<mat-sidenav-container class="app-container">
  <mat-sidenav #sidenav>Sidenav content</mat-sidenav>
  <mat-sidenav-content>
    Main Content
  </mat-sidenav-content>
</mat-sidenav-container>

app-container.component.ts

import { Component, Input, ElementRef, ViewChild } from '@angular/core';
import * as Hammer from 'hammerjs';
import { MatSidenav } from '@angular/material';

@Component({
    selector: 'app-container',
    templateUrl: './app-container.component.html',
    styleUrls: ['./app-container.component.scss']
})
export class AppContainerComponent {

    @ViewChild(MatSidenav)
    public sidenav: MatSidenav;

    constructor(elementRef: ElementRef) {
        const hammertime = new Hammer(elementRef.nativeElement, {});
        hammertime.on('panright', (ev) => {
            this.sidenav.open();
        });
        hammertime.on('panleft', (ev) => {
            this.sidenav.close();
        });
    }
}

It works as long as the content doesn't exceed the width of 100%, otherwise it will scroll the page and open the menu.

angular have helpers for hammer which uses zone runOutsideOfAngular to improve perf
import { HAMMER_GESTURE_CONFIG, HammerGestureConfig } from '@angular/platform-browser';

no need to use hammerjs directly