MDL-70846 accessibility: update tree attributes to pass a11y check

- Move aria-* atrributes from <p> to <li>
- Move "role" attribute from <p> to <li>
- Update behat tests

Based on reference implementation from:
- https://www.w3.org/TR/wai-aria-practices-1.1/examples/treeview/treeview-2/treeview-2a.html
- https://www.w3.org/WAI/GL/wiki/Using_ARIA_trees
This commit is contained in:
Dongsheng Cai 2021-05-25 20:38:51 +10:00
parent 30b8ad51f4
commit e3690a392d
14 changed files with 67 additions and 66 deletions

View File

@ -1,2 +1,2 @@
function _typeof(a){"@babel/helpers - typeof";if("function"==typeof Symbol&&"symbol"==typeof Symbol.iterator){_typeof=function(a){return typeof a}}else{_typeof=function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a}}return _typeof(a)}define ("block_navigation/ajax_response_renderer",["jquery","core/templates","core/notification","core/url","core/aria"],function(a,b,c,d,e){var g={ACTIVITY:40,RESOURCE:50};function f(h,i){var j=a("<ul></ul>");j.attr("role","group");e.hide(j);a.each(i,function(e,h){if("object"!==_typeof(h)){return}var i=a("<li></li>"),k=a("<p></p>"),l=h.id||h.key+"_tree_item",m=null,n=h.expandable||h.haschildren?!0:!1;k.addClass("tree_item");k.attr("id",l);k.attr("role","treeitem");k.attr("tabindex","-1");if(h.requiresajaxloading){k.attr("data-requires-ajax",!0);k.attr("data-node-id",h.id);k.attr("data-node-key",h.key);k.attr("data-node-type",h.type)}if(n){i.addClass("collapsed contains_branch");k.attr("aria-expanded",!1);k.addClass("branch")}var o=null;if(h.link){var p=a("<a title=\""+h.title+"\" href=\""+h.link+"\"></a>");o=p;p.append("<span class=\"item-content-wrap\">"+h.name+"</span>");if(h.hidden){p.addClass("dimmed")}k.append(p)}else{var q=a("<span></span>");o=q;q.append("<span class=\"item-content-wrap\">"+h.name+"</span>");if(h.hidden){q.addClass("dimmed")}k.append(q)}if(h.icon&&(!n||h.type===g.ACTIVITY||h.type===g.RESOURCE)){i.addClass("item_with_icon");k.addClass("hasicon");if(h.type===g.ACTIVITY||h.type===g.RESOURCE){m=a("<img/>");m.attr("alt",h.icon.alt);m.attr("title",h.icon.title);m.attr("src",d.imageUrl(h.icon.pix,h.icon.component));a.each(h.icon.classes,function(a,b){m.addClass(b)});o.prepend(m)}else{if("moodle"==h.icon.component){h.icon.component="core"}b.renderPix(h.icon.pix,h.icon.component,h.icon.title).then(function(a){o.prepend(a)}).catch(c.exception)}}i.append(k);j.append(i);if(h.children&&h.children.length){f(k,h.children)}else if(n&&!h.requiresajaxloading){i.removeClass("contains_branch");k.addClass("emptybranch")}});h.parent().append(j);var k=h.attr("id")+"_group";j.attr("id",k);h.attr("aria-owns",k);h.attr("role","treeitem")}return{render:function render(a,b){if(b.children&&b.children.length){f(a,b.children);var c=a.children("[role='treeitem']").first(),d=a.find("#"+c.attr("aria-owns"));c.attr("aria-expanded",!0);e.unhide(d)}else{if(a.parent().hasClass("contains_branch")){a.parent().removeClass("contains_branch");a.addClass("emptybranch")}}}}});
function _typeof(a){"@babel/helpers - typeof";if("function"==typeof Symbol&&"symbol"==typeof Symbol.iterator){_typeof=function(a){return typeof a}}else{_typeof=function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a}}return _typeof(a)}define ("block_navigation/ajax_response_renderer",["jquery","core/templates","core/notification","core/url","core/aria"],function(a,b,c,d,e){var g={ACTIVITY:40,RESOURCE:50};function f(h,i){var j=a("<ul></ul>");j.attr("role","group");e.hide(j);a.each(i,function(e,h){if("object"!==_typeof(h)){return}var i=a("<li></li>"),k=a("<p></p>"),l=h.id||h.key+"_tree_item",m=null,n=h.expandable||h.haschildren?!0:!1;i.attr("role","treeitem");k.addClass("tree_item");k.attr("id",l);k.attr("tabindex","-1");if(h.requiresajaxloading){i.attr("data-requires-ajax",!0);i.attr("data-node-id",h.id);i.attr("data-node-key",h.key);i.attr("data-node-type",h.type)}if(n){i.addClass("collapsed contains_branch");i.attr("aria-expanded",!1);k.addClass("branch")}var o=null;if(h.link){var p=a("<a title=\""+h.title+"\" href=\""+h.link+"\"></a>");o=p;p.append("<span class=\"item-content-wrap\">"+h.name+"</span>");if(h.hidden){p.addClass("dimmed")}k.append(p)}else{var q=a("<span></span>");o=q;q.append("<span class=\"item-content-wrap\">"+h.name+"</span>");if(h.hidden){q.addClass("dimmed")}k.append(q)}if(h.icon&&(!n||h.type===g.ACTIVITY||h.type===g.RESOURCE)){i.addClass("item_with_icon");k.addClass("hasicon");if(h.type===g.ACTIVITY||h.type===g.RESOURCE){m=a("<img/>");m.attr("alt",h.icon.alt);m.attr("title",h.icon.title);m.attr("src",d.imageUrl(h.icon.pix,h.icon.component));a.each(h.icon.classes,function(a,b){m.addClass(b)});o.prepend(m)}else{if("moodle"==h.icon.component){h.icon.component="core"}b.renderPix(h.icon.pix,h.icon.component,h.icon.title).then(function(a){o.prepend(a)}).catch(c.exception)}}i.append(k);j.append(i);if(h.children&&h.children.length){f(i,h.children)}else if(n&&!h.requiresajaxloading){i.removeClass("contains_branch");k.addClass("emptybranch")}});h.append(j);var k=h.attr("id")+"_group";j.attr("id",k);h.attr("aria-owns",k);h.attr("role","treeitem")}return{render:function render(a,b){if(b.children&&b.children.length){f(a,b.children);var c=a.children("[role='treeitem']").first(),d=a.find("#"+c.attr("aria-owns"));c.attr("aria-expanded",!0);e.unhide(d)}else{if(a.hasClass("contains_branch")){a.removeClass("contains_branch");a.addClass("emptybranch")}}}}});
//# sourceMappingURL=ajax_response_renderer.min.js.map

