diff --git a/src/lib/expansion/expansion-panel-header.ts b/src/lib/expansion/expansion-panel-header.ts
index f3d3bb10e1b..41982cc6e61 100644
--- a/src/lib/expansion/expansion-panel-header.ts
+++ b/src/lib/expansion/expansion-panel-header.ts
@@ -46,13 +46,14 @@ import {matExpansionAnimations} from './expansion-animations';
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: {
@@ -116,8 +117,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 af3a6eaed84..de6e62ed000 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 18b81ac89fa..5c2b54afb77 100644
--- a/src/lib/expansion/expansion-panel.ts
+++ b/src/lib/expansion/expansion-panel.ts
@@ -57,6 +57,9 @@ export const _MatExpansionPanelMixinBase = mixinDisabled(MatExpansionPanelBase);
/** MatExpansionPanel's states. */
export type MatExpansionPanelState = 'expanded' | 'collapsed';
+/** Counter for generating unique element ids. */
+let uniqueId = 0;
+
/**
*
*
@@ -107,6 +110,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 470f7c8120d..29a279fa651 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', () => {
@@ -72,7 +74,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);
@@ -85,9 +87,58 @@ describe('MatExpansionPanel', () => {
expect(panelIdOne).not.toBe(panelIdTwo);
});
+ 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);