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

Commit 5c455d3

Browse files
committed
fix(list): case where list items are read twice by screen readers
- if we create an `aria-label` from the element's text content then mark the content as `aria-hidden="true"` so that screen readers don't announce/traverse the content twice - improve Closure types Fixes #11582
1 parent c644d6a commit 5c455d3

File tree

3 files changed

+25
-8
lines changed

3 files changed

+25
-8
lines changed

src/components/list/list.js

+12-7
Original file line numberDiff line numberDiff line change
@@ -343,16 +343,21 @@ function mdListItemDirective($mdAria, $mdConstant, $mdUtil, $timeout) {
343343
);
344344

345345
// Button which shows ripple and executes primary action.
346-
var buttonWrap = angular.element(
347-
'<md-button class="md-no-style"></md-button>'
348-
);
346+
var buttonWrap = angular.element('<md-button class="md-no-style"></md-button>');
349347

350348
moveAttributes(tElement[0], buttonWrap[0]);
351349

352350
// If there is no aria-label set on the button (previously copied over if present)
353351
// we determine the label from the content and copy it to the button.
354352
if (!buttonWrap.attr('aria-label')) {
355353
buttonWrap.attr('aria-label', $mdAria.getText(tElement));
354+
355+
// If we set the button's aria-label to the text content, then make the content hidden
356+
// from screen readers so that it isn't read/traversed twice.
357+
var listItemInner = itemContainer[0].querySelector('.md-list-item-inner');
358+
if (listItemInner) {
359+
listItemInner.setAttribute('aria-hidden', 'true');
360+
}
356361
}
357362

358363
// We allow developers to specify the `md-no-focus` class, to disable the focus style
@@ -386,7 +391,7 @@ function mdListItemDirective($mdAria, $mdConstant, $mdUtil, $timeout) {
386391

387392
/**
388393
* @param {HTMLElement} secondaryItem
389-
* @param container
394+
* @param {HTMLDivElement} container
390395
*/
391396
function wrapSecondaryItem(secondaryItem, container) {
392397
// If the current secondary item is not a button, but contains a ng-click attribute,
@@ -423,9 +428,9 @@ function mdListItemDirective($mdAria, $mdConstant, $mdUtil, $timeout) {
423428
* Moves attributes from a source element to the destination element.
424429
* By default, the function will copy the most necessary attributes, supported
425430
* by the button executor for clickable list items.
426-
* @param source Element with the specified attributes
427-
* @param destination Element which will receive the attributes
428-
* @param extraAttrs Additional attributes, which will be moved over
431+
* @param {Element} source Element with the specified attributes
432+
* @param {Element} destination Element which will receive the attributes
433+
* @param {string|string[]} extraAttrs Additional attributes, which will be moved over
429434
*/
430435
function moveAttributes(source, destination, extraAttrs) {
431436
var copiedAttrs = $mdUtil.prefixer([

src/components/list/list.spec.js

+4
Original file line numberDiff line numberDiff line change
@@ -512,10 +512,12 @@ describe('mdListItem directive', function() {
512512
it('should copy label to the button executor element', function() {
513513
var listItem = setup('<md-list-item ng-click="null" aria-label="Test">');
514514
var buttonEl = listItem.find('button');
515+
var listItemInnerElement = listItem[0].querySelector('.md-list-item-inner');
515516

516517
// The aria-label attribute should be moved to the button element.
517518
expect(buttonEl.attr('aria-label')).toBe('Test');
518519
expect(listItem.attr('aria-label')).toBeFalsy();
520+
expect(listItemInnerElement.getAttribute('aria-hidden')).toBeFalsy();
519521
});
520522

521523
it('should determine the label from the content if not set', function() {
@@ -527,9 +529,11 @@ describe('mdListItem directive', function() {
527529
);
528530

529531
var buttonEl = listItem.find('button');
532+
var listItemInnerElement = listItem[0].querySelector('.md-list-item-inner');
530533

531534
// The aria-label attribute should be determined from the content.
532535
expect(buttonEl.attr('aria-label')).toBe('Content');
536+
expect(listItemInnerElement.getAttribute('aria-hidden')).toBeTruthy();
533537
});
534538

535539
it('should determine the label from the bound content if aria-label is not set', function() {

src/core/services/aria/aria.js

+9-1
Original file line numberDiff line numberDiff line change
@@ -128,9 +128,13 @@ function MdAriaService($$rAF, $log, $window, $interpolate) {
128128
}
129129
}
130130

131+
/**
132+
* @param {Element|JQLite} element
133+
* @returns {string}
134+
*/
131135
function getText(element) {
132136
element = element[0] || element;
133-
var walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null, false);
137+
var walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null);
134138
var text = '';
135139

136140
var node;
@@ -142,6 +146,10 @@ function MdAriaService($$rAF, $log, $window, $interpolate) {
142146

143147
return text.trim() || '';
144148

149+
/**
150+
* @param {Node} node
151+
* @returns {boolean}
152+
*/
145153
function isAriaHiddenNode(node) {
146154
while (node.parentNode && (node = node.parentNode) !== element) {
147155
if (node.getAttribute && node.getAttribute('aria-hidden') === 'true') {

0 commit comments

Comments
 (0)