Skip to content

Commit

Permalink
fix(material/bottom-sheet): switch away from animations module (#30402)
Browse files Browse the repository at this point in the history
Reworks the bottom sheet so it doesn't use the animations module to animate itself.
  • Loading branch information
crisbeto authored Jan 28, 2025
1 parent de40f2e commit 0776acc
Show file tree
Hide file tree
Showing 6 changed files with 99 additions and 34 deletions.
11 changes: 7 additions & 4 deletions src/material/bottom-sheet/bottom-sheet-animations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,12 @@ import {
query,
animateChild,
} from '@angular/animations';
import {AnimationCurves, AnimationDurations} from '@angular/material/core';

/** Animations used by the Material bottom sheet. */
/**
* Animations used by the Material bottom sheet.
* @deprecated No longer used. Will be removed.
* @breaking-change 21.0.0
*/
export const matBottomSheetAnimations: {
readonly bottomSheetState: AnimationTriggerMetadata;
} = {
Expand All @@ -29,14 +32,14 @@ export const matBottomSheetAnimations: {
transition(
'visible => void, visible => hidden',
group([
animate(`${AnimationDurations.COMPLEX} ${AnimationCurves.ACCELERATION_CURVE}`),
animate('375ms cubic-bezier(0.4, 0, 1, 1)'),
query('@*', animateChild(), {optional: true}),
]),
),
transition(
'void => visible',
group([
animate(`${AnimationDurations.EXITING} ${AnimationCurves.DECELERATION_CURVE}`),
animate('195ms cubic-bezier(0, 0, 0.2, 1)'),
query('@*', animateChild(), {optional: true}),
]),
),
Expand Down
36 changes: 36 additions & 0 deletions src/material/bottom-sheet/bottom-sheet-container.scss
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,26 @@ $_width-increment: 64px;
$container-vertical-padding: 8px !default;
$container-horizontal-padding: 16px !default;

@keyframes _mat-bottom-sheet-enter {
from {
transform: translateY(100%);
}

to {
transform: none;
}
}

@keyframes _mat-bottom-sheet-exit {
from {
transform: none;
}

to {
transform: translateY(100%);
}
}

.mat-bottom-sheet-container {
@include elevation.elevation(16);
padding: $container-vertical-padding
Expand All @@ -21,6 +41,10 @@ $container-horizontal-padding: 16px !default;
max-height: 80vh;
overflow: auto;

// We don't use this, but it's useful for consumers to position
// elements (e.g. close buttons) inside the bottom sheet.
position: relative;

@include token-utils.use-tokens(
tokens-mat-bottom-sheet.$prefix, tokens-mat-bottom-sheet.get-token-slots()) {
@include token-utils.create-token-slot(background, container-background-color);
Expand All @@ -37,6 +61,18 @@ $container-horizontal-padding: 16px !default;
}
}

.mat-bottom-sheet-container-animations-enabled {
transform: translateY(100%);

&.mat-bottom-sheet-container-enter {
animation: _mat-bottom-sheet-enter 195ms cubic-bezier(0, 0, 0.2, 1) forwards;
}

&.mat-bottom-sheet-container-exit {
animation: _mat-bottom-sheet-exit 375ms cubic-bezier(0.4, 0, 1, 1) backwards;
}
}

// Applies a border radius to the bottom sheet. Should only be applied when it's not full-screen.
%_mat-bottom-sheet-container-border-radius {
@include token-utils.use-tokens(
Expand Down
55 changes: 41 additions & 14 deletions src/material/bottom-sheet/bottom-sheet-container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
* found in the LICENSE file at https://angular.dev/license
*/

import {AnimationEvent} from '@angular/animations';
import {CdkDialogContainer} from '@angular/cdk/dialog';
import {BreakpointObserver, Breakpoints} from '@angular/cdk/layout';
import {
ANIMATION_MODULE_TYPE,
ChangeDetectionStrategy,
Component,
EventEmitter,
Expand All @@ -18,9 +18,11 @@ import {
inject,
} from '@angular/core';
import {Subscription} from 'rxjs';
import {matBottomSheetAnimations} from './bottom-sheet-animations';
import {CdkPortalOutlet} from '@angular/cdk/portal';

const ENTER_ANIMATION = '_mat-bottom-sheet-enter';
const EXIT_ANIMATION = '_mat-bottom-sheet-exit';

/**
* Internal component that wraps user-provided bottom sheet content.
* @docs-private
Expand All @@ -35,27 +37,34 @@ import {CdkPortalOutlet} from '@angular/cdk/portal';
// tslint:disable-next-line:validate-decorators
changeDetection: ChangeDetectionStrategy.Default,
encapsulation: ViewEncapsulation.None,
animations: [matBottomSheetAnimations.bottomSheetState],
host: {
'class': 'mat-bottom-sheet-container',
'[class.mat-bottom-sheet-container-animations-enabled]': '!_animationsDisabled',
'[class.mat-bottom-sheet-container-enter]': '_animationState === "visible"',
'[class.mat-bottom-sheet-container-exit]': '_animationState === "hidden"',
'tabindex': '-1',
'[attr.role]': '_config.role',
'[attr.aria-modal]': '_config.ariaModal',
'[attr.aria-label]': '_config.ariaLabel',
'[@state]': '_animationState',
'(@state.start)': '_onAnimationStart($event)',
'(@state.done)': '_onAnimationDone($event)',
'(animationstart)': '_handleAnimationEvent(true, $event.animationName)',
'(animationend)': '_handleAnimationEvent(false, $event.animationName)',
'(animationcancel)': '_handleAnimationEvent(false, $event.animationName)',
},
imports: [CdkPortalOutlet],
})
export class MatBottomSheetContainer extends CdkDialogContainer implements OnDestroy {
private _breakpointSubscription: Subscription;
protected _animationsDisabled =
inject(ANIMATION_MODULE_TYPE, {optional: true}) === 'NoopAnimations';

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

/** Emits whenever the state of the animation changes. */
_animationStateChanged = new EventEmitter<AnimationEvent>();
_animationStateChanged = new EventEmitter<{
toState: 'visible' | 'hidden';
phase: 'start' | 'done';
}>();

/** Whether the component has been destroyed. */
private _destroyed: boolean;
Expand Down Expand Up @@ -93,14 +102,21 @@ export class MatBottomSheetContainer extends CdkDialogContainer implements OnDes
this._animationState = 'visible';
this._changeDetectorRef.markForCheck();
this._changeDetectorRef.detectChanges();
if (this._animationsDisabled) {
this._simulateAnimation(ENTER_ANIMATION);
}
}
}

/** Begin animation of the bottom sheet exiting from view. */
exit(): void {
if (!this._destroyed) {
this._elementRef.nativeElement.setAttribute('mat-exit', '');
this._animationState = 'hidden';
this._changeDetectorRef.markForCheck();
if (this._animationsDisabled) {
this._simulateAnimation(EXIT_ANIMATION);
}
}
}

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

_onAnimationDone(event: AnimationEvent) {
if (event.toState === 'visible') {
private _simulateAnimation(name: typeof ENTER_ANIMATION | typeof EXIT_ANIMATION) {
this._ngZone.run(() => {
this._handleAnimationEvent(true, name);
setTimeout(() => this._handleAnimationEvent(false, name));
});
}

protected _handleAnimationEvent(isStart: boolean, animationName: string) {
const isEnter = animationName === ENTER_ANIMATION;
const isExit = animationName === EXIT_ANIMATION;

if (isEnter) {
this._trapFocus();
}

this._animationStateChanged.emit(event);
}

_onAnimationStart(event: AnimationEvent) {
this._animationStateChanged.emit(event);
if (isEnter || isExit) {
this._animationStateChanged.emit({
toState: isEnter ? 'visible' : 'hidden',
phase: isStart ? 'start' : 'done',
});
}
}

protected override _captureInitialFocus(): void {}
Expand Down
13 changes: 5 additions & 8 deletions src/material/bottom-sheet/bottom-sheet-ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export class MatBottomSheetRef<T = any, R = any> {
// Emit when opening animation completes
containerInstance._animationStateChanged
.pipe(
filter(event => event.phaseName === 'done' && event.toState === 'visible'),
filter(event => event.phase === 'done' && event.toState === 'visible'),
take(1),
)
.subscribe(() => {
Expand All @@ -71,7 +71,7 @@ export class MatBottomSheetRef<T = any, R = any> {
// Dispose overlay when closing animation is complete
containerInstance._animationStateChanged
.pipe(
filter(event => event.phaseName === 'done' && event.toState === 'hidden'),
filter(event => event.phase === 'done' && event.toState === 'hidden'),
take(1),
)
.subscribe(() => {
Expand Down Expand Up @@ -109,19 +109,16 @@ export class MatBottomSheetRef<T = any, R = any> {
// Transition the backdrop in parallel to the bottom sheet.
this.containerInstance._animationStateChanged
.pipe(
filter(event => event.phaseName === 'start'),
filter(event => event.phase === 'start'),
take(1),
)
.subscribe(event => {
.subscribe(() => {
// The logic that disposes of the overlay depends on the exit animation completing, however
// it isn't guaranteed if the parent view is destroyed while it's running. Add a fallback
// timeout which will clean everything up if the animation hasn't fired within the specified
// amount of time plus 100ms. We don't need to run this outside the NgZone, because for the
// vast majority of cases the timeout will have been cleared before it has fired.
this._closeFallbackTimeout = setTimeout(() => {
this._ref.close(this._result);
}, event.totalTime + 100);

this._closeFallbackTimeout = setTimeout(() => this._ref.close(this._result), 500);
this._ref.overlayRef.detachBackdrop();
});

Expand Down
2 changes: 1 addition & 1 deletion src/material/bottom-sheet/testing/bottom-sheet-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {BottomSheetHarnessFilters} from './bottom-sheet-harness-filters';
export class MatBottomSheetHarness extends ContentContainerComponentHarness<string> {
// Developers can provide a custom component or template for the
// bottom sheet. The canonical parent is the ".mat-bottom-sheet-container".
static hostSelector = '.mat-bottom-sheet-container';
static hostSelector = '.mat-bottom-sheet-container:not([mat-exit])';

/**
* Gets a `HarnessPredicate` that can be used to search for a bottom sheet with
Expand Down
16 changes: 9 additions & 7 deletions tools/public_api_guard/material/bottom-sheet.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
```ts

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

// @public
// @public @deprecated
export const matBottomSheetAnimations: {
readonly bottomSheetState: AnimationTriggerMetadata;
};
Expand Down Expand Up @@ -76,18 +75,21 @@ export class MatBottomSheetConfig<D = any> {
// @public
export class MatBottomSheetContainer extends CdkDialogContainer implements OnDestroy {
constructor(...args: unknown[]);
// (undocumented)
protected _animationsDisabled: boolean;
_animationState: 'void' | 'visible' | 'hidden';
_animationStateChanged: EventEmitter<AnimationEvent_2>;
_animationStateChanged: EventEmitter<{
toState: "visible" | "hidden";
phase: "start" | "done";
}>;
// (undocumented)
protected _captureInitialFocus(): void;
enter(): void;
exit(): void;
// (undocumented)
ngOnDestroy(): void;
protected _handleAnimationEvent(isStart: boolean, animationName: string): void;
// (undocumented)
_onAnimationDone(event: AnimationEvent_2): void;
// (undocumented)
_onAnimationStart(event: AnimationEvent_2): void;
ngOnDestroy(): void;
// (undocumented)
static ɵcmp: i0.ɵɵComponentDeclaration<MatBottomSheetContainer, "mat-bottom-sheet-container", never, {}, {}, never, never, true, never>;
// (undocumented)
Expand Down

0 comments on commit 0776acc

Please sign in to comment.