Search code examples
angularangular-materialangular-material2angular-cdk

How do I configure a material cdk overlay position strategy that works great, both on big and small screens?


Question

How do I configure a material cdk overlay position strategy that works great, on big and small screens?

Goal

My objective is to create an overlay with the Angular overlay CDK, that follow these rules:

  • Never position overlay outside the viewport
  • Max height of 300 pixels
  • Position overlay y with a priority of top, center bottom.
  • On screens with a height below 300, use the available height.
  • In other words, no minimum height.

What works 😎

I have accomplished some of the above requirements, as you can see here the positioning works great on devices with enough vertical space (in this case 512px):

enter image description here

The broken part 😅

However as you can tell from the following gif, this does not work on small devices with insufficient vertical space (in this case 255px). Actually, as you can tell from the GIF, in 2 of the cases it was very close. The position was correct, only the height was off.

enter image description here

Here the goal is to use the available space, as illustrated in red:

enter image description here

The code 🤓

I have a stack blitz, where you can experiment here.

component

openPopup(element: any) {
    const overlayRef = this.overlay.create({
      hasBackdrop: true,
      positionStrategy: this.overlay.position()
      .flexibleConnectedTo(element)
      .withLockedPosition()
      .withPositions([
        {
          panelClass: 'custom-panel1',
          originX: 'center',
          originY: 'center',
          overlayX: 'start',
          overlayY: 'top',
        },
        {
          panelClass: 'custom-panel2',
          originX: 'center',
          originY: 'center',
          overlayX: 'start',
          overlayY: 'center',
        },
        {
          panelClass: 'custom-panel3',
          originX: 'center',
          originY: 'center',
          overlayX: 'start',
          overlayY: 'bottom',
        },
      ])
      ,
      width: '200px',
      maxHeight: 300,
    });

    const popupComponentPortal = new ComponentPortal(PopupComponent);

    overlayRef.attach(popupComponentPortal);

    overlayRef.backdropClick().subscribe(() => {
      overlayRef.dispose();
    });
  }

global styles

.cdk-overlay-pane {
    max-height: 300px;
}

Notes

I have been thinking about using the global positioning strategy when the height of the viewport is getting small. However, I would prefer to avoid this scenario, as I would love a solution that could tackle any height of the overlay (respecting the max height off-course).

I recommend using the "Open in New Window LIVE" feature of stackbllitz, when testing the stackblitz. Link is here again.

I would be SO grateful, if you could help solve this issue or point me in the right direction 🤷🏾‍♂️


Solution

  • I solved the above problem, by implementing a check - after the dialog is opened. This is done with the resize-observer. The code looks like this:

    /*
            Read more about the resize observer here: https://developers.google.com/web/updates/2016/10/resizeobserver
            Basicaly, what we do is that we subscribe to size events on the overlay.
            Currently we only get one event, (then we disconnet the resize observer).
            But then we simply calculate if we need to improve the layout.
            */
            const ro = new ResizeObserver(entries => {
                for (const entry of entries) {
                    // We get the hight of the element from the the contnetRect, provided by the resize observer
                    const height = entry.contentRect.height;
    
                    const { left: x, top: y } = entry.target.getBoundingClientRect();
    
                    const offsetPlusHeight = y + height;
    
                    const pixelsOverflow = offsetPlusHeight - this.viewPortHeight;
                    // If y is negative, we are off-screen to the top.
                    // If pixelsOverflow is positive, we are off-screen on the bottom
                    // In either case, we adopt a new strategy.
                    if (y < 0 || pixelsOverflow > 1) {
                        ro.disconnect();
                        overlayRef.updateSize({
                            height: height,
                            maxHeight: config.maxHeight,
                            width: config.width,
                        });
                        // Trust the initial positioning strategy, in terms of the x coordinate
    
                        // Now determine if we need to throw the overlap, all the way to the top.
                        const delta = this.viewPortHeight - height;
                        const yOffset = delta > 0 ? delta : 0;
    
                        // Finnaly, we can apply canculate and apply the new position
                        const newPositionStrategy = this.getGlobalPosition(yOffset, x);
                        overlayRef.updatePositionStrategy(newPositionStrategy);
                    }
                }
            });
    
            // Element for which to observe height and width
            ro.observe(overlayRef.overlayElement);
    

    I think the reason I need this extra check, is because what I am actually looking for is a n cdk overlay strategy for items with variable height.

    If you are interested in a working solution, I have a working stackblitz here:

    enter image description here