1/* nav.js 2 Shared navigation + UI behaviors for prototype pages. 3 Requires: bootstrap.bundle.min.js loaded BEFORE this file. 4*/ 5 6function closeStandardSearch(options) { 7 var searchDropdown = options && options.searchDropdown; 8 var searchInput = options && options.searchInput; 9 10 if (searchDropdown && typeof searchDropdown.hide === "function") { 11 searchDropdown.hide(); 12 } 13 14 if (searchInput && typeof searchInput.focus === "function") { 15 searchInput.focus(); 16 } 17} 18 19if (typeof module !== "undefined" && module.exports) { 20 module.exports = { 21 closeStandardSearch, 22 }; 23} 24 25(() => { 26 // Guard: bootstrap must exist 27 if (!window.bootstrap) { 28 console.warn("nav.js: bootstrap not found. Load bootstrap.bundle.min.js first."); 29 return; 30 } 31 32 // ------------------------------ 33 // Mega: left-rail swaps (Products + Resources + Chips) 34 // ------------------------------ 35 (() => { 36 // Support multiple mega menus (future-proof): run for each .c-mega on the page 37 const megaRoots = Array.from(document.querySelectorAll(".c-mega")); 38 if (!megaRoots.length) return; 39 40 megaRoots.forEach((megaRoot) => { 41 const railItems = Array.from(megaRoot.querySelectorAll("[data-mega-target]")); 42 const categories = Array.from(megaRoot.querySelectorAll(".c-mega__category[data-mega-id]")); 43 const asideSets = Array.from(megaRoot.querySelectorAll(".c-mega__asideSet[data-mega-aside]")); 44 45 // Chip sets (static HTML, toggled like categories) 46 const chipSets = Array.from(megaRoot.querySelectorAll(".c-mega__chipSet[data-mega-chips-id]")); 47 48 function setActiveRail(targetId) { 49 railItems.forEach((btn) => { 50 const isActive = btn.getAttribute("data-mega-target") === targetId; 51 btn.classList.toggle("is-active", isActive); 52 }); 53 } 54 55 function setActiveCategory(targetId) { 56 categories.forEach((panel) => { 57 const isMatch = panel.getAttribute("data-mega-id") === targetId; 58 panel.classList.toggle("is-active", isMatch); 59 }); 60 } 61 62 function setActiveAside(targetId) { 63 if (!asideSets.length) return; 64 65 asideSets.forEach((panel) => { 66 const isMatch = panel.getAttribute("data-mega-aside") === targetId; 67 panel.classList.toggle("is-active", isMatch); 68 }); 69 } 70 71 function setActiveChips(targetId) { 72 if (!chipSets.length) return; 73 74 chipSets.forEach((set) => { 75 const isMatch = set.getAttribute("data-mega-chips-id") === targetId; 76 set.classList.toggle("is-active", isMatch); 77 }); 78 } 79 80 function activate(targetId) { 81 if (!targetId) return;
82 setActiveRail(targetId); 83 setActiveCategory(targetId); 84 setActiveAside(targetId); 85 setActiveChips(targetId); 86 } 87 88 railItems.forEach((btn) => { 89 btn.addEventListener("click", () => { 90 const targetId = btn.getAttribute("data-mega-target"); 91 activate(targetId); 92 }); 93 }); 94 95 const initial = 96 railItems.find((b) => b.classList.contains("is-active"))?.getAttribute("data-mega-target") || 97 railItems[0]?.getAttribute("data-mega-target"); 98 99 activate(initial); 100 101 // ------------------------------ 102 // Keyboard model (a11y spec, 1.9): 103 // Tab (L1) confined to the product items â cycles the rail; 104 // the panel previews as each item takes focus 105 // Enter dives into the L2 product-category items and traps 106 // Tab there: products â promo + resources â the 107 // "I need toâ¦" action chips, then wraps 108 // Escape one level up â L2 â its L1 product item; L1 â 109 // menu closes, focus returns to Explore 110 // 111 // DOM order puts the active panel right after the rail, so 112 // without interception Tab would drift from the last group into 113 // whatever panel happens to be open. Panel content and chips are 114 // reachable only inside the L2 trap (the chip set swaps with the 115 // category, so it belongs to that context). 116 // ------------------------------ 117 const railStops = Array.from(megaRoot.querySelectorAll(".c-mega__rail-item")); 118 let panelMode = false; 119 120 function focusables(container) { 121 if (!container) return []; 122 return Array.from( 123 container.querySelectorAll('a[href], button:not([disabled]), [tabindex]:not([tabindex="-1"])') 124 ).filter((el) => el.offsetParent !== null); 125 } 126 127 function contentFocusables(targetId) { 128 const panel = megaRoot.querySelector('.c-mega__category[data-mega-id="' + targetId + '"]'); 129 const aside = megaRoot.querySelector('.c-mega__asideSet[data-mega-aside="' + targetId + '"]'); 130 return focusables(panel).concat(focusables(aside)); 131 } 132 133 function chipsFocusables() { 134 return focusables(megaRoot.querySelector(".c-mega__chipsrow")); 135 } 136 137 function activeTargetId() { 138 const btn = railItems.find((b) => b.classList.contains("is-active")); 139 return btn ? btn.getAttribute("data-mega-target") : null; 140 } 141 142 // Selection follows focus: the panel previews as the user tabs 143 // across groups, so Enter always opens what they're seeing. 144 railItems.forEach((btn) => { 145 btn.addEventListener("focus", () => { 146 panelMode = false; 147 activate(btn.getAttribute("data-mega-target")); 148 }); 149 }); 150 151 // Any close (Escape at group level, outside click, re-toggle) 152 // resets the panel trap. 153 const dropdownParent = megaRoot.closest(".dropdown") || megaRoot; 154 dropdownParent.addEventListener("hidden.bs.dropdown", () => { 155 panelMode = false; 156 }); 157 158 // WINDOW capture: Bootstrap 5.0.2 registers its delegated 159 // dropdown keydown handler at DOCUMENT capture (EventHandler 160 // passes the delegation selector as addEventListener's third 161 // argument â truthy = capture), where it preventDefaults + 162 // stopPropagations Escape for anything inside an open 163 // .dropdown-menu before any deeper listener runs. Window capture 164 // is the only node ahead of it, so the menu's keyboard model is 165 // handled here and stops propagation before Bootstrap acts. 166 window.addEventListener("keydown", (e) => { 167 // Scope: only while THIS mega menu is open and owns focus. 168 const menuEl = megaRoot.querySelector(".dropdown-menu"); 169 if (!menuEl || !menuEl.classList.contains("show")) return; 170 if (!megaRoot.contains(document.activeElement)) return; 171 172 const el = document.activeElement; 173 const railIndex = railStops.indexOf(el); 174
175 // ââ Enter on a group: dive into its panel and trap ââ 176 if (e.key === "Enter" && railIndex !== -1) { 177 const targetId = el.getAttribute("data-mega-target"); 178 if (!targetId) return; // Membership is a real link â follow it 179 e.preventDefault(); // buttons also fire click on Enter 180 activate(targetId); 181 const items = contentFocusables(targetId); 182 if (items.length) { 183 panelMode = true; 184 items[0].focus(); 185 } 186 return; 187 } 188 189 // ââ Escape: one level up. Panel â its group; group level â 190 // close the menu and return focus to Explore. Handled 191 // explicitly at both levels â Bootstrap 5.0.2's own menu 192 // Escape is unreliable with data-bs-display="static". ââ 193 if (e.key === "Escape") { 194 e.preventDefault(); 195 e.stopPropagation(); // keep Bootstrap from double-handling 196 if (panelMode) { 197 panelMode = false; 198 const btn = railItems.find((b) => b.classList.contains("is-active")); 199 if (btn) btn.focus(); 200 return; 201 } 202 const trigger = megaRoot.querySelector(".c-mega__trigger:not(.c-mega__trigger--mobile)"); 203 if (trigger) { 204 if (window.bootstrap && bootstrap.Dropdown) { 205 bootstrap.Dropdown.getOrCreateInstance(trigger).hide(); 206 } 207 trigger.focus(); 208 } 209 return; 210 } 211 212 if (e.key !== "Tab") return; 213 214 // ââ L2 trap: Tab cycles the open category â product items, 215 // then promo + resources, then the "I need toâ¦" action 216 // chips (the chip set swaps with the category, so it 217 // belongs to the L2 context) â and wraps. Escape exits. ââ 218 if (panelMode) { 219 const items = contentFocusables(activeTargetId()).concat(chipsFocusables()); 220 if (!items.length) return; 221 const pos = items.indexOf(el); 222 if (!e.shiftKey && (pos === items.length - 1 || pos === -1)) { 223 e.preventDefault(); 224 items[0].focus(); 225 } else if (e.shiftKey && (pos === 0 || pos === -1)) { 226 e.preventDefault(); 227 items[items.length - 1].focus(); 228 } 229 return; 230 } 231 232 // ââ L1: Tab is confined to the product items â it cycles the 233 // rail (Membership wraps to the first group). Enter goes 234 // deeper, Escape closes; panel content and chips are only 235 // reachable inside the L2 trap. ââ 236 if (railIndex !== -1) { 237 e.preventDefault(); 238 const len = railStops.length; 239 const next = railStops[(railIndex + (e.shiftKey ? -1 : 1) + len) % len]; 240 next.focus(); // focus handler previews its panel 241 } 242 }, true); // capture â see note above 243 }); 244 })(); 245 246 // ------------------------------ 247 // Search dropdown: open on focus/typing (supports multiple instances) 248 // ------------------------------ 249 (() => { 250 const searchRoots = document.querySelectorAll(".c-search"); 251 if (!searchRoots.length) return; 252 253 searchRoots.forEach((root) => { 254 const searchInput = root.querySelector(".c-search__input"); 255 const searchDdEl = root.querySelector(".c-search__dd"); 256 if (!searchInput || !searchDdEl) return; 257 258 const toggleEl = searchDdEl.querySelector(".c-search__control"); 259 if (!toggleEl) return; 260 261 const searchDropdown = bootstrap.Dropdown.getOrCreateInstance(toggleEl, { 262 autoClose: "outside", 263 offset: [0, 8], // 8px gap below the box (Popper-driven; clears the focus ring) 264 popperConfig: { strategy: "fixed" }, 265 }); 266 267 const openSearch = () => searchDropdown.show(); 268 const closeSearch = () => searchDropdown.hide(); 269 270 // Autocomplete behavior: open only once the user types; never on bare 271 // focus/click. Closes again as soon as the field is emptied. 272 searchInput.addEventListener("input", () => { 273 if (searchInput.value.trim()) openSearch(); 274 else closeSearch(); 275 }); 276 277 searchInput.addEventListener("keydown", (e) => { 278 if (e.key === "Escape") { 279 e.preventDefault(); 280 closeStandardSearch({ searchDropdown, searchInput }); 281 } 282 }); 283 284 // After the forEach finishes, add ONE global click listener: 285 document.addEventListener("click", (e) => { 286 document.querySelectorAll(".c-search__dd").forEach((ddEl) => { 287 if (ddEl.contains(e.target)) return; 288 289 const ctrl = ddEl.querySelector(".c-search__control"); 290 if (!ctrl) return; 291 292 const inst = bootstrap.Dropdown.getInstance(ctrl); 293 inst?.hide(); 294 }); 295 }); 296
297 // inside searchRoots.forEach((root) => { ... }) 298 const closeBtn = root.querySelector("[data-search-close]"); 299 300 const hardReset = () => { 301 // 1) clear 302 searchInput.value = ""; 303 304 // 2) hide dropdown (Bootstrap) 305 searchDropdown.hide(); 306 307 // 3) blur so focus doesn't instantly re-open it 308 searchInput.blur(); 309 310 // 4) hard cleanup (prevents "sticky" show state) 311 const panel = searchDdEl.querySelector(".c-search__panel"); 312 toggleEl.classList.remove("show"); 313 panel?.classList.remove("show"); 314 panel?.removeAttribute("data-popper-placement"); 315 if (panel) { 316 panel.style.transform = ""; 317 panel.style.inset = ""; 318 } 319 }; 320 321 closeBtn?.addEventListener("click", (e) => { 322 e.preventDefault(); 323 e.stopPropagation(); // don't let it trigger other click handlers 324 hardReset(); 325 }); 326 }); 327 })(); 328 329 // ------------------------------
330 // Breakpoint safety: close any open search dropdowns on resize / breakpoint change 331 // ------------------------------ 332 (() => { 333 const LG = window.matchMedia("(min-width: 992px)"); // Bootstrap lg 334 335 const hardCloseAllSearch = () => { 336 document.querySelectorAll(".c-search__dd").forEach((ddEl) => { 337 const ctrl = ddEl.querySelector(".c-search__control"); 338 const panel = ddEl.querySelector(".c-search__panel"); 339 if (!ctrl) return; 340 341 const inst = bootstrap.Dropdown.getInstance(ctrl); 342 inst?.hide(); 343 344 // Hard cleanup so nothing âsticksâ after layout changes 345 ctrl.classList.remove("show"); 346 panel?.classList.remove("show"); 347 panel?.removeAttribute("data-popper-placement"); 348 if (panel) { 349 panel.style.transform = ""; 350 panel.style.inset = ""; 351 } 352 }); 353 354 // If product overlay is open, close it too 355 document.querySelectorAll(".c-nav__row2--product.is-search-open") 356 .forEach((row2) => row2.classList.remove("is-search-open")); 357 }; 358 359 // Close when the breakpoint flips 360 if (LG.addEventListener) LG.addEventListener("change", hardCloseAllSearch); 361 else LG.addListener(hardCloseAllSearch); // Safari fallback 362 363 // Close on resize as extra insurance 364 window.addEventListener("resize", hardCloseAllSearch); 365 })(); 366 367 // ------------------------------ 368 // Product Row 2: mini search expands overlay + chip rail arrows/keyboard 369 // ------------------------------ 370 (() => { 371 const row2 = document.querySelector(".c-nav__row2--product"); 372 if (!row2) return; 373 374 // Search overlay 375 const miniBtn = row2.querySelector("[data-search-mini]"); 376 const overlay = row2.querySelector("[data-search-overlay]"); 377 // The search box differs by environment, and Coveo injects its box AFTER 378 // page load, so resolve it lazily at use time (with a prototype fallback) 379 // instead of capturing a possibly-null const once. 380 const getBox = () => 381 row2.querySelector(".magic-box-input input") || // Coveo (Sitecore) 382 row2.querySelector(".c-search__input"); // prototype fallback 383 384 if (miniBtn && overlay) { 385 miniBtn.addEventListener("click", () => { 386 row2.classList.add("is-search-open"); 387 // focus after paint (box may be Coveo's late-rendered input) 388 requestAnimationFrame(() => getBox()?.focus()); 389 }); 390 391 // Close on Escape (desktop) 392 row2.addEventListener("keydown", (e) => { 393 if (e.key !== "Escape") return; 394 e.preventDefault(); 395 closeOverlay(); 396 }); 397 398 // Click outside overlay closes (desktop) 399 document.addEventListener("click", (e) => { 400 if (!row2.classList.contains("is-search-open")) return; 401 if (overlay.contains(e.target) || miniBtn.contains(e.target)) return; 402 closeOverlay(); 403 }); 404 } 405 406 const closeBtn = row2.querySelector("[data-search-close]"); 407 408 const closeOverlay = () => { 409 // clear input (resolved lazily â see getBox) 410 const box = getBox(); 411 if (box) box.value = ""; 412 // remove open class 413 row2.classList.remove("is-search-open"); 414 // return focus to mini trigger 415 miniBtn?.focus(); 416 417 // if bootstrap dropdown is open, close it 418 const ctrl = row2.querySelector(".c-search__control"); 419 if (ctrl && window.bootstrap) { 420 const dd = window.bootstrap.Dropdown.getInstance(ctrl); 421 dd?.hide(); 422 } 423 }; 424 425 closeBtn?.addEventListener("click", (e) => { 426 e.preventDefault(); 427 closeOverlay(); 428 }); 429 430 // Chip rail scroll controls 431 const rail = row2.querySelector("[data-chip-rail]"); 432 const scroller = row2.querySelector("[data-chip-scroller]"); 433 const prev = row2.querySelector("[data-chip-prev]"); 434 const next = row2.querySelector("[data-chip-next]"); 435 436 if (rail && scroller && prev && next) { 437 const step = () => Math.max(240, Math.floor(scroller.clientWidth * 0.6)); 438 439 prev.addEventListener("click", () => scroller.scrollBy({ left: -step(), behavior: "smooth" })); 440 next.addEventListener("click", () => scroller.scrollBy({ left: step(), behavior: "smooth" })); 441 442 // Keyboard scrolling for a11y 443 scroller.addEventListener("keydown", (e) => { 444 if (e.key === "ArrowLeft") scroller.scrollBy({ left: -step(), behavior: "smooth" }); 445 if (e.key === "ArrowRight") scroller.scrollBy({ left: step(), behavior: "smooth" }); 446 }); 447 448 const update = () => { 449 const maxScroll = scroller.scrollWidth - scroller.clientWidth; 450 451 // If content doesn't overflow, hide both arrows 452 const noOverflow = maxScroll <= 1; 453 rail.classList.toggle("is-no-overflow", noOverflow); 454 455 // "Start" if you're basically at 0 456 rail.classList.toggle("is-at-start", scroller.scrollLeft <= 1); 457 458 // "End" if you're basically at max 459 rail.classList.toggle("is-at-end", scroller.scrollLeft >= maxScroll - 1); 460 }; 461 462 // Run on load + on interaction 463 update(); 464 scroller.addEventListener("scroll", update, { passive: true });
465 window.addEventListener("resize", update); 466 } 467 })(); 468 469 // ------------------------------ 470 // Anchor-chip active state (IntersectionObserver) 471 // Watches sections linked from the product nav chip rail. 472 // When a section scrolls into view, its chip gets .is-active. 473 // ------------------------------ 474 (() => { 475 const scroller = document.querySelector( 476 ".c-nav__row2--product .c-chipRail__scroller" 477 ); 478 if (!scroller) return; 479 480 // Build a map: sectionId â chip element 481 const chips = Array.from(scroller.querySelectorAll("a.c-chip[href^='#']")); 482 if (!chips.length) return; 483 484 const map = new Map(); // id â chip 485 const sections = []; 486 487 chips.forEach((chip) => { 488 const id = chip.getAttribute("href").slice(1); // strip leading # 489 const section = id && document.getElementById(id); 490 if (section) { 491 map.set(id, chip); 492 sections.push(section); 493 } 494 }); 495 496 if (!sections.length) return; 497 498 let currentId = null; 499 500 // Centre the active chip inside the rail scroller (horizontal only). 501 // Uses scroller.scrollLeft â does NOT touch the page scroll. 502 const scrollChipIntoRail = (chip) => { 503 const chipLeft = chip.offsetLeft; 504 const chipW = chip.offsetWidth; 505 const railW = scroller.clientWidth; 506 const target = chipLeft - (railW / 2) + (chipW / 2); 507 scroller.scrollTo({ left: Math.max(0, target), behavior: "smooth" }); 508 }; 509 510 const activate = (id) => { 511 if (id === currentId) return; 512 currentId = id; 513 514 chips.forEach((c) => c.classList.remove("is-active")); 515 const active = map.get(id); 516 if (active) { 517 active.classList.add("is-active"); 518 scrollChipIntoRail(active); 519 } 520 }; 521 522 // rootMargin: negative top = offset for sticky nav, 523 // negative bottom = only top half of viewport triggers 524 const observer = new IntersectionObserver( 525 (entries) => { 526 entries.forEach((entry) => { 527 if (entry.isIntersecting) { 528 activate(entry.target.id); 529 } 530 }); 531 }, 532 { 533 rootMargin: "-100px 0px -50% 0px", 534 threshold: 0, 535 } 536 ); 537 538 sections.forEach((s) => observer.observe(s)); 539 })(); 540 541 // ------------------------------ 542 // Offcanvas (single): DOM-driven + smooth sliding panels (L1 <-> L2) 543 // ------------------------------ 544 (() => { 545 const offEl = document.getElementById("offcanvasNav"); 546 if (!offEl) return; 547 548 bootstrap.Offcanvas.getOrCreateInstance(offEl); 549 550 const shell = offEl.querySelector(".c-offnav__shell"); 551 const l1Panel = offEl.querySelector(".c-offnav__panel--l1"); 552 const l2Panel = offEl.querySelector(".c-offnav__panel--l2"); 553 const backBtn = offEl.querySelector("[data-offnav-back]"); 554 const titleEl = offEl.querySelector("#offcanvasNavLabel"); 555 556 const tabButtons = offEl.querySelectorAll("[data-offnav-segment]"); 557 const lists = offEl.querySelectorAll(".c-offnav__list[data-segment]"); 558 const l2Blocks = offEl.querySelectorAll(".c-offnav__l2[data-l2-segment][data-l2-id]"); 559 560 //let currentSegment = "personal"; 561 let currentSegment = 562 Array.from(tabButtons).find(btn => btn.classList.contains("is-active"))?.getAttribute("data-offnav-segment") 563 || "personal"; // fallback to personal if nothing is active 564 let lastL1Trigger = null; 565 566 function setSegment(segment) { 567 currentSegment = segment; 568 569 tabButtons.forEach((btn) => { 570 const isActive = btn.getAttribute("data-offnav-segment") === segment; 571 btn.classList.toggle("is-active", isActive); 572 btn.classList.toggle("active", isActive); 573 btn.setAttribute("aria-selected", isActive ? "true" : "false"); 574 }); 575
576 lists.forEach((list) => { 577 const isMatch = list.getAttribute("data-segment") === segment; 578 list.classList.toggle("d-none", !isMatch); 579 }); 580 } 581 582 function setActiveL2(segment, categoryId) { 583 let active = null; 584 585 l2Blocks.forEach((block) => { 586 const match = 587 block.getAttribute("data-l2-segment") === segment && 588 block.getAttribute("data-l2-id") === categoryId; 589 590 block.classList.toggle("is-active", match); 591 if (match) active = block; 592 }); 593 594 if (titleEl && active) { 595 const label = active.getAttribute("aria-label") || "Category"; 596 titleEl.textContent = label; 597 } 598 599 return !!active; 600 } 601 602 function onShellTransitionEnd(cb) { 603 if (!shell) return cb(); 604 605 const reduce = 606 window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches; 607 if (reduce) return cb(); 608 609 const cs = window.getComputedStyle(shell); 610 const dur = parseFloat(cs.transitionDuration || "0"); 611 if (!dur) return cb(); 612 613 const handler = (e) => { 614 if (e.target !== shell || e.propertyName !== "transform") return; 615 shell.removeEventListener("transitionend", handler); 616 cb(); 617 }; 618 619 shell.addEventListener("transitionend", handler); 620 } 621 622 function goL2() { 623 offEl.classList.add("is-l2"); 624 625 onShellTransitionEnd(() => { 626 if (l1Panel) l1Panel.setAttribute("aria-hidden", "true"); 627 if (l2Panel) l2Panel.setAttribute("aria-hidden", "false"); 628 629 const focusTarget = 630 l2Panel?.querySelector(".c-chip, .c-offnav__itemLink, button, a") || backBtn; 631 if (focusTarget?.focus) focusTarget.focus(); 632 }); 633 } 634 635 function goL1() { 636 offEl.classList.remove("is-l2"); 637 638 onShellTransitionEnd(() => { 639 if (l1Panel) l1Panel.setAttribute("aria-hidden", "false"); 640 if (l2Panel) l2Panel.setAttribute("aria-hidden", "true"); 641 642 if (lastL1Trigger?.focus) lastL1Trigger.focus(); 643 }); 644 } 645 646 // init 647 setSegment(currentSegment); 648 if (l1Panel) l1Panel.setAttribute("aria-hidden", "false"); 649 if (l2Panel) l2Panel.setAttribute("aria-hidden", "true"); 650 offEl.classList.remove("is-l2"); 651 652 tabButtons.forEach((btn) => { 653 btn.addEventListener("click", () => { 654 const seg = btn.getAttribute("data-offnav-segment"); 655 if (seg) setSegment(seg); 656 }); 657 }); 658 659 if (l1Panel) { 660 l1Panel.addEventListener("click", (e) => { 661 const trigger = e.target.closest("[data-open-l2]"); 662 if (!trigger) return; 663 664 lastL1Trigger = trigger; 665 666 const categoryId = trigger.getAttribute("data-open-l2"); 667 const segment = trigger.getAttribute("data-segment") || currentSegment; 668 669 if (segment !== currentSegment) setSegment(segment); 670 671 const ok = setActiveL2(segment, categoryId); 672 if (!ok) return; 673 674 requestAnimationFrame(() => goL2()); 675 }); 676 } 677 678 if (backBtn) { 679 backBtn.addEventListener("click", (e) => { 680 e.preventDefault(); 681 goL1(); 682 }); 683 } 684 685 offEl.addEventListener("keydown", (e) => { 686 if (e.key !== "Escape") return; 687 if (offEl.classList.contains("is-l2")) { 688 e.preventDefault(); 689 goL1(); 690 } 691 }); 692 693 offEl.addEventListener("shown.bs.offcanvas", () => { 694 setSegment(currentSegment); 695 if (l1Panel) l1Panel.setAttribute("aria-hidden", "false"); 696 if (l2Panel) l2Panel.setAttribute("aria-hidden", "true"); 697 offEl.classList.remove("is-l2"); 698 }); 699 700 offEl.addEventListener("hidden.bs.offcanvas", () => { 701 if (l1Panel) l1Panel.setAttribute("aria-hidden", "false"); 702 if (l2Panel) l2Panel.setAttribute("aria-hidden", "true"); 703 offEl.classList.remove("is-l2"); 704 }); 705 })(); 706 707 // ------------------------------ 708 // Promo card row: keyboard horizontal scroll (a11y) 709 // ------------------------------ 710 (() => { 711 const scrollers = document.querySelectorAll(".card-slider"); 712 713 if (!scrollers.length) return; 714
715 scrollers.forEach((el) => { 716 // Make sure it's focusable (in case markup forgot) 717 if (!el.hasAttribute("tabindex")) { 718 el.setAttribute("tabindex", "0"); 719 } 720 721 el.addEventListener("keydown", (e) => { 722 const step = 80; 723 const page = Math.round(el.clientWidth * 0.9); 724 725 switch (e.key) { 726 case "ArrowRight": 727 e.preventDefault(); 728 el.scrollBy({ left: step, behavior: "smooth" }); 729 break; 730 731 case "ArrowLeft": 732 e.preventDefault(); 733 el.scrollBy({ left: -step, behavior: "smooth" }); 734 break; 735 736 case "PageDown": 737 e.preventDefault(); 738 el.scrollBy({ left: page, behavior: "smooth" }); 739 break; 740 741 case "PageUp": 742 e.preventDefault(); 743 el.scrollBy({ left: -page, behavior: "smooth" }); 744 break; 745 746 case "Home": 747 e.preventDefault(); 748 el.scrollTo({ left: 0, behavior: "smooth" }); 749 break; 750 751 case "End": 752 e.preventDefault(); 753 el.scrollTo({ left: el.scrollWidth, behavior: "smooth" }); 754 break; 755 } 756 }); 757 }); 758 })(); 759 760 // Hero carousel moved to public/js/hero-carousel.js (Embla + Autoplay) 761})(); 762 763// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 764// Member-consultant greeting wave 765// When the desktop mega menu opens, wave the consultant's arm once as a 766// greeting. Hover/focus waves are handled entirely in CSS; this only adds the 767// one-shot greeting on open. Respects prefers-reduced-motion. 768// 769// Delegated on document: shown.bs.dropdown fires for every Bootstrap dropdown, 770// so we confirm the MEGA trigger is the one that expanded before greeting (the 771// search dropdown shouldn't trigger it). The .is-greeting class is removed on 772// animationend so each open re-triggers cleanly. 773// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 774(function () { 775 if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return; 776 777 document.addEventListener("shown.bs.dropdown", function () { 778 // Only when the mega menu is the dropdown that opened 779 if (!document.querySelector('.c-mega__trigger[aria-expanded="true"]')) return; 780 var arm = document.getElementById("arm"); 781 if (!arm) return; 782 var icon = arm.closest(".c-mega__rail-ic"); 783 if (icon) icon.classList.add("is-greeting"); 784 }); 785 786 document.addEventListener("animationend", function (e) { 787 if (e.animationName !== "c-mega-consultant-wave") return; 788 var arm = document.getElementById("arm"); 789 if (!arm) return; 790 var icon = arm.closest(".c-mega__rail-ic"); 791 if (icon) icon.classList.remove("is-greeting"); 792 }); 793})(); 794 795// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 796// Offcanvas Membership greeting â same wave as the mega rail, triggered when 797// the mobile menu opens. The offcanvas arm path uses class="arm" (the mega 798// rail owns id="arm" on the same page). 799// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 800(function () { 801 if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return; 802 803 var offEl = document.getElementById("offcanvasNav"); 804 if (!offEl) return; 805 806 offEl.addEventListener("shown.bs.offcanvas", function () { 807 var icon = offEl.querySelector(".c-offnav__membership-ic"); 808 if (icon) icon.classList.add("is-greeting"); 809 });
810 811 offEl.addEventListener("animationend", function (e) { 812 if (e.animationName !== "c-mega-consultant-wave") return; 813 var icon = offEl.querySelector(".c-offnav__membership-ic.is-greeting"); 814 if (icon) icon.classList.remove("is-greeting"); 815 }); 816})();
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.