[LON-CAPA-cvs] cvs: loncom /interface loncommon.pm lonmenu.pm
raeburn
raeburn at source.lon-capa.org
Tue Nov 25 11:54:00 EST 2025
raeburn Tue Nov 25 16:54:00 2025 EDT
Modified files:
/loncom/interface lonmenu.pm loncommon.pm
Log:
- WCAG 2 compliance
Set aria-expanded attribiute for a tag in list item, not li tag itself.
Eliminate duplicate javascript code supporting keyboard access (enter/tab)
for primary or secondary menu items with sub-menus, and also roles options
in Functions bar on Courses/Roles page in browser.mobile mode.
-------------- next part --------------
Index: loncom/interface/lonmenu.pm
diff -u loncom/interface/lonmenu.pm:1.565 loncom/interface/lonmenu.pm:1.566
--- loncom/interface/lonmenu.pm:1.565 Thu Nov 20 17:06:26 2025
+++ loncom/interface/lonmenu.pm Tue Nov 25 16:54:00 2025
@@ -1,7 +1,7 @@
# The LearningOnline Network with CAPA
# Routines to control the menu
#
-# $Id: lonmenu.pm,v 1.565 2025/11/20 17:06:26 raeburn Exp $
+# $Id: lonmenu.pm,v 1.566 2025/11/25 16:54:00 raeburn Exp $
#
# Copyright Michigan State University Board of Trustees
#
@@ -814,10 +814,15 @@
if (($target ne '') && ($link ne '#')) {
$targetattr = ' target="'.$target.'"';
}
- my $cssclass = "LC_hoverable $addclass";
- $cssclass =~ s/\s+$//;
- my $menu = '<li class="'.$cssclass.'" aria-expanded="false">'.
- '<a href="'.$link.'"'.$targetattr.'>'.
+ my $cssclass = 'LC_hoverable';
+ if ($addclass) {
+ $addclass =~ s/^\s+|\s+$//g;
+ if ($addclass) {
+ $cssclass .= " $addclass";
+ }
+ }
+ my $menu = '<li class="'.$cssclass.'">'.
+ '<a href="'.$link.'"'.$targetattr.' aria-expanded="false">'.
'<span class="LC_nobreak">'.$title.
'<span class="LC_fontsize_medium" style="font-weight:normal;">'.
' ▼</span></span></a>'.
@@ -835,7 +840,7 @@
# build the dropdown (and nested submenus) recursively
# see perldoc create_submenu documentation for further information
sub build_submenu {
- my ($target, $submenu, $translate, $first_level, $listclass, $linkattr) = @_;
+ my ($target, $submenu, $translate, $first_level, $listclass, $linkattr) = @_;
unless (@{$submenu}) {
return '';
}
@@ -872,7 +877,7 @@
$menu .= '<ul>';
$menu .= &build_submenu($target, $href, $translate);
$menu .= '</ul>';
- $menu .= '</li>';
+ $menu .= '</li>';
} else { # href is the actual hyperlink and does not represent another submenu
# for the current menu title
if ($href =~ /(aboutme|rss\.html)$/) {
Index: loncom/interface/loncommon.pm
diff -u loncom/interface/loncommon.pm:1.1483 loncom/interface/loncommon.pm:1.1484
--- loncom/interface/loncommon.pm:1.1483 Thu Nov 20 17:06:26 2025
+++ loncom/interface/loncommon.pm Tue Nov 25 16:54:00 2025
@@ -1,7 +1,7 @@
# The LearningOnline Network with CAPA
# a pile of common routines
#
-# $Id: loncommon.pm,v 1.1483 2025/11/20 17:06:26 raeburn Exp $
+# $Id: loncommon.pm,v 1.1484 2025/11/25 16:54:00 raeburn Exp $
#
# Copyright Michigan State University Board of Trustees
#
@@ -6998,140 +6998,130 @@
return $bodytag;
}
+ my $dropdownjs;
if ($public) {
undef($role);
- } else {
- my $dropdownjs = <<ENDJS;
-<script type="text/javascript">
-// <![CDATA[
-
-document.addEventListener("DOMContentLoaded", function() {
- var menuItems1 = document.querySelectorAll('#LC_nav_bar > ol > li.LC_hoverable');
- var hovertimer, tabtimer;
-
- Array.prototype.forEach.call(menuItems1, function(el, i){
- el.addEventListener("mouseover", function(event){
- this.className = "LC_hoverable LC_open";
- clearTimeout(hovertimer);
- });
- el.addEventListener("mouseout", function(event){
- hovertimer = setTimeout(function(event) {
- var opennav = document.querySelector("#LC_nav_bar .LC_hoverable.LC_open");
- if (opennav) {
- opennav.className = "LC_hoverable";
- opennav.querySelector('a').setAttribute('aria-expanded', "false");
+ }
+ my @hasdropdowns;
+ unless ($args->{'no_primary_menu'}) {
+ push(@hasdropdowns,'#LC_nav_bar > ol >');
+ }
+ unless ($args->{'no_secondary_menu'}) {
+ push(@hasdropdowns,'ul#LC_secondary_menu');
+ }
+ if (($env{'request.noversionuri'} eq '/adm/roles') && ($env{'browser.mobile'}) &&
+ (!$env{'form.selectrole'})) {
+ push(@hasdropdowns,'div#LC_breadcrumbs ol.LC_primary_menu');
+ }
+ if (@hasdropdowns) {
+ my $jsarray = '["'.join('","', at hasdropdowns).'"]';
+ $dropdownjs = <<ENDJS;
+ const ancestorArray = $jsarray;
+ for (var i = 0; i < ancestorArray.length; i++) {
+ const anc = ancestorArray[i];
+ var menuItems = document.querySelectorAll(anc+" li.LC_hoverable");
+ let hoverTimeoutIds = [];
+ let tabTimeoutIds = [];
+
+ Array.prototype.forEach.call(menuItems, function(el, i){
+ el.addEventListener("mouseover", function(event){
+ while (hoverTimeoutIds.length > 0) {
+ const timeoutId = hoverTimeoutIds.pop();
+ clearTimeout(timeoutId);
}
- }, 1000);
- });
- el.querySelector('a').addEventListener("click", function(event) {
- if (this.parentNode.className == "LC_hoverable") {
- this.parentNode.className = "LC_hoverable LC_open";
- this.setAttribute('aria-expanded', "true");
- } else {
- this.parentNode.className = "LC_hoverable";
- this.setAttribute('aria-expanded', "false");
- }
- event.preventDefault();
- });
- var links = el.querySelectorAll('a');
- Array.prototype.forEach.call(links, function(el, i) {
- el.addEventListener("focus", function(event) {
- if (tabtimer) {
- clearTimeout(tabtimer);
- tabtimer = null;
+ document.querySelectorAll(anc+" li.LC_hoverable.LC_open").forEach(element => {
+ element.classList.remove("LC_open");
+ });
+ if (!hasClass(this,"LC_open")) {
+ this.classList.add("LC_open");
}
});
- el.addEventListener("blur", function(event) {
- tabtimer = setTimeout(function() {
- var opennav = document.querySelector("#LC_nav_bar .LC_hoverable.LC_open");
+ el.addEventListener("mouseout", function(event) {
+ const timeoutId = setTimeout(function() {
+ var opennav = document.querySelector(anc+" li.LC_hoverable.LC_open");
if (opennav) {
- opennav.className = "LC_hoverable";
+ opennav.classList.remove("LC_open");
opennav.querySelector('a').setAttribute('aria-expanded', "false");
}
- }, 10);
+ }, 500);
+ hoverTimeoutIds.push(timeoutId);
});
- });
- });
- var menuItems2 = document.querySelectorAll("ul#LC_secondary_menu li.LC_hoverable");
- let hoverTimeoutIds = [];
- let tabTimeoutIds = [];
-
- Array.prototype.forEach.call(menuItems2, function(el, i){
- el.addEventListener("mouseover", function(event){
- while (hoverTimeoutIds.length > 0) {
- const timeoutId = hoverTimeoutIds.pop();
- clearTimeout(timeoutId);
- }
- document.querySelectorAll("ul#LC_secondary_menu li.LC_hoverable.LC_open").forEach(element => {
- element.className = "LC_hoverable";
- });
- this.className = "LC_hoverable LC_open";
- });
- el.addEventListener("mouseout", function(event) {
- const timeoutId = setTimeout(function() {
- var opennav = document.querySelector("ul#LC_secondary_menu li.LC_hoverable.LC_open");
- if (opennav) {
- opennav.className = "LC_hoverable";
- opennav.querySelector('a').setAttribute('aria-expanded', "false");
- }
- }, 500);
- hoverTimeoutIds.push(timeoutId);
- });
- el.querySelector('a').addEventListener("click", function(event){
- if (tabTimeoutIds.length) {
- while (tabTimeoutIds.length > 0) {
- const timeoutId = tabTimeoutIds.pop();
- clearTimeout(timeoutId);
- }
- }
- document.querySelectorAll("ul#LC_secondary_menu li.LC_hoverable.LC_open").forEach(element => {
- if (element !== this.parentNode) {
- element.className = "LC_hoverable";
- }
- });
- if (this.parentNode.className == "LC_hoverable") {
- this.parentNode.className = "LC_hoverable LC_open";
- this.setAttribute('aria-expanded', "true");
- } else {
- this.parentNode.className = "LC_hoverable";
- this.setAttribute('aria-expanded', "false");
- }
- event.preventDefault();
- });
-
- var links = el.querySelectorAll('ul#LC_secondary_menu li a');
- var numlinks = links.length;
- Array.prototype.forEach.call(links, function(el, i){
- el.addEventListener("focus", function(event) {
+ el.querySelector('a').addEventListener("click", function(event){
if (tabTimeoutIds.length) {
while (tabTimeoutIds.length > 0) {
const timeoutId = tabTimeoutIds.pop();
clearTimeout(timeoutId);
}
}
- document.querySelectorAll("ul#LC_secondary_menu li.LC_hoverable.LC_open").forEach(element => {
- if (element !== event.target.closest('li.LC_hoverable.LC_open')) {
- element.className = "LC_hoverable";
+ document.querySelectorAll(anc+" li.LC_hoverable.LC_open").forEach(element => {
+ if (element !== this.parentNode) {
+ element.classList.remove("LC_open");
}
});
+ if (hasClass(this.parentNode,"LC_hoverable")) {
+ if (!hasClass(this.parentNode,"LC_open")) {
+ this.parentNode.classList.add("LC_open");
+ }
+ this.setAttribute('aria-expanded', "true");
+ } else {
+ this.parentNode.add("LC_hoverable");
+ this.setAttribute('aria-expanded', "false");
+ }
+ event.preventDefault();
});
- el.addEventListener("blur", function(event) {
- const timeoutId = setTimeout(function () {
- var opennav = document.querySelector("ul#LC_secondary_menu li.LC_hoverable.LC_open");
- if (opennav) {
- opennav.className = "LC_hoverable";
- opennav.querySelector('a').setAttribute('aria-expanded',"false");
+
+ var links = el.querySelectorAll(anc+' li a');
+ var numlinks = links.length;
+ Array.prototype.forEach.call(links, function(el, i){
+ el.addEventListener("focus", function(event) {
+ if (tabTimeoutIds.length) {
+ while (tabTimeoutIds.length > 0) {
+ const timeoutId = tabTimeoutIds.pop();
+ clearTimeout(timeoutId);
+ }
}
- }, 10);
- tabTimeoutIds.push(timeoutId);
+ document.querySelectorAll(anc+" li.LC_hoverable.LC_open").forEach(element => {
+ if (element !== event.target.closest('li.LC_hoverable.LC_open')) {
+ element.classList.remove("LC_open");
+ }
+ });
+ });
+ el.addEventListener("blur", function(event) {
+ const timeoutId = setTimeout(function () {
+ var opennav = document.querySelector(anc+" li.LC_hoverable.LC_open");
+ if (opennav) {
+ opennav.classList.remove("LC_open");
+ opennav.querySelector('a').setAttribute('aria-expanded',"false");
+ }
+ }, 10);
+ tabTimeoutIds.push(timeoutId);
+ });
});
});
- });
+ }
+
+ENDJS
+ }
+ if ($dropdownjs) {
+ $bodytag .= <<ENDJS;
+
+<script type="text/javascript">
+// <![CDATA[
+document.addEventListener("DOMContentLoaded", function() {
+$dropdownjs
});
+
+function hasClass(el, className) {
+ if (el.classList) {
+ return el.classList.contains(className);
+ } else {
+ return new RegExp('(^| )' + className + '( |$)', 'gi').test(el.className);
+ }
+}
// ]]>
</script>
+
ENDJS
- $bodytag .= "\n$dropdownjs\n";
}
my $showcrstitle = 1;
More information about the LON-CAPA-cvs
mailing list