Skip to content
This repository was archived by the owner on Sep 5, 2024. It is now read-only.
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
29 changes: 26 additions & 3 deletions src/components/icon/icon.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,16 @@ describe('MdIcon directive', function() {

describe('with ARIA support', function() {

it('should apply "img" role by default', function() {
el = make('<md-icon md-svg-icon="android" ></md-icon>');
expect(el.attr('role')).toEqual('img');
});

it('should apply not replace current role', function() {
el = make('<md-icon md-svg-icon="android" role="presentation" ></md-icon>');
expect(el.attr('role')).toEqual('presentation');
});

it('should apply aria-hidden="true" when parent has valid label', function() {
el = make('<button aria-label="Android"><md-icon md-svg-icon="android"></md-icon></button>');
expect(el.find('md-icon').attr('aria-hidden')).toEqual('true');
Expand All @@ -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('<button>Android <md-icon md-svg-icon="android"></md-icon></button>');
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('<button aria-label="Android" role="command"><md-icon md-svg-icon="android"></md-icon></button>');
expect(el.find('md-icon').attr('aria-hidden')).toBeUndefined();

el = make('<md-radio-button aria-label="avatar 2" role="command"> '+
'<div class="md-container"></div> '+
'<div class="md-label"> '+
'<md-icon md-svg-icon="android"></md-icon> '+
'</div></md-radio-button>');

expect(el.find('md-icon').attr('aria-hidden')).toBeUndefined();
});

it('should apply aria-hidden="true" when aria-label is empty string', function() {
Expand All @@ -339,6 +357,11 @@ describe('MdIcon directive', function() {
el = make('<md-icon md-svg-icon="android"></md-icon>');
expect(el.attr('aria-label')).toEqual('android');
});

it('should apply use alt text for aria-label value when not set', function() {
el = make('<md-icon md-svg-icon="android" alt="my android icon"></md-icon>');
expect(el.attr('aria-label')).toEqual('my android icon');
});
});
});

Expand Down
43 changes: 20 additions & 23 deletions src/components/icon/js/iconDirective.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
}
Expand All @@ -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) {
Expand Down
151 changes: 149 additions & 2 deletions src/core/services/aria/aria.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,9 @@ function MdAriaService($$rAF, $log, $window, $interpolate) {
expectAsync: expectAsync,
expectWithText: expectWithText,
expectWithoutText: expectWithoutText,
getText: getText
getText: getText,
hasAriaLabel: hasAriaLabel,
parentHasAriaLabel: parentHasAriaLabel
};

/**
Expand Down Expand Up @@ -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;
}
}
}
52 changes: 52 additions & 0 deletions src/core/services/aria/aria.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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('<div aria-label="hello"></div>')($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('<div aria-labelledby="myOtherElementId"></div>')($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('<div aria-describedby="myOtherElementId"></div>')($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('<button aria-label="hello"><img></img></button>')($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('<button role="log" aria-label="hello"><img></img></button>')($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('<span aria-label="hello"><img></img></span>')($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('<button aria-label="hello"><div><img></img></div></button>')($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) {
Expand Down