Skip to content

Commit b90d900

Browse files
committed
refactor(tour): drop usage of overlay helper
The previous `getOverlay` function has some serious flaws. Replacing it with direct cdk usage. See #808
1 parent ab5609f commit b90d900

File tree

3 files changed

+65
-41
lines changed

3 files changed

+65
-41
lines changed
Lines changed: 2 additions & 2 deletions
Loading
Lines changed: 1 addition & 1 deletion
Loading

projects/element-ng/tour/si-tour.service.ts

Lines changed: 62 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,10 @@
22
* Copyright (c) Siemens 2016 - 2025
33
* SPDX-License-Identifier: MIT
44
*/
5-
import {
6-
FlexibleConnectedPositionStrategy,
7-
Overlay,
8-
OverlayOutsideClickDispatcher,
9-
OverlayRef,
10-
PositionStrategy
11-
} from '@angular/cdk/overlay';
5+
import { FlexibleConnectedPositionStrategy, Overlay, OverlayRef } from '@angular/cdk/overlay';
126
import { ComponentPortal } from '@angular/cdk/portal';
13-
import { ElementRef, inject, Injectable, Injector, signal, DOCUMENT } from '@angular/core';
14-
import { makeOverlay, makePositionStrategy } from '@siemens/element-ng/common';
7+
import { DOCUMENT, ElementRef, inject, Injectable, Injector, signal } from '@angular/core';
8+
import { isRTL } from '@siemens/element-ng/common';
159
import { ResizeObserverService } from '@siemens/element-ng/resize-observer';
1610
import { map, merge, Subject, Subscription, tap, throttleTime } from 'rxjs';
1711

