Skip to content
This repository was archived by the owner on Sep 5, 2024. It is now read-only.

Commit 6b29548

Browse files
MarcoZehemmalerba
authored andcommitted
fix(nav-bar): improve screen reader support (#11486)
… reader users * make the nav-bar component a tablist. * make the nav-item's li element presentational and the button a tab. * apply the aria-label, if provided, to the button directly, not the li element. Fixes #11485 <!-- Filling out this template is required! Do not delete it when submitting a Pull Request! Without this information, your Pull Request may be auto-closed. --> ## PR Checklist Please check that your PR fulfills the following requirements: - [x] The commit message follows [our guidelines](https://github.com/angular/material/blob/master/.github/CONTRIBUTING.md#-commit-message-format) - [x] Tests for the changes have been added or this is not a bug fix / enhancement - [x] Docs have been added, updated, or were not required ## PR Type What kind of change does this PR introduce? <!-- Please check the one that applies to this PR using "x". --> ``` [x] Bugfix [ ] Enhancement [ ] Documentation content changes [ ] Code style update (formatting, local variables) [ ] Refactoring (no functional changes, no api changes) [ ] Build related changes [ ] CI related changes [ ] Infrastructure changes [ ] Other... Please describe: ``` ## What is the current behavior? <!-- Please describe the current behavior that you are modifying and link to one or more relevant issues. --> Issue Number: #11485 Currently, the nav-bar element is exposed as a listbox accessible to screen reader users. Its nav-item child consists of a li element which currently has role "option" (as appropriate for a listbox child), and the button inside that li is exposed as is. Screen readers do have a hard time interacting with this construct, since they do not expect an option to contain a button. ## What is the new behavior? The new behavior is that the nav-bar element is exposed as a tablist, and the button inside the li of the nav-item as a tab accessible. In addition, the aria-label that was originally applied to the nav-item's li element is now applied to the buton, since the li in the tablist/tab pattern is to be marked presentational. aria-label must therefore go onto the element that has role "tab". No changes besides semantic ARIA exposure has been changed, the keyboard behavior is the same as well as any other behavior this component exposes. Web developers needn't change anything, either. ## Does this PR introduce a breaking change? ``` [ ] Yes [x] No ``` <!-- If this PR contains a breaking change, please describe the impact and migration path for existing applications below. --> <!-- Note that breaking changes are highly unlikely to get merged to master unless the validation is clear and the use case is critical. --> ## Other information The tests have been adjusted to look for the aria-label on the tab directly, not the parent.
1 parent e876eec commit 6b29548

File tree

2 files changed

+37
-18
lines changed

2 files changed

+37
-18
lines changed

src/components/navBar/navBar.js

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,10 @@ angular.module('material.components.navBar', ['material.core'])
3131
* Alternatively, the user could simply watch the value of `md-selected-nav-item`
3232
* (`currentNavItem` in the below example) for changes.
3333
*
34-
* Accessibility functionality is implemented as a site navigator with a
35-
* listbox, according to the
36-
* <a href="https://www.w3.org/TR/2016/WD-wai-aria-practices-1.1-20160317/#Site_Navigator_Tabbed_Style">
37-
* WAI-ARIA Authoring Practices 1.1 Working Draft from March 2016</a>.
34+
* Accessibility functionality is implemented as a
35+
* <a href="https://www.w3.org/TR/wai-aria-1.0/complete#tablist">
36+
* tablist</a> with
37+
* <a href="https://www.w3.org/TR/wai-aria-1.0/complete#tab">tabs</a>.
3838
* We've kept the `role="navigation"` on the `<nav>`, for backwards compatibility, even though
3939
* it is not required in the
4040
* <a href="https://www.w3.org/TR/wai-aria-practices/#aria_lh_navigation">
@@ -43,7 +43,7 @@ angular.module('material.components.navBar', ['material.core'])
4343
* @param {string=} md-selected-nav-item The name of the current tab; this must
4444
* match the `name` attribute of `<md-nav-item>`.
4545
* @param {boolean=} md-no-ink-bar If set to true, the ink bar will be hidden.
46-
* @param {string=} nav-bar-aria-label An `aria-label` applied to the `md-nav-bar`'s listbox
46+
* @param {string=} nav-bar-aria-label An `aria-label` applied to the `md-nav-bar`'s tablist
4747
* for accessibility.
4848
*
4949
* @usage
@@ -90,7 +90,10 @@ angular.module('material.components.navBar', ['material.core'])
9090
* Exactly one of the `md-nav-click`, `md-nav-href`, or `md-nav-sref` attributes are required
9191
* to be specified.
9292
*
93-
* @param {string=} aria-label Adds alternative text for accessibility.
93+
* @param {string=} nav-item-aria-label Allows setting or overriding the label that is announced by
94+
* a screen reader for the nav item's button. If this is not set, the nav item's transcluded
95+
* content will be announced. Make sure to set this if the nav item's transcluded content does
96+
* not include descriptive text, for example only an icon.
9497
* @param {expression=} md-nav-click Expression which will be evaluated when the
9598
* link is clicked to change the page. Renders as an `ng-click`.
9699
* @param {string=} md-nav-href url to transition to when this link is clicked.
@@ -127,7 +130,7 @@ function MdNavBar($mdAria, $mdTheming) {
127130
template:
128131
'<div class="md-nav-bar">' +
129132
'<nav role="navigation">' +
130-
'<ul class="_md-nav-bar-list" ng-transclude role="listbox" ' +
133+
'<ul class="_md-nav-bar-list" ng-transclude role="tablist" ' +
131134
'tabindex="0" ' +
132135
'ng-focus="ctrl.onFocus()" ' +
133136
'ng-keydown="ctrl.onKeydown($event)" ' +
@@ -444,6 +447,9 @@ function MdNavItem($mdAria, $$rAF, $mdUtil, $window) {
444447
'ng-blur="ctrl.setFocused(false)" ' +
445448
'ng-disabled="ctrl.disabled" ' +
446449
'tabindex="-1" ' +
450+
'role="tab" ' +
451+
'ng-attr-aria-label="{{ctrl.navItemAriaLabel ? ctrl.navItemAriaLabel : undefined}}" ' +
452+
'aria-selected="{{ctrl.isSelected()}}" ' +
447453
navigationOptions +
448454
navigationAttribute + '>' +
449455
'<span ng-transclude class="_md-nav-button-text"></span>' +
@@ -452,8 +458,7 @@ function MdNavItem($mdAria, $$rAF, $mdUtil, $window) {
452458

453459
return '' +
454460
'<li class="md-nav-item" ' +
455-
'role="option" ' +
456-
'aria-selected="{{ctrl.isSelected()}}">' +
461+
'role="presentation">' +
457462
(buttonTemplate || '') +
458463
'</li>';
459464
},
@@ -463,6 +468,7 @@ function MdNavItem($mdAria, $$rAF, $mdUtil, $window) {
463468
'mdNavSref': '@?',
464469
'srefOpts': '=?',
465470
'name': '@',
471+
'navItemAriaLabel': '@?',
466472
},
467473
link: function(scope, element, attrs, controllers) {
468474
var disconnect;
@@ -504,7 +510,9 @@ function MdNavItem($mdAria, $$rAF, $mdUtil, $window) {
504510
});
505511
}
506512

507-
$mdAria.expectWithText(element, 'aria-label');
513+
if (!mdNavItem.navItemAriaLabel) {
514+
$mdAria.expectWithText(navButton, 'aria-label');
515+
}
508516
});
509517

