PageSourceSearch

https://law.rwu.edu/themes/custom/rwu/assets/js/header-accessibility-menu-esc.js?tltgc1

js rwu.edu collected 2026-09-24 08:38:06 UTC 14,413 bytes, 399 lines download raw bytes

1(function ($, Drupal, once) {
2  Drupal.behaviors.hamburgerMenuAccessibility = {
3    attach: function (context) {
4      
5      // Detect mobile/tablet
6      var isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
7      var isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
8      
9      // ========================================
10      // MAIN MENU TOGGLE (#menu-toggle)
11      // ========================================
12      once('main-menu-accessibility', '#menu-toggle', context).forEach(function(menuToggleButton) {
13        
14        var $menuToggle = $(menuToggleButton);
15        var $mainMenu = $('#main-menu');
16        var isUsingKeyboard = false;
17        var keyboardTimeout;
18
19        if (!$mainMenu.length) {
20          return;
21        }
22
23        // Logo and search sit between the toggle and the menu in the DOM.
24        // While the menu is open, they should be skipped so Tab goes
25        // straight from the toggle into the menu items.
26        var $headerRow = $mainMenu.parent();
27        var $skipWhileMenuOpen = $headerRow.find('.l-header__logo a')
28          .add($headerRow.children('.header__super').find('.search-toggle'));
29
30        // Track keyboard usage
31        $(document).on('keydown.mainMenuInput', function() {
32          isUsingKeyboard = true;
33          clearTimeout(keyboardTimeout);
34          keyboardTimeout = setTimeout(function() {
35            isUsingKeyboard = false;
36          }, 2000);
37        });
38
39        $(document).on('mousedown.mainMenuInput touchstart.mainMenuInput', function() {
40          isUsingKeyboard = false;
41          clearTimeout(keyboardTimeout);
42        });
43
44        // Close function
45        var closeMainMenu = function() {
46          $menuToggle.removeClass('open');
47          $mainMenu.slideUp(300);
48          $menuToggle.attr('aria-expanded', 'false');
49          $skipWhileMenuOpen.removeAttr('tabindex');
50          if (isMobile || isTouchDevice) {
51            $('body').css('overflow', '');
52          }
53          isUsingKeyboard = false;
54        };
55
56        // Open function
57        var openMainMenu = function() {
58          $menuToggle.addClass('open');
59          $mainMenu.slideDown(300);
60          $menuToggle.attr('aria-expanded', 'true');
61          $skipWhileMenuOpen.attr('tabindex', '-1');
62          if (isMobile || isTouchDevice) {
63            $('body').css('overflow', 'hidden');
64          }
65
66          // Move focus straight into the menu so Tab doesn't have to
67          // pass back through the logo/search first.
68          setTimeout(function() {
69            var $firstFocusable = $mainMenu.find('a, button, [tabindex]:not([tabindex="-1"])').first();
70            if ($firstFocusable.length) {
71              $firstFocusable.trigger('focus');
72            }
73          }, 50);
74        };
75
76        // Click handler
77        $menuToggle.on('click.mainMenuToggle', function(e) {
78          e.preventDefault();
79          e.stopPropagation();
80          
81          var isOpen = $menuToggle.hasClass('open');
82          if (isOpen) {
83            closeMainMenu();
84          } else {
85            openMainMenu();
86          }
87        });
88
89        // ESC key handlers
90        $menuToggle.on('keydown.mainMenuEsc', function(event) {
91          if (event.key === 'Escape' || event.key === 'Esc' || event.keyCode === 27) {
92            if ($menuToggle.hasClass('open')) {
93              event.preventDefault();
94              event.stopPropagation();
95              closeMainMenu();
96              $menuToggle.trigger('focus');
97            }
98          }
99        });
100
101        $mainMenu.on('keydown.mainMenuEsc', 'a, button, [tabindex]', function(event) {
102          if (event.key === 'Escape' || event.key === 'Esc' || event.keyCode === 27) {
103            if ($menuToggle.hasClass('open')) {
104              event.preventDefault();
105              event.stopPropagation();
106              closeMainMenu();
107              setTimeout(function() {
108                $menuToggle.trigger('focus');
109              }, 50);
110            }
111          }
112        });
113
114        $(document).on('keydown.mainMenuEscGlobal', function(event) {
115          if (event.key === 'Escape' || event.key === 'Esc' || event.keyCode === 27) {
116            if ($menuToggle.hasClass('open')) {
117              event.preventDefault();
118              closeMainMenu();
119              setTimeout(function() {
120                $menuToggle.trigger('focus');
121              }, 50);
122            }
123          }
124        });
125
126        // Auto-close on tab past last item.
127        // This whole block only makes sense while the mobile #menu-toggle
128        // drawer is actually open ($menuToggle has class "open"). Without
129        // that guard, this fired on ANY Tab press landing on whatever
130        // element happened to be #main-menu's last focusable descendant -
131        // including, on desktop, elements inside the separate audience-menu
132        // hamburger widget (button#hamburger / #secondary-nav), which is
133        // nested inside #main-menu purely for layout. That incorrectly
134        // slid up (hid) the entire main nav any time a keyboard user tabbed
135        // near the audience menu, even though the mobile drawer was never
136        // opened in the first place.
137        var $focusableItems = $mainMenu.find('a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])')
138          .not('.audience-menu, .audience-menu *');
139        if ($focusableItems.length > 0) {
140          var $lastItem = $focusableItems.last();
141          $lastItem.on('keydown', function(event) {
142            if (event.key === 'Tab' && !event.shiftKey && $menuToggle.hasClass('open')) {
143              setTimeout(function() {
144                closeMainMenu();
145              }, 100);
146            }
147          });
148        }
149
150        // Click outside
151        $(document).on('click.mainMenuOutside', function(event) {
152          if (isUsingKeyboard) return;
153          if ($menuToggle.hasClass('open')) {
154            if (!$(event.target).closest('#menu-toggle').length && 
155                !$(event.target).closest('#main-menu').length) {
156              closeMainMenu();
157            }
158          }
159        });
160
161        // Mobile features
162        if (isMobile || isTouchDevice) {
163          var scrollTimeout;
164          var lastScrollTop = 0;
165          
166          $(window).on('scroll.mainMenu', function() {
167            if (isUsingKeyboard) return;
168            var st = $(this).scrollTop();
169            if (Math.abs(lastScrollTop - st) > 50) {
170              clearTimeout(scrollTimeout);
171              scrollTimeout = setTimeout(function() {
172                if ($menuToggle.hasClass('open') && !isUsingKeyboard) {
173                  closeMainMenu();
174                }
175              }, 150);
176            }
177            lastScrollTop = st;
178          });
179
180          $(document).on('touchstart.mainMenu', function(event) {
181            if (isUsingKeyboard) return;
182            if ($menuToggle.hasClass('open')) {
183              if (!$(event.target).closest('#menu-toggle').length && 
184                  !$(event.target).closest('#main-menu').length) {
185                closeMainMenu();
186              }
187            }
188          });
189
190          $(window).on('orientationchange.mainMenu', function() {
191            if ($menuToggle.hasClass('open')) {
192              closeMainMenu();
193            }
194          });
195        }
196
197      }); // End main menu
198
199      // ========================================
200      // AUDIENCE MENU (button#hamburger.hamburger)
201      // ========================================
202      // Note: HTML has data-once="hamburger-accessibility" so we use that ID
203      once('hamburger-accessibility', 'button#hamburger.hamburger', context).forEach(function(hamburgerButton) {
204        
205        var $hamburger = $(hamburgerButton);
206        var $secondaryNav = $('#secondary-nav');
207        var isUsingKeyboard = false;
208        var keyboardTimeout;
209        
210        if (!$secondaryNav.length) {
211          return;
212        }
213        
214        // Set initial ARIA state
215        if ($secondaryNav.hasClass('hidden')) {
216          $hamburger.attr('aria-expanded', 'false');
217        } else {
218          $hamburger.attr('aria-expanded', 'true');
219        }
220
221        // Track keyboard usage
222        $(document).on('keydown.hamburgerInput', function() {
223          isUsingKeyboard = true;
224          clearTimeout(keyboardTimeout);
225          // Reset after 2 seconds of no keyboard activity
226          keyboardTimeout = setTimeout(function() {
227            isUsingKeyboard = false;
228          }, 2000);
229        });
230
231        // Track mouse/touch usage
232        $(document).on('mousedown.hamburgerInput touchstart.hamburgerInput', function() {
233          isUsingKeyboard = false;
234          clearTimeout(keyboardTimeout);
235        });
236
237        // Function to close the hamburger menu
238        var closeHamburgerMenu = function() {
239          $secondaryNav.addClass('hidden').removeClass('active');
240          $hamburger.attr('aria-expanded', 'false');
241          
242          // Restore body scroll on mobile
243          if (isMobile || isTouchDevice) {
244            $('body').css('overflow', '');
245          }
246          
247          // Reset keyboard tracking
248          isUsingKeyboard = false;
249        };
250
251        // Function to open the hamburger menu
252        var openHamburgerMenu = function() {
253          $secondaryNav.removeClass('hidden').addClass('active');
254          $hamburger.attr('aria-expanded', 'true');
255          
256          // Prevent body scroll on mobile
257          if (isMobile || isTouchDevice) {
258            $('body').css('overflow', 'hidden');
259          }
260        };
261
262        // Click/tap handler for hamburger button
263        $hamburger.on('click.hamburgerToggle', function(e) {
264          e.preventDefault();
265          e.stopPropagation();
266          
267          var isHidden = $secondaryNav.hasClass('hidden');
268          
269          if (isHidden) {
270            openHamburgerMenu();
271          } else {
272            closeHamburgerMenu();
273          }
274        });
275
276        // ESC key handler on hamburger button
277        $hamburger.on('keydown.hamburgerEsc', function(event) {
278          if (event.key === 'Escape' || event.key === 'Esc' || event.keyCode === 27) {
279            if (!$secondaryNav.hasClass('hidden')) {
280              event.preventDefault();
281              event.stopPropagation();
282              closeHamburgerMenu();
283              // Return focus to hamburger button
284              $hamburger.trigger('focus');
285            }
286          }
287        });
288
289        // ESC key handler on secondary nav items
290        $secondaryNav.on('keydown.hamburgerEsc', 'a, button, [tabindex]', function(event) {
291          if (event.key === 'Escape' || event.key === 'Esc' || event.keyCode === 27) {
292            event.preventDefault();
293            event.stopPropagation();
294            closeHamburgerMenu();
295            // Return focus to hamburger button
296            setTimeout(function() {
297              $hamburger.trigger('focus');
298            }, 50);
299          }
300        });
301
302        // Global ESC key handler (catches ESC anywhere when menu is open)
303        $(document).on('keydown.hamburgerEsc', function(event) {
304          if (event.key === 'Escape' || event.key === 'Esc' || event.keyCode === 27) {
305            if (!$secondaryNav.hasClass('hidden')) {
306              event.preventDefault();
307              closeHamburgerMenu();
308              setTimeout(function() {
309                $hamburger.trigger('focus');
310              }, 50);
311            }
312          }
313        });
314
315        // Auto-close when tabbing past last item
316        var $focusableItems = $secondaryNav.find('a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])');
317        
318        if ($focusableItems.length > 0) {
319          var $lastItem = $focusableItems.last();
320          
321          $lastItem.on('keydown', function(event) {
322            // If Tab key (not Shift+Tab) is pressed on last item
323            if (event.key === 'Tab' && !event.shiftKey) {
324              // Close the menu after a short delay to allow natural tab flow
325              setTimeout(function() {
326                closeHamburgerMenu();
327              }, 100);
328            }
329          });
330        }
331
332        // Close menu when clicking outside (but not when using keyboard)
333        $(document).on('click.hamburgerOutside', function(event) {
334          // Don't auto-close if user is navigating with keyboard
335          if (isUsingKeyboard) {
336            return;
337          }
338          
339          if (!$secondaryNav.hasClass('hidden')) {
340            if (!$(event.target).closest('.audience-menu').length && 
341                !$(event.target).closest('#secondary-nav').length) {
342              closeHamburgerMenu();
343            }
344          }
345        });
346
347        // Mobile/Tablet specific features
348        if (isMobile || isTouchDevice) {
349          
350          // Close on scroll (but not when using keyboard)
351          var scrollTimeout;
352          var lastScrollTop = 0;
353          $(window).on('scroll.hamburgerMenu', function() {
354            // Don't auto-close if user is navigating with keyboard
355            if (isUsingKeyboard) {
356              return;
357            }
358            
359            var st = $(this).scrollTop();
360            
361            if (Math.abs(lastScrollTop - st) > 50) {
362              clearTimeout(scrollTimeout);
363              scrollTimeout = setTimeout(function() {
364                if (!$secondaryNav.hasClass('hidden') && !isUsingKeyboard) {
365                  closeHamburgerMenu();
366                }
367              }, 150);
368            }
369            lastScrollTop = st;
370          });
371
372          // Close on touch outside (but not when using keyboard)
373          $(document).on('touchstart.hamburgerMenu', function(event) {
374            // Don't auto-close if user is navigating with keyboard
375            if (isUsingKeyboard) {
376              return;
377            }
378            
379            if (!$secondaryNav.hasClass('hidden')) {
380              if (!$(event.target).closest('.audience-menu').length && 
381                  !$(event.target).closest('#secondary-nav').length) {
382                closeHamburgerMenu();
383              }
384            }
385          });
386
387          // Handle orientation change
388          $(window).on('orientationchange.hamburgerMenu', function() {
389            if (!$secondaryNav.hasClass('hidden')) {
390              closeHamburgerMenu();
391            }
392          });
393        }
394
395      }); // End forEach
396
397    }
398  };
399})(jQuery, Drupal, once);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.