Skip to content

Commit a44b347

Browse files
committed
fix(material/select): switch away from animations module
Reworks the select so it doesn't depend on the animations module.
1 parent 5a6b94d commit a44b347

File tree

5 files changed

+106
-48
lines changed

5 files changed

+106
-48
lines changed

src/material/select/select-animations.ts

+2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import {
2323
*
2424
* The values below match the implementation of the AngularJS Material mat-select animation.
2525
* @docs-private
26+
* @deprecated No longer used, will be removed.
27+
* @breaking-change 21.0.0
2628
*/
2729
export const matSelectAnimations: {
2830
/**

src/material/select/select.html

+4-5
Original file line numberDiff line numberDiff line change
@@ -33,27 +33,26 @@
3333
cdkConnectedOverlayLockPosition
3434
cdkConnectedOverlayHasBackdrop
3535
cdkConnectedOverlayBackdropClass="cdk-overlay-transparent-backdrop"
36+
[cdkConnectedOverlayDisableClose]="true"
3637
[cdkConnectedOverlayPanelClass]="_overlayPanelClass"
3738
[cdkConnectedOverlayScrollStrategy]="_scrollStrategy"
3839
[cdkConnectedOverlayOrigin]="_preferredOverlayOrigin || fallbackOverlayOrigin"
39-
[cdkConnectedOverlayOpen]="panelOpen"
4040
[cdkConnectedOverlayPositions]="_positions"
4141
[cdkConnectedOverlayWidth]="_overlayWidth"
4242
(backdropClick)="close()"
43-
(attach)="_onAttached()"
44-
(detach)="close()">
43+
(detach)="openedChange.emit(false)">
4544
<div
4645
#panel
4746
role="listbox"
4847
tabindex="-1"
4948
class="mat-mdc-select-panel mdc-menu-surface mdc-menu-surface--open {{ _getPanelTheme() }}"
49+
[class.mat-select-panel-animations-enabled]="!_animationsDisabled"
5050
[attr.id]="id + '-panel'"
5151
[attr.aria-multiselectable]="multiple"
5252
[attr.aria-label]="ariaLabel || null"
5353
[attr.aria-labelledby]="_getPanelAriaLabelledby()"
5454
[ngClass]="panelClass"
55-
[@transformPanel]="'showing'"
56-
(@transformPanel.done)="_panelDoneAnimatingStream.next($event.toState)"
55+
(animationend)="_handleAnimationEndEvent($event)"
5756
(keydown)="_handleKeydown($event)">
5857
<ng-content></ng-content>
5958
</div>

src/material/select/select.scss

+29
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,27 @@ $mat-select-placeholder-arrow-space: 2 *
1313
$leading-width: 12px !default;
1414
$scale: 0.75 !default;
1515

16+
@keyframes _mat-select-enter {
17+
from {
18+
opacity: 0;
19+
transform: scaleY(0.8);
20+
}
21+
22+
to {
23+
opacity: 1;
24+
transform: none;
25+
}
26+
}
27+
28+
@keyframes _mat-select-exit {
29+
from {
30+
opacity: 1;
31+
}
32+
33+
to {
34+
opacity: 0;
35+
}
36+
}
1637

1738
.mat-mdc-select {
1839
display: inline-block;
@@ -173,6 +194,14 @@ div.mat-mdc-select-panel {
173194
}
174195
}
175196

197+
.mat-select-panel-animations-enabled {
198+
animation: _mat-select-enter 120ms cubic-bezier(0, 0, 0.2, 1);
199+
200+
&.mat-select-panel-exit {
201+
animation: _mat-select-exit 100ms linear;
202+
}
203+
}
204+
176205
.mat-mdc-select-placeholder {
177206
// Delay the transition until the label has animated about a third of the way through, in
178207
// order to prevent the placeholder from overlapping for a split second.

src/material/select/select.ts

+67-39
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
A,
2020
DOWN_ARROW,
2121
ENTER,
22+
ESCAPE,
2223
hasModifierKey,
2324
LEFT_ARROW,
2425
RIGHT_ARROW,
@@ -58,6 +59,9 @@ import {
5859
ViewChild,
5960
ViewEncapsulation,
6061
HostAttributeToken,
62+
ANIMATION_MODULE_TYPE,
63+
Renderer2,
64+
NgZone,
6165
} from '@angular/core';
6266
import {
6367
AbstractControl,
@@ -80,16 +84,7 @@ import {
8084
} from '@angular/material/core';
8185
import {MAT_FORM_FIELD, MatFormField, MatFormFieldControl} from '@angular/material/form-field';
8286
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';
9388
import {
9489
getMatSelectDynamicMultipleError,
9590
getMatSelectNonArrayValueError,
@@ -199,7 +194,6 @@ export class MatSelectChange {
199194
'(focus)': '_onFocus()',
200195
'(blur)': '_onBlur()',
201196
},
202-
animations: [matSelectAnimations.transformPanel],
203197
providers: [
204198
{provide: MatFormFieldControl, useExisting: MatSelect},
205199
{provide: MAT_OPTION_PARENT_COMPONENT, useExisting: MatSelect},
@@ -221,11 +215,16 @@ export class MatSelect
221215
readonly _elementRef = inject(ElementRef);
222216
private _dir = inject(Directionality, {optional: true});
223217
private _idGenerator = inject(_IdGenerator);
218+
private _renderer = inject(Renderer2);
219+
private _ngZone = inject(NgZone);
224220
protected _parentFormField = inject<MatFormField>(MAT_FORM_FIELD, {optional: true});
225221
ngControl = inject(NgControl, {self: true, optional: true})!;
226222
private _liveAnnouncer = inject(LiveAnnouncer);
227223
protected _defaultOptions = inject(MAT_SELECT_CONFIG, {optional: true});
224+
protected _animationsDisabled =
225+
inject(ANIMATION_MODULE_TYPE, {optional: true}) === 'NoopAnimations';
228226
private _initialized = new Subject();
227+
private _cleanupDetach: (() => void) | undefined;
229228

230229
/** All of the defined select options. */
231230
@ContentChildren(MatOption, {descendants: true}) options: QueryList<MatOption>;
@@ -375,9 +374,6 @@ export class MatSelect
375374
/** ID for the DOM node containing the select's value. */
376375
_valueId = this._idGenerator.getId('mat-select-value-');
377376

378-
/** Emits when the panel element is finished transforming in. */
379-
readonly _panelDoneAnimatingStream = new Subject<string>();
380-
381377
/** Strategy that will be used to handle scrolling while the select panel is open. */
382378
_scrollStrategy: ScrollStrategy;
383379

@@ -643,14 +639,6 @@ export class MatSelect
643639
ngOnInit() {
644640
this._selectionModel = new SelectionModel<MatOption>(this.multiple);
645641
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-
654642
this._viewportRuler
655643
.change()
656644
.pipe(takeUntil(this._destroy))
@@ -727,6 +715,7 @@ export class MatSelect
727715
}
728716

729717
ngOnDestroy() {
718+
this._cleanupDetach?.();
730719
this._keyManager?.destroy();
731720
this._destroy.next();
732721
this._destroy.complete();
@@ -752,15 +741,27 @@ export class MatSelect
752741
this._preferredOverlayOrigin = this._parentFormField.getConnectedOverlayOrigin();
753742
}
754743

744+
this._cleanupDetach?.();
755745
this._overlayWidth = this._getOverlayWidth(this._preferredOverlayOrigin);
756746
this._applyModalPanelOwnership();
757747
this._panelOpen = true;
748+
this._overlayDir.positionChange.pipe(take(1)).subscribe(() => {
749+
this._changeDetectorRef.detectChanges();
750+
this._positioningSettled();
751+
});
752+
this._overlayDir.attachOverlay();
758753
this._keyManager.withHorizontalOrientation(null);
759754
this._highlightCorrectOption();
760755
this._changeDetectorRef.markForCheck();
761756

762757
// Required for the MDC form field to pick up when the overlay has been opened.
763758
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+
}
764765
}
765766

