Ggros
Ggros

Reputation: 23

Angular Material - Overlay X Datepicker

I have a question regarding overlay and datepicker with Angular Material. I'm using the overlayOutsideClick event to close the overlay whenever I click outside the overlay.

It's working fine but if I have a datepicker and I choose a date it is considered as a click outside the overlay, how can I prevent this behavior ?

Thanks

Here is a stackblitz to reproduce the situation: https://stackblitz.com/edit/ghjvsh-zy2lf2?file=src%2Fexample%2Fcdk-overlay-basic-example.html

Upvotes: 2

Views: 455

Answers (1)

Tibère B.
Tibère B.

Reputation: 1226

So I came up with a solution that may be a little more complex than it needs to be.

To fix the overlay closing on click on the datepicker toggle button, I didn't make use of the overlayOutsideClick, I instead relied on the blur event to run logic when the date input is loosing focus.

By encapsulating the MatFormField in a div, I will check if the new focused element is inside of that div before closing the overlay, leaving it open if we clicked the datepicker toggle button.

Minimal code example :

Typescript :
export class CdkOverlayBasicExample {
  protected isOpen = signal<boolean>(false);

  @ViewChild('container', { static: false })
  datepickerContainer!: ElementRef<HTMLDivElement>;

  protected onBlur($event: FocusEvent) {
    const relatedTarget = $event.relatedTarget;
    if (
      !relatedTarget || // If the related target is null, we didn't click on any HTML element
      !(relatedTarget instanceof HTMLElement) ||
      !this.datepickerContainer.nativeElement.contains(relatedTarget)
    ) {
      // If the relatedTarget is not contained inside our container div, close the overlay
      this.isOpen.set(false);
    }
  }
}
Template :
<ng-template
  cdkConnectedOverlay
  [cdkConnectedOverlayOrigin]="trigger"
  [cdkConnectedOverlayOpen]="isOpen()"
>
  <div #container>
    <mat-form-field>
      <mat-label>Choose a date</mat-label>
      <input
        #input
        matInput
        [matDatepicker]="picker"
        (blur)="onBlur($event)"
        [cdkTrapFocusAutoCapture]="true"
        cdkTrapFocus
      />
      <mat-hint>MM/DD/YYYY</mat-hint>
      <mat-datepicker-toggle
        matIconSuffix
        [for]="picker"
      ></mat-datepicker-toggle>
      <mat-datepicker
        #picker
        [restoreFocus]="false"
        (closed)="input.focus()"
      ></mat-datepicker>
    </mat-form-field>
  </div>
</ng-template>

While working as intended this solution had a few caveats, notably the datepicker input is not focused by default and closing the datepicker would not set the focus on the input.

To achieve a better result I've added the cdkFocusTrap from the A11yModule to the datepicker input, I've disabled the datepicker restore focus function and I manually set the focus on the datepicker input when the datepicker is closed.

Full example:

import {
  Component,
  ElementRef,
  TemplateRef,
  ViewChild,
  signal,
} from '@angular/core';
import { OverlayModule } from '@angular/cdk/overlay';
import {
  MatDatepicker,
  MatDatepickerModule,
} from '@angular/material/datepicker';
import { MatFormField, MatInputModule } from '@angular/material/input';
import { MatFormFieldModule } from '@angular/material/form-field';
import { A11yModule } from '@angular/cdk/a11y';

/**
 * @title Overlay basic example
 */
@Component({
  selector: 'cdk-overlay-basic-example',
  template: `
    <!-- This button triggers the overlay and is it's origin -->
<button
  (click)="isOpen.set(!isOpen());"
  type="button"
  cdkOverlayOrigin
  #trigger="cdkOverlayOrigin"
>
  {{isOpen() ? "Close" : "Open"}}
</button>

    <ng-template
      cdkConnectedOverlay
      [cdkConnectedOverlayOrigin]="trigger"
      [cdkConnectedOverlayOpen]="isOpen()"
    >
      <div #container>
        <mat-form-field>
          <mat-label>Choose a date</mat-label>
          <input
            #input
            matInput
            [matDatepicker]="picker"
            (blur)="onBlur($event)"
            [cdkTrapFocusAutoCapture]="true"
            cdkTrapFocus
          />
          <mat-hint>MM/DD/YYYY</mat-hint>
          <mat-datepicker-toggle
            matIconSuffix
            [for]="picker"
          ></mat-datepicker-toggle>
          <mat-datepicker
            #picker
            [restoreFocus]="false"
            (closed)="input.focus()"
          ></mat-datepicker>
        </mat-form-field>
      </div>
    </ng-template>

  `,
  standalone: true,
  imports: [
    OverlayModule,
    MatFormFieldModule,
    MatInputModule,
    MatDatepickerModule,
    A11yModule,
  ],
})
export class CdkOverlayBasicExample {
  protected isOpen = signal<boolean>(false);

  @ViewChild('container', { static: false })
  datepickerContainer!: ElementRef<HTMLDivElement>;

  protected onBlur($event: FocusEvent) {
    const relatedTarget = $event.relatedTarget;
    if (
      !relatedTarget ||
      !(relatedTarget instanceof HTMLElement) ||
      !this.datepickerContainer.nativeElement.contains(relatedTarget)
    ) {
      this.isOpen.set(false);
    }
  }
}

And here's a working stackblitz forked from yours :
https://stackblitz.com/edit/ghjvsh-bmd6my?file=src%2Fexample%2Fcdk-overlay-basic-example.html

I hope this helps !

Update :

I'd like to add that the encapsulation of the mat-form-field inside a div element is not mandatory, as one could also just use the HTMLElement of the mat-form-field instead :

Template :

<ng-template
  cdkConnectedOverlay
  [cdkConnectedOverlayOrigin]="trigger"
  [cdkConnectedOverlayOpen]="isOpen()"
>
  <mat-form-field #container>
    ...
  </mat-form-field>
</ng-template>

Typescript :

@ViewChild('container', { static: false })
datepickerContainer!: ElementRef<HTMLElement>;

Upvotes: 1

Related Questions