Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(expansion-panel): improved accessibility labelling and keyboard default action not being prevented #9174

Merged
merged 1 commit into from
Jan 23, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions src/lib/expansion/expansion-panel-header.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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:
Expand Down
4 changes: 3 additions & 1 deletion src/lib/expansion/expansion-panel.html
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
<ng-content select="mat-expansion-panel-header"></ng-content>
<div class="mat-expansion-panel-content"
role="region"
[class.mat-expanded]="expanded"
[@bodyExpansion]="_getExpandedState()"
[id]="id">
[id]="id"
[attr.aria-labelledby]="_headerId">
<div class="mat-expansion-panel-body">
<ng-content></ng-content>
<ng-template [cdkPortalOutlet]="_portal"></ng-template>
Expand Down
6 changes: 6 additions & 0 deletions src/lib/expansion/expansion-panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
* <mat-expansion-panel> component.
*
Expand Down Expand Up @@ -120,6 +123,9 @@ export class MatExpansionPanel extends _MatExpansionPanelMixinBase
/** Portal holding the user's content. */
_portal: TemplatePortal<any>;

/** 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,
Expand Down
55 changes: 53 additions & 2 deletions src/lib/expansion/expansion.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down