766767
/**
@@ -832,6 +833,7 @@ export class MatSelect
832833
close(): void {
833834
if (this._panelOpen) {
834835
this._panelOpen = false;
836+
this._exitAndDetach();
835837
this._keyManager.withHorizontalOrientation(this._isRtl() ? 'rtl' : 'ltr');
836838
this._changeDetectorRef.markForCheck();
837839
this._onTouched();
@@ -840,6 +842,40 @@ export class MatSelect
840842
}
841843
}
842844

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+
843879
/**
844880
* Sets the select's value. Part of the ControlValueAccessor interface
845881
* required to integrate with Angular's core forms API.
@@ -970,7 +1006,7 @@ export class MatSelect
9701006
const isArrowKey = keyCode === DOWN_ARROW || keyCode === UP_ARROW;
9711007
const isTyping = manager.isTyping();
9721008

973-
if (isArrowKey && event.altKey) {
1009+
if ((isArrowKey && event.altKey) || (keyCode === ESCAPE && !hasModifierKey(event))) {
9741010
// Close the select on ALT + arrow key to match the native <select>
9751011
event.preventDefault();
9761012
this.close();
@@ -1032,16 +1068,6 @@ export class MatSelect
10321068
}
10331069
}
10341070

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-
10451071
/** Returns the theme to be used on the panel. */
10461072
_getPanelTheme(): string {
10471073
return this._parentFormField ? `mat-${this._parentFormField.color}` : '';
@@ -1052,6 +1078,13 @@ export class MatSelect
10521078
return !this._selectionModel || this._selectionModel.isEmpty();
10531079
}
10541080

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+
10551088
private _initializeSelection(): void {
10561089
// Defer setting the value in order to avoid the "Expression
10571090
// has changed after it was checked" errors from Angular.
@@ -1356,7 +1389,7 @@ export class MatSelect
13561389

13571390
/** Whether the panel is allowed to open. */
13581391
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;
13601393
}
13611394

