Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Commit

Permalink
fix(filter-field, autocomplete): Fixes an issue that the panels did n…
Browse files Browse the repository at this point in the history
…ot react to viewport boundaries correctly.

Closes #1747.
  • Loading branch information
ffriedl89 committed Oct 22, 2020
1 parent 879da98 commit 0abae23
Show file tree
Hide file tree
Showing 5 changed files with 155 additions and 26 deletions.
54 changes: 39 additions & 15 deletions libs/barista-components/autocomplete/src/autocomplete-trigger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ import {
DtViewportResizer,
isDefined,
stringify,
ViewportBoundaries,
mixinViewportBoundaries,
Constructor,
} from '@dynatrace/barista-components/core';
import { DtFormField } from '@dynatrace/barista-components/form-field';

Expand Down Expand Up @@ -136,6 +139,14 @@ export function calculateOptionHeight(
};
}

// Boilerplate for applying mixins to DtAutocompleteTrigger.
export class DtAutocompleteTriggerBase {
constructor(public _viewportResizer: DtViewportResizer) {}
}
export const _DtAutocompleteTriggerMixinBase = mixinViewportBoundaries<
Constructor<DtAutocompleteTriggerBase>
>(DtAutocompleteTriggerBase);

@Directive({
selector: `input[dtAutocomplete], textarea[dtAutocomplete]`,
exportAs: 'dtAutocompleteTrigger',
Expand All @@ -157,6 +168,7 @@ export function calculateOptionHeight(
providers: [DT_AUTOCOMPLETE_VALUE_ACCESSOR],
})
export class DtAutocompleteTrigger<T>
extends _DtAutocompleteTriggerMixinBase
implements ControlValueAccessor, OnDestroy {
private _optionHeight: number;
private _maxPanelHeight: number;
Expand Down Expand Up @@ -291,13 +303,14 @@ export class DtAutocompleteTrigger<T>
/** Old value of the native input. Used to work around issues with the `input` event on IE. */
private _previousValue: string | number | null;

private _destroy$ = new Subject<void>();
/** The stream that holds the viewport boundaries in order to be able to limit where an overlay can be positioned */
private _viewportBoundaries: ViewportBoundaries = { left: 0, top: 0 };

constructor(
private _element: ElementRef<HTMLInputElement>,
private _overlay: Overlay,
private _changeDetectorRef: ChangeDetectorRef,
private _viewportResizer: DtViewportResizer,
public _viewportResizer: DtViewportResizer,
private _zone: NgZone,
private _viewportRuler: ViewportRuler,
private _platform: Platform,
Expand All @@ -312,6 +325,7 @@ export class DtAutocompleteTrigger<T>
@Inject(DT_OPTION_CONFIG)
optionConfig?: DtOptionConfiguration,
) {
super(_viewportResizer);
// tslint:disable-next-line:strict-type-predicates
if (typeof window !== 'undefined') {
_zone.runOutsideAngular(() => {
Expand All @@ -328,16 +342,23 @@ export class DtAutocompleteTrigger<T>
});
}

if (this._viewportResizer) {
this._viewportResizer
.change()
.pipe(takeUntil(this._destroy$))
.subscribe(() => {
if (this.panelOpen && this._overlayRef) {
this._overlayRef.updateSize({ maxWidth: this._getPanelWidth() });
}
});
}
this._viewportResizer
.change()
.pipe(takeUntil(this._destroy$))
.subscribe(() => {
if (this.panelOpen && this._overlayRef) {
this._overlayRef.updateSize({ maxWidth: this._getPanelWidth() });
}
});

this._viewportBoundaries$.subscribe((boundaries) => {
this._viewportBoundaries = boundaries;
if (this.panelOpen) {
// attachOverlay updates the position strategy of an already existing
// overlay
this._attachOverlay();
}
});

const heightConfig = calculateOptionHeight(optionConfig?.height ?? 0);

Expand Down Expand Up @@ -495,7 +516,9 @@ export class DtAutocompleteTrigger<T>
} else {
// Update the panel width and position in case anything has changed.
this._overlayRef.updateSize({ maxWidth: this._getPanelWidth() });
this._overlayRef.updatePosition();
this._overlayRef.updatePositionStrategy(
this._getOverlayPositionStrategy(),
);
}

if (this._overlayRef && !this._overlayRef.hasAttached()) {
Expand Down Expand Up @@ -555,13 +578,13 @@ export class DtAutocompleteTrigger<T>
// TODO: reconsider if this config should be providable
private _getOverlayConfig(): OverlayConfig {
return new OverlayConfig({
positionStrategy: this._getOverlayPosition(),
positionStrategy: this._getOverlayPositionStrategy(),
scrollStrategy: this._overlay.scrollStrategies.reposition(),
maxWidth: this._getPanelWidth(),
});
}

private _getOverlayPosition(): PositionStrategy {
private _getOverlayPositionStrategy(): PositionStrategy {
const originalPositionStrategy = new DtFlexibleConnectedPositionStrategy(
this._getConnectedElement(),
this._viewportRuler,
Expand All @@ -573,6 +596,7 @@ export class DtAutocompleteTrigger<T>
this._positionStrategy = originalPositionStrategy
.withFlexibleDimensions(false)
.withPush(false)
.withViewportBoundaries(this._viewportBoundaries)
.withPositions([
{
originX: 'start',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ export * from './progress';
export * from './tabindex';
export * from './dom-exit';
export * from './id';
export * from './viewport-boundaries';
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { ViewportRuler } from '@angular/cdk/overlay';
import { Platform } from '@angular/cdk/platform';
import { NgZone } from '@angular/core';
import { MockNgZone } from '@dynatrace/testing/browser';
import { DtDefaultViewportResizer, DtViewportResizer } from '../viewport';
import { mixinViewportBoundaries } from './viewport-boundaries';

describe('MixinViewportBoundaries', () => {
it('should augment an existing class with a _viewportBoundaries$ property', () => {
const classWithDisabled = mixinViewportBoundaries(TestClass);
const instance = new classWithDisabled();

// Expected the mixed-into class to have a _viewportBoundaries property
expect(instance).toHaveProperty('_viewportBoundaries$');
});

it('should augment an existing class with a _destroy$ property', () => {
const classWithDisabled = mixinViewportBoundaries(TestClass);
const instance = new classWithDisabled();

// Expected the mixed-into class to have a _viewportBoundaries property
expect(instance).toHaveProperty('_destroy$');
});
});

class TestClass {
_platform = new Platform('testid');
_zone: NgZone = new MockNgZone();
_viewportRuler = new ViewportRuler(this._platform, this._zone);
/** Fake instance of an DtViewportResizer. */
_viewportResizer: DtViewportResizer = new DtDefaultViewportResizer(
this._viewportRuler,
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { EMPTY, Observable, of, Subject } from 'rxjs';
import { distinctUntilChanged, startWith, takeUntil } from 'rxjs/operators';
import { ViewportBoundaries } from '../overlay/flexible-connected-position-strategy';
import { DtViewportResizer } from '../viewport';
import { Constructor } from './constructor';

export interface HasViewportBoundaries {
/** The current viewport boundaries */
_viewportBoundaries$: Observable<ViewportBoundaries>;
}

export interface HasDestroySubject {
_destroy$: Subject<void>;
}

export interface HasDtViewportResizer {
_viewportResizer: DtViewportResizer;
}

/** Mixin to augment a directive with a `viewportBoundaries$` property. */
export function mixinViewportBoundaries<
T extends Constructor<HasDtViewportResizer>
>(
base: T,
): Constructor<HasViewportBoundaries> & Constructor<HasDestroySubject> & T {
return class extends base {
_viewportBoundaries$: Observable<ViewportBoundaries> = EMPTY;

_destroy$ = new Subject<void>();

// tslint:disable-next-line:no-any
constructor(...args: any[]) {
super(...args);

this._viewportBoundaries$ = this._viewportResizer
? this._viewportResizer.offset$.pipe(
startWith({ top: 0, left: 0 }),
distinctUntilChanged(),
takeUntil(this._destroy$),
)
: of({ left: 0, top: 0 });
}
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
NgZone,
OnDestroy,
Inject,
Optional,
} from '@angular/core';
import {
EMPTY,
Expand All @@ -42,17 +43,28 @@ import {
merge,
of as observableOf,
} from 'rxjs';
import { filter, map, take } from 'rxjs/operators';
import { filter, map, take, takeUntil } from 'rxjs/operators';

import {
_readKeyCode,
DtFlexibleConnectedPositionStrategy,
DtViewportResizer,
mixinViewportBoundaries,
Constructor,
} from '@dynatrace/barista-components/core';

import { DtFilterFieldRange } from './filter-field-range';
import { Platform } from '@angular/cdk/platform';
import { DOCUMENT } from '@angular/common';

// Boilerplate for applying mixins to DtFilterFieldRangeTrigger.
export class DtFilterFieldRangeTriggerBase {
constructor(public _viewportResizer: DtViewportResizer) {}
}
export const _DtFilterFieldRangeTrgigerMixinBase = mixinViewportBoundaries<
Constructor<DtFilterFieldRangeTriggerBase>
>(DtFilterFieldRangeTriggerBase);

@Directive({
selector: `input[dtFilterFieldRange]`,
exportAs: 'dtFilterFieldRangeTrigger',
Expand All @@ -62,7 +74,9 @@ import { DOCUMENT } from '@angular/common';
'[attr.aria-owns]': '(rangeDisabled || !panelOpen) ? null : range?.id',
},
})
export class DtFilterFieldRangeTrigger implements OnDestroy {
export class DtFilterFieldRangeTrigger
extends _DtFilterFieldRangeTrgigerMixinBase
implements OnDestroy {
/** The filter-field range panel to be attached to this trigger. */
@Input('dtFilterFieldRange')
get range(): DtFilterFieldRange {
Expand Down Expand Up @@ -132,16 +146,16 @@ export class DtFilterFieldRangeTrigger implements OnDestroy {
/** The subscription for closing actions (some are bound to document). */
private _closingActionsSubscription = EMPTY.subscribe();

/** The subscription for the window blur event */
private _windowBlurSubscription = EMPTY.subscribe();

/**
* Whether the autocomplete can open the next time it is focused. Used to prevent a focused,
* closed autocomplete from being reopened if the user switches to another browser tab and then
* comes back.
*/
private _canOpenOnNextFocus = true;

/** The current viewport boundaries */
private _viewportBoundaries = { left: 0, top: 0 };

constructor(
private _elementRef: ElementRef,
private _overlay: Overlay,
Expand All @@ -152,27 +166,34 @@ export class DtFilterFieldRangeTrigger implements OnDestroy {
zone: NgZone,
// tslint:disable-next-line:no-any
@Inject(DOCUMENT) private _document: any,
// @breaking-change Will be made mandatory with 9.0.0
@Optional() public _viewportResizer: DtViewportResizer,
) {
super(_viewportResizer);
// tslint:disable-next-line:strict-type-predicates
if (typeof window !== 'undefined') {
zone.runOutsideAngular(() => {
this._windowBlurSubscription = fromEvent(window, 'blur').subscribe(
() => {
fromEvent(window, 'blur')
.pipe(takeUntil(this._destroy$))
.subscribe(() => {
// If the user blurred the window while the autocomplete is focused, it means that it'll be
// refocused when they come back. In this case we want to skip the first focus event, if the
// pane was closed, in order to avoid reopening it unintentionally.
this._canOpenOnNextFocus =
document.activeElement !== this._elementRef.nativeElement ||
this.panelOpen;
},
);
});
});
}
this._viewportBoundaries$.subscribe((boundaries) => {
this._viewportBoundaries = boundaries;
});
}

ngOnDestroy(): void {
this._destroy$.next();
this._destroy$.complete();
this._closingActionsSubscription.unsubscribe();
this._windowBlurSubscription.unsubscribe();
this._componentDestroyed = true;
this._destroyPanel();
this._closeKeyEventStream.complete();
Expand Down Expand Up @@ -239,14 +260,18 @@ export class DtFilterFieldRangeTrigger implements OnDestroy {
}
if (this._overlayRef && !this._overlayRef.hasAttached()) {
this._overlayRef.attach(this._range._portal);
// Always unsubscribe here, it might happen that no
// closing action was triggered in between 2 calls
// to this method
this._closingActionsSubscription.unsubscribe();
this._closingActionsSubscription = this._subscribeToClosingActions();
}

this.range._isOpen = this._overlayRef.hasAttached();
this.range.opened.emit();

if (this.panelOpen) {
this._overlayRef.updatePosition();
this._overlayRef.updatePositionStrategy(this._getOverlayPosition());
}

this.range._markForCheck();
Expand Down Expand Up @@ -288,6 +313,7 @@ export class DtFilterFieldRangeTrigger implements OnDestroy {
)
.withFlexibleDimensions(false)
.withPush(false)
.withViewportBoundaries(this._viewportBoundaries)
.withPositions([
{
originX: 'start',
Expand Down

0 comments on commit 0abae23

Please sign in to comment.