diff --git a/projects/scion/dimension/src/lib/dimension.module.ts b/projects/scion/dimension/src/lib/dimension.module.ts index 123e62e0b..1308bea4a 100644 --- a/projects/scion/dimension/src/lib/dimension.module.ts +++ b/projects/scion/dimension/src/lib/dimension.module.ts @@ -19,6 +19,9 @@ import { SciDimensionDirective } from './dimension.directive'; * Web Performance Working Group is working on a W3C recommendation for natively observing changes to Element’s size. * The Web API draft is still work in progress and support limited to Google Chrome and Opera. * + * You can control if to use the native {ResizeObserver} by default with {USE_NATIVE_RESIZE_OBSERVER} DI injection token. + * If not provided, the native resize observable is used, unless explicitly set via options object when creating the resize observable. + * * @see https://wicg.github.io/ResizeObserver/ * @see https://caniuse.com/#feat=resizeobserver */ diff --git a/projects/scion/dimension/src/lib/dimension.service.ts b/projects/scion/dimension/src/lib/dimension.service.ts index 333382b7e..cacb1f5be 100644 --- a/projects/scion/dimension/src/lib/dimension.service.ts +++ b/projects/scion/dimension/src/lib/dimension.service.ts @@ -10,10 +10,21 @@ import { concat, fromEvent, Observable, Observer, of, ReplaySubject, Subject, TeardownLogic } from 'rxjs'; import { map, multicast, refCount, switchMap, takeUntil, tap } from 'rxjs/operators'; -import { Injectable } from '@angular/core'; +import { Injectable, InjectionToken, Injector } from '@angular/core'; +/** + * CSS class added to the HTML <object> element emitting resize events if not using the native {ResizeObserver}. + */ const SYNTH_RESIZE_OBSERVABLE_OBJECT_MARKER = 'synth-resize-observable'; +/** + * DI injection token to control if to use the native {ResizeObserver} by default. + * If not provided, the native resize observable is used, unless explicitly set via options object when creating the resize observable. + * + * This flag is only evaluated if the user agent supports {ResizeObserver}. + */ +export const USE_NATIVE_RESIZE_OBSERVER = new InjectionToken<boolean>('USE_NATIVE_RESIZE_OBSERVER'); + /** * Allows observing the dimension of an element. * @@ -37,6 +48,9 @@ export class SciDimensionService { */ public _objectObservableRegistry = new Map<HTMLElement, Observable<SciDimension>>(); + constructor(private _injector: Injector) { + } + /** * Upon subscription, it emits the element's dimension, and then continuously emits when the dimension of the element changes. It never completes. * @@ -48,9 +62,12 @@ export class SciDimensionService { * By default, this flag is enabled. */ public dimension$(target: HTMLElement, options?: { useNativeResizeObserver: boolean }): Observable<SciDimension> { - const useNativeResizeObserver = !options || options.useNativeResizeObserver === undefined || options.useNativeResizeObserver === true; + options = { + useNativeResizeObserver: this._injector.get(USE_NATIVE_RESIZE_OBSERVER, true), + ...options, + }; - if (useNativeResizeObserver && supportsNativeResizeObserver()) { + if (options.useNativeResizeObserver && supportsNativeResizeObserver()) { return createNativeResizeObservable$(target); } diff --git a/projects/scion/dimension/src/public_api.ts b/projects/scion/dimension/src/public_api.ts index 9a68ef84c..cbf97a239 100644 --- a/projects/scion/dimension/src/public_api.ts +++ b/projects/scion/dimension/src/public_api.ts @@ -14,4 +14,4 @@ export { SciDimensionModule } from './lib/dimension.module'; export { SciDimensionDirective } from './lib/dimension.directive'; export { SciMutationService } from './lib/mutation.service'; -export { SciDimensionService, SciDimension } from './lib/dimension.service'; +export { SciDimensionService, SciDimension, USE_NATIVE_RESIZE_OBSERVER } from './lib/dimension.service'; diff --git a/projects/scion/workbench-application.core/src/lib/intent.service.ts b/projects/scion/workbench-application.core/src/lib/intent.service.ts index 6955371f8..3a5477209 100644 --- a/projects/scion/workbench-application.core/src/lib/intent.service.ts +++ b/projects/scion/workbench-application.core/src/lib/intent.service.ts @@ -19,6 +19,7 @@ import { map } from 'rxjs/operators'; * Allows issuing an intent to interact with the platform. */ export class IntentService implements Service { + /** * Issues an intent to the application platform and receives a series of replies. * diff --git a/projects/scion/workbench-application.core/src/lib/message-bus.service.ts b/projects/scion/workbench-application.core/src/lib/message-bus.service.ts index ce5b8e2f3..012102c79 100644 --- a/projects/scion/workbench-application.core/src/lib/message-bus.service.ts +++ b/projects/scion/workbench-application.core/src/lib/message-bus.service.ts @@ -8,7 +8,7 @@ * SPDX-License-Identifier: EPL-2.0 */ -import { fromEvent, Observable, Subject } from 'rxjs'; +import { fromEvent, merge, Observable, Observer, Subject, TeardownLogic } from 'rxjs'; import { filter, first, takeUntil } from 'rxjs/operators'; import { UUID } from './uuid.util'; import { Service } from './metadata'; @@ -102,14 +102,22 @@ export class DefaultMessageBus implements MessageBus { envelope.replyToUid = replyToUid; envelope.protocol = PROTOCOL; - const reply$ = this._stream$ - .pipe( - filter(env => env.channel === 'reply'), - filter(env => env.replyToUid === replyToUid), - takeUntil(this._destroy$), - ); - this.postMessage(envelope); - return reply$; + return new Observable((observer: Observer<MessageEnvelope>): TeardownLogic => { + const destroy$ = new Subject<void>(); + this._stream$ + .pipe( + filter(env => env.channel === 'reply'), + filter(env => env.replyToUid === replyToUid), + takeUntil(merge(destroy$, this._destroy$)), + ) + .subscribe(observer); + + this.postMessage(envelope); + + return (): void => { + destroy$.next(); + }; + }); } public requestReply(envelope: MessageEnvelope): Promise<MessageEnvelope> { diff --git a/projects/scion/workbench/src/lib/spec/workbench-testing.module.ts b/projects/scion/workbench/src/lib/spec/workbench-testing.module.ts index 71e466586..c3a74d371 100644 --- a/projects/scion/workbench/src/lib/spec/workbench-testing.module.ts +++ b/projects/scion/workbench/src/lib/spec/workbench-testing.module.ts @@ -2,6 +2,7 @@ import { Injectable, ModuleWithProviders, NgModule } from '@angular/core'; import { ViewActivationInstantProvider } from '../view-activation-instant-provider.service'; import { WorkbenchModule } from '../workbench.module'; import { WorkbenchConfig } from '../workbench.config'; +import { USE_NATIVE_RESIZE_OBSERVER } from '@scion/dimension'; @Injectable() export class ViewActivationTestingInstantProvider implements ViewActivationInstantProvider { @@ -27,6 +28,14 @@ export class WorkbenchTestingModule { providers: [ WorkbenchModule.forRoot(config).providers, {provide: ViewActivationInstantProvider, useClass: ViewActivationTestingInstantProvider}, + /** + * Disable native `ResizeObserver` in tests because it sometimes throws 'loop limit exceeded' error which can safely be ignored. + * + * Comment from the specification authors: + * This error means that `ResizeObserver` was not able to deliver all observations within a single animation frame. It is benign and the site will not break. + * See https://stackoverflow.com/a/50387233 + */ + {provide: USE_NATIVE_RESIZE_OBSERVER, useValue: false}, ], }; } diff --git a/projects/scion/workbench/src/lib/view-part/view-part-bar/view-part-bar.component.html b/projects/scion/workbench/src/lib/view-part/view-part-bar/view-part-bar.component.html index d3d2db276..d4fb4b73c 100644 --- a/projects/scion/workbench/src/lib/view-part/view-part-bar/view-part-bar.component.html +++ b/projects/scion/workbench/src/lib/view-part/view-part-bar/view-part-bar.component.html @@ -1,8 +1,7 @@ -<sci-viewport [style.width.px]="viewTabsWidthPx" - scrollbarStyle="hidden" +<sci-viewport scrollbarStyle="hidden" sciDimension (sciDimensionChange)="onViewportChange()" (scroll)="onScroll()"> - <div #viewport_client class="viewport-client" sciDimension (sciDimensionChange)="onViewportClientChange()"> + <div class="viewport-client" sciDimension (sciDimensionChange)="onViewportClientChange($event)"> <wb-view-tab *ngFor="let viewRef of viewPartService.viewRefs" [viewRef]="viewRef" [renderingHint]="'tab-item'"></wb-view-tab> </div> </sci-viewport> diff --git a/projects/scion/workbench/src/lib/view-part/view-part-bar/view-part-bar.component.ts b/projects/scion/workbench/src/lib/view-part/view-part-bar/view-part-bar.component.ts index 5981e1326..da72023cb 100644 --- a/projects/scion/workbench/src/lib/view-part/view-part-bar/view-part-bar.component.ts +++ b/projects/scion/workbench/src/lib/view-part/view-part-bar/view-part-bar.component.ts @@ -16,6 +16,8 @@ import { VIEW_DRAG_TYPE } from '../../workbench.constants'; import { WorkbenchLayoutService } from '../../workbench-layout.service'; import { takeUntil } from 'rxjs/operators'; import { Subject } from 'rxjs'; +import { SciViewportComponent } from '@scion/viewport'; +import { SciDimension } from '@scion/dimension'; @Component({ selector: 'wb-view-part-bar', @@ -29,10 +31,8 @@ export class ViewPartBarComponent implements OnDestroy { @ViewChildren(ViewTabComponent) private _viewTabs: QueryList<ViewTabComponent>; - @ViewChild('viewport_client', {static: true}) - private _viewportClient: ElementRef<HTMLElement>; - - public viewTabsWidthPx: number; + @ViewChild(SciViewportComponent, {static: true, read: ElementRef}) + private _viewport: ElementRef<HTMLElement>; constructor(private _workbench: InternalWorkbenchService, private _workbenchLayout: WorkbenchLayoutService, @@ -68,20 +68,23 @@ export class ViewPartBarComponent implements OnDestroy { } public onViewportChange(): void { - this.layout(); + this.computeHiddenViewTabs(); } - public onViewportClientChange(): void { - this.layout(); + public onViewportClientChange(dimension: SciDimension): void { + // The viewport is set to 'flex: initial' with its width set equal to the viewport client width. It shrinks if there is not enough space available. + // The viewport is not set to 'flex: auto' to render left-aligned actions right after the view tabs. + + // The width is set directly to the viewport DOM element not to waste a change detection cycle when computing the hidden view tabs. + this._viewport.nativeElement.style.width = `${dimension.clientWidth}px`; + this.computeHiddenViewTabs(); } public onScroll(): void { - this.layout(); + this.computeHiddenViewTabs(); } - private layout(): void { - this.viewTabsWidthPx = this._viewportClient.nativeElement.clientWidth; - + private computeHiddenViewTabs(): void { // Compute tabs which are not visible in the viewtabs viewport. this._viewTabs && this.viewPartService.setHiddenViewTabs(this._viewTabs .filter(viewTab => !viewTab.isVisibleInViewport())