Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions api-goldens/element-ng/common/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { OverlayRef } from '@angular/cdk/overlay';
import { PositionStrategy } from '@angular/cdk/overlay';
import { Provider } from '@angular/core';
import * as rxjs from 'rxjs';
import { ScrollStrategy } from '@angular/cdk/overlay';
import { TranslatableString } from '@siemens/element-translate-ng/translate';
import { Type } from '@angular/core';

Expand Down Expand Up @@ -111,7 +112,7 @@ export const getContentPositionString: (params: {
}) => string;

// @public (undocumented)
export function getOverlay(elementRef: ElementRef<any>, overlay: Overlay, hasBackdrop: boolean, placement: keyof typeof positions | ConnectionPositionPair[], constrain?: boolean, center?: boolean): OverlayRef;
export function getOverlay(elementRef: ElementRef<any>, overlay: Overlay, hasBackdrop: boolean, placement: keyof typeof positions | ConnectionPositionPair[], constrain?: boolean, center?: boolean, scrollStrategy?: ScrollStrategy): OverlayRef;

// @public (undocumented)
export function getOverlayPositions(elementRef: ElementRef<any>, placement: keyof typeof positions | ConnectionPositionPair[], center?: boolean): ConnectionPositionPair[];
Expand All @@ -129,7 +130,7 @@ export const isRTL: (elem?: HTMLElement) => boolean;
export const listenGlobal: (eventName: string, handler: (e: any) => void, active?: boolean) => (() => void);

// @public (undocumented)
export function makeOverlay(positionStrategy: PositionStrategy, overlay: Overlay, hasBackdrop: boolean): OverlayRef;
export function makeOverlay(positionStrategy: PositionStrategy, overlay: Overlay, hasBackdrop: boolean, scrollStrategy?: ScrollStrategy): OverlayRef;

