[LON-CAPA-cvs] cvs: loncom /interface loncommon.pm lonmenu.pm

raeburn raeburn at source.lon-capa.org
Thu Nov 20 12:06:26 EST 2025


raeburn		Thu Nov 20 17:06:26 2025 EDT

  Modified files:              
    /loncom/interface	loncommon.pm lonmenu.pm 
  Log:
  - WCAG 2 compliance.
    Access to primary and secondary menu items with hoverable drop-down lists 
    by keyboard actions with aria-expanded attribute updated accordingly.
  
  
-------------- next part --------------
Index: loncom/interface/loncommon.pm
diff -u loncom/interface/loncommon.pm:1.1482 loncom/interface/loncommon.pm:1.1483
--- loncom/interface/loncommon.pm:1.1482	Sun Nov  2 01:42:00 2025
+++ loncom/interface/loncommon.pm	Thu Nov 20 17:06:26 2025
@@ -1,7 +1,7 @@
 # The LearningOnline Network with CAPA
 # a pile of common routines
 #
-# $Id: loncommon.pm,v 1.1482 2025/11/02 01:42:00 raeburn Exp $
+# $Id: loncommon.pm,v 1.1483 2025/11/20 17:06:26 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -7000,6 +7000,138 @@
 
     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");
+        }
+      }, 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;
+        }
+      });
+      el.addEventListener("blur", function(event) {
+        tabtimer = setTimeout(function() {
+          var opennav = document.querySelector("#LC_nav_bar .LC_hoverable.LC_open");
+          if (opennav) {
+            opennav.className = "LC_hoverable";
+            opennav.querySelector('a').setAttribute('aria-expanded', "false");
+          }
+        }, 10);
+      });
+    });
+  });
+  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) {
+        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";
+          }
+        });
+      });
+      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");
+          }
+        }, 10);
+        tabTimeoutIds.push(timeoutId);
+      });
+    });
+  });
+});
+// ]]>
+</script>
+ENDJS
+        $bodytag .= "\n$dropdownjs\n";
     }
 
     my $showcrstitle = 1;
@@ -8880,7 +9012,7 @@
   top: 0;
 }
 
-ol.LC_primary_menu li:hover > ul, ol.LC_primary_menu li.hover > ul {
+ol.LC_primary_menu li:hover > ul, ol.LC_primary_menu li.hover > ul, ol.LC_primary_menu li.LC_open > ul { 
   display: block;
   position: absolute;
   margin: 0;
@@ -8888,7 +9020,7 @@
   z-index: 2;
 }
 
-ol.LC_primary_menu li:hover li, ol.LC_primary_menu li.hover li {
+ol.LC_primary_menu li:hover li, ol.LC_primary_menu li.hover li, ol.LC_primary_menu li.LC_open li {
 /* First Submenu -> size should be smaller than the menu title of the whole menu */
   font-size: 90%;
   vertical-align: top;
@@ -8912,7 +9044,7 @@
 }
 
 /* Font-size equal to the size of the predecessors*/
-ol.LC_primary_menu li:hover li li {
+ol.LC_primary_menu li:hover li li, ol.LC_primary_menu li.LC_open li li {
   font-size: 100%;
 }
 
@@ -8975,7 +9107,7 @@
   float: left;
 }
 
-ul#LC_secondary_menu li.LC_hoverable:hover, ul#LC_secondary_menu li.hover {
+ul#LC_secondary_menu li.LC_hoverable:hover, ul#LC_secondary_menu li.hover, ul#LC_secondary_menu li.LC_open {
   background-color: $data_table_light;
 }
 
@@ -8987,7 +9119,7 @@
   display: none;
 }
 
-ul#LC_secondary_menu li:hover ul, ul#LC_secondary_menu li.hover ul {
+ul#LC_secondary_menu li:hover ul, ul#LC_secondary_menu li.hover ul, ul#LC_secondary_menu li.LC_open ul {
   display: block;
   position: absolute;
   margin: 0;
@@ -9009,7 +9141,7 @@
   float: none;
 }
 
-ul#LC_secondary_menu li ul li:hover, ul#LC_secondary_menu li ul li.hover {
+ul#LC_secondary_menu li ul li:hover, ul#LC_secondary_menu li ul li.hover, ul#LC_secondary_menu li ul li.LC_open {
   background-color: $data_table_dark;
 }
 
Index: loncom/interface/lonmenu.pm
diff -u loncom/interface/lonmenu.pm:1.564 loncom/interface/lonmenu.pm:1.565
--- loncom/interface/lonmenu.pm:1.564	Sun Sep  7 04:21:13 2025
+++ loncom/interface/lonmenu.pm	Thu Nov 20 17:06:26 2025
@@ -1,7 +1,7 @@
 # The LearningOnline Network with CAPA
 # Routines to control the menu
 #
-# $Id: lonmenu.pm,v 1.564 2025/09/07 04:21:13 raeburn Exp $
+# $Id: lonmenu.pm,v 1.565 2025/11/20 17:06:26 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -814,7 +814,9 @@
     if (($target ne '') && ($link ne '#')) {
         $targetattr = ' target="'.$target.'"';
     }
-    my $menu = '<li class="LC_hoverable '.$addclass.'">'.
+    my $cssclass = "LC_hoverable $addclass";
+    $cssclass =~ s/\s+$//;
+    my $menu = '<li class="'.$cssclass.'" aria-expanded="false">'.
                '<a href="'.$link.'"'.$targetattr.'>'.
                '<span class="LC_nobreak">'.$title.
                '<span class="LC_fontsize_medium" style="font-weight:normal;">'.


More information about the LON-CAPA-cvs mailing list