Skip to content

Commit

Permalink
fix(material/select): switch away from animations module
Browse files Browse the repository at this point in the history
Reworks the select so it doesn't depend on the animations module.
crisbeto committed Jan 23, 2025

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
1 parent 5a6b94d commit a44b347
Showing 5 changed files with 106 additions and 48 deletions.
2 changes: 2 additions & 0 deletions src/material/select/select-animations.ts
Original file line number Diff line number Diff line change
@@ -23,6 +23,8 @@ import {
*
* The values below match the implementation of the AngularJS Material mat-select animation.
* @docs-private
* @deprecated No longer used, will be removed.
* @breaking-change 21.0.0
*/
export const matSelectAnimations: {
/**
9 changes: 4 additions & 5 deletions src/material/select/select.html
Original file line number Diff line number Diff line change
@@ -33,27 +33,26 @@
cdkConnectedOverlayLockPosition
cdkConnectedOverlayHasBackdrop
cdkConnectedOverlayBackdropClass="cdk-overlay-transparent-backdrop"
[cdkConnectedOverlayDisableClose]="true"
[cdkConnectedOverlayPanelClass]="_overlayPanelClass"
[cdkConnectedOverlayScrollStrategy]="_scrollStrategy"
[cdkConnectedOverlayOrigin]="_preferredOverlayOrigin || fallbackOverlayOrigin"
[cdkConnectedOverlayOpen]="panelOpen"
[cdkConnectedOverlayPositions]="_positions"
[cdkConnectedOverlayWidth]="_overlayWidth"
(backdropClick)="close()"
(attach)="_onAttached()"
(detach)="close()">
(detach)="openedChange.emit(false)">
<div
#panel
role="listbox"
tabindex="-1"
class="mat-mdc-select-panel mdc-menu-surface mdc-menu-surface--open {{ _getPanelTheme() }}"
[class.mat-select-panel-animations-enabled]="!_animationsDisabled"
[attr.id]="id + '-panel'"
[attr.aria-multiselectable]="multiple"
[attr.aria-label]="ariaLabel || null"
[attr.aria-labelledby]="_getPanelAriaLabelledby()"
[ngClass]="panelClass"
[@transformPanel]="'showing'"
(@transformPanel.done)="_panelDoneAnimatingStream.next($event.toState)"
(animationend)="_handleAnimationEndEvent($event)"
(keydown)="_handleKeydown($event)">
<ng-content></ng-content>
</div>
29 changes: 29 additions & 0 deletions src/material/select/select.scss
Original file line number Diff line number Diff line change
@@ -13,6 +13,27 @@ $mat-select-placeholder-arrow-space: 2 *
$leading-width: 12px !default;
$scale: 0.75 !default;

@keyframes _mat-select-enter {
from {
opacity: 0;
transform: scaleY(0.8);
}

to {
opacity: 1;
transform: none;
}
}

@keyframes _mat-select-exit {
from {
opacity: 1;
}

to {
opacity: 0;
}
}

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

.mat-select-panel-animations-enabled {
animation: _mat-select-enter 120ms cubic-bezier(0, 0, 0.2, 1);

&.mat-select-panel-exit {
animation: _mat-select-exit 100ms linear;
}
}

.mat-mdc-select-placeholder {
// Delay the transition until the label has animated about a third of the way through, in
// order to prevent the placeholder from overlapping for a split second.
106 changes: 67 additions & 39 deletions src/material/select/select.ts
Original file line number Diff line number Diff line change
@@ -19,6 +19,7 @@ import {
A,
DOWN_ARROW,
ENTER,
ESCAPE,
hasModifierKey,
LEFT_ARROW,
RIGHT_ARROW,
@@ -58,6 +59,9 @@ import {
ViewChild,
ViewEncapsulation,
HostAttributeToken,
ANIMATION_MODULE_TYPE,
Renderer2,
NgZone,
} from '@angular/core';
import {
AbstractControl,
@@ -80,16 +84,7 @@ import {
} from '@angular/material/core';
import {MAT_FORM_FIELD, MatFormField, MatFormFieldControl} from '@angular/material/form-field';
import {defer, merge, Observable, Subject} from 'rxjs';
import {
distinctUntilChanged,
filter,
map,
startWith,
switchMap,
take,
takeUntil,
} from 'rxjs/operators';
import {matSelectAnimations} from './select-animations';
import {filter, map, startWith, switchMap, take, takeUntil} from 'rxjs/operators';
import {
getMatSelectDynamicMultipleError,
getMatSelectNonArrayValueError,
@@ -199,7 +194,6 @@ export class MatSelectChange {
'(focus)': '_onFocus()',
'(blur)': '_onBlur()',
},
animations: [matSelectAnimations.transformPanel],
providers: [
{provide: MatFormFieldControl, useExisting: MatSelect},
{provide: MAT_OPTION_PARENT_COMPONENT, useExisting: MatSelect},
@@ -221,11 +215,16 @@ export class MatSelect
readonly _elementRef = inject(ElementRef);
private _dir = inject(Directionality, {optional: true});
private _idGenerator = inject(_IdGenerator);
private _renderer = inject(Renderer2);
private _ngZone = inject(NgZone);
protected _parentFormField = inject<MatFormField>(MAT_FORM_FIELD, {optional: true});
ngControl = inject(NgControl, {self: true, optional: true})!;
private _liveAnnouncer = inject(LiveAnnouncer);
protected _defaultOptions = inject(MAT_SELECT_CONFIG, {optional: true});
protected _animationsDisabled =
inject(ANIMATION_MODULE_TYPE, {optional: true}) === 'NoopAnimations';
private _initialized = new Subject();
private _cleanupDetach: (() => void) | undefined;

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

/** Emits when the panel element is finished transforming in. */
readonly _panelDoneAnimatingStream = new Subject<string>();

/** Strategy that will be used to handle scrolling while the select panel is open. */
_scrollStrategy: ScrollStrategy;

@@ -643,14 +639,6 @@ export class MatSelect
ngOnInit() {
this._selectionModel = new SelectionModel<MatOption>(this.multiple);
this.stateChanges.next();

// We need `distinctUntilChanged` here, because some browsers will
// fire the animation end event twice for the same animation. See:
// https://github.com/angular/angular/issues/24084
this._panelDoneAnimatingStream
.pipe(distinctUntilChanged(), takeUntil(this._destroy))
.subscribe(() => this._panelDoneAnimating(this.panelOpen));

this._viewportRuler
.change()
.pipe(takeUntil(this._destroy))
@@ -727,6 +715,7 @@ export class MatSelect
}

ngOnDestroy() {
this._cleanupDetach?.();
this._keyManager?.destroy();
this._destroy.next();
this._destroy.complete();
@@ -752,15 +741,27 @@ export class MatSelect
this._preferredOverlayOrigin = this._parentFormField.getConnectedOverlayOrigin();
}

this._cleanupDetach?.();
this._overlayWidth = this._getOverlayWidth(this._preferredOverlayOrigin);
this._applyModalPanelOwnership();
this._panelOpen = true;
this._overlayDir.positionChange.pipe(take(1)).subscribe(() => {
this._changeDetectorRef.detectChanges();
this._positioningSettled();
});
this._overlayDir.attachOverlay();
this._keyManager.withHorizontalOrientation(null);
this._highlightCorrectOption();
this._changeDetectorRef.markForCheck();

// Required for the MDC form field to pick up when the overlay has been opened.
this.stateChanges.next();

// This usually fires at the end of the animation,
// but that won't happen if animations are disabled.
if (this._animationsDisabled) {
this.openedChange.emit(true);
}
}

/**
@@ -832,6 +833,7 @@ export class MatSelect
close(): void {
if (this._panelOpen) {
this._panelOpen = false;
this._exitAndDetach();
this._keyManager.withHorizontalOrientation(this._isRtl() ? 'rtl' : 'ltr');
this._changeDetectorRef.markForCheck();
this._onTouched();
@@ -840,6 +842,40 @@ export class MatSelect
}
}

/** Triggers the exit animation and detaches the overlay at the end. */
private _exitAndDetach() {
if (this._animationsDisabled) {
this._overlayDir.detachOverlay();
return;
}

this._ngZone.runOutsideAngular(() => {
this._cleanupDetach?.();
this._cleanupDetach = () => {
cleanupEvent();
clearTimeout(exitFallbackTimer);
this._cleanupDetach = undefined;
};

const panel: HTMLElement = this.panel.nativeElement;
const cleanupEvent = this._renderer.listen(panel, 'animationend', (event: AnimationEvent) => {
if (event.animationName === '_mat-select-exit') {
this._cleanupDetach?.();
this._overlayDir.detachOverlay();
}
});

// Since closing the overlay depends on the animation, we have a fallback in case the panel
// doesn't animate. This can happen in some internal tests that do `* {animation: none}`.
const exitFallbackTimer = setTimeout(() => {
this._cleanupDetach?.();
this._overlayDir.detachOverlay();
}, 200);

panel.classList.add('mat-select-panel-exit');
});
}

/**
* Sets the select's value. Part of the ControlValueAccessor interface
* required to integrate with Angular's core forms API.
@@ -970,7 +1006,7 @@ export class MatSelect
const isArrowKey = keyCode === DOWN_ARROW || keyCode === UP_ARROW;
const isTyping = manager.isTyping();

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

/**
* Callback that is invoked when the overlay panel has been attached.
*/
_onAttached(): void {
this._overlayDir.positionChange.pipe(take(1)).subscribe(() => {
this._changeDetectorRef.detectChanges();
this._positioningSettled();
});
}

/** Returns the theme to be used on the panel. */
_getPanelTheme(): string {
return this._parentFormField ? `mat-${this._parentFormField.color}` : '';
@@ -1052,6 +1078,13 @@ export class MatSelect
return !this._selectionModel || this._selectionModel.isEmpty();
}

/** Handles animation events from the panel. */
protected _handleAnimationEndEvent(event: AnimationEvent) {
if (event.target === this.panel.nativeElement && event.animationName === '_mat-select-enter') {
this.openedChange.emit(true);
}
}

private _initializeSelection(): void {
// Defer setting the value in order to avoid the "Expression
// has changed after it was checked" errors from Angular.
@@ -1356,7 +1389,7 @@ export class MatSelect

/** Whether the panel is allowed to open. */
protected _canOpen(): boolean {
return !this._panelOpen && !this.disabled && this.options?.length > 0;
return !this._panelOpen && !this.disabled && this.options?.length > 0 && !!this._overlayDir;
}

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

/** Called when the overlay panel is done animating. */
protected _panelDoneAnimating(isOpen: boolean) {
this.openedChange.emit(isOpen);
}

/**
* Implemented as part of MatFormFieldControl.
* @docs-private
8 changes: 4 additions & 4 deletions tools/public_api_guard/material/select.md
Original file line number Diff line number Diff line change
@@ -81,6 +81,8 @@ export { MatPrefix }
// @public (undocumented)
export class MatSelect implements AfterContentInit, OnChanges, OnDestroy, OnInit, DoCheck, ControlValueAccessor, MatFormFieldControl<any> {
constructor(...args: unknown[]);
// (undocumented)
protected _animationsDisabled: boolean;
ariaLabel: string;
ariaLabelledby: string;
protected _canOpen(): boolean;
@@ -112,6 +114,7 @@ export class MatSelect implements AfterContentInit, OnChanges, OnDestroy, OnInit
_getAriaActiveDescendant(): string | null;
_getPanelAriaLabelledby(): string | null;
_getPanelTheme(): string;
protected _handleAnimationEndEvent(event: AnimationEvent): void;
_handleKeydown(event: KeyboardEvent): void;
get hideSingleSelectionIndicator(): boolean;
set hideSingleSelectionIndicator(value: boolean);
@@ -151,7 +154,6 @@ export class MatSelect implements AfterContentInit, OnChanges, OnDestroy, OnInit
ngOnDestroy(): void;
// (undocumented)
ngOnInit(): void;
_onAttached(): void;
_onBlur(): void;
_onChange: (value: any) => void;
onContainerClick(): void;
@@ -172,8 +174,6 @@ export class MatSelect implements AfterContentInit, OnChanges, OnDestroy, OnInit
panelClass: string | string[] | Set<string> | {
[key: string]: any;
};
protected _panelDoneAnimating(isOpen: boolean): void;
readonly _panelDoneAnimatingStream: Subject<string>;
get panelOpen(): boolean;
panelWidth: string | number | null;
// (undocumented)
@@ -217,7 +217,7 @@ export class MatSelect implements AfterContentInit, OnChanges, OnDestroy, OnInit
static ɵfac: i0.ɵɵFactoryDeclaration<MatSelect, never>;
}

// @public
// @public @deprecated
export const matSelectAnimations: {
readonly transformPanelWrap: AnimationTriggerMetadata;
readonly transformPanel: AnimationTriggerMetadata;

0 comments on commit a44b347

Please sign in to comment.