Skip to content

Commit

Permalink
fix(expansion-panel): focus lost if focused element is inside closing…
Browse files Browse the repository at this point in the history
… panel (#12692)

Currently when an expansion panel is closed, we make the content non-focusable using `visibility: hidden`, but that means that if the focused element was inside the panel, focus will be returned back to the body. These changes add a listener that will restore focus to the panel header, if the focused element is inside the panel when it is closed.
  • Loading branch information
crisbeto authored and jelbourn committed Aug 29, 2018
1 parent 81e0542 commit 3596e9d
Show file tree
Hide file tree
Showing 3 changed files with 53 additions and 4 deletions.
7 changes: 6 additions & 1 deletion src/lib/expansion/expansion-panel-header.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,12 @@ export class MatExpansionPanelHeader implements OnDestroy {
)
.subscribe(() => this._changeDetectorRef.markForCheck());

_focusMonitor.monitor(_element.nativeElement);
// Avoids focus being lost if the panel contained the focused element and was closed.
panel.closed
.pipe(filter(() => panel._containsFocus()))
.subscribe(() => _focusMonitor.focusVia(_element.nativeElement, 'program'));

_focusMonitor.monitor(_element.nativeElement);
}

/** Height of the header while the panel is expanded. */
Expand Down
31 changes: 28 additions & 3 deletions src/lib/expansion/expansion-panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,23 @@ import {CdkAccordionItem} from '@angular/cdk/accordion';
import {coerceBooleanProperty} from '@angular/cdk/coercion';
import {UniqueSelectionDispatcher} from '@angular/cdk/collections';
import {TemplatePortal} from '@angular/cdk/portal';
import {DOCUMENT} from '@angular/common';
import {
AfterContentInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ContentChild,
Directive,
ElementRef,
Inject,
Input,
OnChanges,
OnDestroy,
Optional,
SimpleChanges,
SkipSelf,
ViewChild,
ViewContainerRef,
ViewEncapsulation,
} from '@angular/core';
Expand Down Expand Up @@ -70,8 +74,13 @@ let uniqueId = 0;
'[class.mat-expansion-panel-spacing]': '_hasSpacing()',
}
})
export class MatExpansionPanel extends _CdkAccordionItem
implements AfterContentInit, OnChanges, OnDestroy {
export class MatExpansionPanel extends CdkAccordionItem implements AfterContentInit, OnChanges,
OnDestroy {

// @breaking-change 8.0.0 Remove `| undefined` from here
// when the `_document` constructor param is required.
private _document: Document | undefined;

/** Whether the toggle indicator should be hidden. */
@Input()
get hideToggle(): boolean {
Expand All @@ -91,6 +100,9 @@ export class MatExpansionPanel extends _CdkAccordionItem
/** Content that will be rendered lazily. */
@ContentChild(MatExpansionPanelContent) _lazyContent: MatExpansionPanelContent;

/** Element containing the panel's user-provided content. */
@ViewChild('body') _body: ElementRef<HTMLElement>;

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

Expand All @@ -100,9 +112,11 @@ export class MatExpansionPanel extends _CdkAccordionItem
constructor(@Optional() @SkipSelf() accordion: MatAccordion,
_changeDetectorRef: ChangeDetectorRef,
_uniqueSelectionDispatcher: UniqueSelectionDispatcher,
private _viewContainerRef: ViewContainerRef) {
private _viewContainerRef: ViewContainerRef,
@Inject(DOCUMENT) _document?: any) {
super(accordion, _changeDetectorRef, _uniqueSelectionDispatcher);
this.accordion = accordion;
this._document = _document;
}

/** Determines whether the expansion panel should have spacing between it and its siblings. */
Expand Down Expand Up @@ -158,6 +172,17 @@ export class MatExpansionPanel extends _CdkAccordionItem
classList.remove(cssClass);
}
}

/** Checks whether the expansion panel's content contains the currently-focused element. */
_containsFocus(): boolean {
if (this._body && this._document) {
const focusedElement = this._document.activeElement;
const bodyElement = this._body.nativeElement;
return focusedElement === bodyElement || bodyElement.contains(focusedElement);
}

return false;
}
}

@Directive({
Expand Down
19 changes: 19 additions & 0 deletions src/lib/expansion/expansion.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,25 @@ describe('MatExpansionPanel', () => {
expect(document.activeElement).not.toBe(button, 'Expected button to no longer be focusable.');
}));

it('should restore focus to header if focused element is inside panel on close', fakeAsync(() => {
const fixture = TestBed.createComponent(PanelWithContent);
fixture.componentInstance.expanded = true;
fixture.detectChanges();
tick(250);

const button = fixture.debugElement.query(By.css('button')).nativeElement;
const header = fixture.debugElement.query(By.css('mat-expansion-panel-header')).nativeElement;

button.focus();
expect(document.activeElement).toBe(button, 'Expected button to start off focusable.');

fixture.componentInstance.expanded = false;
fixture.detectChanges();
tick(250);

expect(document.activeElement).toBe(header, 'Expected header to be focused.');
}));

it('should not override the panel margin if it is not inside an accordion', fakeAsync(() => {
let fixture = TestBed.createComponent(PanelWithCustomMargin);
fixture.detectChanges();
Expand Down

0 comments on commit 3596e9d

Please sign in to comment.