Skip to content

Commit ba6e809

Browse files
committed
fix(material/expansion): prevent focus from entering the panel while it's animating (#28646)
Currently the expansion panel prevents focus from entering it using `visibility: hidden`, but that only works when it's closed. This means that if the user tabs into it while it's animating, they may scroll the content make the component look broken. These changes resolve the issue by setting `inert` on the panel content while it's animating. Also cleans up an old workaround for IE. Fixes #27430. Fixes #28644. (cherry picked from commit 24eaa2e)
1 parent 4af777a commit ba6e809

File tree

3 files changed

+41
-26
lines changed

3 files changed

+41
-26
lines changed

src/material/expansion/expansion-panel.html

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
<div class="mat-expansion-panel-content"
33
role="region"
44
[@bodyExpansion]="_getExpandedState()"
5-
(@bodyExpansion.done)="_bodyAnimationDone.next($event)"
5+
(@bodyExpansion.start)="_animationStarted($event)"
6+
(@bodyExpansion.done)="_animationDone($event)"
67
[attr.aria-labelledby]="_headerId"
78
[id]="id"
89
#body>

src/material/expansion/expansion-panel.ts

+35-24
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import {
3636
ANIMATION_MODULE_TYPE,
3737
} from '@angular/core';
3838
import {Subject} from 'rxjs';
39-
import {distinctUntilChanged, filter, startWith, take} from 'rxjs/operators';
39+
import {filter, startWith, take} from 'rxjs/operators';
4040
import {MatAccordionBase, MatAccordionTogglePosition, MAT_ACCORDION} from './accordion-base';
4141
import {matExpansionAnimations} from './expansion-animations';
4242
import {MAT_EXPANSION_PANEL} from './expansion-panel-base';
@@ -91,7 +91,7 @@ export const MAT_EXPANSION_PANEL_DEFAULT_OPTIONS =
9191
host: {
9292
'class': 'mat-expansion-panel',
9393
'[class.mat-expanded]': 'expanded',
94-
'[class._mat-animation-noopable]': '_animationMode === "NoopAnimations"',
94+
'[class._mat-animation-noopable]': '_animationsDisabled',
9595
'[class.mat-expansion-panel-spacing]': '_hasSpacing()',
9696
},
9797
standalone: true,
@@ -101,6 +101,7 @@ export class MatExpansionPanel
101101
extends CdkAccordionItem
102102
implements AfterContentInit, OnChanges, OnDestroy
103103
{
104+
protected _animationsDisabled: boolean;
104105
private _document: Document;
105106

106107
/** Whether the toggle indicator should be hidden. */
@@ -147,9 +148,6 @@ export class MatExpansionPanel
147148
/** ID for the associated header element. Used for a11y labelling. */
148149
_headerId = `mat-expansion-panel-header-${uniqueId++}`;
149150

150-
/** Stream of body animation done events. */
151-
readonly _bodyAnimationDone = new Subject<AnimationEvent>();
152-
153151
constructor(
154152
@Optional() @SkipSelf() @Inject(MAT_ACCORDION) accordion: MatAccordionBase,
155153
_changeDetectorRef: ChangeDetectorRef,
@@ -164,24 +162,7 @@ export class MatExpansionPanel
164162
super(accordion, _changeDetectorRef, _uniqueSelectionDispatcher);
165163
this.accordion = accordion;
166164
this._document = _document;
167-
168-
// We need a Subject with distinctUntilChanged, because the `done` event
169-
// fires twice on some browsers. See https://github.com/angular/angular/issues/24084
170-
this._bodyAnimationDone
171-
.pipe(
172-
distinctUntilChanged((x, y) => {
173-
return x.fromState === y.fromState && x.toState === y.toState;
174-
}),
175-
)
176-
.subscribe(event => {
177-
if (event.fromState !== 'void') {
178-
if (event.toState === 'expanded') {
179-
this.afterExpand.emit();
180-
} else if (event.toState === 'collapsed') {
181-
this.afterCollapse.emit();
182-
}
183-
}
184-
});
165+
this._animationsDisabled = _animationMode === 'NoopAnimations';
185166

186167
if (defaultOptions) {
187168
this.hideToggle = defaultOptions.hideToggle;
@@ -237,7 +218,6 @@ export class MatExpansionPanel
237218

238219
override ngOnDestroy() {
239220
super.ngOnDestroy();
240-
this._bodyAnimationDone.complete();
241221
this._inputChanges.complete();
242222
}
243223

@@ -251,6 +231,37 @@ export class MatExpansionPanel
251231

252232
return false;
253233
}
234+
235+
/** Called when the expansion animation has started. */
236+
protected _animationStarted(event: AnimationEvent) {
237+
if (!isInitialAnimation(event) && !this._animationsDisabled && this._body) {
238+
// Prevent the user from tabbing into the content while it's animating.
239+
// TODO(crisbeto): maybe use `inert` to prevent focus from entering while closed as well
240+
// instead of `visibility`? Will allow us to clean up some code but needs more testing.
241+
this._body?.nativeElement.setAttribute('inert', '');
242+
}
243+
}
244+
245+
/** Called when the expansion animation has finished. */
246+
protected _animationDone(event: AnimationEvent) {
247+
if (!isInitialAnimation(event)) {
248+
if (event.toState === 'expanded') {
249+
this.afterExpand.emit();
250+
} else if (event.toState === 'collapsed') {
251+
this.afterCollapse.emit();
252+
}
253+
254+
// Re-enable tabbing once the animation is finished.
255+
if (!this._animationsDisabled && this._body) {
256+
this._body.nativeElement.removeAttribute('inert');
257+
}
258+
}
259+
}
260+
}
261+
262+
/** Checks whether an animation is the initial setup animation. */
263+
function isInitialAnimation(event: AnimationEvent): boolean {
264+
return event.fromState === 'void';
254265
}
255266

256267
/**

tools/public_api_guard/material/expansion.md

+4-1
Original file line numberDiff line numberDiff line change
@@ -101,10 +101,13 @@ export class MatExpansionPanel extends CdkAccordionItem implements AfterContentI
101101
accordion: MatAccordionBase;
102102
readonly afterCollapse: EventEmitter<void>;
103103
readonly afterExpand: EventEmitter<void>;
104+
protected _animationDone(event: AnimationEvent_2): void;
104105
// (undocumented)
105106
_animationMode: string;
107+
// (undocumented)
108+
protected _animationsDisabled: boolean;
109+
protected _animationStarted(event: AnimationEvent_2): void;
106110
_body: ElementRef<HTMLElement>;
107-
readonly _bodyAnimationDone: Subject<AnimationEvent_2>;
108111
close(): void;
109112
_containsFocus(): boolean;
110113
_getExpandedState(): MatExpansionPanelState;

0 commit comments

Comments
 (0)