// @public
export function makePositionStrategy(elementRef: ElementRef<any> | undefined, overlay: Overlay, placement: keyof typeof positions | ConnectionPositionPair[], constrain?: boolean, center?: boolean): PositionStrategy;
Expand Down
2 changes: 2 additions & 0 deletions api-goldens/element-ng/popover/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { Injector } from '@angular/core';
import { OnDestroy } from '@angular/core';
import { OnInit } from '@angular/core';
import { OverlayArrowPosition } from '@siemens/element-ng/common';
import { ScrollStrategy } from '@angular/cdk/overlay';
import * as _siemens_element_translate_ng_translate from '@siemens/element-translate-ng/translate';
import { TemplateRef } from '@angular/core';
import { TranslatableString } from '@siemens/element-translate-ng/translate-types';
Expand All @@ -30,6 +31,7 @@ export class SiPopoverDirective implements OnDestroy {
readonly placement: _angular_core.InputSignal<"auto" | "start" | "end" | "top" | "bottom">;
// (undocumented)
readonly placementInternal: _angular_core.Signal<"auto" | "start" | "end" | "top" | "bottom">;
readonly scrollStrategy: _angular_core.InputSignal<ScrollStrategy | undefined>;
show(): void;
readonly siPopover: _angular_core.InputSignal<TranslatableString | TemplateRef<unknown> | undefined>;
readonly title: _angular_core.InputSignal<TranslatableString | undefined>;
Expand Down
4 changes: 3 additions & 1 deletion api-goldens/element-ng/tooltip/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,18 @@ import { Injector } from '@angular/core';
import { OnDestroy } from '@angular/core';
import { OverlayRef } from '@angular/cdk/overlay';
import { positions } from '@siemens/element-ng/common';
import { ScrollStrategy } from '@angular/cdk/overlay';
import { TemplateRef } from '@angular/core';
import { TranslatableString } from '@siemens/element-translate-ng/translate';
import { Type } from '@angular/core';

// @public (undocumented)
export class SiTooltipDirective implements OnDestroy {
readonly isDisabled: i0.InputSignalWithTransform<boolean, unknown>;
readonly placement: i0.InputSignal<"auto" | "top" | "start" | "end" | "bottom">;
readonly placement: i0.InputSignal<"auto" | "start" | "end" | "top" | "bottom">;
readonly siTooltip: i0.InputSignal<TemplateRef<any> | TranslatableString>;
readonly tooltipContext: i0.InputSignal<unknown>;
readonly tooltipScrollStrategy: i0.InputSignal<ScrollStrategy | undefined>;
}

// @public (undocumented)
Expand Down
13 changes: 8 additions & 5 deletions projects/element-ng/common/helpers/overlay-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import {
Overlay,
OverlayConfig,
OverlayRef,
PositionStrategy
PositionStrategy,
ScrollStrategy
} from '@angular/cdk/overlay';
import { ElementRef } from '@angular/core';

Expand Down Expand Up @@ -51,11 +52,12 @@ export function makePositionStrategy(
export function makeOverlay(
positionStrategy: PositionStrategy,
overlay: Overlay,
hasBackdrop: boolean
hasBackdrop: boolean,
scrollStrategy?: ScrollStrategy
): OverlayRef {
const config = new OverlayConfig();
config.positionStrategy = positionStrategy;
config.scrollStrategy = overlay.scrollStrategies.reposition();
config.scrollStrategy = scrollStrategy ?? overlay.scrollStrategies.reposition();
config.direction = isRTL() ? 'rtl' : 'ltr';
if (hasBackdrop) {
config.hasBackdrop = true;
Expand All @@ -72,10 +74,11 @@ export function getOverlay(
hasBackdrop: boolean,
placement: keyof typeof positions | ConnectionPositionPair[],
constrain = false,
center = true
center = true,
scrollStrategy?: ScrollStrategy
): OverlayRef {
const positionStrategy = makePositionStrategy(elementRef, overlay, placement, constrain, center);
return makeOverlay(positionStrategy, overlay, hasBackdrop);
return makeOverlay(positionStrategy, overlay, hasBackdrop, scrollStrategy);
}

export function getPositionStrategy(
Expand Down
41 changes: 40 additions & 1 deletion projects/element-ng/popover/si-popover.directive.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
* Copyright (c) Siemens 2016 - 2026
* SPDX-License-Identifier: MIT
*/
import { Component, viewChild } from '@angular/core';
import { Overlay, ScrollStrategy } from '@angular/cdk/overlay';
import { ChangeDetectionStrategy, Component, signal, viewChild } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';

import { SiPopoverDirective } from './si-popover.directive';
Expand Down Expand Up @@ -193,3 +194,41 @@ describe('with custom template', () => {
vi.useRealTimers();
});
});

describe('with scrollStrategy', () => {
let fixture: ComponentFixture<ScrollStrategyHostComponent>;
let button: HTMLButtonElement;

@Component({
imports: [SiPopoverDirective],
template: `<button type="button" siPopover="test" [siPopoverScrollStrategy]="scrollStrategy()"
>Test</button
>`,
changeDetection: ChangeDetectionStrategy.OnPush
})
Comment thread
spike-rabbit marked this conversation as resolved.
class ScrollStrategyHostComponent {
readonly scrollStrategy = signal<ScrollStrategy | undefined>(undefined);
}

beforeEach(() => {
fixture = TestBed.createComponent(ScrollStrategyHostComponent);
button = fixture.nativeElement.querySelector('button') as HTMLButtonElement;
fixture.detectChanges();
});

it('should close popover on scroll when custom close scroll strategy is provided', async () => {
const overlay = TestBed.inject(Overlay);
fixture.componentInstance.scrollStrategy.set(overlay.scrollStrategies.close());
fixture.detectChanges();

button.click();
await fixture.whenStable();

expect(document.querySelector('.popover')).toBeInTheDocument();

document.dispatchEvent(new Event('scroll', { bubbles: true }));
await fixture.whenStable();

expect(document.querySelector('.popover')).not.toBeInTheDocument();
});
});
20 changes: 18 additions & 2 deletions projects/element-ng/popover/si-popover.directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Copyright (c) Siemens 2016 - 2026
* SPDX-License-Identifier: MIT
*/
import { Overlay, OverlayRef } from '@angular/cdk/overlay';
import { Overlay, OverlayRef, ScrollStrategy } from '@angular/cdk/overlay';
import { ComponentPortal } from '@angular/cdk/portal';
import {
ComponentRef,
Expand Down Expand Up @@ -87,6 +87,14 @@ export class SiPopoverDirective implements OnDestroy {
*/
readonly context = input<unknown>(undefined, { alias: 'siPopoverContext' });

/**
* Optional CDK scroll strategy used for the popover overlay.
* If not provided, the default reposition strategy is used.
*
* @defaultValue undefined
*/
readonly scrollStrategy = input<ScrollStrategy>(undefined, { alias: 'siPopoverScrollStrategy' });

/**
* Emits an event when the popover is shown/hidden
*/
Expand Down Expand Up @@ -118,7 +126,15 @@ export class SiPopoverDirective implements OnDestroy {
if (this.overlayref?.hasAttached()) {
return;
}
this.overlayref = getOverlay(this.elementRef, this.overlay, false, this.placementInternal());
this.overlayref = getOverlay(
this.elementRef,
this.overlay,
false,
this.placementInternal(),
false,
true,
this.scrollStrategy()
);
Comment thread
spike-rabbit marked this conversation as resolved.
this.overlayref
.outsidePointerEvents()
.pipe(takeUntil(this.destroyer))
Expand Down
49 changes: 48 additions & 1 deletion projects/element-ng/tooltip/si-tooltip.directive.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
* Copyright (c) Siemens 2016 - 2026
* SPDX-License-Identifier: MIT
*/
import { Component, signal } from '@angular/core';
import { Overlay, ScrollStrategy } from '@angular/cdk/overlay';
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';

import { SiTooltipModule } from './si-tooltip.module';
Expand Down Expand Up @@ -137,4 +138,50 @@ describe('SiTooltipDirective', () => {
expect(document.querySelector('.tooltip')).not.toBeInTheDocument();
});
});

describe('with scrollStrategy', () => {
let fixture: ComponentFixture<TestHostComponent>;
let button: HTMLButtonElement;

@Component({
imports: [SiTooltipModule],
template: `<button type="button" siTooltip="test" [tooltipScrollStrategy]="scrollStrategy()"
>Test</button
>`,
changeDetection: ChangeDetectionStrategy.OnPush
})
Comment thread
spike-rabbit marked this conversation as resolved.
class TestHostComponent {
readonly scrollStrategy = signal<ScrollStrategy | undefined>(undefined);
}

beforeEach(() => {
vi.useFakeTimers();
fixture = TestBed.createComponent(TestHostComponent);
button = fixture.nativeElement.querySelector('button') as HTMLButtonElement;
vi.spyOn(button, 'matches').mockReturnValue(true);
fixture.detectChanges();
});

afterEach(() => {
vi.useRealTimers();
});

it('should close tooltip on scroll when custom close scroll strategy is provided', async () => {
const overlay = TestBed.inject(Overlay);
fixture.componentInstance.scrollStrategy.set(overlay.scrollStrategies.close());
fixture.detectChanges();

button.dispatchEvent(new Event('focus'));
vi.advanceTimersByTime(0);
await fixture.whenStable();

expect(document.querySelector('.tooltip')).toBeInTheDocument();

document.dispatchEvent(new Event('scroll', { bubbles: true }));
vi.advanceTimersByTime(0);
await fixture.whenStable();

expect(document.querySelector('.tooltip')).not.toBeInTheDocument();
});
});
});
10 changes: 9 additions & 1 deletion projects/element-ng/tooltip/si-tooltip.directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* Copyright (c) Siemens 2016 - 2026
* SPDX-License-Identifier: MIT
*/
import { ScrollStrategy } from '@angular/cdk/overlay';
import { isPlatformBrowser } from '@angular/common';
import {
booleanAttribute,
Expand Down Expand Up @@ -54,6 +55,12 @@ export class SiTooltipDirective implements OnDestroy {
*/
readonly isDisabled = input(false, { transform: booleanAttribute });

/**
* Optional CDK scroll strategy used for the tooltip overlay.
* If not provided, the default reposition strategy is used.
*/
readonly tooltipScrollStrategy = input<ScrollStrategy>();
Comment thread
chintankavathia marked this conversation as resolved.

/**
* The context for the attached template
*/
Expand Down Expand Up @@ -95,7 +102,8 @@ export class SiTooltipDirective implements OnDestroy {
element: this.elementRef,
placement: this.placement(),
tooltip: this.siTooltip,
tooltipContext: this.tooltipContext
tooltipContext: this.tooltipContext,
scrollStrategy: this.tooltipScrollStrategy()
});
this.tooltipRef.show();
}, delay);
Expand Down
13 changes: 11 additions & 2 deletions projects/element-ng/tooltip/si-tooltip.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Copyright (c) Siemens 2016 - 2026
* SPDX-License-Identifier: MIT
*/
import { Overlay, OverlayRef } from '@angular/cdk/overlay';
import { Overlay, OverlayRef, ScrollStrategy } from '@angular/cdk/overlay';
import { ComponentPortal } from '@angular/cdk/portal';
import { ComponentRef, ElementRef, inject, Injectable, Injector } from '@angular/core';
import { getOverlay, getPositionStrategy, positions } from '@siemens/element-ng/common';
Expand Down Expand Up @@ -71,6 +71,7 @@ export class SiTooltipService {
injector?: Injector;
tooltip: () => SiTooltipContent;
tooltipContext: () => unknown;
scrollStrategy?: ScrollStrategy;
}): TooltipRef {
const injector = Injector.create({
parent: config.injector,
Expand All @@ -87,7 +88,15 @@ export class SiTooltipService {
});

return new TooltipRef(
getOverlay(config.element, this.overlay, false, config.placement),
getOverlay(
config.element,
this.overlay,
false,
config.placement,
false,
true,
config.scrollStrategy
),
config.element,
injector
);
Expand Down
Loading