Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/app-degree-pages/.storybook/preview.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import ListingPage from "../src/components/ListingPage/index.stories";
import DetailPage from "../src/components/DetailPage/index.stories";
import "@asu/unity-bootstrap-theme/src/scss/unity-bootstrap-theme.bundle.scss";
import "@asu/unity-bootstrap-theme/src/js/unity-bootstrap.js";
import "bootstrap/dist/js/bootstrap.bundle.js";

const parameters = {
Expand Down
128 changes: 94 additions & 34 deletions packages/unity-bootstrap-theme/src/js/anchor-menu.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,37 @@
// @ts-check
import { EventHandler } from "./bootstrap-helper";
import { throttle } from "@asu/shared";

/**
* Initializes the anchor menu functionality.
*
* @param {string} idPrefix - The prefix for the IDs of the anchor menu elements
* @returns {void}
* @param {Object} [options] - Configuration options
* @param {boolean} [options.ignoreReactCheck] - If true, bypasses the check for React/Styled Components
* @returns {function} Cleanup function to remove event listeners
*/
function initAnchorMenu() {
const HEADER_IDS = ["asu-header", "asuHeader"];
function initAnchorMenu(options = { ignoreReactCheck: false }) {
const HEADER_IDS = ["asu-header", "asuHeader", "headerContainer"];
const SCROLL_DELAY = 100;

const globalHeaderId = HEADER_IDS.find(id => document.getElementById(id));
const globalHeader = document.getElementById(globalHeaderId);
const navbar = document.getElementById("uds-anchor-menu");
if (!navbar || !globalHeader) {

// Check if this is the React version (Styled Components) to exclude react version
// Styled components generate classes starting with "sc-"
if (
!options.ignoreReactCheck &&
navbar &&
Array.from(navbar.classList).some(cls => cls.startsWith("sc-"))
) {
return () => {};
}

if (!navbar) {
console.warn(
"Anchor menu initialization failed: required elements not found"
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets remove this since it warns anytime an anchor menu is not found and it just adds noise. (pages that do not have an anchor menu on purposed get a warning)

return;
return () => {};
}

const navbarOriginalParent = navbar.parentNode;
Expand Down Expand Up @@ -68,15 +81,17 @@ 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 {HTMLElement} 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, depth = 0) {
if (!el || depth > 10) {
return 0;
}

if (!(el instanceof HTMLElement)) {
return 0;
}
if (el.offsetHeight === 0 || el.offsetWidth === 0) {
return calculateVisiblePercentage(el.parentElement, depth + 1);
}
Expand Down Expand Up @@ -129,6 +144,7 @@ function initAnchorMenu() {
);
if (activeAnchor) {
activeAnchor.classList.add("active");
activeAnchor.setAttribute("aria-current", "location");
}

// Remove active class from all other nav links in the navbar
Expand All @@ -138,35 +154,69 @@ function initAnchorMenu() {
)
.forEach(function (e) {
e.classList.remove("active");
e.removeAttribute("aria-current");
});
}

// Handle navbar attachment/detachment
const navbarY = navbar.getBoundingClientRect().top;
const headerBottom = globalHeader.getBoundingClientRect().bottom;
const headerBottom = globalHeader
? globalHeader.getBoundingClientRect().bottom
: 0;
const isScrollingDown = window.scrollY > previousScrollPosition;

// If scrolling DOWN and the bottom of globalHeader touches or overlaps the top of navbar
if (isScrollingDown && headerBottom >= navbarY) {
if (!isNavbarAttached) {
// Attach navbar to globalHeader
globalHeader.appendChild(navbar);
isNavbarAttached = true;
navbar.classList.add("uds-anchor-menu-attached");
if (isScrollingDown) {
if (globalHeader) {
if (headerBottom >= navbarY && !isNavbarAttached) {
// Attach navbar to globalHeader
globalHeader.appendChild(navbar);
isNavbarAttached = true;
navbar.classList.add("uds-anchor-menu-attached");
}
} else {
if (window.scrollY >= navbarInitialTop && !isNavbarAttached) {
// Attach fixed to body
document.body.appendChild(navbar);
navbar.style.position = "fixed";
navbar.style.top = combinedToolbarHeightOffset + "px";
navbar.style.width = "100%";
navbar.style.zIndex = "1000";
isNavbarAttached = true;
navbar.classList.add("uds-anchor-menu-attached");
}
}
}

// If scrolling UP and the header bottom no longer overlaps with the navbar
if (!isScrollingDown && isNavbarAttached) {
const currentHeaderBottom = globalHeader.getBoundingClientRect().bottom;
const navbarCurrentTop = navbar.getBoundingClientRect().top;

// Only detach if we're back to the initial navbar position or if header no longer overlaps navbar
if (
window.scrollY <= navbarInitialTop ||
currentHeaderBottom < navbarCurrentTop
) {
let shouldDetach = false;

if (globalHeader) {
const currentHeaderBottom = globalHeader.getBoundingClientRect().bottom;
const navbarCurrentTop = navbar.getBoundingClientRect().top;
if (
window.scrollY <= navbarInitialTop ||
currentHeaderBottom < navbarCurrentTop
) {
shouldDetach = true;
}
} else {
if (window.scrollY <= navbarInitialTop) {
shouldDetach = true;
}
}

if (shouldDetach) {
navbarOriginalParent.insertBefore(navbar, navbarOriginalNextSibling);
if (!globalHeader) {
// Reset styles
navbar.style.position = "";
navbar.style.top = "";
navbar.style.width = "";
navbar.style.zIndex = "";
}
isNavbarAttached = false;
navbar.classList.remove("uds-anchor-menu-attached");
}
Expand Down Expand Up @@ -197,37 +247,47 @@ function initAnchorMenu() {
for (let [anchor, anchorTarget] of anchorTargets) {
anchor.addEventListener("click", function (e) {
e.preventDefault();
const hash = anchor.getAttribute("href");
history?.pushState
? history.pushState(null, "", hash)
: (window.location.hash = hash);

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;
const targetQuarterPosition = Math.round(viewportHeight * 0.25);

const targetAbsoluteTop =
anchorTarget.getBoundingClientRect().top + window.scrollY;

let scrollToPosition = targetAbsoluteTop - targetQuarterPosition;

window.scrollTo({
top: scrollToPosition,
anchorTarget.scrollIntoView({
behavior: "smooth",
block: "start",
});

// Focus the target element to ensure correct tab order after navigation
anchorTarget.setAttribute("tabindex", "-1");
anchorTarget.focus({ preventScroll: true });

// Remove active class from other anchor in navbar, and add it to the clicked anchor
const active = navbar.querySelector(".nav-link.active");

if (active) {
active.classList.remove("active");
active.removeAttribute("aria-current");
}

// @ts-ignore
e.target.classList.add("active");
// @ts-ignore
e.target.setAttribute("aria-current", "location");
});
}

// Cleanup function
return () => {
window.removeEventListener("scroll", throttledScrollHandler);
if (isNavbarAttached && navbarOriginalParent) {
navbarOriginalParent.insertBefore(navbar, navbarOriginalNextSibling);
}
};
}

EventHandler.on(window, "load.uds.anchor-menu", initAnchorMenu);
Expand Down
2 changes: 1 addition & 1 deletion packages/unity-react-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"main": "./dist/unityReactCore.umd.js",
"module": "./dist/unityReactCore.es.js",
"browser": "./dist/unityReactCore.umd.js",
"types": "./dist/types/index.d.ts",
"types": "./dist/types/unity-react-core/src/index.d.ts",
"description": "Core UDS React UI components required by other higher-order React packages",
"author": "Nathan Rollins <nathan.rollins@asu.edu>",
"homepage": "https://github.com/ASU/asu-unity-stack#readme",
Expand Down
Loading