angular / angular/components

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

Abierto
#27,644 0 comentarios 3 reacciones 0 asignados Ver en GitHub
area: cdk/overlay feature P5
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

Abrir la 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

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.