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.