Skip to content

Commit

Permalink
refactor(components/viewport): migrate viewport to signals
Browse files Browse the repository at this point in the history
  • Loading branch information
danielwiehl authored and Marcarrian committed Oct 28, 2024
1 parent 948061e commit 04003c5
Show file tree
Hide file tree
Showing 7 changed files with 226 additions and 278 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ describe('SciNativeScrollbarTrackSizeProvider', () => {
const fixture = TestBed.createComponent(AppComponent);
advance(fixture);

expect(testee.trackSize!.vScrollbarTrackWidth).withContext('vScrollbarTrackWidth').toEqual(fixture.componentInstance.vScrollbarTrackWidth);
expect(testee.trackSize!.hScrollbarTrackHeight).withContext('hScrollbarTrackHeight').toEqual(fixture.componentInstance.hScrollbarTrackHeight);
expect(testee.trackSize()!.vScrollbarTrackWidth).withContext('vScrollbarTrackWidth').toEqual(fixture.componentInstance.vScrollbarTrackWidth);
expect(testee.trackSize()!.hScrollbarTrackHeight).withContext('hScrollbarTrackHeight').toEqual(fixture.componentInstance.hScrollbarTrackHeight);
tick();
})));
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2019 Swiss Federal Railways
* Copyright (c) 2018-2024 Swiss Federal Railways
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
Expand All @@ -8,38 +8,29 @@
* SPDX-License-Identifier: EPL-2.0
*/

import {Inject, Injectable, NgZone, OnDestroy} from '@angular/core';
import {inject, Injectable, NgZone, Signal} from '@angular/core';
import {DOCUMENT} from '@angular/common';
import {BehaviorSubject, fromEvent, Observable, Subject} from 'rxjs';
import {debounceTime, distinctUntilChanged, map, startWith, takeUntil} from 'rxjs/operators';
import {fromEvent} from 'rxjs';
import {debounceTime, distinctUntilChanged, map, startWith} from 'rxjs/operators';
import {toSignal} from '@angular/core/rxjs-interop';
import {subscribeIn} from '@scion/toolkit/operators';

