From 07246e2b59106256e624582ecf2a08786a87f895 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Guillot?= Date: Sat, 2 Aug 2025 13:05:20 -0700 Subject: [PATCH] refactor(js): improve menu handlers --- internal/ui/static/js/app.js | 114 ++++++++++++++++------------- internal/ui/static/js/bootstrap.js | 16 +--- 2 files changed, 67 insertions(+), 63 deletions(-) diff --git a/internal/ui/static/js/app.js b/internal/ui/static/js/app.js index 7ee67337..97cbd9fe 100644 --- a/internal/ui/static/js/app.js +++ b/internal/ui/static/js/app.js @@ -270,7 +270,7 @@ function goToListItem(offset) { if (items[i].classList.contains("current-item")) { items[i].classList.remove("current-item"); - // By default adjust selection by offset + // By default adjust selection to the next item let itemOffset = (i + offset + items.length) % items.length; // Allow jumping to top or bottom if (offset === TOP) { @@ -338,73 +338,85 @@ async function triggerWebShare(title, url) { window.location.reload(); } -// make logo element as button on mobile layout -function checkMenuToggleModeByLayout() { +/** + * Toggle the ARIA attributes on the main menu based on the viewport width. + */ +function toggleAriaAttributesOnMainMenu() { const logoElement = document.querySelector(".logo"); - if (!logoElement) return; - const homePageLinkElement = document.querySelector(".logo > a"); - if (document.documentElement.clientWidth < 620) { + if (!logoElement || !homePageLinkElement) return; + + const isMobile = document.documentElement.clientWidth < 650; + + if (isMobile) { const navMenuElement = document.getElementById("header-menu"); - const navMenuElementIsExpanded = navMenuElement.classList.contains("js-menu-show"); - const logoToggleButtonLabel = logoElement.getAttribute("data-toggle-button-label"); - logoElement.setAttribute("role", "button"); - logoElement.setAttribute("tabindex", "0"); - logoElement.setAttribute("aria-label", logoToggleButtonLabel); - logoElement.setAttribute("aria-expanded", navMenuElementIsExpanded?"true":"false"); - homePageLinkElement.setAttribute("tabindex", "-1"); + const isExpanded = navMenuElement?.classList.contains("js-menu-show") ?? false; + const toggleButtonLabel = logoElement.getAttribute("data-toggle-button-label"); + + // Set mobile menu button attributes + Object.assign(logoElement, { + role: "button", + tabIndex: 0, + ariaLabel: toggleButtonLabel, + ariaExpanded: isExpanded.toString() + }); + homePageLinkElement.tabIndex = -1; } else { - logoElement.removeAttribute("role"); - logoElement.removeAttribute("tabindex"); - logoElement.removeAttribute("aria-expanded"); - logoElement.removeAttribute("aria-label"); + // Remove mobile menu button attributes + ["role", "tabindex", "aria-expanded", "aria-label"].forEach(attr => + logoElement.removeAttribute(attr) + ); homePageLinkElement.removeAttribute("tabindex"); } } -function fixVoiceOverDetailsSummaryBug() { - document.querySelectorAll("details").forEach((details) => { - const summaryElement = details.querySelector("summary"); - summaryElement.setAttribute("role", "button"); - summaryElement.setAttribute("aria-expanded", details.open? "true": "false"); - - details.addEventListener("toggle", () => { - summaryElement.setAttribute("aria-expanded", details.open? "true": "false"); - }); - }); -} - -// Show and hide the main menu on mobile devices. -function toggleMainMenu(event) { - if (event.type === "keydown" && !(event.key === "Enter" || event.key === " ")) { +/** + * Toggle the main menu dropdown. + * + * @param {Event} event - The event object. + */ +function toggleMainMenuDropdown(event) { + // Only handle Enter, Space, or click events + if (event.type === "keydown" && !["Enter", " "].includes(event.key)) { return; } + // Prevent default only if element has role attribute (mobile menu button) if (event.currentTarget.getAttribute("role")) { event.preventDefault(); } - const menu = document.querySelector(".header nav ul"); + const navigationMenu = document.querySelector(".header nav ul"); const menuToggleButton = document.querySelector(".logo"); - if (menu.classList.contains("js-menu-show")) { - menuToggleButton.setAttribute("aria-expanded", "false"); - } else { - menuToggleButton.setAttribute("aria-expanded", "true"); + + if (!navigationMenu || !menuToggleButton) { + return; } - menu.classList.toggle("js-menu-show"); + + const isShowing = navigationMenu.classList.toggle("js-menu-show"); + menuToggleButton.setAttribute("aria-expanded", isShowing.toString()); } -// Handle click events for the main menu (
  • and ). -function onClickMainMenuListItem(event) { - const element = event.target; +/** + * Initialize the main menu handlers. + */ +function initializeMainMenuHandlers() { + toggleAriaAttributesOnMainMenu(); + window.addEventListener("resize", toggleAriaAttributesOnMainMenu, { passive: true }); - if (element.tagName === "A") { - window.location.href = element.getAttribute("href"); - } else { - const linkElement = element.querySelector("a") || element.closest("a"); - window.location.href = linkElement.getAttribute("href"); + const logoElement = document.querySelector(".logo"); + if (logoElement) { + logoElement.addEventListener("click", toggleMainMenuDropdown); + logoElement.addEventListener("keydown", toggleMainMenuDropdown); } + + onClick(".header nav li", (event) => { + const linkElement = event.target.closest("a") || event.target.querySelector("a"); + if (linkElement) { + window.location.href = linkElement.getAttribute("href"); + } + }); } /** @@ -412,7 +424,7 @@ function onClickMainMenuListItem(event) { * * @returns {void} */ -function disableSubmitButtonsOnFormSubmit() { +function initializeFormHandlers() { document.querySelectorAll("form").forEach((element) => { element.onsubmit = () => { const buttons = element.querySelectorAll("button[type=submit]"); @@ -426,13 +438,17 @@ function disableSubmitButtonsOnFormSubmit() { }); } -// Show modal dialog with the list of keyboard shortcuts. +/** + * Show the keyboard shortcuts modal. + */ function showKeyboardShortcuts() { const template = document.getElementById("keyboard-shortcuts"); ModalHandler.open(template.content, "dialog-title"); } -// Mark as read visible items of the current page. +/** + * Mark all visible entries on the current page as read. + */ function markPageAsRead() { const items = getVisibleEntries(); const entryIDs = []; diff --git a/internal/ui/static/js/bootstrap.js b/internal/ui/static/js/bootstrap.js index 79bf357b..d6fe9cf4 100644 --- a/internal/ui/static/js/bootstrap.js +++ b/internal/ui/static/js/bootstrap.js @@ -1,4 +1,5 @@ -disableSubmitButtonsOnFormSubmit(); +initializeMainMenuHandlers(); +initializeFormHandlers(); initializeMediaPlayerHandlers(); // Initialize the keyboard shortcuts if enabled. @@ -85,19 +86,6 @@ onAuxClick("a[data-original-link='true']", (event) => { } }, true); -checkMenuToggleModeByLayout(); -window.addEventListener("resize", checkMenuToggleModeByLayout, { passive: true }); - -fixVoiceOverDetailsSummaryBug(); - -const logoElement = document.querySelector(".logo"); -if (logoElement) { - logoElement.addEventListener("click", toggleMainMenu); - logoElement.addEventListener("keydown", toggleMainMenu); -} - -onClick(".header nav li", (event) => onClickMainMenuListItem(event)); - // Register the service worker if supported. if ("serviceWorker" in navigator) { const serviceWorkerURL = document.body.dataset.serviceWorkerUrl;