File diff suppressed because one or more lines are too long

View File

@ -68,22 +68,22 @@ define([
var icon = null;
var isBranch = (node.expandable || node.haschildren) ? true : false;
li.attr('role', 'treeitem');
p.addClass('tree_item');
p.attr('id', id);
p.attr('role', 'treeitem');
// Negative tab index to allow it to receive focus.
p.attr('tabindex', '-1');
if (node.requiresajaxloading) {
p.attr('data-requires-ajax', true);
p.attr('data-node-id', node.id);
p.attr('data-node-key', node.key);
p.attr('data-node-type', node.type);
li.attr('data-requires-ajax', true);
li.attr('data-node-id', node.id);
li.attr('data-node-key', node.key);
li.attr('data-node-type', node.type);
}
if (isBranch) {
li.addClass('collapsed contains_branch');
p.attr('aria-expanded', false);
li.attr('aria-expanded', false);
p.addClass('branch');
}
@ -141,14 +141,14 @@ define([
ul.append(li);
if (node.children && node.children.length) {
buildDOM(p, node.children);
buildDOM(li, node.children);
} else if (isBranch && !node.requiresajaxloading) {
li.removeClass('contains_branch');
p.addClass('emptybranch');
}
});
rootElement.parent().append(ul);
rootElement.append(ul);
var id = rootElement.attr('id') + '_group';
ul.attr('id', id);
rootElement.attr('aria-owns', id);
@ -167,8 +167,8 @@ define([
item.attr('aria-expanded', true);
Aria.unhide(group);
} else {
if (element.parent().hasClass('contains_branch')) {
element.parent().removeClass('contains_branch');
if (element.hasClass('contains_branch')) {
element.removeClass('contains_branch');
element.addClass('emptybranch');
}
}

View File

@ -73,6 +73,7 @@ class block_navigation_renderer extends plugin_renderer_base {
$lis = array();
// Set the number to be static for unique id's.
static $number = 0;
$htmlidprefix = html_writer::random_id();
foreach ($items as $item) {
$number++;
if (!$item->display && !$item->contains_active_node()) {
@ -90,8 +91,8 @@ class block_navigation_renderer extends plugin_renderer_base {
$content = $item->get_content();
$title = $item->get_title();
$ulattr = ['id' => $id . '_group', 'role' => 'group'];
$liattr = ['class' => [$item->get_css_type(), 'depth_'.$depth]];
$pattr = ['class' => ['tree_item'], 'role' => 'treeitem'];
$liattr = ['class' => [$item->get_css_type(), 'depth_'.$depth], 'role' => 'treeitem'];
$pattr = ['class' => ['tree_item']];
$pattr += !empty($item->id) ? ['id' => $item->id] : [];
$isbranch = $isexpandable && ($item->children->count() > 0 || ($item->has_children() && (isloggedin() || $item->type <= navigation_node::TYPE_CATEGORY)));
$hasicon = ((!$isbranch || $item->type == navigation_node::TYPE_ACTIVITY || $item->type == navigation_node::TYPE_RESOURCE) && $item->icon instanceof renderable);
@ -112,7 +113,7 @@ class block_navigation_renderer extends plugin_renderer_base {
continue;
}
$nodetextid = 'label_' . $depth . '_' . $number;
$nodetextid = $htmlidprefix . '_label_' . $depth . '_' . $number;
$attributes = array('tabindex' => '-1', 'id' => $nodetextid);
if ($title !== '') {
$attributes['title'] = $title;
@ -135,11 +136,12 @@ class block_navigation_renderer extends plugin_renderer_base {
}
if ($isbranch) {
$ariaexpanded = $item->has_children() && (!$item->forceopen || $item->collapse);
$pattr['class'][] = 'branch';
$liattr['class'][] = 'contains_branch';
$pattr += ['aria-expanded' => ($item->has_children() && (!$item->forceopen || $item->collapse)) ? "false" : "true"];
$liattr += ['aria-expanded' => $ariaexpanded ? "false" : "true"];
if ($item->requiresajaxloading) {
$pattr += [
$liattr += [
'data-requires-ajax' => 'true',
'data-loaded' => 'false',
'data-node-id' => $item->id,
@ -147,7 +149,7 @@ class block_navigation_renderer extends plugin_renderer_base {
'data-node-type' => $item->type
];
} else {
$pattr += ['aria-owns' => $id . '_group'];
$liattr += ['aria-owns' => $id . '_group'];
}
}
@ -161,8 +163,8 @@ class block_navigation_renderer extends plugin_renderer_base {
$liattr['class'] = join(' ', $liattr['class']);
$pattr['class'] = join(' ', $pattr['class']);
$pattr += $depth == 1 ? ['data-collapsible' => 'false'] : [];
if (isset($pattr['aria-expanded']) && $pattr['aria-expanded'] === 'false') {
$liattr += $depth == 1 ? ['data-collapsible' => 'false'] : [];
if (isset($liattr['aria-expanded']) && $liattr['aria-expanded'] === 'false') {
$ulattr += ['aria-hidden' => 'true'];
}

View File

@ -57,7 +57,7 @@
background-image: url('[[pix:t/collapsed_empty]]');
}
.block_navigation .block_tree [aria-expanded="false"].loading {
.block_navigation .block_tree [aria-expanded="false"] p.loading {
background-image: url('[[pix:i/loading_small]]');
}

View File

@ -77,8 +77,8 @@ class block_settings_renderer extends plugin_renderer_base {
$content = $this->output->render($item);
$id = $item->id ? $item->id : html_writer::random_id();
$ulattr = ['id' => $id . '_group', 'role' => 'group'];
$liattr = ['class' => [$item->get_css_type(), 'depth_'.$depth], 'tabindex' => '-1'];
$pattr = ['class' => ['tree_item'], 'role' => 'treeitem'];
$liattr = ['class' => [$item->get_css_type(), 'depth_'.$depth], 'tabindex' => '-1', 'role' => 'treeitem'];
$pattr = ['class' => ['tree_item']];
$pattr += !empty($item->id) ? ['id' => $item->id] : [];
$hasicon = (!$isbranch && $item->icon instanceof renderable);
@ -86,15 +86,15 @@ class block_settings_renderer extends plugin_renderer_base {
$liattr['class'][] = 'contains_branch';
if (!$item->forceopen || (!$item->forceopen && $item->collapse) || ($item->children->count() == 0
&& $item->nodetype == navigation_node::NODETYPE_BRANCH)) {
$pattr += ['aria-expanded' => 'false'];
$liattr += ['aria-expanded' => 'false'];
} else {
$pattr += ['aria-expanded' => 'true'];
$liattr += ['aria-expanded' => 'true'];
}
if ($item->requiresajaxloading) {
$pattr['data-requires-ajax'] = 'true';
$pattr['data-loaded'] = 'false';
$liattr['data-requires-ajax'] = 'true';
$liattr['data-loaded'] = 'false';
} else {
$pattr += ['aria-owns' => $id . '_group'];
$liattr += ['aria-owns' => $id . '_group'];
}
} else if ($hasicon) {
$liattr['class'][] = 'item_with_icon';
@ -106,7 +106,6 @@ class block_settings_renderer extends plugin_renderer_base {
if (!empty($item->classes) && count($item->classes) > 0) {
$pattr['class'] = array_merge($pattr['class'], $item->classes);
}
$nodetextid = 'label_' . $depth . '_' . $number;
// class attribute on the div item which only contains the item content
$pattr['class'][] = 'tree_item';
@ -119,7 +118,7 @@ class block_settings_renderer extends plugin_renderer_base {
$liattr['class'] = join(' ', $liattr['class']);
$pattr['class'] = join(' ', $pattr['class']);
if (isset($pattr['aria-expanded']) && $pattr['aria-expanded'] === 'false') {
if (isset($liattr['aria-expanded']) && $liattr['aria-expanded'] === 'false') {
$ulattr += ['aria-hidden' => 'true'];
}
@ -127,7 +126,6 @@ class block_settings_renderer extends plugin_renderer_base {
if (!empty($item->preceedwithhr) && $item->preceedwithhr===true) {
$content = html_writer::empty_tag('hr') . $content;
}
$liattr['aria-labelledby'] = $nodetextid;
$content = html_writer::tag('li', $content, $liattr);
$lis[] = $content;
}

View File

@ -46,7 +46,7 @@
background-image: url('[[pix:t/collapsed_empty]]');
}
.block_settings .block_tree [aria-expanded="false"].loading {
.block_settings .block_tree [aria-expanded="false"] p.loading {
background-image: url('[[pix:i/loading_small]]');
}
/*rtl:raw:

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -269,7 +269,8 @@ define(['jquery'], function($) {
var moduleName = item.closest('[data-ajax-loader]').attr('data-ajax-loader');
var thisTree = this;
// Flag this node as loading.
item.addClass('loading');
const p = item.find('p');
p.addClass('loading');
// Require the ajax module (must be AMD) and try to load the items.
require([moduleName], function(loader) {
// All ajax module must implement a "load" method.
@ -280,7 +281,7 @@ define(['jquery'], function($) {
thisTree.initialiseNodes(item);
thisTree.finishExpandingGroup(item);
// Make sure no child elements of the item we just loaded are tabbable.
item.removeClass('loading');
p.removeClass('loading');
promise.resolve();
});
});

View File

@ -64,8 +64,8 @@ class behat_navigation extends behat_base {
$nodetextliteral = behat_context_helper::escape($text);
$hasblocktree = "[contains(concat(' ', normalize-space(@class), ' '), ' block_tree ')]";
$hasbranch = "[contains(concat(' ', normalize-space(@class), ' '), ' branch ')]";
$hascollapsed = "p[@aria-expanded='false']";
$notcollapsed = "p[@aria-expanded='true']";
$hascollapsed = "li[@aria-expanded='false']/p";
$notcollapsed = "li[@aria-expanded='true']/p";
$match = "[normalize-space(.)={$nodetextliteral}]";
// Avoid problems with quotes.
@ -75,18 +75,18 @@ class behat_navigation extends behat_base {
} else if ($collapsed === false) {
$iscollapsed = $notcollapsed;
} else {
$iscollapsed = 'p';
$iscollapsed = 'li/p';
}
// First check root nodes, it can be a span or link.
$xpath = "//ul{$hasblocktree}/li/{$hascollapsed}{$isbranch}/span{$match}|";
$xpath .= "//ul{$hasblocktree}/li/{$hascollapsed}{$isbranch}/a{$match}|";
$xpath = "//ul{$hasblocktree}/{$hascollapsed}{$isbranch}/span{$match}|";
$xpath .= "//ul{$hasblocktree}/{$hascollapsed}{$isbranch}/a{$match}|";
// Next search for the node containing the text within a link.
$xpath .= "//ul{$hasblocktree}//ul/li/{$iscollapsed}{$isbranch}/a{$match}|";
$xpath .= "//ul{$hasblocktree}//ul/{$iscollapsed}{$isbranch}/a{$match}|";
// Finally search for the node containing the text within a span.
$xpath .= "//ul{$hasblocktree}//ul/li/{$iscollapsed}{$isbranch}/span{$match}";
$xpath .= "//ul{$hasblocktree}//ul/{$iscollapsed}{$isbranch}/span{$match}";
$node = $this->find('xpath', $xpath, $exception);
$this->ensure_node_is_visible($node);
@ -263,16 +263,16 @@ class behat_navigation extends behat_base {
// The p node contains the aria jazz.
$pnodexpath = "/p[contains(concat(' ', normalize-space(@class), ' '), ' tree_item ')]";
$pnode = $node->find('xpath', $pnodexpath);
$linode = $pnode->getParent();
// Keep expanding all sub-parents if js enabled.
if ($pnode && $this->running_javascript() && $pnode->hasAttribute('aria-expanded') &&
($pnode->getAttribute('aria-expanded') == "false")) {
if ($pnode && $this->running_javascript() && $linode->hasAttribute('aria-expanded') &&
($linode->getAttribute('aria-expanded') == "false")) {
$this->js_trigger_click($pnode);
// Wait for node to load, if not loaded before.
if ($pnode->hasAttribute('data-loaded') && $pnode->getAttribute('data-loaded') == "false") {
$jscondition = '(document.evaluate("' . $pnode->getXpath() . '", document, null, '.
if ($linode->hasAttribute('data-loaded') && $linode->getAttribute('data-loaded') == "false") {
$jscondition = '(document.evaluate("' . $linode->getXpath() . '", document, null, '.
'XPathResult.ANY_TYPE, null).iterateNext().getAttribute(\'data-loaded\') == "true")';
$this->getSession()->wait(behat_base::get_extended_timeout() * 1000, $jscondition);

View File

@ -322,8 +322,8 @@ body.drawer-open-left #region-main.has-blocks {
.block_navigation .block_tree [aria-expanded="false"] {
background-image: none;
}
.block_settings .block_tree [aria-expanded="true"]:before,
.block_navigation .block_tree [aria-expanded="true"]:before {
.block_settings .block_tree [aria-expanded="true"] > p:before,
.block_navigation .block_tree [aria-expanded="true"] > p:before {
content: $fa-var-angle-down;
margin-right: 0;
@include fa-icon();
@ -331,8 +331,8 @@ body.drawer-open-left #region-main.has-blocks {
width: 16px;
}
.block_settings .block_tree [aria-expanded="false"]:before,
.block_navigation .block_tree [aria-expanded="false"]:before {
.block_settings .block_tree [aria-expanded="false"] > p:before,
.block_navigation .block_tree [aria-expanded="false"] > p:before {
content: $fa-var-angle-right;
margin-right: 0;
@include fa-icon();
@ -340,8 +340,8 @@ body.drawer-open-left #region-main.has-blocks {
width: 16px;
}
.dir-rtl {
.block_settings .block_tree [aria-expanded="false"]:before,
.block_navigation .block_tree [aria-expanded="false"]:before {
.block_settings .block_tree [aria-expanded="false"] > p:before,
.block_navigation .block_tree [aria-expanded="false"] > p:before {
content: $fa-var-angle-left;
}
}

View File

@ -12744,8 +12744,8 @@ input[disabled] {
.block_navigation .block_tree [aria-expanded="false"] {
background-image: none; }
.block_settings .block_tree [aria-expanded="true"]:before,
.block_navigation .block_tree [aria-expanded="true"]:before {
.block_settings .block_tree [aria-expanded="true"] > p:before,
.block_navigation .block_tree [aria-expanded="true"] > p:before {
content: "";
margin-right: 0;
display: inline-block;
@ -12757,8 +12757,8 @@ input[disabled] {
font-size: 16px;
width: 16px; }
.block_settings .block_tree [aria-expanded="false"]:before,
.block_navigation .block_tree [aria-expanded="false"]:before {
.block_settings .block_tree [aria-expanded="false"] > p:before,
.block_navigation .block_tree [aria-expanded="false"] > p:before {
content: "";
margin-right: 0;
display: inline-block;
@ -12770,8 +12770,8 @@ input[disabled] {
font-size: 16px;
width: 16px; }
.dir-rtl .block_settings .block_tree [aria-expanded="false"]:before,
.dir-rtl .block_navigation .block_tree [aria-expanded="false"]:before {
.dir-rtl .block_settings .block_tree [aria-expanded="false"] > p:before,
.dir-rtl .block_navigation .block_tree [aria-expanded="false"] > p:before {
content: ""; }
.block_navigation .block_tree p.hasicon,

View File

@ -12966,8 +12966,8 @@ input[disabled] {
.block_navigation .block_tree [aria-expanded="false"] {
background-image: none; }
.block_settings .block_tree [aria-expanded="true"]:before,
.block_navigation .block_tree [aria-expanded="true"]:before {
.block_settings .block_tree [aria-expanded="true"] > p:before,
.block_navigation .block_tree [aria-expanded="true"] > p:before {
content: "";
margin-right: 0;
display: inline-block;
@ -12979,8 +12979,8 @@ input[disabled] {
font-size: 16px;
width: 16px; }
.block_settings .block_tree [aria-expanded="false"]:before,
.block_navigation .block_tree [aria-expanded="false"]:before {
.block_settings .block_tree [aria-expanded="false"] > p:before,
.block_navigation .block_tree [aria-expanded="false"] > p:before {
content: "";
margin-right: 0;
display: inline-block;
@ -12992,8 +12992,8 @@ input[disabled] {
font-size: 16px;
width: 16px; }
.dir-rtl .block_settings .block_tree [aria-expanded="false"]:before,
.dir-rtl .block_navigation .block_tree [aria-expanded="false"]:before {
.dir-rtl .block_settings .block_tree [aria-expanded="false"] > p:before,
.dir-rtl .block_navigation .block_tree [aria-expanded="false"] > p:before {
content: ""; }
.block_navigation .block_tree p.hasicon,