Skip to content

fix(material/expansion): switch away from animations module #30119

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Dec 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/material/expansion/expansion-animations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ export const EXPANSION_PANEL_ANIMATION_TIMING = '225ms cubic-bezier(0.4,0.0,0.2,
* Angular Bug: https://github.com/angular/angular/issues/18847
*
* @docs-private
* @deprecated No longer being used, to be removed.
* @breaking-change 21.0.0
*/
export const matExpansionAnimations: {
readonly indicatorRotate: AnimationTriggerMetadata;
Expand Down
2 changes: 1 addition & 1 deletion src/material/expansion/expansion-panel-header.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
</span>

@if (_showToggle()) {
<span [@indicatorRotate]="_getExpandedState()" class="mat-expansion-indicator">
<span class="mat-expansion-indicator">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 -960 960 960"
Expand Down
13 changes: 12 additions & 1 deletion src/material/expansion/expansion-panel-header.scss
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@
align-items: center;
padding: 0 24px;
border-radius: inherit;
transition: height expansion-variables.$header-transition;

.mat-expansion-panel-animations-enabled & {
transition: height expansion-variables.$header-transition;
}

@include token-utils.use-tokens(
tokens-mat-expansion.$prefix, tokens-mat-expansion.get-token-slots()) {
Expand Down Expand Up @@ -141,6 +144,14 @@
// Creates the expansion indicator arrow. Done using ::after
// rather than having additional nodes in the template.
.mat-expansion-indicator {
.mat-expansion-panel-animations-enabled & {
transition: transform 225ms cubic-bezier(0.4, 0, 0.2, 1);
}

.mat-expansion-panel-header.mat-expanded & {
transform: rotate(180deg);
}

&::after {
border-style: solid;
border-width: 0 2px 2px 0;
Expand Down
5 changes: 0 additions & 5 deletions src/material/expansion/expansion-panel-header.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,12 @@ import {
numberAttribute,
OnDestroy,
ViewEncapsulation,
ANIMATION_MODULE_TYPE,
inject,
HostAttributeToken,
} from '@angular/core';
import {EMPTY, merge, Subscription} from 'rxjs';
import {filter} from 'rxjs/operators';
import {MatAccordionTogglePosition} from './accordion-base';
import {matExpansionAnimations} from './expansion-animations';
import {
MatExpansionPanel,
MatExpansionPanelDefaultOptions,
Expand All @@ -44,7 +42,6 @@ import {_StructuralStylesLoader} from '@angular/material/core';
templateUrl: 'expansion-panel-header.html',
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
animations: [matExpansionAnimations.indicatorRotate],
host: {
'class': 'mat-expansion-panel-header mat-focus-indicator',
'role': 'button',
Expand All @@ -56,7 +53,6 @@ import {_StructuralStylesLoader} from '@angular/material/core';
'[class.mat-expanded]': '_isExpanded()',
'[class.mat-expansion-toggle-indicator-after]': `_getTogglePosition() === 'after'`,
'[class.mat-expansion-toggle-indicator-before]': `_getTogglePosition() === 'before'`,
'[class._mat-animation-noopable]': '_animationMode === "NoopAnimations"',
'[style.height]': '_getHeaderHeight()',
'(click)': '_toggle()',
'(keydown)': '_keydown($event)',
Expand All @@ -67,7 +63,6 @@ export class MatExpansionPanelHeader implements AfterViewInit, OnDestroy, Focusa
private _element = inject(ElementRef);
private _focusMonitor = inject(FocusMonitor);
private _changeDetectorRef = inject(ChangeDetectorRef);
_animationMode = inject(ANIMATION_MODULE_TYPE, {optional: true});

private _parentChangeSubscription = Subscription.EMPTY;

Expand Down
23 changes: 11 additions & 12 deletions src/material/expansion/expansion-panel.html
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
<ng-content select="mat-expansion-panel-header"></ng-content>
<div class="mat-expansion-panel-content"
role="region"
[@bodyExpansion]="_getExpandedState()"
(@bodyExpansion.start)="_animationStarted($event)"
(@bodyExpansion.done)="_animationDone($event)"
[attr.aria-labelledby]="_headerId"
[id]="id"
#body>
<div class="mat-expansion-panel-body">
<ng-content></ng-content>
<ng-template [cdkPortalOutlet]="_portal"></ng-template>
<div class="mat-expansion-panel-content-wrapper" [attr.inert]="expanded ? null : ''" #bodyWrapper>
<div class="mat-expansion-panel-content"
role="region"
[attr.aria-labelledby]="_headerId"
[id]="id"
#body>
<div class="mat-expansion-panel-body">
<ng-content></ng-content>
<ng-template [cdkPortalOutlet]="_portal"></ng-template>
</div>
<ng-content select="mat-action-row"></ng-content>
</div>
<ng-content select="mat-action-row"></ng-content>
</div>
65 changes: 49 additions & 16 deletions src/material/expansion/expansion-panel.scss
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@
display: block;
margin: 0;
overflow: hidden;
transition: margin 225ms variables.$fast-out-slow-in-timing-function,
elevation.private-transition-property-value();

&.mat-expansion-panel-animations-enabled {
transition: margin 225ms variables.$fast-out-slow-in-timing-function,
elevation.private-transition-property-value();
}

// Required so that the `box-shadow` works after the
// focus indicator relatively positions the header.
Expand Down Expand Up @@ -48,18 +51,58 @@
@include cdk.high-contrast {
outline: solid 1px;
}
}

&.ng-animate-disabled,
.ng-animate-disabled &,
&._mat-animation-noopable {
transition: none;
.mat-expansion-panel-content-wrapper {
// Note: we can't use `overflow: hidden` here, because it can clip content with
// ripples or box shadows. Instead we transition the `visibility` below.
// Based on https://css-tricks.com/css-grid-can-do-auto-height-transitions.
display: grid;
grid-template-rows: 0fr;
grid-template-columns: 100%;

.mat-expansion-panel-animations-enabled & {
transition: grid-template-rows 225ms cubic-bezier(0.4, 0, 0.2, 1);
}

.mat-expansion-panel.mat-expanded > & {
grid-template-rows: 1fr;
}

// All the browsers we support have support for `grid` as well, but
// given that these styles are load-bearing for the expansion panel,
// we have a fallback to `height` which doesn't animate, just in case.
// stylelint-disable material/no-prefixes
@supports not (grid-template-rows: 0fr) {
height: 0;

.mat-expansion-panel.mat-expanded > & {
height: auto;
}
}
// stylelint-enable material/no-prefixes
}

.mat-expansion-panel-content {
display: flex;
flex-direction: column;
overflow: visible;
min-height: 0;

// The visibility here serves two purposes:
// 1. Hiding content from assistive technology.
// 2. Hiding any content that might be overflowing.
visibility: hidden;

.mat-expansion-panel-animations-enabled & {
// The duration here is slightly lower so the content
// goes away quicker than the collapse transition.
transition: visibility 190ms linear;
}

.mat-expansion-panel.mat-expanded > .mat-expansion-panel-content-wrapper > & {
visibility: visible;
}

@include token-utils.use-tokens(
tokens-mat-expansion.$prefix, tokens-mat-expansion.get-token-slots()) {
Expand All @@ -69,16 +112,6 @@
@include token-utils.create-token-slot(line-height, container-text-line-height);
@include token-utils.create-token-slot(letter-spacing, container-text-tracking);
}

// Usually the `visibility: hidden` added by the animation is enough to prevent focus from
// entering the collapsed content, but children with their own `visibility` can override it.
// In other components we set a `display: none` at the root to stop focus from reaching the
// elements, however we can't do that here, because the content can determine the width
// of an expansion panel. The most practical fallback is to use `!important` to override
// any custom visibility.
&[style*='visibility: hidden'] * {
visibility: hidden !important;
}
}

.mat-expansion-panel-body {
Expand Down
75 changes: 40 additions & 35 deletions src/material/expansion/expansion-panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
* found in the LICENSE file at https://angular.dev/license
*/

import {AnimationEvent} from '@angular/animations';
import {CdkAccordionItem} from '@angular/cdk/accordion';
import {UniqueSelectionDispatcher} from '@angular/cdk/collections';
import {CdkPortalOutlet, TemplatePortal} from '@angular/cdk/portal';
Expand All @@ -31,12 +30,12 @@ import {
booleanAttribute,
ANIMATION_MODULE_TYPE,
inject,
NgZone,
} from '@angular/core';
import {_IdGenerator} from '@angular/cdk/a11y';
import {Subject} from 'rxjs';
import {filter, startWith, take} from 'rxjs/operators';
import {MatAccordionBase, MatAccordionTogglePosition, MAT_ACCORDION} from './accordion-base';
import {matExpansionAnimations} from './expansion-animations';
import {MAT_EXPANSION_PANEL} from './expansion-panel-base';
import {MatExpansionPanelContent} from './expansion-panel-content';

Expand Down Expand Up @@ -76,7 +75,6 @@ export const MAT_EXPANSION_PANEL_DEFAULT_OPTIONS =
templateUrl: 'expansion-panel.html',
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
animations: [matExpansionAnimations.bodyExpansion],
providers: [
// Provide MatAccordion as undefined to prevent nested expansion panels from registering
// to the same accordion.
Expand All @@ -86,7 +84,6 @@ export const MAT_EXPANSION_PANEL_DEFAULT_OPTIONS =
host: {
'class': 'mat-expansion-panel',
'[class.mat-expanded]': 'expanded',
'[class._mat-animation-noopable]': '_animationsDisabled',
'[class.mat-expansion-panel-spacing]': '_hasSpacing()',
},
imports: [CdkPortalOutlet],
Expand All @@ -96,10 +93,11 @@ export class MatExpansionPanel
implements AfterContentInit, OnChanges, OnDestroy
{
private _viewContainerRef = inject(ViewContainerRef);
_animationMode = inject(ANIMATION_MODULE_TYPE, {optional: true});

protected _animationsDisabled: boolean;
private readonly _animationsDisabled =
inject(ANIMATION_MODULE_TYPE, {optional: true}) === 'NoopAnimations';
private _document = inject(DOCUMENT);
private _ngZone = inject(NgZone);
private _elementRef = inject<ElementRef<HTMLElement>>(ElementRef);

/** Whether the toggle indicator should be hidden. */
@Input({transform: booleanAttribute})
Expand Down Expand Up @@ -139,6 +137,10 @@ export class MatExpansionPanel
/** Element containing the panel's user-provided content. */
@ViewChild('body') _body: ElementRef<HTMLElement>;

/** Element wrapping the panel body. */
@ViewChild('bodyWrapper')
protected _bodyWrapper: ElementRef<HTMLElement> | undefined;

/** Portal holding the user's content. */
_portal: TemplatePortal;

Expand All @@ -156,7 +158,6 @@ export class MatExpansionPanel
);

this._expansionDispatcher = inject(UniqueSelectionDispatcher);
this._animationsDisabled = this._animationMode === 'NoopAnimations';

if (defaultOptions) {
this.hideToggle = defaultOptions.hideToggle;
Expand Down Expand Up @@ -204,6 +205,8 @@ export class MatExpansionPanel
this._portal = new TemplatePortal(this._lazyContent._template, this._viewContainerRef);
});
}

this._setupAnimationEvents();
}

ngOnChanges(changes: SimpleChanges) {
Expand All @@ -212,6 +215,10 @@ export class MatExpansionPanel

override ngOnDestroy() {
super.ngOnDestroy();
this._bodyWrapper?.nativeElement.removeEventListener(
'transitionend',
this._transitionEndListener,
);
this._inputChanges.complete();
}

Expand All @@ -226,38 +233,36 @@ export class MatExpansionPanel
return false;
}

/** Called when the expansion animation has started. */
protected _animationStarted(event: AnimationEvent) {
if (!isInitialAnimation(event) && !this._animationsDisabled && this._body) {
// Prevent the user from tabbing into the content while it's animating.
// TODO(crisbeto): maybe use `inert` to prevent focus from entering while closed as well
// instead of `visibility`? Will allow us to clean up some code but needs more testing.
this._body?.nativeElement.setAttribute('inert', '');
private _transitionEndListener = ({target, propertyName}: TransitionEvent) => {
if (target === this._bodyWrapper?.nativeElement && propertyName === 'grid-template-rows') {
this._ngZone.run(() => {
if (this.expanded) {
this.afterExpand.emit();
} else {
this.afterCollapse.emit();
}
});
}
}

/** Called when the expansion animation has finished. */
protected _animationDone(event: AnimationEvent) {
if (!isInitialAnimation(event)) {
if (event.toState === 'expanded') {
this.afterExpand.emit();
} else if (event.toState === 'collapsed') {
this.afterCollapse.emit();
};

protected _setupAnimationEvents() {
// This method is defined separately, because we need to
// disable this logic in some internal components.
this._ngZone.runOutsideAngular(() => {
if (this._animationsDisabled) {
this.opened.subscribe(() => this._ngZone.run(() => this.afterExpand.emit()));
this.closed.subscribe(() => this._ngZone.run(() => this.afterCollapse.emit()));
} else {
setTimeout(() => {
const element = this._elementRef.nativeElement;
element.addEventListener('transitionend', this._transitionEndListener);
element.classList.add('mat-expansion-panel-animations-enabled');
}, 200);
}

// Re-enable tabbing once the animation is finished.
if (!this._animationsDisabled && this._body) {
this._body.nativeElement.removeAttribute('inert');
}
}
});
}
}

/** Checks whether an animation is the initial setup animation. */
function isInitialAnimation(event: AnimationEvent): boolean {
return event.fromState === 'void';
}

/**
* Actions of a `<mat-expansion-panel>`.
*/
Expand Down
Loading
Loading