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

2

6

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 πŸ€·πŸΎβ€β™‚οΈ

Hartzog answered 2/11, 2019 at 19:3 Comment(0)
H
8

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

Hartzog answered 4/11, 2019 at 19:58 Comment(0)
C
0

For the height, you can replace :

maxHeight: 300,

by

height: window.innerHeight < 300 ? window.innerHeight + 'px' : '300px',

It's just an idea. It's not a good idea to directly use window in your componnet because you can't mock it. Check this article if you want to know how to do it properly.

Clerissa answered 2/11, 2019 at 20:59 Comment(2)
Hi @Clerissa - thanks for the answer. Setting the height like proposed, does not solve the problem with the positioning on small screens. The overlay continues to be positioned outside the viewport. – Hartzog
I have found no nice solution for that, sorry. – Clerissa

© 2022 - 2024 β€” McMap. All rights reserved.