Skip to content

Commit 143aebb

Browse files
committed
fix(material/expansion): prevent focus from entering the panel while it's animating
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.
1 parent 53d3fda commit 143aebb

File tree

3 files changed

+34
-25
lines changed

3 files changed

+34
-25
lines changed

src/material/expansion/expansion-panel.html

Lines changed: 2 additions & 1 deletion
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

Lines changed: 30 additions & 23 deletions
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';
@@ -147,9 +147,6 @@ export class MatExpansionPanel
147147
/** ID for the associated header element. Used for a11y labelling. */
148148
_headerId = `mat-expansion-panel-header-${uniqueId++}`;
149149

150-
/** Stream of body animation done events. */
151-
readonly _bodyAnimationDone = new Subject<AnimationEvent>();
152-
153150
constructor(
154151
@Optional() @SkipSelf() @Inject(MAT_ACCORDION) accordion: MatAccordionBase,
155152
_changeDetectorRef: ChangeDetectorRef,
@@ -165,24 +162,6 @@ export class MatExpansionPanel
165162
this.accordion = accordion;
166163
this._document = _document;
167164

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-
});
185-
186165
if (defaultOptions) {
187166
this.hideToggle = defaultOptions.hideToggle;
188167
}
@@ -237,7 +216,6 @@ export class MatExpansionPanel
237216

238217
override ngOnDestroy() {
239218
super.ngOnDestroy();
240-
this._bodyAnimationDone.complete();
241219
this._inputChanges.complete();
242220
}
243221

@@ -251,6 +229,35 @@ export class MatExpansionPanel
251229

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

256263
/**

tools/public_api_guard/material/expansion.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,10 +101,11 @@ export class MatExpansionPanel extends CdkAccordionItem implements AfterContentI
101101
accordion: MatAccordionBase;
102102
readonly afterCollapse: EventEmitter<void>;
103103
readonly afterExpand: EventEmitter<void>;
104+
_animationDone(event: AnimationEvent_2): void;
104105
// (undocumented)
105106
_animationMode: string;
107+
_animationStarted(event: AnimationEvent_2): void;
106108
_body: ElementRef<HTMLElement>;
107-
readonly _bodyAnimationDone: Subject<AnimationEvent_2>;
108109
close(): void;
109110
_containsFocus(): boolean;
110111
_getExpandedState(): MatExpansionPanelState;

0 commit comments

Comments
 (0)