Custom scrollbar in Angular

After Edge joined the valiant ranks of Chromium browsers, customization of scrollbars via CSS is absent only in Firefox. This is great, but apart from Firefox, the CSS solution has a ton of limitations. See what black magic you have to use to fade out. To get full control over the appearance, you still need to resort to JavaScript. Let's see how to do it through the Angular component in a good way.



CSS magic


, , . -, . . scrollbar-width, Firefox. Edge IE -ms-overflow-style. IE , position: sticky. Chrome Safari ::-webkit-scrollbar.


-, , . , - position: fixed . : .


position: sticky display: flex . :


<div class="bars">...</div>
<div class="content"><ng-content></ng-content></div>

. , - z-index: 1000, 1001, 9999. , position: relative, z-index: 0. - .


z-index: 1, , 100%. :



, . . margin-right: -100%, «» .


, , float, 100%, (max-height, flex: 1 ).

Angular


Angular, , . . . , . :


<div class="bars">
    <div *ngIf="hasVerticalBar" class="bar">
        <div
            class="thumb"
            [class.thumb_active]="verticalThumbActive"
            [style.height.%]="verticalView"
            [style.top.%]="verticalThumb"
        ></div>
    </div>
</div>

:


  //     
  get verticalScrolled(): number {
    const {
      scrollTop,
      scrollHeight,
      clientHeight
    } = this.elementRef.nativeElement;

    return scrollTop / (scrollHeight - clientHeight);
  }

  //    
  get verticalSize(): number {
    const { clientHeight, scrollHeight } = this.elementRef.nativeElement;

    return Math.ceil(clientHeight / scrollHeight * 100);
  }

  //     
  get verticalPosition(): number {
    return this.verticalScrolled * (100 - this.verticalSize);
  }

  //   ,  
  get hasVerticalBar(): boolean {
    return this.verticalSize < 100;
  }

, , β€” . . , .


mousedown , mousemove mouseup .

:


        <div
            #vertical
            class="thumb"
            [class.thumb_active]="verticalThumbActive"
            [style.height.%]="verticalSize"
            [style.top.%]="verticalPosition"
            (mousedown)="onVerticalStart($event)"
            (document:mousemove)="onVerticalMove($event, vertical)"
        ></div>

mouseup:


  @HostListener('document:mouseup)
  onDragEnd() {
    this.verticalThumbActive = false;
  }

  onVerticalStart(event: MouseEvent) {
    event.preventDefault();

    const { target, clientY } = event;
    const { top, height } = target.getBoundingClientRect();

    this.verticalThumbDragOffset = (clientY - top) / height;
    this.verticalThumbActive = true;
  }

  onVerticalMove(
    { clientY }: MouseEvent,
    { offsetHeight }: HTMLElement
  ) {
    if (!this.verticalThumbActive) {
      return;
    }

    const { nativeElement } = this.elementRef;
    const { top, height } = nativeElement.getBoundingClientRect();
    const maxScrollTop = nativeElement.scrollHeight - height;
    const scrolled =
      (clientY - top - offsetHeight * this.verticalThumbDragOffset) /
      (height - offsetHeight);

    nativeElement.scrollTop = maxScrollTop * scrolled;
  }

.


Angular


, . mousemove . . , Angular , . @tinkoff/ng-event-plugins, . .silent @shouldCall :


(document:mousemove.silent)="onVerticalMove($event, vertical)"

  @shouldCall(isActive)
  @HostListener('init.end', ['$event'])
  @HostListener('document:mouseup.silent')
  onDragEnd() {
    this.verticalThumbActive = false;
  }

  @shouldCall(isActive)
  @HostListener('init.move', ['$event'])
  onVerticalMove(
    { clientY }: MouseEvent,
    { offsetHeight }: HTMLElement
  ) {
    const { nativeElement } = this.elementRef;
    const { top, height } = nativeElement.getBoundingClientRect();
    const maxScrollTop = nativeElement.scrollHeight - height;
    const scrolled =
      (clientY - top - offsetHeight * this.verticalThumbDragOffset) /
      (height - offsetHeight);

    nativeElement.scrollTop = maxScrollTop * scrolled;
  }

: Angular 10 markDirty(this) @shouldCall @HostListener(β€˜init.xxx’, [β€˜$event’]), , β€” .

, . . , . ResizeObserver @ng-web-apis/resize-observer, . Stackblitz.


Edit:


, , . . Output fromEvent, :


  @Output()
  dragged = fromEvent(
    this.elementRef.nativeElement,
    'mousedown'
  ).pipe(
    switchMap(event => {
      event.preventDefault();

      const clientRect = event.target.getBoundingClientRect();
      const offsetVertical = getOffsetVertical(event, clientRect);
      const offsetHorizontal = getOffsetHorizontal(event, clientRect);

      return fromEvent(this.documentRef, 'mousemove').pipe(
        map(event => this.getScrolled(event, offsetVertical, offsetHorizontal)),
        takeUntil(fromEvent(this.documentRef, 'mouseup'))
      );
    })
  );

. , .


All Articles