From 457690e1910b387c1f978ce86c8d635f19aef42e Mon Sep 17 00:00:00 2001 From: jjroelofs <904576+jjroelofs@users.noreply.github.com> Date: Wed, 8 Jul 2026 07:30:37 +0200 Subject: [PATCH 1/5] Move custom active trail detection to client-side JavaScript Remove the server-side active trail workaround that added url.path cache context to every menu render, causing the menu block to be cached per URL path. Replace with a lightweight client-side JavaScript that detects the active trail by comparing menu link hrefs against the current URL pathname. Core's built-in in_active_trail continues to work for standard menu links; the JS covers edge cases like Views-generated menu links (core issue #3359511). Fixes #827 --- dxpr_theme.libraries.yml | 6 +++++ dxpr_theme.theme | 24 +++++++++-------- js/dist/dxpr-theme-active-trail.js | 41 ++++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 11 deletions(-) create mode 100644 js/dist/dxpr-theme-active-trail.js diff --git a/dxpr_theme.libraries.yml b/dxpr_theme.libraries.yml index 983bdf36..3b01a017 100644 --- a/dxpr_theme.libraries.yml +++ b/dxpr_theme.libraries.yml @@ -214,3 +214,9 @@ enhanced-dropdowns: vendor/enhanced-dropdowns/dist/css/main.min.css: { minified: true } dependencies: - core/drupal + +active-trail: + js: + js/dist/dxpr-theme-active-trail.js: {} + dependencies: + - core/drupal diff --git a/dxpr_theme.theme b/dxpr_theme.theme index 48089005..93ad0121 100644 --- a/dxpr_theme.theme +++ b/dxpr_theme.theme @@ -276,31 +276,33 @@ function dxpr_theme_preprocess_page(&$variables) { /** * Implements hook_preprocess_menu(). * - * Workaround for Drupal core regression where Views-generated menu links don't - * receive proper active trail detection. Compares menu item URLs against the - * current page path to set active states and propagates to parent items. + * Attaches client-side active trail detection for Views-generated menu links + * that core does not mark as active (core issue #3359511). This replaces + * the previous server-side workaround that added a url.path cache context, + * which caused the menu render cache to vary per URL path. * * @see https://www.drupal.org/project/dxpr_theme/issues/3553914 * @see https://www.drupal.org/project/drupal/issues/3359511 - * - * @todo Remove when core issue #3359511 is resolved. */ function dxpr_theme_preprocess_menu(&$variables) { if (empty($variables['items'])) { return; } - $current_path = \Drupal::service('path.current')->getPath(); - $is_front = \Drupal::service('path.matcher')->isFrontPage(); - dxpr_theme_menu_set_active_trail($variables['items'], $current_path, $is_front); - - // Ensure menu is cached per URL to prevent wrong active states. - $variables['#cache']['contexts'][] = 'url.path'; + // Attach client-side active trail detection for edge cases (e.g. + // Views-generated menu links) not covered by core's built-in active trail. + $variables['#attached']['library'][] = 'dxpr_theme/active-trail'; } /** * Recursively sets active trail on menu items by comparing URL paths. * + * @deprecated in dxpr_theme:8.x-2.1.0 and is removed from dxpr_theme:9.x-1.0.0. + * Active trail detection is now handled client-side via the active-trail + * library. This function is retained for backward compatibility but is no + * longer called by dxpr_theme_preprocess_menu(). + * @see https://github.com/dxpr/dxpr_theme/issues/827 + * * @param array $items * Menu items array (passed by reference). * @param string $current_path diff --git a/js/dist/dxpr-theme-active-trail.js b/js/dist/dxpr-theme-active-trail.js new file mode 100644 index 00000000..853a2a06 --- /dev/null +++ b/js/dist/dxpr-theme-active-trail.js @@ -0,0 +1,41 @@ +/** + * @file + * Client-side active trail detection for menu links. + * + * Supplements core's built-in active trail with client-side detection so that + * the server-rendered menu does not need a url.path cache context. This covers + * edge cases such as Views-generated menu links that core does not mark as + * active (see https://www.drupal.org/project/drupal/issues/3359511). + */ + +(function (Drupal) { + 'use strict'; + + Drupal.behaviors.dxprThemeActiveTrail = { + attach: function (context) { + if (context !== document) { + return; + } + var currentPath = window.location.pathname; + var menuLinks = document.querySelectorAll('.menu--main a[href]'); + menuLinks.forEach(function (link) { + var linkPath = link.pathname; + if (linkPath === currentPath) { + link.classList.add('is-active'); + var li = link.closest('li'); + while (li) { + li.classList.add('menu-item--active-trail'); + li.classList.add('active'); + var parentUl = li.parentElement; + if (parentUl) { + li = parentUl.closest('li'); + } else { + break; + } + } + } + }); + } + }; + +})(Drupal); From 4a667508aef1c04c9b47a12575d69b2f0c2509cf Mon Sep 17 00:00:00 2001 From: jjroelofs <904576+jjroelofs@users.noreply.github.com> Date: Wed, 8 Jul 2026 07:48:17 +0200 Subject: [PATCH 2/5] Fix eslint and drupal-lint violations --- dxpr_theme.theme | 11 +++++------ js/dist/dxpr-theme-active-trail.js | 27 ++++++++++++--------------- 2 files changed, 17 insertions(+), 21 deletions(-) diff --git a/dxpr_theme.theme b/dxpr_theme.theme index 93ad0121..8ca5d3d3 100644 --- a/dxpr_theme.theme +++ b/dxpr_theme.theme @@ -297,12 +297,6 @@ function dxpr_theme_preprocess_menu(&$variables) { /** * Recursively sets active trail on menu items by comparing URL paths. * - * @deprecated in dxpr_theme:8.x-2.1.0 and is removed from dxpr_theme:9.x-1.0.0. - * Active trail detection is now handled client-side via the active-trail - * library. This function is retained for backward compatibility but is no - * longer called by dxpr_theme_preprocess_menu(). - * @see https://github.com/dxpr/dxpr_theme/issues/827 - * * @param array $items * Menu items array (passed by reference). * @param string $current_path @@ -312,6 +306,11 @@ function dxpr_theme_preprocess_menu(&$variables) { * * @return bool * TRUE if any item in this level or below is active. + * + * @deprecated in dxpr_theme:2.1.0 and is removed from dxpr_theme:3.0.0. + * Active trail detection is now handled client-side via the active-trail + * library. This function is retained for backward compatibility but is no + * longer called by dxpr_theme_preprocess_menu(). */ function dxpr_theme_menu_set_active_trail(array &$items, $current_path, $is_front) { $has_active = FALSE; diff --git a/js/dist/dxpr-theme-active-trail.js b/js/dist/dxpr-theme-active-trail.js index 853a2a06..39dc37e9 100644 --- a/js/dist/dxpr-theme-active-trail.js +++ b/js/dist/dxpr-theme-active-trail.js @@ -9,33 +9,30 @@ */ (function (Drupal) { - 'use strict'; - Drupal.behaviors.dxprThemeActiveTrail = { - attach: function (context) { + attach(context) { if (context !== document) { return; } - var currentPath = window.location.pathname; - var menuLinks = document.querySelectorAll('.menu--main a[href]'); - menuLinks.forEach(function (link) { - var linkPath = link.pathname; + const currentPath = window.location.pathname; + const menuLinks = document.querySelectorAll(".menu--main a[href]"); + menuLinks.forEach((link) => { + const linkPath = link.pathname; if (linkPath === currentPath) { - link.classList.add('is-active'); - var li = link.closest('li'); + link.classList.add("is-active"); + let li = link.closest("li"); while (li) { - li.classList.add('menu-item--active-trail'); - li.classList.add('active'); - var parentUl = li.parentElement; + li.classList.add("menu-item--active-trail"); + li.classList.add("active"); + const parentUl = li.parentElement; if (parentUl) { - li = parentUl.closest('li'); + li = parentUl.closest("li"); } else { break; } } } }); - } + }, }; - })(Drupal); From ffd7d1cb6a0ba0e0c66505fc0916b97b7340729c Mon Sep 17 00:00:00 2001 From: jjroelofs <904576+jjroelofs@users.noreply.github.com> Date: Wed, 8 Jul 2026 07:54:02 +0200 Subject: [PATCH 3/5] Add required @see tag after @deprecated annotation --- dxpr_theme.theme | 1 + 1 file changed, 1 insertion(+) diff --git a/dxpr_theme.theme b/dxpr_theme.theme index 8ca5d3d3..a7b7ed1f 100644 --- a/dxpr_theme.theme +++ b/dxpr_theme.theme @@ -311,6 +311,7 @@ function dxpr_theme_preprocess_menu(&$variables) { * Active trail detection is now handled client-side via the active-trail * library. This function is retained for backward compatibility but is no * longer called by dxpr_theme_preprocess_menu(). + * @see https://www.drupal.org/project/dxpr_theme/issues/3509999 */ function dxpr_theme_menu_set_active_trail(array &$items, $current_path, $is_front) { $has_active = FALSE; From dd51a0ec2c0d0c096de4fae4e210d0e9905300ce Mon Sep 17 00:00:00 2001 From: jjroelofs <904576+jjroelofs@users.noreply.github.com> Date: Wed, 8 Jul 2026 08:15:39 +0200 Subject: [PATCH 4/5] Fix external URL matching and deprecation versions Skip external links in client-side active trail detection by checking origin before comparing pathnames. Fix @deprecated version to 8.2.0 since the theme is already past 8.x. Clarify scope: this PR removes the url.path cache context, not the core active trail cache context. --- dxpr_theme.theme | 8 ++++---- js/dist/dxpr-theme-active-trail.js | 8 ++++++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/dxpr_theme.theme b/dxpr_theme.theme index a7b7ed1f..5730fb8b 100644 --- a/dxpr_theme.theme +++ b/dxpr_theme.theme @@ -277,9 +277,9 @@ function dxpr_theme_preprocess_page(&$variables) { * Implements hook_preprocess_menu(). * * Attaches client-side active trail detection for Views-generated menu links - * that core does not mark as active (core issue #3359511). This replaces - * the previous server-side workaround that added a url.path cache context, - * which caused the menu render cache to vary per URL path. + * that core does not mark as active (core issue #3359511). This removes the + * extra url.path cache context that the previous server-side workaround added; + * the core route.menu_active_trails cache context remains on menu blocks. * * @see https://www.drupal.org/project/dxpr_theme/issues/3553914 * @see https://www.drupal.org/project/drupal/issues/3359511 @@ -307,7 +307,7 @@ function dxpr_theme_preprocess_menu(&$variables) { * @return bool * TRUE if any item in this level or below is active. * - * @deprecated in dxpr_theme:2.1.0 and is removed from dxpr_theme:3.0.0. + * @deprecated in dxpr_theme:8.2.0 and is removed from dxpr_theme:9.0.0. * Active trail detection is now handled client-side via the active-trail * library. This function is retained for backward compatibility but is no * longer called by dxpr_theme_preprocess_menu(). diff --git a/js/dist/dxpr-theme-active-trail.js b/js/dist/dxpr-theme-active-trail.js index 39dc37e9..bbbda204 100644 --- a/js/dist/dxpr-theme-active-trail.js +++ b/js/dist/dxpr-theme-active-trail.js @@ -15,10 +15,14 @@ return; } const currentPath = window.location.pathname; + const currentOrigin = window.location.origin; const menuLinks = document.querySelectorAll(".menu--main a[href]"); menuLinks.forEach((link) => { - const linkPath = link.pathname; - if (linkPath === currentPath) { + const linkUrl = new URL(link.href, currentOrigin); + if (linkUrl.origin !== currentOrigin) { + return; + } + if (linkUrl.pathname === currentPath) { link.classList.add("is-active"); let li = link.closest("li"); while (li) { From 77893bdda6a4ac76b7c93c279ecf7f6a14fe1729 Mon Sep 17 00:00:00 2001 From: jjroelofs <904576+jjroelofs@users.noreply.github.com> Date: Wed, 8 Jul 2026 08:20:26 +0200 Subject: [PATCH 5/5] Skip hash-only links in active trail detection Links with href="#" resolve to the current page URL, causing them to be falsely marked active on every page. Check the raw href attribute and skip fragment-only values before pathname comparison. --- js/dist/dxpr-theme-active-trail.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/js/dist/dxpr-theme-active-trail.js b/js/dist/dxpr-theme-active-trail.js index bbbda204..3485a659 100644 --- a/js/dist/dxpr-theme-active-trail.js +++ b/js/dist/dxpr-theme-active-trail.js @@ -18,6 +18,10 @@ const currentOrigin = window.location.origin; const menuLinks = document.querySelectorAll(".menu--main a[href]"); menuLinks.forEach((link) => { + const rawHref = link.getAttribute("href"); + if (!rawHref || rawHref.startsWith("#")) { + return; + } const linkUrl = new URL(link.href, currentOrigin); if (linkUrl.origin !== currentOrigin) { return;