web-dev-qa-db-fra.com

Empêcher le défilement de l'élément enfant de se propager dans Angular 2

C'est un classique. Vous avez un élément parent et un élément enfant. L'élément enfant est absolument positionné et vous souhaitez que l'utilisateur fasse défiler son contenu. Cependant, lorsque vous atteignez le bas de l'élément enfant, l'élément parent (qui est également autorisé à avoir des barres de défilement) commence à défiler. C'est indésirable. Le comportement de base que je veux reproduire est celui de la section des commentaires du New York Times. Pour exemple :

enter image description here

Le corps est autorisé à défiler vers le bas, mais lorsque vous êtes au bas de la section des commentaires, le défilement vers le bas ne fait rien. Je pense que la principale différence dans ce cas, c'est que je veux laisser l'utilisateur défiler vers le bas lorsque le curseur est positionné sur l'élément corps. D'autres approches nécessitent d'ajouter une classe au corps pour empêcher tout événement de défilement dans le corps. Je pensais pouvoir le faire avec un peu de Javascript dans Angular 2, mais c'est ma tentative échouée jusqu'à présent:

enter image description here

J'ai une directive personnalisée dans mon composant enfant:

<child-element scroller class="child"></child-element>

et cette directive est censée arrêter la propagation de l'événement scroll à l'élément body:

import {Component} from 'angular2/core'
import {Directive, ElementRef, Renderer} from 'angular2/core';

@Directive({
    selector: '[scroller]',
})
export class ScrollerDirective {
    constructor(private elRef: ElementRef, private renderer: Renderer) {
        renderer.listen(elRef.nativeElement, 'scroll', (event) => {
            console.log('event!');
            event.stopPropagation();
            event.preventDefault();
        })
    }

}

Il écoute réellement l'événement mais il n'arrête pas la propagation.

Demo : Faites défiler la liste des nombres et lorsque vous atteignez le bas, son élément parent commence à défiler vers le bas. C'est le problème.

Si vous avez une autre approche pour y parvenir, veuillez me le faire savoir.

[~ # ~] mise à jour [~ # ~] : Sur la base de la réponse fournie par Günter Zöchbauer, j'essaie d'empêcher l'événement de roue lorsque le l'utilisateur atteint le bas. C'est essentiellement ce que j'ai jusqu'à présent dans cette démo mise à jour :

renderer.listen(elRef.nativeElement, 'wheel', (e) => {
    console.log('event', e);
    console.log('scrollTop', elRef.nativeElement.scrollTop);
    console.log('lastScrollTop', lastScrollTop);

    if (elRef.nativeElement.scrollTop == lastScrollTop && e.deltaY > 0) {
      e = e || window.event;
      if (e.preventDefault)
          e.preventDefault();
      e.returnValue = false; 
    }
    else if (elRef.nativeElement.scrollTop == 0) {
      lastScrollTop = -1;
    } 
    else {
      lastScrollTop = elRef.nativeElement.scrollTop;
    }


}, false)

Cependant, la logique est moche et ne fonctionne pas très bien. Par exemple, lorsque l'utilisateur atteint le bas, fait un peu défiler vers le haut et fait de nouveau défiler vers le bas, le composant parent se déplace légèrement. Quelqu'un sait-il comment gérer cela? Une (bien) meilleure mise en œuvre?

MISE À JOUR 2 :

This est beaucoup mieux, mais il est tard maintenant, donc je vérifierai à nouveau demain.

10
Robert Smith

Je suggérerais quelque chose de plus simple à mon avis: ajouter une classe arbitraire à un parent arbitraire et empêcher le défilement via CSS overflow: hidden.

Dans cet exemple, j'ai écrit la directive pour empêcher le parent de défiler pendant que l'élément existe, car c'était mon comportement souhaité. Au lieu de OnDestroy et AfterViewInit, pour votre cas d'utilisation, vous devez vous lier à mouseenter et mouseleave

HTML:

<div add-class="noscroll" to="body">Some Content Here</div>

CSS:

.noscroll { overflow: hidden; }

TS:

import {Directive, AfterViewInit, OnDestroy, Input} from "@angular/core";

@Directive({
  selector: '[add-class]'
})
export class AddClassDirective implements AfterViewInit, OnDestroy {
  @Input('add-class')
  className: string;

  @Input('to')
  selector: string;

  ngOnDestroy(): void {
    document.querySelector(this.selector).classList.remove(this.className);
  }

  ngAfterViewInit(): void {
    document.querySelector(this.selector).classList.add(this.className);
  }
}
6
William B

C'est ma meilleure tentative jusqu'à présent.

renderer.listen(elRef.nativeElement, 'wheel', (e) => {
    let el = elRef.nativeElement;
    let conditions = ((el.scrollTop +  el.offsetHeight >  el.scrollHeight)) && e.deltaY > 0 || el.scrollTop === 0 & e.deltaY < 0;
    if (conditions) {
        e = e || window.event;
        if (e.preventDefault)
            e.preventDefault();
        e.returnValue = false; 
    } 
}, false)

Démo

Cela fonctionne très bien dans Firefox, Chrome stable et bêta, mais pour une raison quelconque, Chrome dev se comporte un peu différemment. Si l'utilisateur fait défiler très fort près de la en bas (ou de manière équivalente, défile vers le haut), l'élément parent bouge un peu. Malheureusement, j'ai remarqué que la section des commentaires du New York Times a également la même agacement . Si vous avez des suggestions, faites le moi savoir.

J'ai signalé que le problème se produisait dans Chrome dev et this a été la réponse jusqu'à présent.

4
Robert Smith
3

Robert ' réponse était vraiment utile, mais il était obsolète et n'a pas fonctionné pour moi en premier. Je l'ai amélioré, pour le faire fonctionner pour moi, et j'ai donc voulu le partager avec vous:

import { Component, Directive, Renderer2, ElementRef } from '@angular/core';

@Directive({
  selector: '[scroller-directive]',
})
export class ScrollerDirective {
  constructor(elRef: ElementRef, renderer: Renderer2) {
    renderer.listen(elRef.nativeElement, 'wheel', (event) => {
      const el = elRef.nativeElement;
      const conditions = ((el.scrollTop + el.offsetHeight >= el.scrollHeight)) && event.deltaY > 0 || el.scrollTop === 0 && event.deltaY < 0;
      if (conditions === true) {
        event.preventDefault();
        event.returnValue = false;
      }
    });
  }
}

Utilisé en html comme:

<div class="scrollable-inner-div" scroller-directive>
// Code
</div>
2
Etoon

J'ai eu le même problème. Je voulais désactiver le défilement pendant que mon composant modal était ouvert.

Voici comment je l'ai résolu:

import { Component, HostListener } from '@angular/core';

@Component({
  selector   : 'app-modal',
  templateUrl: './modal.component.html',
  styleUrls  : ['./modal.component.scss'],
})
export class ModalComponent {
  @HostListener('wheel', ['$event'])
  handleWheelEvent(event) {
    event.preventDefault();
  }

...

J'espère que vous le trouverez utile.

2
Mihailo