Description
Feature Description
I would like to be able to provide a custom viewport to calculate FlexibleConnectedTo
positions. Now a custom overlay container can be provided by the user, but in tandem with the FlexibleConnectedTo
it does not work correctly, because under the hood _getOriginRect
works with real viewport and pushing element into the custom container frame is calculated relative to the real document.clientWidth
and document.clientHeight
For example, lets imagine we have div.custom-overlay-container
with 500x200px
dimensions and tooltip directive built with CDK. How I solve this problem now to achieve desired behavior:
Tooltip.directive.ts
import { FlexibleConnectedPositionStrategy, OriginConnectionPosition, Overlay, OverlayConnectionPosition, OverlayRef } from '@angular/cdk/overlay';
import { ComponentPortal } from '@angular/cdk/portal';
import {
ComponentRef,
Directive,
ElementRef,
HostBinding,
HostListener,
Input,
OnInit,
TemplateRef,
} from '@angular/core';
const FIXED_WIDTH = 200;
const FIXED_HEIGHT = 500;
@Directive({
selector: '[cdkTooltip]',
standalone: true,
})
export class TooltipDirective implements OnInit {
@Input('cdkTooltip') content: string | TemplateRef<unknown> | null = '';
@Input() position: 'top' | 'right' | 'bottom' | 'left' = 'bottom';
private overlayRef: Maybe<OverlayRef>;
private tooltipHovered = false;
@HostListener('mouseenter')
show(): void {
if (!this.content) {
return;
}
this.overlayRef?.detach();
const tooltipPortal = new ComponentPortal(TooltipComponent);
if (this.overlayRef) {
const tooltipRef: ComponentRef<TooltipComponent> = this.overlayRef.attach(tooltipPortal);
const tooltipHostElement = tooltipRef.location.nativeElement as HTMLElement;
tooltipRef.setInput(typeof this.content === 'string' ? 'text' : 'template', this.content);
}
}
@HostListener('mouseleave')
hide(): void {
this.overlayRef?.detach();
}
constructor(
private readonly overlay: Overlay,
private readonly elementRef: ElementRef<HTMLElement>,
) {}
ngOnInit(): void {
this.createOverlay();
}
private createOverlay(): void {
const strategy = this.overlay.position().flexibleConnectedTo(this.elementRef).withPush(true);
// Fix for correct tooltip position calculations:
// the problem is in the `_getOriginRect` function, it calculates the `DomRect` object
// for tooltip host relative to the real viewport, but the application content area is limited
// by the size of the `div.custom-overlay-container`, so we need to override it for correct calculations
// CDK source where problem method defined -
(strategy as any)._getOriginRect = () => {
const customViewportContainer = document.querySelector<HTMLDivElement>('.custom-overlay-container')!;
return this.getRelativeBoundingClientRect(
this.elementRef.nativeElement,
customViewportContainer,
);
};
// Override position's `_document.documentElement` for correct position adjustment,
// which is used for pushing element inside viewport (`div.custom-overlay-container` in our case)
// https://github.com/angular/components/blob/b13c6aa194cf560a304213961ae28725f8d0a4e2/src/cdk/overlay/position/flexible-connected-position-strategy.ts#L1036
Object.defineProperty(strategy, '_document', {
writable: true,
value: {
documentElement: {
clientWidth: FIXED_WIDTH,
clientHeight: FIXED_HEIGHT,
},
},
});
this.overlayRef = this.overlay.create({
positionStrategy: strategy,
});
this.setOverlayPosition(this.overlayRef);
}
private setOverlayPosition(overlayRef: OverlayRef): void {
const pos = overlayRef.getConfig().positionStrategy as FlexibleConnectedPositionStrategy;
pos.withPositions([
{
...this.getOriginPosition(),
...this.getOverlayPosition(),
panelClass: this.position,
},
]);
}
private getOverlayPosition(): OverlayConnectionPosition {
const position = this.position;
if (position === 'top') {
return { overlayX: 'center', overlayY: 'bottom' };
}
if (position === 'bottom') {
return { overlayX: 'center', overlayY: 'top' };
}
if (position === 'right') {
return { overlayX: 'start', overlayY: 'center' };
}
return { overlayX: 'end', overlayY: 'center' };
}
private getOriginPosition(): OriginConnectionPosition {
const position = this.position;
if (position === 'bottom' || position === 'top') {
return {
originX: 'center',
originY: position === 'top' ? 'top' : 'bottom',
};
}
if (position === 'right') {
return { originX: 'start', originY: 'center' };
}
return { originX: 'end', originY: 'center' };
}
private getRelativeBoundingClientRect(element: HTMLElement, relativeTo: HTMLElement): DOMRect {
const elemRect = element.getBoundingClientRect();
const containerRect = relativeTo.getBoundingClientRect();
return {
top: elemRect.top - containerRect.top,
bottom: elemRect.bottom - containerRect.top,
left: elemRect.left - containerRect.left,
right: elemRect.right - containerRect.left,
width: elemRect.width,
height: elemRect.height,
x: elemRect.left - containerRect.left,
y: elemRect.top - containerRect.top,
} as DOMRect;
}
}
Use Case
the problem that I propose to solve is to add the ability FlexibleConnectedTo
position to be able to work strictly within the custom container, that is, when withPush
is enabled - the elements should not go beyond the custom container