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
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.
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);
}
}
}
<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.
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 !
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 :
<ng-template
cdkConnectedOverlay
[cdkConnectedOverlayOrigin]="trigger"
[cdkConnectedOverlayOpen]="isOpen()"
>
<mat-form-field #container>
...
</mat-form-field>
</ng-template>
@ViewChild('container', { static: false })
datepickerContainer!: ElementRef<HTMLElement>;