Commit f8895446 authored by John Okely's avatar John Okely Committed by Simey Lameze
Browse files

MDL-35590 block_navigation: Add aria roles to navigation block tree

parent e8d51002
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Parse the response from the navblock ajax page and render the correct DOM
* structure for the tree from it.
*
* @module block_navigation/ajax_response_renderer
* @package core
* @copyright 2015 John Okely <john@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define(['jquery'], function($) {
// Mappings for the different types of nodes coming from the navigation.
// Copied from lib/navigationlib.php navigation_node constants.
var NODETYPE = {
// @type int Root node = 0
ROOTNODE : 0,
// @type int System context = 1
SYSTEM : 1,
// @type int Course category = 10
CATEGORY : 10,
// @type int MYCATEGORY = 11
MYCATEGORY : 11,
// @type int Course = 20
COURSE : 20,
// @type int Course section = 30
SECTION : 30,
// @type int Activity (course module) = 40
ACTIVITY : 40,
// @type int Resource (course module = 50
RESOURCE : 50,
// @type int Custom node (could be anything) = 60
CUSTOM : 60,
// @type int Setting = 70
SETTING : 70,
// @type int site administration = 71
SITEADMIN : 71,
// @type int User context = 80
USER : 80,
// @type int Container = 90
CONTAINER : 90
};
function buildDOM(rootElement, nodes) {
var ul = $('<ul></ul>');
ul.attr('role', 'group');
$.each(nodes, function(index, node) {
if (typeof node !== 'object') {
return;
}
var li = $('<li></li>');
var p = $('<p></p>');
var icon = null;
var isBranch = (node.expandable || node.haschildren) ? true : false;
p.addClass('tree_item');
p.attr('id', node.id);
li.attr('role', 'treeitem');
if (node.requiresajaxloading) {
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');
li.attr('aria-expanded', false);
p.addClass('branch');
}
if (node.icon && (!isBranch || node.type === NODETYPE.ACTIVITY || node.type === NODETYPE.RESOURCE)) {
li.addClass('item_with_icon');
p.addClass('hasicon');
icon = $('<img/>');
icon.attr('alt', node.icon.alt);
icon.attr('title', node.icon.title);
icon.attr('src', M.util.image_url(node.icon.pix, node.icon.component));
$.each(node.icon.classes, function(index, className) {
icon.addClass(className);
});
}
if (node.link) {
var link = $('<a></a>');
link.attr('title', node.title);
link.attr('href', node.link);
if (icon) {
link.append(icon);
link.append('<span class="item-content-wrap">'+node.name+'</span>');
} else {
link.text(node.name);
}
if (node.hidden) {
link.addClass('dimmed');
}
p.append(link);
} else {
var span = $('<span></span>');
if (icon) {
span.append(icon);
span.append('<span class="item-content-wrap">'+node.name+'</span>');
} else {
span.text(node.name);
}
if (node.hidden) {
span.addClass('dimmed');
}
p.append(span);
}
li.append(p);
ul.append(li);
if (node.children && node.children.length) {
buildDOM(li, node.children);
} else if (isBranch && !node.requiresajaxloading) {
li.removeClass('contains_branch');
li.addClass('emptybranch');
}
});
rootElement.append(ul);
}
return {
render: function(element, nodes) {
// The first element of the response is the existing node
// so we start with processing the children.
if (nodes.children && nodes.children.length) {
buildDOM(element, nodes.children);
} else {
if (element.hasClass('contains_branch')) {
element.removeClass('contains_branch').addClass('emptybranch');
}
}
}
};
});
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Load the nav tree items via ajax and render the response.
*
* @module block_navigation/nav_loader
* @package core
* @copyright 2015 John Okely <john@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define(['jquery', 'core/ajax', 'core/config', 'block_navigation/ajax_response_renderer'],
function($, ajax, config, renderer) {
var URL = config.wwwroot + '/lib/ajax/getnavbranch.php';
function getBlockInstanceId(element) {
return element.closest('[data-block]').attr('data-instanceid');
}
return {
load: function(element) {
element = $(element);
var promise = $.Deferred();
var data = {
elementid: element.attr('data-node-id'),
id: element.attr('data-node-key'),
type: element.attr('data-node-type'),
sesskey: config.sesskey,
instance: getBlockInstanceId(element)
};
var settings = {
type: 'POST',
dataType: 'json',
data: data
};
$.ajax(URL, settings).done(function(nodes) {
renderer.render(element, nodes);
promise.resolve();
});
return promise;
}
};
});
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Load the navtree javscript
*
* @module block_navigation/navblock
* @package core
* @copyright 2015 John Okely <john@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define(['jquery', 'core/tree'], function($, Tree) {
return {
init: function() {
new Tree(".block_navigation .block_tree");
}
};
});
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Load the site admin nav tree via ajax and render the response.
*
* @module block_navigation/site_admin_loader
* @package core
* @copyright 2015 John Okely <john@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define(['jquery', 'core/ajax', 'core/config', 'block_navigation/ajax_response_renderer'],
function($, ajax, config, renderer) {
var SITE_ADMIN_NODE_TYPE = 71;
var URL = config.wwwroot + '/lib/ajax/getsiteadminbranch.php';
return {
load: function(element) {
element = $(element);
var promise = $.Deferred();
var data = {
type: SITE_ADMIN_NODE_TYPE,
sesskey: config.sesskey,
};
var settings = {
type: 'POST',
dataType: 'json',
data: data
};
$.ajax(URL, settings).done(function(nodes) {
renderer.render(element, nodes);
promise.resolve();
});
return promise;
}
};
});
...@@ -109,23 +109,8 @@ class block_navigation extends block_base { ...@@ -109,23 +109,8 @@ class block_navigation extends block_base {
function get_required_javascript() { function get_required_javascript() {
global $CFG; global $CFG;
parent::get_required_javascript(); parent::get_required_javascript();
$limit = 20;
if (!empty($CFG->navcourselimit)) {
$limit = $CFG->navcourselimit;
}
$expansionlimit = 0;
if (!empty($this->config->expansionlimit)) {
$expansionlimit = $this->config->expansionlimit;
}
$arguments = array(
'id' => $this->instance->id,
'instance' => $this->instance->id,
'candock' => $this->instance_can_be_docked(),
'courselimit' => $limit,
'expansionlimit' => $expansionlimit
);
$this->page->requires->string_for_js('viewallcourses', 'moodle'); $this->page->requires->string_for_js('viewallcourses', 'moodle');
$this->page->requires->yui_module('moodle-block_navigation-navigation', 'M.block_navigation.init_add_tree', array($arguments)); $this->page->requires->js_call_amd('block_navigation/navblock', 'init', array());
} }
/** /**
...@@ -196,7 +181,21 @@ class block_navigation extends block_base { ...@@ -196,7 +181,21 @@ class block_navigation extends block_base {
} }
} }
$this->page->requires->data_for_js('navtreeexpansions'.$this->instance->id, $expandable); $limit = 20;
if (!empty($CFG->navcourselimit)) {
$limit = $CFG->navcourselimit;
}
$expansionlimit = 0;
if (!empty($this->config->expansionlimit)) {
$expansionlimit = $this->config->expansionlimit;
}
$arguments = array(
'id' => $this->instance->id,
'instance' => $this->instance->id,
'candock' => $this->instance_can_be_docked(),
'courselimit' => $limit,
'expansionlimit' => $expansionlimit
);
$options = array(); $options = array();
$options['linkcategories'] = (!empty($this->config->linkcategories) && $this->config->linkcategories == 'yes'); $options['linkcategories'] = (!empty($this->config->linkcategories) && $this->config->linkcategories == 'yes');
......
...@@ -42,7 +42,11 @@ class block_navigation_renderer extends plugin_renderer_base { ...@@ -42,7 +42,11 @@ class block_navigation_renderer extends plugin_renderer_base {
*/ */
public function navigation_tree(global_navigation $navigation, $expansionlimit, array $options = array()) { public function navigation_tree(global_navigation $navigation, $expansionlimit, array $options = array()) {
$navigation->add_class('navigation_node'); $navigation->add_class('navigation_node');
$content = $this->navigation_node(array($navigation), array('class'=>'block_tree list'), $expansionlimit, $options); $navigationattrs = array(
'class' => 'block_tree list',
'role' => 'tree',
'data-ajax-loader' => 'block_navigation/nav_loader');
$content = $this->navigation_node(array($navigation), $navigationattrs, $expansionlimit, $options);
if (isset($navigation->id) && !is_numeric($navigation->id) && !empty($content)) { if (isset($navigation->id) && !is_numeric($navigation->id) && !empty($content)) {
$content = $this->output->box($content, 'block_tree_box', $navigation->id); $content = $this->output->box($content, 'block_tree_box', $navigation->id);
} }
...@@ -66,7 +70,9 @@ class block_navigation_renderer extends plugin_renderer_base { ...@@ -66,7 +70,9 @@ class block_navigation_renderer extends plugin_renderer_base {
// Turn our navigation items into list items. // Turn our navigation items into list items.
$lis = array(); $lis = array();
$number = 0;
foreach ($items as $item) { foreach ($items as $item) {
$number++;
if (!$item->display && !$item->contains_active_node()) { if (!$item->display && !$item->contains_active_node()) {
continue; continue;
} }
...@@ -100,7 +106,8 @@ class block_navigation_renderer extends plugin_renderer_base { ...@@ -100,7 +106,8 @@ class block_navigation_renderer extends plugin_renderer_base {
continue; continue;
} }
$attributes = array(); $nodetextid = 'label_' . $depth . '_' . $number;
$attributes = array('tabindex' => '-1', 'id' => $nodetextid);
if ($title !== '') { if ($title !== '') {
$attributes['title'] = $title; $attributes['title'] = $title;
} }
...@@ -110,7 +117,6 @@ class block_navigation_renderer extends plugin_renderer_base { ...@@ -110,7 +117,6 @@ class block_navigation_renderer extends plugin_renderer_base {
if (is_string($item->action) || empty($item->action) || if (is_string($item->action) || empty($item->action) ||
(($item->type === navigation_node::TYPE_CATEGORY || $item->type === navigation_node::TYPE_MY_CATEGORY) && (($item->type === navigation_node::TYPE_CATEGORY || $item->type === navigation_node::TYPE_MY_CATEGORY) &&
empty($options['linkcategories']))) { empty($options['linkcategories']))) {
$attributes['tabindex'] = '0'; //add tab support to span but still maintain character stream sequence.
$content = html_writer::tag('span', $content, $attributes); $content = html_writer::tag('span', $content, $attributes);
} else if ($item->action instanceof action_link) { } else if ($item->action instanceof action_link) {
//TODO: to be replaced with something else //TODO: to be replaced with something else
...@@ -129,12 +135,27 @@ class block_navigation_renderer extends plugin_renderer_base { ...@@ -129,12 +135,27 @@ class block_navigation_renderer extends plugin_renderer_base {
$divclasses = array('tree_item'); $divclasses = array('tree_item');
$liexpandable = array(); $liexpandable = array();
if ($item->has_children() && (!$item->forceopen || $item->collapse)) { $lirole = array('role' => 'treeitem');
$liclasses[] = 'collapsed';
}
if ($isbranch) { if ($isbranch) {
$liclasses[] = 'contains_branch'; $liclasses[] = 'contains_branch';
$liexpandable = array('aria-expanded' => in_array('collapsed', $liclasses) ? "false" : "true"); if ($depth == 1) {
$liexpandable = array(
'data-expandable' => 'false'
);
} else {
$liexpandable = array(
'aria-expanded' => ($item->has_children() &&
(!$item->forceopen || $item->collapse)) ? "false" : "true");
}
if ($item->requiresajaxloading) {
$liexpandable['data-requires-ajax'] = 'true';
$liexpandable['data-loaded'] = 'false';
$liexpandable['data-node-id'] = $item->id;
$liexpandable['data-node-key'] = $item->key;
$liexpandable['data-node-type'] = $item->type;
}
$divclasses[] = 'branch'; $divclasses[] = 'branch';
} else { } else {
$divclasses[] = 'leaf'; $divclasses[] = 'leaf';
...@@ -152,7 +173,7 @@ class block_navigation_renderer extends plugin_renderer_base { ...@@ -152,7 +173,7 @@ class block_navigation_renderer extends plugin_renderer_base {
} }
// Now build attribute arrays. // Now build attribute arrays.
$liattr = array('class' => join(' ', $liclasses)) + $liexpandable; $liattr = array('class' => join(' ', $liclasses)) + $liexpandable + $lirole;
$divattr = array('class'=>join(' ', $divclasses)); $divattr = array('class'=>join(' ', $divclasses));
if (!empty($item->id)) { if (!empty($item->id)) {
$divattr['id'] = $item->id; $divattr['id'] = $item->id;
...@@ -161,11 +182,15 @@ class block_navigation_renderer extends plugin_renderer_base { ...@@ -161,11 +182,15 @@ class block_navigation_renderer extends plugin_renderer_base {
// Create the structure. // Create the structure.
$content = html_writer::tag('p', $content, $divattr); $content = html_writer::tag('p', $content, $divattr);
if ($isexpandable) { if ($isexpandable) {
$content .= $this->navigation_node($item->children, array(), $expansionlimit, $options, $depth+1); $content .= $this->navigation_node($item->children, array('role' => 'group'), $expansionlimit, $options, $depth+1);
} }
if (!empty($item->preceedwithhr) && $item->preceedwithhr===true) { if (!empty($item->preceedwithhr) && $item->preceedwithhr===true) {
$content = html_writer::empty_tag('hr') . $content; $content = html_writer::empty_tag('hr') . $content;
} }
if ($depth == 1) {
$liattr['tabindex'] = '0';
}
$liattr['aria-labelledby'] = $nodetextid;
$content = html_writer::tag('li', $content, $liattr); $content = html_writer::tag('li', $content, $liattr);
$lis[] = $content; $lis[] = $content;
} }
......
...@@ -30,6 +30,16 @@ ...@@ -30,6 +30,16 @@
background-image: url('[[pix:i/loading_small]]'); background-image: url('[[pix:i/loading_small]]');
} }
.block_navigation .block_tree .loading .tree_item.branch {
background-image: url('[[pix:i/loading_small]]');
}
.block_navigation .block_tree .emptybranch .tree_item,
.block_navigation .block_tree [aria-expanded="false"].emptybranch .tree_item.branch {
padding-left: 21px;
background-image: url('[[pix:t/collapsed_empty]]');
}
.block_navigation .block_tree .tree_item img { .block_navigation .block_tree .tree_item img {
width: 16px; width: 16px;
height: 16px; height: 16px;
...@@ -60,14 +70,23 @@ ...@@ -60,14 +70,23 @@
list-style: none; list-style: none;
} }
.jsenabled .block_navigation .block_tree li.collapsed ul { .jsenabled .block_navigation .block_tree [aria-expanded="false"] ul {
display: none; display: none;
} }
.jsenabled .block_navigation .block_tree li.collapsed .tree_item.branch { .jsenabled .block_navigation .block_tree [aria-expanded="false"] .tree_item.branch {
background-image: url('[[pix:t/collapsed]]'); background-image: url('[[pix:t/collapsed]]');
} }
.jsenabled .block_navigation .block_tree [aria-expanded="false"].loading .tree_item.branch {
background-image: url('[[pix:i/loading_small]]');
}
.jsenabled .block_navigation .block_tree [aria-expanded="false"].emptybranch .tree_item.branch {
padding-left: 21px;
background-image: url('[[pix:t/collapsed_empty]]');
}
.jsenabled .block_navigation.dock_on_load { .jsenabled .block_navigation.dock_on_load {
display: none; display: none;
} }
...@@ -85,7 +104,8 @@ ...@@ -85,7 +104,8 @@
padding-left: 0; padding-left: 0;
} }
.dir-rtl .block_navigation .block_tree .tree_item.emptybranch { .dir-rtl .block_navigation .block_tree .tree_item.emptybranch,
.dir-rtl .block_navigation .block_tree .emptybranch .tree_item {
padding-right: 21px; padding-right: 21px;
padding-left: 0; padding-left: 0;
background-image: url('[[pix:t/collapsed_empty_rtl]]'); background-image: url('[[pix:t/collapsed_empty_rtl]]');
...@@ -100,6 +120,10 @@ ...@@ -100,6 +120,10 @@
margin: 0 16px 0 0; margin: 0 16px 0 0;
} }
.dir-rtl.jsenabled .block_navigation .block_tree .collapsed .tree_item.branch { .dir-rtl.jsenabled .block_navigation .block_tree [aria-expanded="false"] .tree_item.branch {
background-image: url('[[pix:t/collapsed_rtl]]'); background-image: url('[[pix:t/collapsed_rtl]]');
} }
.dir-rtl.jsenabled .block_navigation .block_tree [aria-expanded="false"].emptybranch .tree_item.branch {
background-image: url('[[pix:t/collapsed_empty_rtl]]');
}
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
{
"name": "moodle-block_navigation-navigation",
"builds": {
"moodle-block_navigation-navigation": {
"jsfiles": [
"navigation.js"
]
}