Prevent scrolling of child element to propagate in Angular 2

27,353

Solution 1

I would suggest something more straightforward in my opinion: add an arbitrary class to an arbitrary parent, and prevent scrolling via CSS overflow: hidden.

In this example, I wrote the directive to prevent the parent from scrolling while the element exists at all, as this was my desired behavior. Instead of OnDestroy and AfterViewInit, for your use case you should bind to mouseenter and 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);
  }
}

Solution 2

I had the similar problem. I wanted to disable scrolling while my modal component was opened.

Here is how I've solved it:

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();
  }

...

Hope you find it useful.

Solution 3

This is my best attempt so far.

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)

Demo

It works great in Firefox, Chrome stable and beta, but for some reason, Chrome dev behaves a bit differently. If the user scrolls down really hard near the bottom (or equivalently, scrolls up near the top), the parent element moves a little. Unfortunately, I noticed that the comment section of the New York Times also has the same annoyance. If you have suggestions, let me know.

I reported the issue occurring in Chrome dev and this has been the response so far.

Solution 4

Robert' answer was really helpful, but it was outdated and didn't work for me first. I improved it, to make it work for me, and therefore I wanted to share it with you:

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;
      }
    });
  }
}

Used in html like:

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

Solution 5

You can't prevent or stop-propagate the scroll event. You need to prevent the events that cause the scroll event.

Share:
27,353
r_31415
Author by

r_31415

Updated on December 14, 2020

Comments

  • r_31415
    r_31415 over 3 years

    This is a classic. You have a parent and a child element. Child element is absolutely positioned and you want the user to scroll through its content. However, when you reach the bottom of the child element, the parent element (which also is allowed to have scroll bars) begins to scroll. That's undesirable. The basic behavior I want to reproduce is that of the comment section of the New York Times. For example:

    enter image description here

    The body is allowed to scroll down, but when you are at the bottom of the comment section, scrolling down doesn't do anything. I think the main difference in this case, is that I want to let the user scroll down when the cursor is positioned over the body element. Other approaches require to add a class to the body to prevent any scroll event in the body. I thought I would be able to do this with a bit of Javascript in Angular 2, but this is my failed attempt so far:

    enter image description here

    I have a custom directive in my child component:

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

    and this directive is supposed to stop the propagation of the scroll event to the body element:

    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();
            })
        }
    
    }
    

    It actually listens to the event but it doesn't stop the propagation.

    Demo: Scroll down through the list of numbers and when you reach the bottom, its parent element starts to scroll down. That's the problem.

    If you have another approach to accomplish this, please let me know.

    UPDATE: Based on the answer provided by Günter Zöchbauer, I'm trying to prevent the wheel event when the user reaches the bottom. This is basically what I have so far in this updated demo:

    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)
    

    However, the logic is ugly and doesn't work great. For example, when the user reaches the bottom, scrolls up a little and scrolls down again, the parent component moves slightly. Does anyone know how to deal with this? A (much) better implementation?

    UPDATE 2:

    This is much better, but it is late now, so I will check again tomorrow.

  • r_31415
    r_31415 about 8 years
    Thanks. Very interesting. I'm just trying to figure out how to use the wheel event to prevent scrolling. I suppose I need to detect when the user has scrolled to the bottom but I don't see a value on the event object to do that.
  • Günter Zöchbauer
    Günter Zöchbauer about 8 years
    See stackoverflow.com/questions/36468318/… and stackoverflow.com/questions/36471927/… - especially the last comment on the answer.
  • r_31415
    r_31415 about 8 years
    I think I have something working, but I'm struggling with the correct logic. I will update my question.
  • godblessstrawberry
    godblessstrawberry about 6 years
    how about touch scrolling?
  • Frank Goortani
    Frank Goortani almost 6 years
    I had the same problem with keydown event and this solved my problem. just replace wheel with your event
  • Joniras
    Joniras almost 5 years
    Searched for this 2 straight hours, thanks for the solution!