mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
fix(expansion-panel): improved accessibility labelling and keyboard default action not being prevented (#9174)
Adds the following improvements to the expansion accessibility, based on the [accessible accordion guidelines](https://www.w3.org/TR/wai-aria-practices-1.1/examples/accordion/accordion.html): * Adds the `region` role to the content element. * Adds `aria-labelledby` pointing to the header. * Fixes the page being scrolled down when a panel is opened using space. The issue was that we were preventing the default action during `keydown` which is too late in the lifecycle. Also adds a couple of extra tests for the keyboard controls.
This commit is contained in:
committed by
Jeremy Elbourn
parent
e09c0a694d
commit
e62afdf06e
@@ -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:
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* <mat-expansion-panel>
|
||||
*
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user