angular / angular/components

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

Ouverte
#27,644 0 commentaires 3 réactions 0 personnes assignées Voir sur GitHub
area: cdk/overlay feature P5
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

Recevez les nouvelles issues par e-mail

Un résumé court des issues GitHub adaptées aux débutants.