Skip to content

Commit 0776acc

Browse files
authoredJan 28, 2025··
fix(material/bottom-sheet): switch away from animations module (#30402)
Reworks the bottom sheet so it doesn't use the animations module to animate itself.
1 parent de40f2e commit 0776acc

File tree

6 files changed

+99
-34
lines changed

6 files changed

+99
-34
lines changed
 

‎src/material/bottom-sheet/bottom-sheet-animations.ts

+7-4
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,12 @@ import {
1616
query,
1717
animateChild,
1818
} from '@angular/animations';
19-
import {AnimationCurves, AnimationDurations} from '@angular/material/core';
2019

21-
/** Animations used by the Material bottom sheet. */
20+
/**
21+
* Animations used by the Material bottom sheet.
22+
* @deprecated No longer used. Will be removed.
23+
* @breaking-change 21.0.0
24+
*/
2225
export const matBottomSheetAnimations: {
2326
readonly bottomSheetState: AnimationTriggerMetadata;
2427
} = {
@@ -29,14 +32,14 @@ export const matBottomSheetAnimations: {
2932
transition(
3033
'visible => void, visible => hidden',
3134
group([
32-
animate(`${AnimationDurations.COMPLEX} ${AnimationCurves.ACCELERATION_CURVE}`),
35+
animate('375ms cubic-bezier(0.4, 0, 1, 1)'),
3336
query('@*', animateChild(), {optional: true}),
3437
]),
3538
),
3639
transition(
3740
'void => visible',
3841
group([
39-
animate(`${AnimationDurations.EXITING} ${AnimationCurves.DECELERATION_CURVE}`),
42+
animate('195ms cubic-bezier(0, 0, 0.2, 1)'),
4043
query('@*', animateChild(), {optional: true}),
4144
]),
4245
),

‎src/material/bottom-sheet/bottom-sheet-container.scss

+36
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,26 @@ $_width-increment: 64px;
1010
$container-vertical-padding: 8px !default;
1111
$container-horizontal-padding: 16px !default;
1212

13+
@keyframes _mat-bottom-sheet-enter {
14+
from {
15+
transform: translateY(100%);
16+
}
17+
18+
to {
19+
transform: none;
20+
}
21+
}
22+
23+
@keyframes _mat-bottom-sheet-exit {
24+
from {
25+
transform: none;
26+
}
27+
28+
to {
29+
transform: translateY(100%);
30+
}
31+
}
32+
1333
.mat-bottom-sheet-container {
1434
@include elevation.elevation(16);
1535
padding: $container-vertical-padding
@@ -21,6 +41,10 @@ $container-horizontal-padding: 16px !default;
2141
max-height: 80vh;
2242
overflow: auto;
2343

44+
// We don't use this, but it's useful for consumers to position
45+
// elements (e.g. close buttons) inside the bottom sheet.
46+
position: relative;
47+
2448
@include token-utils.use-tokens(
2549
tokens-mat-bottom-sheet.$prefix, tokens-mat-bottom-sheet.get-token-slots()) {
2650
@include token-utils.create-token-slot(background, container-background-color);
@@ -37,6 +61,18 @@ $container-horizontal-padding: 16px !default;
3761
}
3862
}
3963

64+
.mat-bottom-sheet-container-animations-enabled {
65+
transform: translateY(100%);
66+
67+
&.mat-bottom-sheet-container-enter {
68+
animation: _mat-bottom-sheet-enter 195ms cubic-bezier(0, 0, 0.2, 1) forwards;
69+
}
70+
71+
&.mat-bottom-sheet-container-exit {
72+
animation: _mat-bottom-sheet-exit 375ms cubic-bezier(0.4, 0, 1, 1) backwards;
73+
}
74+
}
75+
4076
// Applies a border radius to the bottom sheet. Should only be applied when it's not full-screen.
4177
%_mat-bottom-sheet-container-border-radius {
4278
@include token-utils.use-tokens(

‎src/material/bottom-sheet/bottom-sheet-container.ts

+41-14
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import {AnimationEvent} from '@angular/animations';
109
import {CdkDialogContainer} from '@angular/cdk/dialog';
1110
import {BreakpointObserver, Breakpoints} from '@angular/cdk/layout';
1211
import {
12+
ANIMATION_MODULE_TYPE,
1313
ChangeDetectionStrategy,
1414
Component,
1515
EventEmitter,
@@ -18,9 +18,11 @@ import {
1818
inject,
1919
} from '@angular/core';
2020
import {Subscription} from 'rxjs';
21-
import {matBottomSheetAnimations} from './bottom-sheet-animations';
2221
import {CdkPortalOutlet} from '@angular/cdk/portal';
2322

23+
const ENTER_ANIMATION = '_mat-bottom-sheet-enter';
24+
const EXIT_ANIMATION = '_mat-bottom-sheet-exit';
25+
2426
/**
2527
* Internal component that wraps user-provided bottom sheet content.
2628
* @docs-private
@@ -35,27 +37,34 @@ import {CdkPortalOutlet} from '@angular/cdk/portal';
3537
// tslint:disable-next-line:validate-decorators
3638
changeDetection: ChangeDetectionStrategy.Default,
3739
encapsulation: ViewEncapsulation.None,
38-
animations: [matBottomSheetAnimations.bottomSheetState],
3940
host: {
4041
'class': 'mat-bottom-sheet-container',
42+
'[class.mat-bottom-sheet-container-animations-enabled]': '!_animationsDisabled',
43+
'[class.mat-bottom-sheet-container-enter]': '_animationState === "visible"',
44+
'[class.mat-bottom-sheet-container-exit]': '_animationState === "hidden"',
4145
'tabindex': '-1',
4246
'[attr.role]': '_config.role',
4347
'[attr.aria-modal]': '_config.ariaModal',
4448
'[attr.aria-label]': '_config.ariaLabel',
45-
'[@state]': '_animationState',
46-
'(@state.start)': '_onAnimationStart($event)',
47-
'(@state.done)': '_onAnimationDone($event)',
49+
'(animationstart)': '_handleAnimationEvent(true, $event.animationName)',
50+
'(animationend)': '_handleAnimationEvent(false, $event.animationName)',
51+
'(animationcancel)': '_handleAnimationEvent(false, $event.animationName)',
4852
},
4953
imports: [CdkPortalOutlet],
5054
})
5155
export class MatBottomSheetContainer extends CdkDialogContainer implements OnDestroy {
5256
private _breakpointSubscription: Subscription;
57+
protected _animationsDisabled =
58+
inject(ANIMATION_MODULE_TYPE, {optional: true}) === 'NoopAnimations';
5359

5460
/** The state of the bottom sheet animations. */
5561
_animationState: 'void' | 'visible' | 'hidden' = 'void';
5662

5763
/** Emits whenever the state of the animation changes. */
58-
_animationStateChanged = new EventEmitter<AnimationEvent>();
64+
_animationStateChanged = new EventEmitter<{
65+
toState: 'visible' | 'hidden';
66+
phase: 'start' | 'done';
67+
}>();
5968

6069
/** Whether the component has been destroyed. */
6170
private _destroyed: boolean;
@@ -93,14 +102,21 @@ export class MatBottomSheetContainer extends CdkDialogContainer implements OnDes
93102
this._animationState = 'visible';
94103
this._changeDetectorRef.markForCheck();
95104
this._changeDetectorRef.detectChanges();
105+
if (this._animationsDisabled) {
106+
this._simulateAnimation(ENTER_ANIMATION);
107+
}
96108
}
97109
}
98110

99111
/** Begin animation of the bottom sheet exiting from view. */
100112
exit(): void {
101113
if (!this._destroyed) {
114+
this._elementRef.nativeElement.setAttribute('mat-exit', '');
102115
this._animationState = 'hidden';
103116
this._changeDetectorRef.markForCheck();
117+
if (this._animationsDisabled) {
118+
this._simulateAnimation(EXIT_ANIMATION);
119+
}
104120
}
105121
}
106122

@@ -110,16 +126,27 @@ export class MatBottomSheetContainer extends CdkDialogContainer implements OnDes
110126
this._destroyed = true;
111127
}
112128

113-
_onAnimationDone(event: AnimationEvent) {
114-
if (event.toState === 'visible') {
129+
private _simulateAnimation(name: typeof ENTER_ANIMATION | typeof EXIT_ANIMATION) {
130+
this._ngZone.run(() => {
131+
this._handleAnimationEvent(true, name);
132+
setTimeout(() => this._handleAnimationEvent(false, name));
133+
});
134+
}
135+
136+
protected _handleAnimationEvent(isStart: boolean, animationName: string) {
137+
const isEnter = animationName === ENTER_ANIMATION;
138+
const isExit = animationName === EXIT_ANIMATION;
139+
140+
if (isEnter) {
115141
this._trapFocus();
116142
}
117143

118-
this._animationStateChanged.emit(event);
119-
}
120-
121-
_onAnimationStart(event: AnimationEvent) {
122-
this._animationStateChanged.emit(event);
144+
if (isEnter || isExit) {
145+
this._animationStateChanged.emit({
146+
toState: isEnter ? 'visible' : 'hidden',
147+
phase: isStart ? 'start' : 'done',
148+
});
149+
}
123150
}
124151

125152
protected override _captureInitialFocus(): void {}

‎src/material/bottom-sheet/bottom-sheet-ref.ts

+5-8
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ export class MatBottomSheetRef<T = any, R = any> {
6060
// Emit when opening animation completes
6161
containerInstance._animationStateChanged
6262
.pipe(
63-
filter(event => event.phaseName === 'done' && event.toState === 'visible'),
63+
filter(event => event.phase === 'done' && event.toState === 'visible'),
6464
take(1),
6565
)
6666
.subscribe(() => {
@@ -71,7 +71,7 @@ export class MatBottomSheetRef<T = any, R = any> {
7171
// Dispose overlay when closing animation is complete
7272
containerInstance._animationStateChanged
7373
.pipe(
74-
filter(event => event.phaseName === 'done' && event.toState === 'hidden'),
74+
filter(event => event.phase === 'done' && event.toState === 'hidden'),
7575
take(1),
7676
)
7777
.subscribe(() => {
@@ -109,19 +109,16 @@ export class MatBottomSheetRef<T = any, R = any> {
109109
// Transition the backdrop in parallel to the bottom sheet.
110110
this.containerInstance._animationStateChanged
111111
.pipe(
112-
filter(event => event.phaseName === 'start'),
112+
filter(event => event.phase === 'start'),
113113
take(1),
114114
)
115-
.subscribe(event => {
115+
.subscribe(() => {
116116
// The logic that disposes of the overlay depends on the exit animation completing, however
117117
// it isn't guaranteed if the parent view is destroyed while it's running. Add a fallback
118118
// timeout which will clean everything up if the animation hasn't fired within the specified
119119
// amount of time plus 100ms. We don't need to run this outside the NgZone, because for the
120120
// vast majority of cases the timeout will have been cleared before it has fired.
121-
this._closeFallbackTimeout = setTimeout(() => {
122-
this._ref.close(this._result);
123-
}, event.totalTime + 100);
124-
121+
this._closeFallbackTimeout = setTimeout(() => this._ref.close(this._result), 500);
125122
this._ref.overlayRef.detachBackdrop();
126123
});
127124

‎src/material/bottom-sheet/testing/bottom-sheet-harness.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {BottomSheetHarnessFilters} from './bottom-sheet-harness-filters';
1313
export class MatBottomSheetHarness extends ContentContainerComponentHarness<string> {
1414
// Developers can provide a custom component or template for the
1515
// bottom sheet. The canonical parent is the ".mat-bottom-sheet-container".
16-
static hostSelector = '.mat-bottom-sheet-container';
16+
static hostSelector = '.mat-bottom-sheet-container:not([mat-exit])';
1717

1818
/**
1919
* Gets a `HarnessPredicate` that can be used to search for a bottom sheet with

‎tools/public_api_guard/material/bottom-sheet.md

+9-7
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
55
```ts
66

7-
import { AnimationEvent as AnimationEvent_2 } from '@angular/animations';
87
import { AnimationTriggerMetadata } from '@angular/animations';
98
import { CdkDialogContainer } from '@angular/cdk/dialog';
109
import { ComponentRef } from '@angular/core';
@@ -48,7 +47,7 @@ export class MatBottomSheet implements OnDestroy {
4847
static ɵprov: i0.ɵɵInjectableDeclaration<MatBottomSheet>;
4948
}
5049

51-
// @public
50+
// @public @deprecated
5251
export const matBottomSheetAnimations: {
5352
readonly bottomSheetState: AnimationTriggerMetadata;
5453
};
@@ -76,18 +75,21 @@ export class MatBottomSheetConfig<D = any> {
7675
// @public
7776
export class MatBottomSheetContainer extends CdkDialogContainer implements OnDestroy {
7877
constructor(...args: unknown[]);
78+
// (undocumented)
79+
protected _animationsDisabled: boolean;
7980
_animationState: 'void' | 'visible' | 'hidden';
80-
_animationStateChanged: EventEmitter<AnimationEvent_2>;
81+
_animationStateChanged: EventEmitter<{
82+
toState: "visible" | "hidden";
83+
phase: "start" | "done";
84+
}>;
8185
// (undocumented)
8286
protected _captureInitialFocus(): void;
8387
enter(): void;
8488
exit(): void;
8589
// (undocumented)
86-
ngOnDestroy(): void;
90+
protected _handleAnimationEvent(isStart: boolean, animationName: string): void;
8791
// (undocumented)
88-
_onAnimationDone(event: AnimationEvent_2): void;
89-
// (undocumented)
90-
_onAnimationStart(event: AnimationEvent_2): void;
92+
ngOnDestroy(): void;
9193
// (undocumented)
9294
static ɵcmp: i0.ɵɵComponentDeclaration<MatBottomSheetContainer, "mat-bottom-sheet-container", never, {}, {}, never, never, true, never>;
9395
// (undocumented)

0 commit comments

Comments
 (0)
Please sign in to comment.