[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