From af2569d94f53cd326b50e71624afae6cad778eb3 Mon Sep 17 00:00:00 2001 From: alanv Date: Wed, 28 Oct 2026 10:48:51 -0500 Subject: [PATCH 1/4] Fix duplicate createRoot warning --- .../labkey/core/view/template/bootstrap/header.jsp | 4 +++- core/webapp/core/ProductNavigationHeader.js | 13 ------------- 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/core/src/org/labkey/core/view/template/bootstrap/header.jsp b/core/src/org/labkey/core/view/template/bootstrap/header.jsp index c43d52c28f5..948207056c4 100644 --- a/core/src/org/labkey/core/view/template/bootstrap/header.jsp +++ b/core/src/org/labkey/core/view/template/bootstrap/header.jsp @@ -214,7 +214,9 @@ <% diff --git a/core/webapp/core/ProductNavigationHeader.js b/core/webapp/core/ProductNavigationHeader.js index 06f67b458e8..b84dcc7f4b7 100644 --- a/core/webapp/core/ProductNavigationHeader.js +++ b/core/webapp/core/ProductNavigationHeader.js @@ -12,18 +12,13 @@ var productNavLoaded = false; $(document).on('mouseenter', '#' + HEADER_ID_PREFIX + ' .dropdown-toggle', function(e) { if (!productNavLoaded) { - $("#" + HEADER_CONTENT_ID).html("
Loading...
"); - LABKEY.requiresScript('gen/productNavigation', loadProductNav); // LABKEY.requiresScript('http://localhost:3001/productNavigation.js', loadProductNav); - } else { - loadProductNav(); } }); var loadProductNav = function() { LABKEY.App.loadApp('productNavigation', HEADER_CONTENT_ID, { show: true }); - $(document).on('click', addProductNavClickHandler); productNavLoaded = true; }; @@ -31,12 +26,4 @@ $(document).on('click', '#' + HEADER_CONTENT_ID, function (e) { e.stopPropagation(); }); - - // on click outside of the open menu, remove click handler and hide menu (which will force it to reset on next open) - var addProductNavClickHandler = function (e) { - if ($(e.target).closest(HEADER_CONTENT_ID).length === 0) { - LABKEY.App.loadApp('productNavigation', HEADER_CONTENT_ID, { show: false }); - $(document).off('click', addProductNavClickHandler); - } - }; })(jQuery); From 5acf41a2eb4c03c78e5792c9d7757dc5e890c0e0 Mon Sep 17 00:00:00 2001 From: alanv Date: Wed, 28 Oct 2026 12:34:29 -0500 Subject: [PATCH 2/4] header.jsp: Improve accessibility of product nav loading state --- core/src/org/labkey/core/view/template/bootstrap/header.jsp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/src/org/labkey/core/view/template/bootstrap/header.jsp b/core/src/org/labkey/core/view/template/bootstrap/header.jsp index 948207056c4..a3c2fd3bd0c 100644 --- a/core/src/org/labkey/core/view/template/bootstrap/header.jsp +++ b/core/src/org/labkey/core/view/template/bootstrap/header.jsp @@ -214,9 +214,9 @@ <% From 2bc86dbb4ef4153b80b69f0cf5d99fe2a2760d16 Mon Sep 17 00:00:00 2001 From: alanv Date: Wed, 28 Oct 2026 12:37:28 -0500 Subject: [PATCH 3/4] ProductNavigation/app.tsx - call createRoot once --- core/src/client/ProductNavigation/app.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/core/src/client/ProductNavigation/app.tsx b/core/src/client/ProductNavigation/app.tsx index 4f1cd00b110..056d953982b 100644 --- a/core/src/client/ProductNavigation/app.tsx +++ b/core/src/client/ProductNavigation/app.tsx @@ -4,11 +4,14 @@ * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 */ import React from 'react'; -import { createRoot } from 'react-dom/client'; +import { createRoot, Root } from 'react-dom/client'; import { App } from '@labkey/api'; import { AppContext, ProductNavigation } from './ProductNavigation'; +let root: Root; + App.registerApp('productNavigation', (target, ctx) => { - createRoot(document.getElementById(target)).render(); + if (!root) root = createRoot(document.getElementById(target)); + root.render(); }); From 36735649291cf93d2a98b4105c023992b60a74c5 Mon Sep 17 00:00:00 2001 From: alanv Date: Wed, 28 Oct 2026 12:38:12 -0500 Subject: [PATCH 4/4] ProductNavigationHeader - improve script preloading, use boostrap events --- core/webapp/core/ProductNavigationHeader.js | 45 +++++++++++++++------ 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/core/webapp/core/ProductNavigationHeader.js b/core/webapp/core/ProductNavigationHeader.js index b84dcc7f4b7..f9e4b326f2c 100644 --- a/core/webapp/core/ProductNavigationHeader.js +++ b/core/webapp/core/ProductNavigationHeader.js @@ -5,22 +5,41 @@ */ (function ($) { - var HEADER_ID_PREFIX = 'headerProductDropdown'; - var HEADER_CONTENT_ID = HEADER_ID_PREFIX + '-content'; + const HEADER_ID_PREFIX = 'headerProductDropdown'; + const HEADER_CONTENT_ID = HEADER_ID_PREFIX + '-content'; + let scriptRequested = false; + let scriptLoaded = false; + let menuOpen = false; - // wait to load the product navigation dependencies until the hover over the header icon - var productNavLoaded = false; - $(document).on('mouseenter', '#' + HEADER_ID_PREFIX + ' .dropdown-toggle', function(e) { - if (!productNavLoaded) { - LABKEY.requiresScript('gen/productNavigation', loadProductNav); - // LABKEY.requiresScript('http://localhost:3001/productNavigation.js', loadProductNav); - } + function showProductMenu(show) { + LABKEY.App.loadApp('productNavigation', HEADER_CONTENT_ID, { show }); + } + + function onLoad() { + scriptLoaded = true; + if (menuOpen) showProductMenu(true); + } + + function requireProductNav() { + if (scriptRequested) return; + scriptRequested = true; + LABKEY.requiresScript('gen/productNavigation', onLoad); + // LABKEY.requiresScript('http://localhost:3001/productNavigation.js', onLoad); + } + + // preload on hover/focus so the menu is usually ready by the time it opens + $(document).on('mouseenter focusin', '#' + HEADER_ID_PREFIX + ' .dropdown-toggle', requireProductNav); + + $(document).on('show.bs.dropdown', '#' + HEADER_ID_PREFIX, function () { + menuOpen = true; + requireProductNav(); + if (scriptLoaded) showProductMenu(true); }); - var loadProductNav = function() { - LABKEY.App.loadApp('productNavigation', HEADER_CONTENT_ID, { show: true }); - productNavLoaded = true; - }; + $(document).on('hidden.bs.dropdown', '#' + HEADER_ID_PREFIX, function () { + menuOpen = false; + if (scriptLoaded) showProductMenu(false); + }); // stop the product navigation menu from closing when click within the menu div $(document).on('click', '#' + HEADER_CONTENT_ID, function (e) {