@@ -25,7 +19,6 @@ export class SiTourService {
2519
private injector = inject(Injector);
2620
private resizeObserver = inject(ResizeObserverService);
2721
private overlay = inject(Overlay);
28-
private outsideClickDispatcher = inject(OverlayOutsideClickDispatcher);
2922
private overlayRefHighlight?: OverlayRef;
3023
private overlayRef?: OverlayRef;
3124
private portal?: ComponentPortal<SiTourComponent>;
@@ -145,7 +138,11 @@ export class SiTourService {
145138
? new ElementRef(anchorElement!)
146139
: undefined;
147140

148-
this.makeOverlay(anchorElementRef);
141+
if (anchorElementRef) {
142+
this.attachOverlay(anchorElementRef);
143+
} else {
144+
this.centerOverlay();
145+
}
149146
this.handleResizeSubscription(anchorElementRef);
150147

151148
this.tourToken.currentStep.next({
@@ -179,17 +176,57 @@ export class SiTourService {
179176
}
180177
}
181178

182-
private makeOverlay(anchorElement: ElementRef<HTMLElement> | undefined): void {
183-
const strategy = makePositionStrategy(anchorElement, this.overlay, 'auto');
184-
this.handlePositionChangeSubscription(strategy, anchorElement);
179+
private centerOverlay(): void {
180+
this.positionChangeSub?.unsubscribe();
181+
this.createOverlays();
182+
183+
const positionStrategy = this.overlayRef!.getConfig().positionStrategy;
184+
if (!positionStrategy || positionStrategy instanceof FlexibleConnectedPositionStrategy) {
185+
this.overlayRef!.updatePositionStrategy(
186+
this.overlay.position().global().centerHorizontally().centerVertically()
187+
);
188+
}
189+
this.tourToken.positionChange.next(undefined);
190+
}
191+
192+
private attachOverlay(anchor: ElementRef<HTMLElement>): void {
193+
this.createOverlays();
185194

195+
const positionStrategy = this.overlayRef!.getConfig().positionStrategy;
196+
if (positionStrategy && positionStrategy instanceof FlexibleConnectedPositionStrategy) {
197+
positionStrategy.setOrigin(anchor);
198+
} else {
199+
this.createFlexiblePositionStrategy(anchor);
200+
}
201+
}
202+
203+
private createFlexiblePositionStrategy(anchor: ElementRef<HTMLElement>): void {
204+
const positionStrategy = this.overlay
205+
.position()
206+
.flexibleConnectedTo(anchor)
207+
.withGrowAfterOpen(true)
208+
.withPositions([
209+
// On top
210+
{ originX: 'center', overlayX: 'center', originY: 'top', overlayY: 'bottom' },
211+
{ originX: 'start', overlayX: 'start', originY: 'top', overlayY: 'bottom' },
212+
{ originX: 'end', overlayX: 'end', originY: 'top', overlayY: 'bottom' },
213+
// On bottom
214+
{ originX: 'center', overlayX: 'center', originY: 'bottom', overlayY: 'top' },
215+
{ originX: 'start', overlayX: 'start', originY: 'bottom', overlayY: 'top' },
216+
{ originX: 'end', overlayX: 'end', originY: 'bottom', overlayY: 'top' },
217+
// Left and right
218+
{ originX: 'start', overlayX: 'end', originY: 'center', overlayY: 'center' },
219+
{ originX: 'end', overlayX: 'start', originY: 'center', overlayY: 'center' }
220+
]);
221+
this.overlayRef!.updatePositionStrategy(positionStrategy);
222+
this.positionChangeSub = positionStrategy.positionChanges
223+
.pipe(map(change => ({ change, anchor })))
224+
// We only want to forward the next channel, as the positionChanges completes when setting a new origin.
225+
.subscribe(value => this.tourToken.positionChange.next(value));
226+
}
227+
228+
private createOverlays(): void {
186229
if (this.overlayRef) {
187-
this.overlayRef.updatePositionStrategy(strategy);
188-
// This moves the dispatcher to the top, allowing it to catch other open overlays.
189-
// Much lighter than to detach and re-attach the portal, not re-creating the si-tour
190-
// component for each step
191-
this.outsideClickDispatcher.remove(this.overlayRef);
192-
this.outsideClickDispatcher.add(this.overlayRef);
193230
return;
194231
}
195232

@@ -207,9 +244,10 @@ export class SiTourService {
207244

208245
// then the dialog
209246
this.portal = new ComponentPortal(SiTourComponent, undefined, componentInjector);
210-
this.overlayRef = makeOverlay(strategy, this.overlay, true);
211-
// needs a subscriber, otherwise events will be ignored and the .backdrop CSS hack doesn't help
212-
this.overlayRef.outsidePointerEvents().subscribe();
247+
this.overlayRef = this.overlay.create({
248+
scrollStrategy: this.overlay.scrollStrategies.reposition(),
249+
direction: isRTL() ? 'rtl' : 'ltr'
250+
});
213251
this.overlayRef.attach(this.portal);
214252
}
215253

@@ -228,7 +266,7 @@ export class SiTourService {
228266
tap(() => {
229267
if (!this.isElementVisible(anchorElement?.nativeElement)) {
230268
// repositions to center if anchor disappears
231-
this.makeOverlay(undefined);
269+
this.centerOverlay();
232270
} else {
233271
this.overlayRef?.updatePosition();
234272
}
@@ -243,20 +281,6 @@ export class SiTourService {
243281
return !!rect?.width && !!rect.height;
244282
}
245283

246-
private handlePositionChangeSubscription(
247-
strategy: PositionStrategy,
248-
anchor?: ElementRef<HTMLElement>
249-
): void {
250-
this.positionChangeSub?.unsubscribe();
251-
if (anchor && strategy instanceof FlexibleConnectedPositionStrategy) {
252-
this.positionChangeSub = strategy.positionChanges
253-
.pipe(map(change => ({ change, anchor })))
254-
.subscribe(this.tourToken.positionChange);
255-
} else {
256-
this.tourToken.positionChange.next(undefined);
257-
}
258-
}
259-
260284
private getElement(
261285
selectorOrElement?: string | HTMLElement | (() => string | HTMLElement)
262286
): HTMLElement | undefined {

0 commit comments

Comments
 (0)