510518
scope.$on('destroy', function() {
@@ -541,6 +549,9 @@ function MdNavItemController($element) {
541549
/** @const {?string} */
542550
this.name;
543551

552+
/** @type {string} */
553+
this.navItemAriaLabel;
554+
544555
// State variables
545556
/** @private {boolean} */
546557
this._selected = false;

src/components/navBar/navBar.spec.js

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ describe('mdNavBar', function() {
4545
' <md-nav-item md-nav-href="#3" name="tab3" aria-label="foo">' +
4646
' tab3' +
4747
' </md-nav-item>' +
48+
' <md-nav-item md-nav-href="#4" name="tab4" nav-item-aria-label="foo">' +
49+
' tab4' +
50+
' </md-nav-item>' +
4851
'</md-nav-bar>');
4952
}
5053

@@ -278,15 +281,15 @@ describe('mdNavBar', function() {
278281
$scope.selectedTabRoute = 'tab1';
279282
createTabs();
280283

281-
expect(getTab('tab1').parent().attr('aria-selected')).toBe('true');
282-
expect(getTab('tab2').parent().attr('aria-selected')).toBe('false');
283-
expect(getTab('tab3').parent().attr('aria-selected')).toBe('false');
284+
expect(getTab('tab1').attr('aria-selected')).toBe('true');
285+
expect(getTab('tab2').attr('aria-selected')).toBe('false');
286+
expect(getTab('tab3').attr('aria-selected')).toBe('false');
284287

285288
updateSelectedTabRoute('tab3');
286289

287-
expect(getTab('tab1').parent().attr('aria-selected')).toBe('false');
288-
expect(getTab('tab2').parent().attr('aria-selected')).toBe('false');
289-
expect(getTab('tab3').parent().attr('aria-selected')).toBe('true');
290+
expect(getTab('tab1').attr('aria-selected')).toBe('false');
291+
expect(getTab('tab2').attr('aria-selected')).toBe('false');
292+
expect(getTab('tab3').attr('aria-selected')).toBe('true');
290293
});
291294

292295
it('sets aria-label on the listbox', function() {
@@ -384,14 +387,19 @@ describe('mdNavBar', function() {
384387

385388
it('automatically adds label to nav items', function() {
386389
createTabs();
387-
expect(getTab('tab1').parent().attr('aria-label')).toBe('tab1');
388-
expect(getTab('tab2').parent().attr('aria-label')).toBe('tab2');
390+
expect(getTab('tab1').attr('aria-label')).toBe('tab1');
391+
expect(getTab('tab2').attr('aria-label')).toBe('tab2');
389392
});
390393

391394
it('does not change aria-label on nav items', function() {
392395
createTabs();
393396
expect(getTab('tab3').parent().attr('aria-label')).toBe('foo');
394397
});
398+
399+
it('does not change nav-item-aria-label on nav item buttons', function() {
400+
createTabs();
401+
expect(getTab('tab4').attr('aria-label')).toBe('foo');
402+
});
395403
});
396404

397405
function getTab(tabName) {

0 commit comments

Comments
 (0)