Skip to content

Commit

Permalink
fix(angular/accordion): prevent focus from entering the panel while i…
Browse files Browse the repository at this point in the history
…t's animating (#2468)

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.
  • Loading branch information
mhaertwig authored Jan 8, 2025
1 parent a261e31 commit 5719fda
Show file tree
Hide file tree
Showing 2 changed files with 39 additions and 20 deletions.
3 changes: 2 additions & 1 deletion src/angular/accordion/expansion-panel.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
class="sbb-expansion-panel-content"
role="region"
[@bodyExpansion]="_getExpandedState()"
(@bodyExpansion.done)="_bodyAnimationDone.next($event)"
(@bodyExpansion.start)="_animationStarted($event)"
(@bodyExpansion.done)="_animationDone($event)"
[attr.aria-labelledby]="_headerId"
[id]="id"
#body
Expand Down
56 changes: 37 additions & 19 deletions src/angular/accordion/expansion-panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { CdkPortalOutlet, TemplatePortal } from '@angular/cdk/portal';
import { DOCUMENT } from '@angular/common';
import {
AfterContentInit,
ANIMATION_MODULE_TYPE,
booleanAttribute,
ChangeDetectionStrategy,
Component,
Expand All @@ -23,7 +24,7 @@ import {
} from '@angular/core';
import { mixinVariant, TypeRef } from '@sbb-esta/angular/core';
import { Subject } from 'rxjs';
import { distinctUntilChanged, filter, startWith, take } from 'rxjs/operators';
import { filter, startWith, take } from 'rxjs/operators';

import type { SbbAccordion } from './accordion';
import { sbbExpansionAnimations } from './accordion-animations';
Expand Down Expand Up @@ -75,7 +76,11 @@ export class SbbExpansionPanel
implements AfterContentInit, OnChanges, OnDestroy
{
private _viewContainerRef = inject(ViewContainerRef);
_animationMode: 'NoopAnimations' | 'BrowserAnimations' | null = inject(ANIMATION_MODULE_TYPE, {
optional: true,
});
private _document = inject(DOCUMENT);
protected _animationsDisabled: boolean;

/** Whether the toggle indicator should be hidden. */
@Input({ transform: booleanAttribute })
Expand Down Expand Up @@ -120,23 +125,7 @@ export class SbbExpansionPanel
constructor(...args: unknown[]);
constructor() {
super();
// We need a Subject with distinctUntilChanged, because the `done` event
// fires twice on some browsers. See https://github.com/angular/angular/issues/24084
this._bodyAnimationDone
.pipe(
distinctUntilChanged((x, y) => {
return x.fromState === y.fromState && x.toState === y.toState;
}),
)
.subscribe((event) => {
if (event.fromState !== 'void') {
if (event.toState === 'expanded') {
this.afterExpand.emit();
} else if (event.toState === 'collapsed') {
this.afterCollapse.emit();
}
}
});
this._animationsDisabled = this._animationMode === 'NoopAnimations';
}

/** Gets the expanded state string. */
Expand Down Expand Up @@ -180,7 +169,6 @@ export class SbbExpansionPanel

override ngOnDestroy() {
super.ngOnDestroy();
this._bodyAnimationDone.complete();
this._inputChanges.complete();
}

Expand All @@ -194,4 +182,34 @@ export class SbbExpansionPanel

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', '');
}
}

/** 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();
}

// 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';
}

0 comments on commit 5719fda

Please sign in to comment.