Kay van Bree
Kay van Bree

Reputation: 2082

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();
    }
  }
}

Upvotes: 4

Views: 10529

Answers (2)

Kay van Bree
Kay van Bree

Reputation: 2082

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
    };
  }

Upvotes: 6

Aleš Doganoc
Aleš Doganoc

Reputation: 12092

I looked at the source code in GitHub. Since you are using a directive to attach the context menu to the element you can also obtain the element reference in the directive. Just add it to the constructor of the directive then send it to your service and you will have it available. Here is your class modified to get also the ElementRef:

import {Directive, HostListener, Input} from '@angular/core';
import {ContexrService} from '../providers/contexr.service';

@Directive({
  selector: '[ctx]'
})
export class ContextDirective {
  @Input('ctx') ctx: string;
  @Input('ctxArgs') ctxArgs: any;

  constructor(private contexr: ContexrService, private elementRef: ElementRef<any>) {}

  @HostListener('contextmenu', ['$event'])
  @HostListener('click', ['$event'])
  public onContextMenu(event) {
    this.contexr.addCurrentContext(this.ctx, this.ctxArgs, this.elementRef);
  }
}

Upvotes: 1

Related Questions