From 4a139a6312b1f8daf0c9eaec8d0df284efe2374f Mon Sep 17 00:00:00 2001 From: crisbeto Date: Wed, 15 Aug 2018 21:41:42 +0200 Subject: [PATCH] fix(expansion-panel): focus lost if focused element is inside closing panel 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. --- src/lib/expansion/expansion-panel-header.ts | 7 ++++- src/lib/expansion/expansion-panel.ts | 31 +++++++++++++++++++-- src/lib/expansion/expansion.spec.ts | 19 +++++++++++++ 3 files changed, 53 insertions(+), 4 deletions(-) diff --git a/src/lib/expansion/expansion-panel-header.ts b/src/lib/expansion/expansion-panel-header.ts index 578fa78f165c..84c7ddbd7d8a 100644 --- a/src/lib/expansion/expansion-panel-header.ts +++ b/src/lib/expansion/expansion-panel-header.ts @@ -83,7 +83,12 @@ export class MatExpansionPanelHeader implements OnDestroy { ) .subscribe(() => this._changeDetectorRef.markForCheck()); - _focusMonitor.monitor(_element); + // 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); } /** Height of the header while the panel is expanded. */ diff --git a/src/lib/expansion/expansion-panel.ts b/src/lib/expansion/expansion-panel.ts index 7e1dbae211bd..56e7581d889c 100644 --- a/src/lib/expansion/expansion-panel.ts +++ b/src/lib/expansion/expansion-panel.ts @@ -19,7 +19,9 @@ import { ContentChild, Directive, EventEmitter, + ElementRef, Input, + Inject, OnChanges, OnDestroy, Optional, @@ -28,7 +30,9 @@ import { SkipSelf, ViewContainerRef, ViewEncapsulation, + ViewChild, } from '@angular/core'; +import {DOCUMENT} from '@angular/common'; import {Subject} from 'rxjs'; import {filter, startWith, take} from 'rxjs/operators'; import {MatAccordion} from './accordion'; @@ -72,8 +76,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 { @@ -99,6 +108,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; + /** Portal holding the user's content. */ _portal: TemplatePortal; @@ -108,9 +120,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. */ @@ -174,6 +188,17 @@ export class MatExpansionPanel extends _CdkAccordionItem this.afterCollapse.emit(); } } + + /** 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({ diff --git a/src/lib/expansion/expansion.spec.ts b/src/lib/expansion/expansion.spec.ts index 4af9dce54d34..47d099ebe249 100644 --- a/src/lib/expansion/expansion.spec.ts +++ b/src/lib/expansion/expansion.spec.ts @@ -162,6 +162,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(() => { const fixture = TestBed.createComponent(PanelWithCustomMargin); fixture.detectChanges();