diff --git a/src/material/bottom-sheet/bottom-sheet-animations.ts b/src/material/bottom-sheet/bottom-sheet-animations.ts index 9ae2d7a57eb0..4b14404c7017 100644 --- a/src/material/bottom-sheet/bottom-sheet-animations.ts +++ b/src/material/bottom-sheet/bottom-sheet-animations.ts @@ -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; } = { @@ -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}), ]), ), diff --git a/src/material/bottom-sheet/bottom-sheet-container.scss b/src/material/bottom-sheet/bottom-sheet-container.scss index c76039aa1c3b..dd6628cd834d 100644 --- a/src/material/bottom-sheet/bottom-sheet-container.scss +++ b/src/material/bottom-sheet/bottom-sheet-container.scss @@ -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 @@ -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); @@ -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( diff --git a/src/material/bottom-sheet/bottom-sheet-container.ts b/src/material/bottom-sheet/bottom-sheet-container.ts index 6e21f49500cd..0dae8f85adff 100644 --- a/src/material/bottom-sheet/bottom-sheet-container.ts +++ b/src/material/bottom-sheet/bottom-sheet-container.ts @@ -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, @@ -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 @@ -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(); + _animationStateChanged = new EventEmitter<{ + toState: 'visible' | 'hidden'; + phase: 'start' | 'done'; + }>(); /** Whether the component has been destroyed. */ private _destroyed: boolean; @@ -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); + } } } @@ -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 {} diff --git a/src/material/bottom-sheet/bottom-sheet-ref.ts b/src/material/bottom-sheet/bottom-sheet-ref.ts index 093b4cd50dce..d52e20e18428 100644 --- a/src/material/bottom-sheet/bottom-sheet-ref.ts +++ b/src/material/bottom-sheet/bottom-sheet-ref.ts @@ -60,7 +60,7 @@ export class MatBottomSheetRef { // 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(() => { @@ -71,7 +71,7 @@ export class MatBottomSheetRef { // 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(() => { @@ -109,19 +109,16 @@ export class MatBottomSheetRef { // 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(); }); diff --git a/src/material/bottom-sheet/testing/bottom-sheet-harness.ts b/src/material/bottom-sheet/testing/bottom-sheet-harness.ts index 7610cab27160..d431988c66d0 100644 --- a/src/material/bottom-sheet/testing/bottom-sheet-harness.ts +++ b/src/material/bottom-sheet/testing/bottom-sheet-harness.ts @@ -13,7 +13,7 @@ import {BottomSheetHarnessFilters} from './bottom-sheet-harness-filters'; export class MatBottomSheetHarness extends ContentContainerComponentHarness { // 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 diff --git a/tools/public_api_guard/material/bottom-sheet.md b/tools/public_api_guard/material/bottom-sheet.md index da17b68955fd..31cae92392f5 100644 --- a/tools/public_api_guard/material/bottom-sheet.md +++ b/tools/public_api_guard/material/bottom-sheet.md @@ -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'; @@ -48,7 +47,7 @@ export class MatBottomSheet implements OnDestroy { static ɵprov: i0.ɵɵInjectableDeclaration; } -// @public +// @public @deprecated export const matBottomSheetAnimations: { readonly bottomSheetState: AnimationTriggerMetadata; }; @@ -76,18 +75,21 @@ export class MatBottomSheetConfig { // @public export class MatBottomSheetContainer extends CdkDialogContainer implements OnDestroy { constructor(...args: unknown[]); + // (undocumented) + protected _animationsDisabled: boolean; _animationState: 'void' | 'visible' | 'hidden'; - _animationStateChanged: EventEmitter; + _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; // (undocumented)