angular / angular/components

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

オープン
#27,644 コメント 0 件 リアクション 3 件 担当者 0 名 GitHub で見る
area: cdk/overlay feature P5
主要言語
TypeScript
スター
25k
フォーク
6.8k
平均マージ
1日 8時間
マージ済み PR(30日)
91

説明

### 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

コントリビューションガイド

コントリビューションガイドを開く

調査の方向性

src/cdk/overlay/position/flexible-connected-position-strategy.ts、特に _getOriginRect と issue で参照されている withPush の viewport 調整から始めます。FlexibleConnectedPositionStrategy が viewport の寸法と origin の境界をどのように取得しているかを追跡し、そのうえでカスタム overlay コンテナのサポートを定義します。withPush が実際のドキュメントの viewport ではなく、そのコンテナ内に overlay を収めるようになり、説明されているカスタムコンテナのケースをカバーするテストがあれば完了です。

索引モデルが issue の本文から書いたものです。

評価

技術スタック
angular, typescript
領域
frontend
issue の種類
機能追加
難易度
4/5
見積もり時間
3〜5日
活発さ
停滞
明瞭さ
おおむね明確
初心者へのやさしさ
35/100

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。