@@ -19,6 +19,7 @@ import {
19
19
A ,
20
20
DOWN_ARROW ,
21
21
ENTER ,
22
+ ESCAPE ,
22
23
hasModifierKey ,
23
24
LEFT_ARROW ,
24
25
RIGHT_ARROW ,
@@ -58,6 +59,9 @@ import {
58
59
ViewChild ,
59
60
ViewEncapsulation ,
60
61
HostAttributeToken ,
62
+ ANIMATION_MODULE_TYPE ,
63
+ Renderer2 ,
64
+ NgZone ,
61
65
} from '@angular/core' ;
62
66
import {
63
67
AbstractControl ,
@@ -80,16 +84,7 @@ import {
80
84
} from '@angular/material/core' ;
81
85
import { MAT_FORM_FIELD , MatFormField , MatFormFieldControl } from '@angular/material/form-field' ;
82
86
import { defer , merge , Observable , Subject } from 'rxjs' ;
83
- import {
84
- distinctUntilChanged ,
85
- filter ,
86
- map ,
87
- startWith ,
88
- switchMap ,
89
- take ,
90
- takeUntil ,
91
- } from 'rxjs/operators' ;
92
- import { matSelectAnimations } from './select-animations' ;
87
+ import { filter , map , startWith , switchMap , take , takeUntil } from 'rxjs/operators' ;
93
88
import {
94
89
getMatSelectDynamicMultipleError ,
95
90
getMatSelectNonArrayValueError ,
@@ -199,7 +194,6 @@ export class MatSelectChange {
199
194
'(focus)' : '_onFocus()' ,
200
195
'(blur)' : '_onBlur()' ,
201
196
} ,
202
- animations : [ matSelectAnimations . transformPanel ] ,
203
197
providers : [
204
198
{ provide : MatFormFieldControl , useExisting : MatSelect } ,
205
199
{ provide : MAT_OPTION_PARENT_COMPONENT , useExisting : MatSelect } ,
@@ -221,11 +215,16 @@ export class MatSelect
221
215
readonly _elementRef = inject ( ElementRef ) ;
222
216
private _dir = inject ( Directionality , { optional : true } ) ;
223
217
private _idGenerator = inject ( _IdGenerator ) ;
218
+ private _renderer = inject ( Renderer2 ) ;
219
+ private _ngZone = inject ( NgZone ) ;
224
220
protected _parentFormField = inject < MatFormField > ( MAT_FORM_FIELD , { optional : true } ) ;
225
221
ngControl = inject ( NgControl , { self : true , optional : true } ) ! ;
226
222
private _liveAnnouncer = inject ( LiveAnnouncer ) ;
227
223
protected _defaultOptions = inject ( MAT_SELECT_CONFIG , { optional : true } ) ;
224
+ protected _animationsDisabled =
225
+ inject ( ANIMATION_MODULE_TYPE , { optional : true } ) === 'NoopAnimations' ;
228
226
private _initialized = new Subject ( ) ;
227
+ private _cleanupDetach : ( ( ) => void ) | undefined ;
229
228
230
229
/** All of the defined select options. */
231
230
@ContentChildren ( MatOption , { descendants : true } ) options : QueryList < MatOption > ;
@@ -375,9 +374,6 @@ export class MatSelect
375
374
/** ID for the DOM node containing the select's value. */
376
375
_valueId = this . _idGenerator . getId ( 'mat-select-value-' ) ;
377
376
378
- /** Emits when the panel element is finished transforming in. */
379
- readonly _panelDoneAnimatingStream = new Subject < string > ( ) ;
380
-
381
377
/** Strategy that will be used to handle scrolling while the select panel is open. */
382
378
_scrollStrategy : ScrollStrategy ;
383
379
@@ -643,14 +639,6 @@ export class MatSelect
643
639
ngOnInit ( ) {
644
640
this . _selectionModel = new SelectionModel < MatOption > ( this . multiple ) ;
645
641
this . stateChanges . next ( ) ;
646
-
647
- // We need `distinctUntilChanged` here, because some browsers will
648
- // fire the animation end event twice for the same animation. See:
649
- // https://github.com/angular/angular/issues/24084
650
- this . _panelDoneAnimatingStream
651
- . pipe ( distinctUntilChanged ( ) , takeUntil ( this . _destroy ) )
652
- . subscribe ( ( ) => this . _panelDoneAnimating ( this . panelOpen ) ) ;
653
-
654
642
this . _viewportRuler
655
643
. change ( )
656
644
. pipe ( takeUntil ( this . _destroy ) )
@@ -727,6 +715,7 @@ export class MatSelect
727
715
}
728
716
729
717
ngOnDestroy ( ) {
718
+ this . _cleanupDetach ?.( ) ;
730
719
this . _keyManager ?. destroy ( ) ;
731
720
this . _destroy . next ( ) ;
732
721
this . _destroy . complete ( ) ;
@@ -752,15 +741,27 @@ export class MatSelect
752
741
this . _preferredOverlayOrigin = this . _parentFormField . getConnectedOverlayOrigin ( ) ;
753
742
}
754
743
744
+ this . _cleanupDetach ?.( ) ;
755
745
this . _overlayWidth = this . _getOverlayWidth ( this . _preferredOverlayOrigin ) ;
756
746
this . _applyModalPanelOwnership ( ) ;
757
747
this . _panelOpen = true ;
748
+ this . _overlayDir . positionChange . pipe ( take ( 1 ) ) . subscribe ( ( ) => {
749
+ this . _changeDetectorRef . detectChanges ( ) ;
750
+ this . _positioningSettled ( ) ;
751
+ } ) ;
752
+ this . _overlayDir . attachOverlay ( ) ;
758
753
this . _keyManager . withHorizontalOrientation ( null ) ;
759
754
this . _highlightCorrectOption ( ) ;
760
755
this . _changeDetectorRef . markForCheck ( ) ;
761
756
762
757
// Required for the MDC form field to pick up when the overlay has been opened.
763
758
this . stateChanges . next ( ) ;
759
+
760
+ // This usually fires at the end of the animation,
761
+ // but that won't happen if animations are disabled.
762
+ if ( this . _animationsDisabled ) {
763
+ this . openedChange . emit ( true ) ;
764
+ }
764
765
}
765
766
766
767
/**
@@ -832,6 +833,7 @@ export class MatSelect
832
833
close ( ) : void {
833
834
if ( this . _panelOpen ) {
834
835
this . _panelOpen = false ;
836
+ this . _exitAndDetach ( ) ;
835
837
this . _keyManager . withHorizontalOrientation ( this . _isRtl ( ) ? 'rtl' : 'ltr' ) ;
836
838
this . _changeDetectorRef . markForCheck ( ) ;
837
839
this . _onTouched ( ) ;
@@ -840,6 +842,40 @@ export class MatSelect
840
842
}
841
843
}
842
844
845
+ /** Triggers the exit animation and detaches the overlay at the end. */
846
+ private _exitAndDetach ( ) {
847
+ if ( this . _animationsDisabled ) {
848
+ this . _overlayDir . detachOverlay ( ) ;
849
+ return ;
850
+ }
851
+
852
+ this . _ngZone . runOutsideAngular ( ( ) => {
853
+ this . _cleanupDetach ?.( ) ;
854
+ this . _cleanupDetach = ( ) => {
855
+ cleanupEvent ( ) ;
856
+ clearTimeout ( exitFallbackTimer ) ;
857
+ this . _cleanupDetach = undefined ;
858
+ } ;
859
+
860
+ const panel : HTMLElement = this . panel . nativeElement ;
861
+ const cleanupEvent = this . _renderer . listen ( panel , 'animationend' , ( event : AnimationEvent ) => {
862
+ if ( event . animationName === '_mat-select-exit' ) {
863
+ this . _cleanupDetach ?.( ) ;
864
+ this . _overlayDir . detachOverlay ( ) ;
865
+ }
866
+ } ) ;
867
+
868
+ // Since closing the overlay depends on the animation, we have a fallback in case the panel
869
+ // doesn't animate. This can happen in some internal tests that do `* {animation: none}`.
870
+ const exitFallbackTimer = setTimeout ( ( ) => {
871
+ this . _cleanupDetach ?.( ) ;
872
+ this . _overlayDir . detachOverlay ( ) ;
873
+ } , 200 ) ;
874
+
875
+ panel . classList . add ( 'mat-select-panel-exit' ) ;
876
+ } ) ;
877
+ }
878
+
843
879
/**
844
880
* Sets the select's value. Part of the ControlValueAccessor interface
845
881
* required to integrate with Angular's core forms API.
@@ -970,7 +1006,7 @@ export class MatSelect
970
1006
const isArrowKey = keyCode === DOWN_ARROW || keyCode === UP_ARROW ;
971
1007
const isTyping = manager . isTyping ( ) ;
972
1008
973
- if ( isArrowKey && event . altKey ) {
1009
+ if ( ( isArrowKey && event . altKey ) || ( keyCode === ESCAPE && ! hasModifierKey ( event ) ) ) {
974
1010
// Close the select on ALT + arrow key to match the native <select>
975
1011
event . preventDefault ( ) ;
976
1012
this . close ( ) ;
@@ -1032,16 +1068,6 @@ export class MatSelect
1032
1068
}
1033
1069
}
1034
1070
1035
- /**
1036
- * Callback that is invoked when the overlay panel has been attached.
1037
- */
1038
- _onAttached ( ) : void {
1039
- this . _overlayDir . positionChange . pipe ( take ( 1 ) ) . subscribe ( ( ) => {
1040
- this . _changeDetectorRef . detectChanges ( ) ;
1041
- this . _positioningSettled ( ) ;
1042
- } ) ;
1043
- }
1044
-
1045
1071
/** Returns the theme to be used on the panel. */
1046
1072
_getPanelTheme ( ) : string {
1047
1073
return this . _parentFormField ? `mat-${ this . _parentFormField . color } ` : '' ;
@@ -1052,6 +1078,13 @@ export class MatSelect
1052
1078
return ! this . _selectionModel || this . _selectionModel . isEmpty ( ) ;
1053
1079
}
1054
1080
1081
+ /** Handles animation events from the panel. */
1082
+ protected _handleAnimationEndEvent ( event : AnimationEvent ) {
1083
+ if ( event . target === this . panel . nativeElement && event . animationName === '_mat-select-enter' ) {
1084
+ this . openedChange . emit ( true ) ;
1085
+ }
1086
+ }
1087
+
1055
1088
private _initializeSelection ( ) : void {
1056
1089
// Defer setting the value in order to avoid the "Expression
1057
1090
// has changed after it was checked" errors from Angular.
@@ -1356,7 +1389,7 @@ export class MatSelect
1356
1389
1357
1390
/** Whether the panel is allowed to open. */
1358
1391
protected _canOpen ( ) : boolean {
1359
- return ! this . _panelOpen && ! this . disabled && this . options ?. length > 0 ;
1392
+ return ! this . _panelOpen && ! this . disabled && this . options ?. length > 0 && ! ! this . _overlayDir ;
1360
1393
}
1361
1394
1362
1395
/** Focuses the select element. */
@@ -1400,11 +1433,6 @@ export class MatSelect
1400
1433
return value ;
1401
1434
}
1402
1435
1403
- /** Called when the overlay panel is done animating. */
1404
- protected _panelDoneAnimating ( isOpen : boolean ) {
1405
- this . openedChange . emit ( isOpen ) ;
1406
- }
1407
-
1408
1436
/**
1409
1437
* Implemented as part of MatFormFieldControl.
1410
1438
* @docs -private
0 commit comments