feat(FlexibleConnectedPositionStrategy): Handle custom containers for flexible connected position strategy
- Lenguaje dominante
- TypeScript
- Estrellas
- 25k
- Forks
- 6.8k
- Merge medio
- 1 d 8 h
- PR fusionados (30 d)
- 91
Descripción
### 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
Guía de contribución
Línea de trabajo
Comienza en src/cdk/overlay/position/flexible-connected-position-strategy.ts, especialmente en _getOriginRect y en el ajuste del viewport con withPush al que hace referencia el issue. Sigue cómo FlexibleConnectedPositionStrategy obtiene las dimensiones del viewport y los límites del origen, y después define la compatibilidad con un contenedor de overlay personalizado. Se considera terminado cuando withPush mantiene los overlays dentro de ese contenedor en lugar del viewport real del documento, con cobertura para el caso descrito de un contenedor personalizado.
Escrito por el modelo de indexación a partir del texto del issue.
Evaluación
- Stack tecnológico
- angular, typescript
- Área
- frontend
- Tipo de issue
- Nueva funcionalidad
- Dificultad
- 4/5
- Tiempo estimado
- 3-5 días
- Estado de actividad
- Estancado
- Claridad
- Bastante claro
- Aptitud para principiantes
- 35/100