13621395
/** Focuses the select element. */
@@ -1400,11 +1433,6 @@ export class MatSelect
14001433
return value;
14011434
}
14021435

1403-
/** Called when the overlay panel is done animating. */
1404-
protected _panelDoneAnimating(isOpen: boolean) {
1405-
this.openedChange.emit(isOpen);
1406-
}
1407-
14081436
/**
14091437
* Implemented as part of MatFormFieldControl.
14101438
* @docs-private

tools/public_api_guard/material/select.md

+4-4
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ export { MatPrefix }
8181
// @public (undocumented)
8282
export class MatSelect implements AfterContentInit, OnChanges, OnDestroy, OnInit, DoCheck, ControlValueAccessor, MatFormFieldControl<any> {
8383
constructor(...args: unknown[]);
84+
// (undocumented)
85+
protected _animationsDisabled: boolean;
8486
ariaLabel: string;
8587
ariaLabelledby: string;
8688
protected _canOpen(): boolean;
@@ -112,6 +114,7 @@ export class MatSelect implements AfterContentInit, OnChanges, OnDestroy, OnInit
112114
_getAriaActiveDescendant(): string | null;
113115
_getPanelAriaLabelledby(): string | null;
114116
_getPanelTheme(): string;
117+
protected _handleAnimationEndEvent(event: AnimationEvent): void;
115118
_handleKeydown(event: KeyboardEvent): void;
116119
get hideSingleSelectionIndicator(): boolean;
117120
set hideSingleSelectionIndicator(value: boolean);
@@ -151,7 +154,6 @@ export class MatSelect implements AfterContentInit, OnChanges, OnDestroy, OnInit
151154
ngOnDestroy(): void;
152155
// (undocumented)
153156
ngOnInit(): void;
154-
_onAttached(): void;
155157
_onBlur(): void;
156158
_onChange: (value: any) => void;
157159
onContainerClick(): void;
@@ -172,8 +174,6 @@ export class MatSelect implements AfterContentInit, OnChanges, OnDestroy, OnInit
172174
panelClass: string | string[] | Set<string> | {
173175
[key: string]: any;
174176
};
175-
protected _panelDoneAnimating(isOpen: boolean): void;
176-
readonly _panelDoneAnimatingStream: Subject<string>;
177177
get panelOpen(): boolean;
178178
panelWidth: string | number | null;
179179
// (undocumented)
@@ -217,7 +217,7 @@ export class MatSelect implements AfterContentInit, OnChanges, OnDestroy, OnInit
217217
static ɵfac: i0.ɵɵFactoryDeclaration<MatSelect, never>;
218218
}
219219

220-
// @public
220+
// @public @deprecated
221221
export const matSelectAnimations: {
222222
readonly transformPanelWrap: AnimationTriggerMetadata;
223223
readonly transformPanel: AnimationTriggerMetadata;

0 commit comments

Comments
 (0)