diff --git a/packages/unity-bootstrap-theme/src/js/anchor-menu.js b/packages/unity-bootstrap-theme/src/js/anchor-menu.js index c7c4776a4..8afdddc12 100644 --- a/packages/unity-bootstrap-theme/src/js/anchor-menu.js +++ b/packages/unity-bootstrap-theme/src/js/anchor-menu.js @@ -14,9 +14,17 @@ function initAnchorMenu() { const globalHeaderId = HEADER_IDS.find(id => document.getElementById(id)); const globalHeader = document.getElementById(globalHeaderId); const navbar = document.getElementById("uds-anchor-menu"); + if (!navbar || !globalHeader) { + console.warn( + "Anchor menu initialization failed: required elements not found" + ); + return; + } + const navbarOriginalParent = navbar.parentNode; const navbarOriginalNextSibling = navbar.nextSibling; - const anchors = navbar.getElementsByClassName("nav-link"); + + const anchors = Array.from(navbar.getElementsByClassName("nav-link")); const anchorTargets = new Map(); let previousScrollPosition = window.scrollY; let isNavbarAttached = false; @@ -36,11 +44,18 @@ function initAnchorMenu() { window.scrollY - combinedToolbarHeightOffset; - // Cache the anchor target elements for (let anchor of anchors) { - const targetId = anchor.getAttribute("href").replace("#", ""); + const href = anchor.getAttribute("href"); + if (!href || !href.startsWith("#")) { + continue; + } + const targetId = href.replace("#", ""); const target = document.getElementById(targetId); - anchorTargets.set(anchor, target); + if (target) { + anchorTargets.set(anchor, target); + } else { + console.warn(`Anchor menu: target element "${targetId}" not found`); + } } const shouldAttachNavbarOnLoad = window.scrollY > navbarInitialTop; @@ -54,11 +69,16 @@ function initAnchorMenu() { * Calculates the percentage of an element that is visible in the viewport. * * @param {Element} el The element to calculate the visible percentage for. + * @param {number} depth Recursion depth counter to prevent infinite loops. * @return {number} The percentage of the element that is visible in the viewport. */ - function calculateVisiblePercentage(el) { + function calculateVisiblePercentage(el, depth = 0) { + if (!el || depth > 10) { + return 0; + } + if (el.offsetHeight === 0 || el.offsetWidth === 0) { - return calculateVisiblePercentage(el.parentElement); + return calculateVisiblePercentage(el.parentElement, depth + 1); } const rect = el.getBoundingClientRect(); const windowHeight = @@ -89,24 +109,32 @@ function initAnchorMenu() { let mostVisibleElementId = null; // Find the element with highest visibility - Array.from(anchors).forEach(anchor => { - let elementId = anchor.getAttribute("href").replace("#", ""); - let el = document.getElementById(elementId); - const visiblePercentage = calculateVisiblePercentage(el); + anchors.forEach(anchor => { + const target = anchorTargets.get(anchor); + if (!target) { + return; + } + + const visiblePercentage = calculateVisiblePercentage(target); if (visiblePercentage > 0 && visiblePercentage > maxVisibility) { maxVisibility = visiblePercentage; - mostVisibleElementId = el.id; + mostVisibleElementId = target.id; } }); // Update active class if we found a visible element if (mostVisibleElementId) { - document - .querySelector('[href="#' + mostVisibleElementId + '"]') - .classList.add("active"); + const activeAnchor = document.querySelector( + '[href="#' + mostVisibleElementId + '"]' + ); + if (activeAnchor) { + activeAnchor.classList.add("active"); + } + + // Remove active class from all other nav links in the navbar navbar .querySelectorAll( - `nav > a.nav-link:not([href="#` + mostVisibleElementId + '"])' + 'a.nav-link:not([href="#' + mostVisibleElementId + '"])' ) .forEach(function (e) { e.classList.remove("active"); @@ -147,17 +175,34 @@ function initAnchorMenu() { previousScrollPosition = window.scrollY; }; - window.addEventListener( - "scroll", - () => throttle(scrollHandlerLogic, SCROLL_DELAY), - { passive: true } - ); + let throttledScrollHandler; + const createThrottledHandler = () => { + let isThrottled = false; + return () => { + if (isThrottled) return; + isThrottled = true; + scrollHandlerLogic(); + setTimeout(() => { + isThrottled = false; + }, SCROLL_DELAY); + }; + }; + + throttledScrollHandler = createThrottledHandler(); + + window.addEventListener("scroll", throttledScrollHandler, { passive: true }); - // Set click event of anchors + // Set click event handlers for all valid anchors + // Only anchors with valid targets were added to anchorTargets Map for (let [anchor, anchorTarget] of anchorTargets) { anchor.addEventListener("click", function (e) { e.preventDefault(); + if (!anchorTarget || !document.body.contains(anchorTarget)) { + console.warn("Anchor target no longer exists in DOM"); + return; + } + // Get current viewport height and calculate the 1/4 position so that the // top of section is visible when you click on the anchor. const viewportHeight = window.innerHeight; diff --git a/packages/unity-bootstrap-theme/stories/docs/anchor-menu/anchor-menu-usage.stories.mdx b/packages/unity-bootstrap-theme/stories/docs/anchor-menu/anchor-menu-usage.stories.mdx new file mode 100644 index 000000000..8f28df527 --- /dev/null +++ b/packages/unity-bootstrap-theme/stories/docs/anchor-menu/anchor-menu-usage.stories.mdx @@ -0,0 +1,315 @@ +import { Meta } from '@storybook/addon-docs'; + + + +# Anchor Menu Usage Guide + +The UDS Anchor Menu is a navigation component that helps users jump to different sections of a long page. It automatically tracks scroll position, highlights the currently visible section, and can attach to the page header when scrolling. + +## Prerequisites + +Before using the anchor menu, ensure you have: + +1. **ASU Unity Bootstrap Theme** CSS loaded in your project +2. **ASU Unity Bootstrap Theme** JavaScript loaded (includes the anchor menu initialization) +3. An **ASU global header** with ID `asu-header` or `asuHeader` +4. **Font Awesome** for icons (optional, but recommended) + +## Basic HTML Structure + +The anchor menu requires specific HTML structure to function properly: + +### 1. The Anchor Menu Container + +Create an element with ID `uds-anchor-menu`: + +```html +
+
+
+ +
+ +
+
+
+
+``` + +### 2. Adding Anchor Links + +Inside the `