angular / angular/components

feat(FlexibleConnectedPositionStrategy): Handle custom containers for flexible connected position strategy

Open
#27,644 0 comments 3 reactions 0 assignees View on GitHub
area: cdk/overlay feature P5
Dominant language
TypeScript
Stars
25k
Forks
6.8k
Avg merge
1d 8h
Merged PRs (30d)
91

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 | null = '';

@Input() position: 'top' | 'right' | 'bottom' | 'left' = 'bottom';

private overlayRef: Maybe;

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 = 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,
) {}

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('.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

Contributor guide

Open the contributing guide

Research direction

Start in src/cdk/overlay/position/flexible-connected-position-strategy.ts, especially _getOriginRect and the withPush viewport adjustment referenced by the issue. Trace how FlexibleConnectedPositionStrategy obtains viewport dimensions and origin bounds, then define support for a custom overlay container. Done means withPush keeps overlays within that container rather than the real document viewport, with coverage for the described custom-container case.

Written by the indexing model from the issue text.

Assessment

Tech stack
angular, typescript
Domain
frontend
Issue type
Feature
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.