1/* ========================================================================== 2 Navigation behaviour. 3 4 The Solutions and About mega-menus open on hover and fold back up when the 5 pointer leaves. The rules: 6 open on mouseenter over the trigger, its panel, or the bridge between 7 them, no click needed 8 close on mouseleave, after a short grace delay so travelling across the 9 trigger / bridge / panel never flickers it shut. Also closes on: 10 clicking the trigger again, Escape, clicking outside the nav, 11 opening a different menu, or following a link 12 13 (An earlier version deliberately stayed open until clicked. That was reversed 14 at Owen's request: a menu that hangs open after the pointer has moved on is 15 the more annoying of the two.) 16 17 Keyboard users are not left out: the triggers are real <button> elements, so 18 Enter and Space toggle, and Escape closes and returns focus to the trigger. 19 The live site builds these out of href="#/" anchors, which do none of that. 20 ========================================================================== */ 21 22(function () { 23 'use strict'; 24 25 var MOBILE_BREAKPOINT = 900; 26 var header = document.querySelector('[data-header]'); 27 var dropdowns = Array.prototype.slice.call(document.querySelectorAll('[data-dropdown]')); 28 var burger = document.querySelector('[data-menu-toggle]'); 29 var nav = document.getElementById('primary-nav'); 30 var hoverCloseTimer; 31 32 if (!dropdowns.length && !burger) return; 33 34 var isMobile = function () { return window.innerWidth < MOBILE_BREAKPOINT; }; 35 36 function parts(dd) { 37 return { 38 trigger: dd.querySelector('.primary-nav__trigger'), 39 panel: dd.querySelector('.mega') 40 }; 41 } 42 43 function setOpen(dd, open) { 44 var p = parts(dd); 45 if (!p.trigger || !p.panel) return; 46 p.trigger.setAttribute('aria-expanded', open ? 'true' : 'false'); 47 dd.classList.toggle('is-open', open); 48 // `hidden` is toggled alongside the class so the panel is genuinely out of 49 // the accessibility tree when closed, not merely transparent. 50 if (open) { 51 p.panel.hidden = false; 52 } else { 53 // Wait for the transition before hiding, otherwise it snaps shut. 54 window.setTimeout(function () { 55 if (!dd.classList.contains('is-open')) p.panel.hidden = true; 56 }, 200); 57 } 58 } 59 60 function closeAll(except) { 61 dropdowns.forEach(function (dd) { 62 if (dd !== except) setOpen(dd, false); 63 }); 64 } 65 66 function isOpen(dd) { return dd.classList.contains('is-open'); } 67 68 dropdowns.forEach(function (dd) { 69 var p = parts(dd); 70 if (!p.trigger || !p.panel) return; 71 72 /* --- hover to open, fold back up on mouse-out. ---------------------- */ 73 dd.addEventListener('mouseenter', function () { 74 if (isMobile()) return; 75 window.clearTimeout(hoverCloseTimer); 76 closeAll(dd); 77 setOpen(dd, true); 78 }); 79 80 // Close on mouse-out, on a short grace delay so moving across the trigger, 81 // the invisible .has-menu::after bridge and the panel does not flicker it 82 // shut. Re-entering (mouseenter above) cancels the pending close. 83 dd.addEventListener('mouseleave', function () { 84 if (isMobile()) return; 85 window.clearTimeout(hoverCloseTimer); 86 hoverCloseTimer = window.setTimeout(function () { 87 setOpen(dd, false); 88 }, 150); 89 }); 90 91 /* --- click the trigger to toggle. This is the only pointer gesture that 92 closes a menu, which is exactly what was asked for. -------------- */ 93 p.trigger.addEventListener('click', function (e) { 94 e.preventDefault(); 95 e.stopPropagation(); 96 var open = isOpen(dd); 97 closeAll(dd); 98 setOpen(dd, !open); 99 }); 100 101 /* --- keyboard ------------------------------------------------------- */ 102 p.trigger.addEventListener('keydown', function (e) { 103 if (e.key === 'ArrowDown') { 104 e.preventDefault(); 105 closeAll(dd); 106 setOpen(dd, true); 107 var first = p.panel.querySelector('a'); 108 if (first) first.focus(); 109 } 110 }); 111 112 // Opening by keyboard focus would be surprising for someone tabbing
113 // straight past, so focus alone does not open. Arrow-down does. 114 p.panel.addEventListener('keydown', function (e) { 115 if (e.key === 'Escape') { 116 setOpen(dd, false); 117 p.trigger.focus(); 118 } 119 }); 120 121 // Following a link should not leave the menu hanging open behind the 122 // new page in a client-side-navigation future. 123 p.panel.addEventListener('click', function (e) { 124 if (e.target.closest('a')) setOpen(dd, false); 125 }); 126 }); 127 128 /* --- Escape anywhere, and click-outside ------------------------------- */ 129 document.addEventListener('keydown', function (e) { 130 if (e.key === 'Escape') closeAll(null); 131 }); 132 133 document.addEventListener('click', function (e) { 134 if (nav && !nav.contains(e.target) && !e.target.closest('[data-menu-toggle]')) { 135 closeAll(null); 136 } 137 }); 138 139 /* --- Mobile menu ------------------------------------------------------ */ 140 if (burger && nav) { 141 burger.addEventListener('click', function () { 142 var open = burger.getAttribute('aria-expanded') === 'true'; 143 burger.setAttribute('aria-expanded', open ? 'false' : 'true'); 144 document.body.classList.toggle('nav-open', !open); 145 if (open) closeAll(null); 146 }); 147 148 // On mobile the panels are accordions inside the drawer, so tapping a 149 // trigger toggles just that section. 150 dropdowns.forEach(function (dd) { 151 var p = parts(dd); 152 if (!p.panel) return; 153 p.panel.hidden = false; // CSS controls visibility in the drawer 154 }); 155 } 156 157 /* --- Sticky header shadow once scrolled ------------------------------- */ 158 if (header) { 159 var onScroll = function () { 160 header.classList.toggle('is-stuck', window.scrollY > 8); 161 }; 162 window.addEventListener('scroll', onScroll, { passive: true }); 163 onScroll(); 164 } 165 166 /* --- Reset state when crossing the breakpoint ------------------------- */ 167 var wasMobile = isMobile(); 168 window.addEventListener('resize', function () { 169 var nowMobile = isMobile(); 170 if (nowMobile !== wasMobile) { 171 wasMobile = nowMobile; 172 closeAll(null); 173 document.body.classList.remove('nav-open'); 174 if (burger) burger.setAttribute('aria-expanded', 'false'); 175 } 176 }); 177})();
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.