Skip to content

Commit b837b5c

Browse files
committed
fix: keep popover open when cursor moves into it
Bootstrap 5 hover trigger fires mouseleave on the trigger before mouseenter on the popover, closing it before the Trace link can be clicked. Switch to trigger:manual and wire mouseenter/mouseleave on both the trigger element and the popover tip itself using a shared dvInitPopover() helper.
1 parent f111350 commit b837b5c

2 files changed

Lines changed: 87 additions & 4 deletions

File tree

netbox_device_view/templates/netbox_device_view/deviceview.html

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,44 @@
9494
</div>
9595
</div>
9696

97+
<script>
98+
// Shared popover helper — keeps popover open when cursor moves into it.
99+
// Bootstrap 5 hover trigger closes on mouseleave of the trigger element
100+
// before mouseenter on the popover fires. We use trigger:"manual" and
101+
// wire both elements ourselves so the popover stays open over either one.
102+
function dvInitPopover(el, opts) {
103+
if (typeof Popover === "undefined") return;
104+
var pop = new Popover(el, {
105+
trigger: "manual",
106+
html: true,
107+
container: "body",
108+
placement: "top",
109+
customClass: "device-view-tooltip",
110+
title: opts.title || "",
111+
content: opts.content || ""
112+
});
113+
var hideTimer = null;
114+
function scheduleHide() {
115+
hideTimer = setTimeout(function () { pop.hide(); }, 300);
116+
}
117+
function cancelHide() {
118+
clearTimeout(hideTimer);
119+
}
120+
el.addEventListener("mouseenter", function () { cancelHide(); pop.show(); });
121+
el.addEventListener("mouseleave", scheduleHide);
122+
el.addEventListener("shown.bs.popover", function () {
123+
var tip = Popover.getInstance(el) && Popover.getInstance(el)._tip;
124+
if (!tip) {
125+
var popovers = document.querySelectorAll(".popover");
126+
tip = popovers[popovers.length - 1];
127+
}
128+
if (tip) {
129+
tip.addEventListener("mouseenter", cancelHide);
130+
tip.addEventListener("mouseleave", scheduleHide);
131+
}
132+
});
133+
}
134+
</script>
97135
{% if use_svg %}
98136
{# ── SVG interactivity script ── #}
99137
<script>
@@ -177,7 +215,7 @@
177215
var svgTitle = g.querySelector("title");
178216
if (svgTitle) svgTitle.textContent = p.name;
179217
if (typeof Popover !== "undefined") {
180-
new Popover(g, { trigger: "hover focus", html: true, container: "body", delay: { show: 200, hide: 300 } });
218+
dvInitPopover(g, { title: popTitle, content: popContent });
181219
}
182220
});
183221
});
@@ -207,7 +245,10 @@ <h4>Options</h4>
207245
<script>
208246
document.addEventListener("DOMContentLoaded", function () {
209247
document.querySelectorAll('.device-view-port[data-bs-toggle="popover"]').forEach(function (el) {
210-
new Popover(el, { trigger: "hover focus", html: true, container: "body", delay: { show: 200, hide: 300 } });
248+
dvInitPopover(el, {
249+
title: el.getAttribute("data-bs-title"),
250+
content: el.getAttribute("data-bs-content")
251+
});
211252
});
212253
});
213254
</script>

netbox_device_view/templates/netbox_device_view/ports.html

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,45 @@ <h2 class="card-header">Deviceview {% if object.virtual_chassis %}{{ object.virt
9191
</div>
9292
</div>
9393

94+
<script>
95+
// Shared popover helper — keeps popover open when cursor moves into it.
96+
// Bootstrap 5 hover trigger closes on mouseleave of the trigger element
97+
// before mouseenter on the popover fires. We use trigger:"manual" and
98+
// wire both elements ourselves so the popover stays open over either one.
99+
function dvInitPopover(el, opts) {
100+
if (typeof Popover === "undefined") return;
101+
var pop = new Popover(el, {
102+
trigger: "manual",
103+
html: true,
104+
container: "body",
105+
placement: "top",
106+
customClass: "device-view-tooltip",
107+
title: opts.title || "",
108+
content: opts.content || ""
109+
});
110+
var hideTimer = null;
111+
function scheduleHide() {
112+
hideTimer = setTimeout(function () { pop.hide(); }, 300);
113+
}
114+
function cancelHide() {
115+
clearTimeout(hideTimer);
116+
}
117+
el.addEventListener("mouseenter", function () { cancelHide(); pop.show(); });
118+
el.addEventListener("mouseleave", scheduleHide);
119+
el.addEventListener("shown.bs.popover", function () {
120+
var tip = Popover.getInstance(el) && Popover.getInstance(el)._tip;
121+
if (!tip) {
122+
// fallback: grab the most recently appended popover in body
123+
var popovers = document.querySelectorAll(".popover");
124+
tip = popovers[popovers.length - 1];
125+
}
126+
if (tip) {
127+
tip.addEventListener("mouseenter", cancelHide);
128+
tip.addEventListener("mouseleave", scheduleHide);
129+
}
130+
});
131+
}
132+
</script>
94133
{% if use_svg %}
95134
{# ── SVG interactivity script ── #}
96135
<script>
@@ -172,7 +211,7 @@ <h2 class="card-header">Deviceview {% if object.virtual_chassis %}{{ object.virt
172211
var svgTitle = g.querySelector("title");
173212
if (svgTitle) svgTitle.textContent = p.name;
174213
if (typeof Popover !== "undefined") {
175-
new Popover(g, { trigger: "hover focus", html: true, container: "body", delay: { show: 200, hide: 300 } });
214+
dvInitPopover(g, { title: popTitle, content: popContent });
176215
}
177216
});
178217
});
@@ -188,7 +227,10 @@ <h2 class="card-header">Deviceview {% if object.virtual_chassis %}{{ object.virt
188227
<script>
189228
document.addEventListener("DOMContentLoaded", function () {
190229
document.querySelectorAll('.device-view-port[data-bs-toggle="popover"]').forEach(function (el) {
191-
new Popover(el, { trigger: "hover focus", html: true, container: "body", delay: { show: 200, hide: 300 } });
230+
dvInitPopover(el, {
231+
title: el.getAttribute("data-bs-title"),
232+
content: el.getAttribute("data-bs-content")
233+
});
192234
});
193235
});
194236
</script>

0 commit comments

Comments
 (0)