/**
* Provides the native scrollbar tracksize.
*/
@Injectable({providedIn: 'root'})
export class SciNativeScrollbarTrackSizeProvider implements OnDestroy {
export class SciNativeScrollbarTrackSizeProvider {

private readonly _trackSize$ = new BehaviorSubject<NativeScrollbarTrackSize | null>(null);
private readonly _destroy$ = new Subject<void>();

constructor(@Inject(DOCUMENT) private _document: Document, private _zone: NgZone) {
this.installNativeScrollbarTrackSizeListener();
}
private _document = inject(DOCUMENT);
private _zone = inject(NgZone);

/**
* Returns an {Observable} which emits the native scrollbar track size, if any, or else `null`.
*
* Upon subscription, it emits the track size immediately, and then continuously emits when the track size changes.
* Provides the track size of the native scrollbar, or `null` if the native scrollbars sit on top of the content.
*/
public get trackSize$(): Observable<NativeScrollbarTrackSize | null> {
return this._trackSize$;
}
public trackSize: Signal<NativeScrollbarTrackSize | null>;

/**
* Returns the native scrollbar track size, if any, or else `null`.
*/
public get trackSize(): NativeScrollbarTrackSize | null {
return this._trackSize$.getValue();
constructor() {
this.trackSize = this.createNativeScrollbarTrackSizeSignal();
}

/**
Expand All @@ -48,7 +39,7 @@ export class SciNativeScrollbarTrackSizeProvider implements OnDestroy {
* @returns native track size, or `null` if the native scrollbars sit on top of the content.
*/
private computeTrackSize(): NativeScrollbarTrackSize | null {
// create temporary viewport and viewport client with native scrollbars to compute scrolltrack width
// Create temporary viewport and viewport client with native scrollbars to compute the scrolltrack width.
const viewportDiv = this._document.createElement('div');
setStyle(viewportDiv, {
position: 'absolute',
Expand Down Expand Up @@ -77,7 +68,7 @@ export class SciNativeScrollbarTrackSizeProvider implements OnDestroy {
vScrollbarTrackWidth: viewportBounds.width - viewportClientBounds.width,
};

// destroy temporary viewport
// Destroy temporary viewport.
this._document.body.removeChild(viewportDiv);
if (trackSize.hScrollbarTrackHeight === 0 && trackSize.vScrollbarTrackWidth === 0) {
return null;
Expand All @@ -86,27 +77,19 @@ export class SciNativeScrollbarTrackSizeProvider implements OnDestroy {
return trackSize;
}

private installNativeScrollbarTrackSizeListener(): void {
private createNativeScrollbarTrackSizeSignal(): Signal<NativeScrollbarTrackSize | null> {
// We compute the size of the native scrollbar track when the browser fires the onresize window event.
// This event is also fired on page zoom or when displaying a hidden document. Hidden documents do not have
// a scrollbar track size until being displayed, e.g., after showing hidden iframes.
this._zone.runOutsideAngular(() => {
fromEvent(window, 'resize')
.pipe(
debounceTime(5),
startWith(null), // trigger the initial computation
map(() => this.computeTrackSize()),
distinctUntilChanged((t1, t2) => JSON.stringify(t1) === JSON.stringify(t2)),
takeUntil(this._destroy$),
)
.subscribe(trackSize => {
this._zone.run(() => this._trackSize$.next(trackSize));
});
});
}

public ngOnDestroy(): void {
this._destroy$.next();
const trackSize$ = fromEvent(window, 'resize')
.pipe(
subscribeIn(fn => this._zone.runOutsideAngular(fn)),
debounceTime(5),
startWith(null), // trigger the initial computation
map(() => this.computeTrackSize()),
distinctUntilChanged((t1, t2) => t1?.hScrollbarTrackHeight === t2?.hScrollbarTrackHeight && t1?.vScrollbarTrackWidth === t2?.vScrollbarTrackWidth),
);
return toSignal(trackSize$, {initialValue: null});
}
}

Expand Down
65 changes: 31 additions & 34 deletions projects/scion/components/viewport/src/scrollable.directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,8 @@
* SPDX-License-Identifier: EPL-2.0
*/

import {Directive, ElementRef, Input, OnChanges, OnDestroy, Renderer2, SimpleChanges} from '@angular/core';
import {Directive, effect, ElementRef, inject, input, Renderer2, untracked} from '@angular/core';
import {NativeScrollbarTrackSize, SciNativeScrollbarTrackSizeProvider} from './native-scrollbar-track-size-provider.service';
import {map, takeUntil} from 'rxjs/operators';
import {merge, Subject} from 'rxjs';
import {Dictionary} from '@scion/toolkit/util';

/**
Expand All @@ -29,41 +27,48 @@ import {Dictionary} from '@scion/toolkit/util';
selector: '[sciScrollable]',
standalone: true,
})
export class SciScrollableDirective implements OnChanges, OnDestroy {
export class SciScrollableDirective {

private _destroy$ = new Subject<void>();
private _inputChange$ = new Subject<void>();
private _host = inject(ElementRef<HTMLDivElement>).nativeElement;
private _renderer = inject(Renderer2);
private _nativeScrollbarTrackSizeProvider = inject(SciNativeScrollbarTrackSizeProvider);

/**
* Controls whether to display native scrollbars.
* Has no effect if the native scrollbar sits on top of the content, e.g. in OS X.
*/
@Input('sciScrollableDisplayNativeScrollbar')// eslint-disable-line @angular-eslint/no-input-rename
public isDisplayNativeScrollbar = false;
public displayNativeScrollbar = input(false, {alias: 'sciScrollableDisplayNativeScrollbar'});

constructor(private _host: ElementRef<HTMLDivElement>,
private _renderer: Renderer2,
nativeScrollbarTrackSizeProvider: SciNativeScrollbarTrackSizeProvider) {
merge(
nativeScrollbarTrackSizeProvider.trackSize$,
this._inputChange$.pipe(map(() => nativeScrollbarTrackSizeProvider.trackSize)),
)
.pipe(takeUntil(this._destroy$))
.subscribe(nativeScrollbarTrackSize => {
if (nativeScrollbarTrackSize === null) { // the native scrollbar sits on top of the content
this.useNativeScrollbars();
}
else {
this.isDisplayNativeScrollbar ? this.useNativeScrollbars() : this.shiftNativeScrollbars(nativeScrollbarTrackSize);
}
});
constructor() {
this.controlDisplayOfNativeScrollbar();
}

/**
* Controls the display of the native scrollbar based on this directive's configuration.
*/
private controlDisplayOfNativeScrollbar(): void {
effect(() => {
const displayNativeScrollbar = this.displayNativeScrollbar();
if (displayNativeScrollbar) {
untracked(() => this.useNativeScrollbars());
return;
}

const nativeScrollbarTrackSize = this._nativeScrollbarTrackSizeProvider.trackSize();
if (nativeScrollbarTrackSize) {
untracked(() => this.shiftNativeScrollbars(nativeScrollbarTrackSize));
}
else {
untracked(() => this.useNativeScrollbars());
}
});
}

/**
* Uses the native scrollbars when content overflows.
*/
private useNativeScrollbars(): void {
this.setStyle(this._host.nativeElement, {
this.setStyle(this._host, {
overflow: 'auto',
width: '100%',
height: '100%',
Expand All @@ -76,7 +81,7 @@ export class SciScrollableDirective implements OnChanges, OnDestroy {
* Shifts the native scrollbars out of the visible viewport area.
*/
private shiftNativeScrollbars(nativeScrollbarTrackSize: NativeScrollbarTrackSize): void {
this.setStyle(this._host.nativeElement, {
this.setStyle(this._host, {
overflow: 'scroll',
width: `calc(100% + ${nativeScrollbarTrackSize.vScrollbarTrackWidth}px`,
height: `calc(100% + ${nativeScrollbarTrackSize.hScrollbarTrackHeight}px`,
Expand All @@ -88,12 +93,4 @@ export class SciScrollableDirective implements OnChanges, OnDestroy {
private setStyle(element: Element, style: Dictionary): void {
Object.keys(style).forEach(key => this._renderer.setStyle(element, key, style[key]));
}

public ngOnChanges(changes: SimpleChanges): void {
this._inputChange$.next();
}

public ngOnDestroy(): void {
this._destroy$.next();
}
}
Loading

0 comments on commit 04003c5

Please sign in to comment.