Search code examples
angularoverlayangular-cdk

How to keep Angular CDK Overlay component within page based on page offset


I am working on my library Contexr. I just refactored my application to use the Angular CDK Overlay to display the context menu, so I don't have to include some component in the actual application anymore (one less installation-step).

I once used the FlexibleConnectedPositionStrategy to create a dropdown beneath an element, which would keep within the page. This position strategy is created like using an ElementRef:

const positionStrategy = this.overlay.position()
      .flexibleConnectedTo(elementRef)
      .left(state.left + 'px')
      .top(state.top + 'px');

The problem is that I don't have an ElementRef to go from. My overlay should be flexible connected to my .left() and .top(). Is there a way to do this with the FlexibleConnectedPositionStrategy? Currently I'm trying to use the GlobalPositionStrategy, but that doesn't account for elements going off-screen.

The class that opens the overlay:

@Injectable({
  providedIn: 'root'
})
export class ContextMenuService {
  private overlayRef: OverlayRef;

  constructor(private overlay: Overlay, private injector: Injector) {}

  public open(state: ContextState) {
    const overlayConfig = this.getOverlayConfig(state);
    this.overlayRef = this.overlay.create(overlayConfig);
    const contextMenuRef = new ContextMenuOverlayRef(this.overlayRef);
    this.attachDialogContainer(this.overlayRef, state, contextMenuRef);
  }

  private getOverlayConfig(state: ContextState) {
    const positionStrategy = this.overlay.position()
      .global()
      .left(state.left + 'px')
      .top(state.top + 'px');

    return {
      positionStrategy: positionStrategy
    };
  }

  private createInjector(state: ContextState, dialogRef: ContextMenuOverlayRef) {
    const injectionTokens = new WeakMap();
    injectionTokens.set(ContextMenuOverlayRef, dialogRef);
    injectionTokens.set(CONTEXT_MENU_OVERLAY_DATA, state);
    return new PortalInjector(this.injector, injectionTokens);
  }

  private attachDialogContainer(overlayRef: OverlayRef, state: ContextState, contextMenuOverlayRef: ContextMenuOverlayRef) {
    const injector = this.createInjector(state, contextMenuOverlayRef);
    const containerPortal = new ComponentPortal(ContextMenuComponent, null, injector);
    overlayRef.attach(containerPortal);
  }

  public close() {
    if (this.overlayRef) {
      this.overlayRef.dispose();
    }
  }
}

Solution

  • Turns out you can use the FlexibleConnectedPositionStrategy after all. I found some context menu library on Github, called ngrx-rightclick (thank you!). Here they created a new ElementRef based on the click event.

    private getOverlayConfig(event: MouseEvent, state: ContextState) {
        const target = {
          getBoundingClientRect: (): ClientRect => ({
            bottom: event.clientY,
            height: 0,
            left: event.clientX,
            right: event.clientX,
            top: event.clientY,
            width: 0,
          }),
        };
    
        const element = new ElementRef(target);
    
        const positionStrategy = this.overlay.position()
          .flexibleConnectedTo(element)
          .withFlexibleDimensions(false)
          .withPositions([
            {
              originX: 'end',
              originY: 'top',
              overlayX: 'start',
              overlayY: 'top',
            },
            {
              originX: 'start',
              originY: 'top',
              overlayX: 'end',
              overlayY: 'top',
            },
            {
              originX: 'end',
              originY: 'bottom',
              overlayX: 'start',
              overlayY: 'bottom',
            },
            {
              originX: 'start',
              originY: 'bottom',
              overlayX: 'end',
              overlayY: 'bottom',
            },
          ]);
    
        return {
          positionStrategy: positionStrategy
        };
      }