feat(FlexibleConnectedPositionStrategy): Handle custom containers for flexible connected position strategy
- Langage dominant
- TypeScript
- Étoiles
- 25k
- Forks
- 6.8k
- Merge moyen
- 1 j 8 h
- PR mergées (30 j)
- 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
Guide de contribution
Ouvrir le guide de contribution
Piste de recherche
Commencez dans src/cdk/overlay/position/flexible-connected-position-strategy.ts, en particulier dans _getOriginRect et l’ajustement du viewport avec withPush mentionné par l’issue. Suivez la manière dont FlexibleConnectedPositionStrategy obtient les dimensions du viewport et les limites de l’origine, puis définissez la prise en charge d’un conteneur d’overlay personnalisé. Le travail est terminé lorsque withPush maintient les overlays à l’intérieur de ce conteneur plutôt que dans le viewport réel du document, avec une couverture du cas décrit de conteneur personnalisé.
Rédigé par le modèle d'indexation à partir du texte de l'issue.
Évaluation
- Stack technique
- angular, typescript
- Domaine
- frontend
- Type d'issue
- Fonctionnalité
- Difficulté
- 4/5
- Temps estimé
- 3-5 jours
- Activité
- À l'abandon
- Clarté
- Plutôt claire
- Accessibilité débutants
- 35/100