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 {
function get_required_javascript() {
global $CFG;
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->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 {
}
}
$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['linkcategories'] = (!empty($this->config->linkcategories) && $this->config->linkcategories == 'yes');
......
......@@ -42,7 +42,11 @@ class block_navigation_renderer extends plugin_renderer_base {
*/
public function navigation_tree(global_navigation $navigation, $expansionlimit, array $options = array()) {
$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)) {
$content = $this->output->box($content, 'block_tree_box', $navigation->id);
}
......@@ -66,7 +70,9 @@ class block_navigation_renderer extends plugin_renderer_base {
// Turn our navigation items into list items.
$lis = array();
$number = 0;
foreach ($items as $item) {
$number++;
if (!$item->display && !$item->contains_active_node()) {
continue;
}
......@@ -100,7 +106,8 @@ class block_navigation_renderer extends plugin_renderer_base {
continue;
}
$attributes = array();
$nodetextid = 'label_' . $depth . '_' . $number;
$attributes = array('tabindex' => '-1', 'id' => $nodetextid);
if ($title !== '') {
$attributes['title'] = $title;
}
......@@ -110,7 +117,6 @@ class block_navigation_renderer extends plugin_renderer_base {
if (is_string($item->action) || empty($item->action) ||
(($item->type === navigation_node::TYPE_CATEGORY || $item->type === navigation_node::TYPE_MY_CATEGORY) &&
empty($options['linkcategories']))) {
$attributes['tabindex'] = '0'; //add tab support to span but still maintain character stream sequence.
$content = html_writer::tag('span', $content, $attributes);
} else if ($item->action instanceof action_link) {
//TODO: to be replaced with something else
......@@ -129,12 +135,27 @@ class block_navigation_renderer extends plugin_renderer_base {
$divclasses = array('tree_item');
$liexpandable = array();
if ($item->has_children() && (!$item->forceopen || $item->collapse)) {
$liclasses[] = 'collapsed';
}
$lirole = array('role' => 'treeitem');
if ($isbranch) {
$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';
} else {
$divclasses[] = 'leaf';
......@@ -152,7 +173,7 @@ class block_navigation_renderer extends plugin_renderer_base {
}
// Now build attribute arrays.
$liattr = array('class' => join(' ', $liclasses)) + $liexpandable;
$liattr = array('class' => join(' ', $liclasses)) + $liexpandable + $lirole;
$divattr = array('class'=>join(' ', $divclasses));
if (!empty($item->id)) {
$divattr['id'] = $item->id;
......@@ -161,11 +182,15 @@ class block_navigation_renderer extends plugin_renderer_base {
// Create the structure.
$content = html_writer::tag('p', $content, $divattr);
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) {
$content = html_writer::empty_tag('hr') . $content;
}
if ($depth == 1) {
$liattr['tabindex'] = '0';
}
$liattr['aria-labelledby'] = $nodetextid;
$content = html_writer::tag('li', $content, $liattr);
$lis[] = $content;
}
......
......@@ -30,6 +30,16 @@
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 {
width: 16px;
height: 16px;
......@@ -60,14 +70,23 @@
list-style: none;
}
.jsenabled .block_navigation .block_tree li.collapsed ul {
.jsenabled .block_navigation .block_tree [aria-expanded="false"] ul {
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]]');
}
.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 {
display: none;
}
......@@ -85,7 +104,8 @@
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-left: 0;
background-image: url('[[pix:t/collapsed_empty_rtl]]');
......@@ -100,6 +120,10 @@
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]]');
}
.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"
]
}
}
}
/**
* Navigation block JS.
*
* This file contains the Navigation block JS..
*
* @module moodle-block_navigation-navigation
*/
/**
* This namespace will contain all of the contents of the navigation blocks
* global navigation and settings.
* @class M.block_navigation
* @static
*/
M.block_navigation = M.block_navigation || {};
/**
* The number of expandable branches in existence.
*
* @property expandablebranchcount
* @protected
* @static
* @type Number
*/
M.block_navigation.expandablebranchcount = 1;
/**
* The maximum number of courses to show as part of a branch.
*
* @property courselimit
* @protected
* @static
* @type Number
*/
M.block_navigation.courselimit = 20;
/**
* Add new instance of navigation tree to tree collection
*
* @method init_add_tree
* @static
* @param {Object} properties
*/
M.block_navigation.init_add_tree = function(properties) {
if (properties.courselimit) {
this.courselimit = properties.courselimit;
}
new TREE(properties);
};
/**
* A 'actionkey' Event to help with Y.delegate().
* The event consists of the left arrow, right arrow, enter and space keys.
* More keys can be mapped to action meanings.
* actions: collapse , expand, toggle, enter.
*
* This event is delegated to branches in the navigation tree.
* The on() method to subscribe allows specifying the desired trigger actions as JSON.
*
* @namespace M.block_navigation
* @class ActionKey
*/
Y.Event.define("actionkey", {
// Webkit and IE repeat keydown when you hold down arrow keys.
// Opera links keypress to page scroll; others keydown.
// Firefox prevents page scroll via preventDefault() on either
// keydown or keypress.
_event: (Y.UA.webkit || Y.UA.ie) ? 'keydown' : 'keypress',