diff --git a/src/lib/expansion/expansion-panel-header.ts b/src/lib/expansion/expansion-panel-header.ts index 5cbc7d07b5df..786a1ad60029 100644 --- a/src/lib/expansion/expansion-panel-header.ts +++ b/src/lib/expansion/expansion-panel-header.ts @@ -44,13 +44,14 @@ import {EXPANSION_PANEL_ANIMATION_TIMING, MatExpansionPanel} from './expansion-p host: { 'class': 'mat-expansion-panel-header', 'role': 'button', + '[attr.id]': 'panel._headerId', '[attr.tabindex]': 'panel.disabled ? -1 : 0', '[attr.aria-controls]': '_getPanelId()', '[attr.aria-expanded]': '_isExpanded()', '[attr.aria-disabled]': 'panel.disabled', '[class.mat-expanded]': '_isExpanded()', '(click)': '_toggle()', - '(keyup)': '_keyup($event)', + '(keydown)': '_keydown($event)', '[@expansionHeight]': `{ value: _getExpandedState(), params: { @@ -134,8 +135,8 @@ export class MatExpansionPanelHeader implements OnDestroy { return !this.panel.hideToggle && !this.panel.disabled; } - /** Handle keyup event calling to toggle() if appropriate. */ - _keyup(event: KeyboardEvent) { + /** Handle keydown event calling to toggle() if appropriate. */ + _keydown(event: KeyboardEvent) { switch (event.keyCode) { // Toggle for space and enter keys. case SPACE: diff --git a/src/lib/expansion/expansion-panel.html b/src/lib/expansion/expansion-panel.html index af3a6eaed84f..de6e62ed0000 100644 --- a/src/lib/expansion/expansion-panel.html +++ b/src/lib/expansion/expansion-panel.html @@ -1,8 +1,10 @@
+ [id]="id" + [attr.aria-labelledby]="_headerId">
diff --git a/src/lib/expansion/expansion-panel.ts b/src/lib/expansion/expansion-panel.ts index 4f94c8a80d1d..aeb5b1d63252 100644 --- a/src/lib/expansion/expansion-panel.ts +++ b/src/lib/expansion/expansion-panel.ts @@ -60,6 +60,9 @@ export const _MatExpansionPanelMixinBase = mixinDisabled(MatExpansionPanelBase); /** MatExpansionPanel's states. */ export type MatExpansionPanelState = 'expanded' | 'collapsed'; +/** Counter for generating unique element ids. */ +let uniqueId = 0; + /** * component. * @@ -120,6 +123,9 @@ export class MatExpansionPanel extends _MatExpansionPanelMixinBase /** Portal holding the user's content. */ _portal: TemplatePortal; + /** ID for the associated header element. Used for a11y labelling. */ + _headerId = `mat-expansion-panel-header-${uniqueId++}`; + constructor(@Optional() @Host() accordion: MatAccordion, _changeDetectorRef: ChangeDetectorRef, _uniqueSelectionDispatcher: UniqueSelectionDispatcher, diff --git a/src/lib/expansion/expansion.spec.ts b/src/lib/expansion/expansion.spec.ts index fddb687bcb6d..7e09a4057c40 100644 --- a/src/lib/expansion/expansion.spec.ts +++ b/src/lib/expansion/expansion.spec.ts @@ -3,6 +3,8 @@ import {Component, ViewChild} from '@angular/core'; import {By} from '@angular/platform-browser'; import {NoopAnimationsModule} from '@angular/platform-browser/animations'; import {MatExpansionModule, MatExpansionPanel} from './index'; +import {SPACE, ENTER} from '@angular/cdk/keycodes'; +import {dispatchKeyboardEvent} from '@angular/cdk/testing'; describe('MatExpansionPanel', () => { @@ -71,7 +73,7 @@ describe('MatExpansionPanel', () => { expect(fixture.componentInstance.closeCallback).toHaveBeenCalled(); }); - it('creates a unique panel id for each panel', () => { + it('should create a unique panel id for each panel', () => { const fixtureOne = TestBed.createComponent(PanelWithContent); const headerElOne = fixtureOne.nativeElement.querySelector('.mat-expansion-panel-header'); const fixtureTwo = TestBed.createComponent(PanelWithContent); @@ -84,9 +86,58 @@ describe('MatExpansionPanel', () => { expect(panelIdOne).not.toBe(panelIdTwo); }); - it('should not be able to focus content while closed', fakeAsync(() => { + it('should set `aria-labelledby` of the content to the header id', () => { + const fixture = TestBed.createComponent(PanelWithContent); + const headerEl = fixture.nativeElement.querySelector('.mat-expansion-panel-header'); + const contentEl = fixture.nativeElement.querySelector('.mat-expansion-panel-content'); + + fixture.detectChanges(); + + const headerId = headerEl.getAttribute('id'); + const contentLabel = contentEl.getAttribute('aria-labelledby'); + + expect(headerId).toBeTruthy(); + expect(contentLabel).toBeTruthy(); + expect(headerId).toBe(contentLabel); + }); + + it('should set the proper role on the content element', () => { + const fixture = TestBed.createComponent(PanelWithContent); + const contentEl = fixture.nativeElement.querySelector('.mat-expansion-panel-content'); + + expect(contentEl.getAttribute('role')).toBe('region'); + }); + + it('should toggle the panel when pressing SPACE on the header', () => { + const fixture = TestBed.createComponent(PanelWithContent); + const headerEl = fixture.nativeElement.querySelector('.mat-expansion-panel-header'); + + spyOn(fixture.componentInstance.panel, 'toggle'); + + const event = dispatchKeyboardEvent(headerEl, 'keydown', SPACE); + + fixture.detectChanges(); + + expect(fixture.componentInstance.panel.toggle).toHaveBeenCalled(); + expect(event.defaultPrevented).toBe(true); + }); + + it('should toggle the panel when pressing ENTER on the header', () => { const fixture = TestBed.createComponent(PanelWithContent); + const headerEl = fixture.nativeElement.querySelector('.mat-expansion-panel-header'); + + spyOn(fixture.componentInstance.panel, 'toggle'); + const event = dispatchKeyboardEvent(headerEl, 'keydown', ENTER); + + fixture.detectChanges(); + + expect(fixture.componentInstance.panel.toggle).toHaveBeenCalled(); + expect(event.defaultPrevented).toBe(true); + }); + + it('should not be able to focus content while closed', fakeAsync(() => { + const fixture = TestBed.createComponent(PanelWithContent); fixture.componentInstance.expanded = true; fixture.detectChanges(); tick(250);