Search code examples
angularangular-materialmat-select

Dynamically position Angular Material 13 mat-select dropdown without affecting autocomplete


Lots of people seem to have this issue but I haven't found a solution that doesn't create problems in other areas.

How to customize mat-select dropdown position?

.cdk-overlay-pane {
  position: absolute;
  pointer-events: auto;
  box-sizing: border-box;
  z-index: 1000;
  display: flex;
  max-width: 100%;
  max-height: 100%;
  transform: none !important;
  margin-top: 30px;
}

This one works great. ...except that it also affects mat-autocompletes, and not in a good way. It lines up the mat-select options perfectly, and moves the mat-autocomplete options down 30px. Not good.

So I thought, maybe isolate the css to mat-select somehow. There are no parent classes unique to mat-select, so anything :child won't work. There is a direct child class though called mat-select-panel-wrap. So I looked for a way to access the parent via this unique child. :has is the only thing I found that claims to do this, but alas, zero browser support.

https://caniuse.com/css-has

Back in 11 and earlier, Angular Material had a great overlayDir: CdkConnectedOverlay; that allowed for changes to the options box positioning. But they depreciated it and then made it protected, without fixing the problem that was making people use it in the first place. And their "solution" was "try our experimental mat-select." That solution is dubious at best.

MatSelect overlayDir now private when updating from Angular 11 to 12. How do I access it now?

My question is, does anyone know of a way to isolate the above css to just mat-selects? Or more specifically, to just the mat-select options box.


Solution

  • The position of the mat-select are defined by his private property _positions, see the github

    You can change it. If you has, e.g.

    <!--see the template reference variable "select"-->
    <mat-select #select>
        ...
    </mat-select>
    

    You can get it using viewChild and change this property. I know that is a private property, so first you "cast to any" to do it

    e.g. If you want to show to the right

      @ViewChild('select',{static:true}) select:MatSelect
    
      ngOnInit(){
        //add a new _positions 
        (this.select as any)._positions.unshift(
          {
            originX: 'end',
            originY: 'top',
            overlayX: 'start',
            overlayY: 'top',
            offsetX:5
          })
      }
    

    See a stackblitz

    NOTE: See that I choose add using unshift -you can also defined all the positions-. In this way, if select have no enough space -in the stackblitz you can make the screen narrowest-, the select it's move to down

    About position strategy, see this Netanel Basal entry blog. See that it's a bit old and it's added new properties: offsetX and offsetY, see the docs in material angular

    Update why not create a directive?

    export const POS: {
      top: ConnectedPosition;
      right: ConnectedPosition;
      bottom: ConnectedPosition;
      left: ConnectedPosition;
    } = {
      top: {
        originX: 'start',
        originY: 'top',
        overlayX: 'start',
        overlayY: 'bottom',
        offsetY: -5,
        offsetX: 0,
        panelClass: '',
        weight: 120,
      },
      right: {
        originX: 'end',
        originY: 'top',
        overlayX: 'start',
        overlayY: 'top',
        offsetX: 5,
        offsetY: 0,
        panelClass: '',
        weight: 120,
      },
      bottom: {
        originX: 'start',
        originY: 'bottom',
        overlayX: 'start',
        overlayY: 'top',
        offsetY: 5,
        offsetX: 0,
        panelClass: '',
        weight: 120,
      },
      left: {
        originX: 'start',
        originY: 'top',
        overlayX: 'end',
        overlayY: 'top',
        offsetX: -5,
        offsetY: 0,
        panelClass: '',
        weight: 120,
      },
    };
    @Directive({
      selector: 'mat-select[position]'
    })
    export class MatSelectPositionDirective implements AfterViewInit {
      @Input('position') position:'top'|'bottom'|'left'|'right'
      constructor(@Host() private select:MatSelect){}
      ngAfterViewInit()
      {
        (this.select as any)._positions.unshift(POS[this.position])
      }
    }
    

    You can use like

    <mat-select [position]='top'>
    ...
    </mat-select>