diff --git a/src/components/icon/icon.spec.js b/src/components/icon/icon.spec.js
index 40e5a19f02..5f57e644a4 100644
--- a/src/components/icon/icon.spec.js
+++ b/src/components/icon/icon.spec.js
@@ -301,6 +301,16 @@ describe('MdIcon directive', function() {
describe('with ARIA support', function() {
+ it('should apply "img" role by default', function() {
+ el = make('');
+ expect(el.attr('role')).toEqual('img');
+ });
+
+ it('should apply not replace current role', function() {
+ el = make('');
+ expect(el.attr('role')).toEqual('presentation');
+ });
+
it('should apply aria-hidden="true" when parent has valid label', function() {
el = make('');
expect(el.find('md-icon').attr('aria-hidden')).toEqual('true');
@@ -314,9 +324,17 @@ describe('MdIcon directive', function() {
expect(el.find('md-icon').attr('aria-hidden')).toEqual('true');
});
- it('should apply aria-hidden="true" when parent has text content', function() {
- el = make('');
- expect(el.find('md-icon').attr('aria-hidden')).toEqual('true');
+ it('should not apply aria-hidden="true" when parent has valid label but invalid role', function() {
+ el = make('');
+ expect(el.find('md-icon').attr('aria-hidden')).toBeUndefined();
+
+ el = make(' '+
+ ' '+
+ '
'+
+ ' '+
+ '
');
+
+ expect(el.find('md-icon').attr('aria-hidden')).toBeUndefined();
});
it('should apply aria-hidden="true" when aria-label is empty string', function() {
@@ -339,6 +357,11 @@ describe('MdIcon directive', function() {
el = make('');
expect(el.attr('aria-label')).toEqual('android');
});
+
+ it('should apply use alt text for aria-label value when not set', function() {
+ el = make('');
+ expect(el.attr('aria-label')).toEqual('my android icon');
+ });
});
});
diff --git a/src/components/icon/js/iconDirective.js b/src/components/icon/js/iconDirective.js
index 7eb013e293..575336141b 100644
--- a/src/components/icon/js/iconDirective.js
+++ b/src/components/icon/js/iconDirective.js
@@ -218,20 +218,29 @@ function mdIconDirective($mdIcon, $mdTheming, $mdAria, $sce) {
// If using a font-icon, then the textual name of the icon itself
// provides the aria-label.
- var label = attr.alt || attr.mdFontIcon || attr.mdSvgIcon || element.text();
var attrName = attr.$normalize(attr.$attr.mdSvgIcon || attr.$attr.mdSvgSrc || '');
- if ( !attr['aria-label'] ) {
-
- if (label !== '' && !parentsHaveText() ) {
-
- $mdAria.expect(element, 'aria-label', label);
- $mdAria.expect(element, 'role', 'img');
-
- } else if ( !element.text() ) {
- // If not a font-icon with ligature, then
- // hide from the accessibility layer.
+ /* Provide a default accessibility role of img */
+ if (!attr.role) {
+ $mdAria.expect(element, 'role', 'img');
+ /* manually update attr variable */
+ attr.role = 'img';
+ }
+ /* Don't process ARIA if already valid */
+ if ( attr.role === "img" && !attr.ariaHidden && !$mdAria.hasAriaLabel(element) ) {
+ var iconName;
+ if (attr.alt) {
+ /* Use alt text by default if available */
+ $mdAria.expect(element, 'aria-label', attr.alt);
+ } else if ($mdAria.parentHasAriaLabel(element, 2)) {
+ /* Parent has ARIA so we will assume it will describe the image */
+ $mdAria.expect(element, 'aria-hidden', 'true');
+ } else if (iconName = (attr.mdFontIcon || attr.mdSvgIcon || element.text())) {
+ /* Use icon name as aria-label */
+ $mdAria.expect(element, 'aria-label', iconName);
+ } else {
+ /* No label found */
$mdAria.expect(element, 'aria-hidden', 'true');
}
}
@@ -247,21 +256,9 @@ function mdIconDirective($mdIcon, $mdTheming, $mdAria, $sce) {
element.append(svg);
});
}
-
});
}
- function parentsHaveText() {
- var parent = element.parent();
- if (parent.attr('aria-label') || parent.text()) {
- return true;
- }
- else if(parent.parent().attr('aria-label') || parent.parent().text()) {
- return true;
- }
- return false;
- }
-
function prepareForFontIcon() {
if (!attr.mdSvgIcon && !attr.mdSvgSrc) {
if (attr.mdFontIcon) {
diff --git a/src/core/services/aria/aria.js b/src/core/services/aria/aria.js
index a14fd08cef..a680befef5 100644
--- a/src/core/services/aria/aria.js
+++ b/src/core/services/aria/aria.js
@@ -66,7 +66,9 @@ function MdAriaService($$rAF, $log, $window, $interpolate) {
expectAsync: expectAsync,
expectWithText: expectWithText,
expectWithoutText: expectWithoutText,
- getText: getText
+ getText: getText,
+ hasAriaLabel: hasAriaLabel,
+ parentHasAriaLabel: parentHasAriaLabel
};
/**
@@ -169,7 +171,152 @@ function MdAriaService($$rAF, $log, $window, $interpolate) {
}
}
}
-
return hasAttr;
}
+
+ /**
+ * Check if expected element has aria label attribute
+ * @param element
+ */
+ function hasAriaLabel(element) {
+ var node = angular.element(element)[0] || element;
+
+ /* Check if compatible node type (ie: not HTML Document node) */
+ if (!node.hasAttribute) {
+ return false;
+ }
+
+ /* Check label or description attributes */
+ return node.hasAttribute('aria-label') || node.hasAttribute('aria-labelledby') || node.hasAttribute('aria-describedby');
+ }
+
+ /**
+ * Check if expected element's parent has aria label attribute and has valid role and tagName
+ * @param element
+ * @param {optional} level Number of levels deep search should be performed
+ */
+ function parentHasAriaLabel(element, level) {
+ level = level || 1;
+ var node = angular.element(element)[0] || element;
+ if (!node.parentNode) {
+ return false;
+ }
+ if (performCheck(node.parentNode)) {
+ return true;
+ }
+ level--;
+ if (level) {
+ return parentHasAriaLabel(node.parentNode, level);
+ }
+ return false;
+
+ function performCheck(parentNode) {
+ if (!hasAriaLabel(parentNode)) {
+ return false;
+ }
+ /* Perform role blacklist check */
+ if (parentNode.hasAttribute('role')) {
+ switch(parentNode.getAttribute('role').toLowerCase()) {
+ case 'command':
+ case 'definition':
+ case 'directory':
+ case 'grid':
+ case 'list':
+ case 'listitem':
+ case 'log':
+ case 'marquee':
+ case 'menu':
+ case 'menubar':
+ case 'note':
+ case 'presentation':
+ case 'separator':
+ case 'scrollbar':
+ case 'status':
+ case 'tablist':
+ return false;
+ }
+ }
+ /* Perform tagName blacklist check */
+ switch(parentNode.tagName.toLowerCase()) {
+ case 'abbr':
+ case 'acronym':
+ case 'address':
+ case 'applet':
+ case 'audio':
+ case 'b':
+ case 'bdi':
+ case 'bdo':
+ case 'big':
+ case 'blockquote':
+ case 'br':
+ case 'canvas':
+ case 'caption':
+ case 'center':
+ case 'cite':
+ case 'code':
+ case 'col':
+ case 'data':
+ case 'dd':
+ case 'del':
+ case 'dfn':
+ case 'dir':
+ case 'div':
+ case 'dl':
+ case 'em':
+ case 'embed':
+ case 'fieldset':
+ case 'figcaption':
+ case 'font':
+ case 'h1':
+ case 'h2':
+ case 'h3':
+ case 'h4':
+ case 'h5':
+ case 'h6':
+ case 'hgroup':
+ case 'html':
+ case 'i':
+ case 'ins':
+ case 'isindex':
+ case 'kbd':
+ case 'keygen':
+ case 'label':
+ case 'legend':
+ case 'li':
+ case 'map':
+ case 'mark':
+ case 'menu':
+ case 'object':
+ case 'ol':
+ case 'output':
+ case 'pre':
+ case 'presentation':
+ case 'q':
+ case 'rt':
+ case 'ruby':
+ case 'samp':
+ case 'small':
+ case 'source':
+ case 'span':
+ case 'status':
+ case 'strike':
+ case 'strong':
+ case 'sub':
+ case 'sup':
+ case 'svg':
+ case 'tbody':
+ case 'td':
+ case 'th':
+ case 'thead':
+ case 'time':
+ case 'tr':
+ case 'track':
+ case 'tt':
+ case 'ul':
+ case 'var':
+ return false;
+ }
+ return true;
+ }
+ }
}
diff --git a/src/core/services/aria/aria.spec.js b/src/core/services/aria/aria.spec.js
index 0ad503e189..8692ceb113 100644
--- a/src/core/services/aria/aria.spec.js
+++ b/src/core/services/aria/aria.spec.js
@@ -113,6 +113,58 @@ describe('$mdAria service', function() {
});
+ describe('aria label checks', function() {
+
+ it('should detect if element has valid aria-label string', inject(function($compile, $rootScope, $log, $mdAria) {
+ var container = $compile('
')($rootScope);
+ expect($mdAria.hasAriaLabel(container)).toBe(true);
+ }));
+
+ it('should detect if element has valid aria-labelledby string', inject(function($compile, $rootScope, $log, $mdAria) {
+ var container = $compile('')($rootScope);
+ expect($mdAria.hasAriaLabel(container)).toBe(true);
+ }));
+
+ it('should detect if element has valid aria-describedby string', inject(function($compile, $rootScope, $log, $mdAria) {
+ var container = $compile('')($rootScope);
+ expect($mdAria.hasAriaLabel(container)).toBe(true);
+ }));
+
+ });
+
+ describe('aria label parent checks', function() {
+
+ it('should detect if parent of element has valid aria label', inject(function($compile, $rootScope, $log, $mdAria) {
+ var container = $compile('')($rootScope);
+ var imgElement = container.find('img');
+ expect($mdAria.hasAriaLabel(imgElement)).toBe(false);
+ expect($mdAria.parentHasAriaLabel(imgElement)).toBe(true);
+ }));
+
+ it('should blacklist parent of element based on role', inject(function($compile, $rootScope, $log, $mdAria) {
+ var container = $compile('')($rootScope);
+ var imgElement = container.find('img');
+ expect($mdAria.hasAriaLabel(imgElement)).toBe(false);
+ expect($mdAria.parentHasAriaLabel(imgElement)).toBe(false);
+ }));
+
+ it('should blacklist parent of element based on tagName', inject(function($compile, $rootScope, $log, $mdAria) {
+ var container = $compile('')($rootScope);
+ var imgElement = container.find('img');
+ expect($mdAria.hasAriaLabel(imgElement)).toBe(false);
+ expect($mdAria.parentHasAriaLabel(imgElement)).toBe(false);
+ }));
+
+ it('should detect if second parent of element has valid aria label', inject(function($compile, $rootScope, $log, $mdAria) {
+ var container = $compile('')($rootScope);
+ var imgElement = container.find('img');
+ expect($mdAria.hasAriaLabel(imgElement)).toBe(false);
+ expect($mdAria.parentHasAriaLabel(imgElement)).toBe(false);
+ expect($mdAria.parentHasAriaLabel(imgElement, 2)).toBe(true);
+ }));
+
+ });
+
describe('with disabled warnings', function() {
beforeEach(module('material.core', function($mdAriaProvider) {