Skip to content

Commit 44802ee

Browse files
committed
feat(gui): 用跟随光标的 JS tooltip 替换 CSS 伪元素提示
- 新增 tooltip.js:鼠标悬停时提示框跟随指针、键盘聚焦时锚定元素下方,并钳制在视口内避免越界 - app.css 移除 [data-tooltip]::after 固定定位样式,改为由 JS 定位的 .cursor-tooltip - index.html 加入 #cursor-tooltip 元素并加载 tooltip.js
1 parent 4a7d239 commit 44802ee

3 files changed

Lines changed: 126 additions & 27 deletions

File tree

plexmuxy_gui/static/app.css

Lines changed: 16 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2624,41 +2624,38 @@ progress::-moz-progress-bar {
26242624
min-width: 0;
26252625
}
26262626

2627-
[data-tooltip] {
2628-
position: relative;
2629-
}
2630-
2631-
[data-tooltip]::after {
2632-
position: absolute;
2633-
z-index: 500;
2634-
top: calc(100% + 7px);
2635-
left: 50%;
2636-
width: max-content;
2637-
max-width: min(280px, calc(100vw - 24px));
2627+
/* Cursor-following tooltip. The element is positioned by JS (tooltip.js) so it
2628+
tracks the pointer and stays inside the viewport, instead of being pinned to
2629+
the centre of the hovered element (which could sit far from the cursor or run
2630+
off-screen near a window edge). */
2631+
.cursor-tooltip {
2632+
position: fixed;
2633+
z-index: 3000;
2634+
top: 0;
2635+
left: 0;
2636+
max-width: min(280px, calc(100vw - 16px));
26382637
border: 1px solid var(--color-border);
26392638
border-radius: 5px;
26402639
padding: 5px 8px;
26412640
background: var(--color-surface-raised);
26422641
box-shadow: var(--shadow-popover);
26432642
color: var(--color-text-body);
2644-
content: attr(data-tooltip);
26452643
font-size: 11px;
26462644
font-weight: 500;
26472645
line-height: 16px;
2648-
opacity: 0;
26492646
overflow-wrap: anywhere;
26502647
pointer-events: none;
2651-
transform: translate(-50%, -3px);
2652-
transition: opacity var(--duration-fast) var(--ease-standard), transform var(--duration-fast) var(--ease-standard);
2653-
visibility: hidden;
26542648
white-space: normal;
2649+
opacity: 0;
2650+
visibility: hidden;
2651+
transform: translateY(2px);
2652+
transition: opacity var(--duration-fast) var(--ease-standard), transform var(--duration-fast) var(--ease-standard), visibility var(--duration-fast) var(--ease-standard);
26552653
}
26562654

2657-
[data-tooltip]:hover::after,
2658-
[data-tooltip]:focus-visible::after {
2655+
.cursor-tooltip.is-visible {
26592656
opacity: 1;
2660-
transform: translate(-50%, 0);
26612657
visibility: visible;
2658+
transform: translateY(0);
26622659
}
26632660

26642661
.custom-select.is-open {
@@ -2995,14 +2992,6 @@ input[aria-disabled="true"] {
29952992
}
29962993
}
29972994

2998-
@media (min-width: 900px) and (max-width: 1199px) {
2999-
.step[data-tooltip]::after {
3000-
top: 50%;
3001-
left: calc(100% + 8px);
3002-
transform: translate(0, -50%);
3003-
}
3004-
}
3005-
30062995
@media (forced-colors: active) {
30072996
.step.active,
30082997
.select-listbox [role="option"][aria-selected="true"],

plexmuxy_gui/static/index.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -718,6 +718,8 @@ <h3 id="about-credits-title" data-i18n="about.credits.title">Open-source foundat
718718

719719
<div id="toast-region" class="toast-region" role="region" aria-label="Notifications" data-i18n-aria-label="toast.region" aria-live="polite" aria-relevant="additions"></div>
720720

721+
<div id="cursor-tooltip" class="cursor-tooltip" role="tooltip"></div>
722+
721723
<button id="plan-save-fab" class="plan-save-fab hidden" type="button" aria-live="polite">
722724
<span class="fab-count" aria-hidden="true">0</span>
723725
<span class="fab-spinner" aria-hidden="true"></span>
@@ -780,6 +782,7 @@ <h2 id="diagnostics-dialog-title" data-i18n="jobs.diagnostics.title">Job diagnos
780782
<script src="./js/locale.js"></script>
781783
<script src="./js/navigation.js"></script>
782784
<script src="./js/window.js"></script>
785+
<script src="./js/tooltip.js"></script>
783786
<script src="./js/settings.js"></script>
784787
<script src="./js/plan.js"></script>
785788
<script src="./js/results.js"></script>

plexmuxy_gui/static/js/tooltip.js

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/* Cursor-following tooltip.
2+
*
3+
* Replaces the old CSS `::after` tooltip, which was pinned to the horizontal
4+
* centre of the hovered element (so it could sit far from the pointer and run
5+
* off-screen near a window edge). This implementation:
6+
* - shows the tooltip next to the pointer on hover/move,
7+
* - anchors it just below the element for keyboard focus, and
8+
* - clamps the position so it never leaves the viewport.
9+
*
10+
* A single shared element (`#cursor-tooltip`) is reused for every target; the
11+
* text comes from each element's `data-tooltip` attribute (set both in markup
12+
* and dynamically by other scripts, e.g. the window controls).
13+
*/
14+
(function () {
15+
const tooltip = document.getElementById("cursor-tooltip");
16+
if (!tooltip) return;
17+
18+
let activeTarget = null;
19+
20+
const GAP = 12; // distance from the pointer / element edge
21+
const MARGIN = 8; // minimum distance kept from the viewport edge
22+
23+
function positionTooltip(preferredLeft, preferredTop) {
24+
const rect = tooltip.getBoundingClientRect();
25+
const viewportWidth = window.innerWidth;
26+
const viewportHeight = window.innerHeight;
27+
28+
let left = preferredLeft;
29+
let top = preferredTop;
30+
31+
if (left + rect.width > viewportWidth - MARGIN) {
32+
left = viewportWidth - MARGIN - rect.width;
33+
}
34+
if (left < MARGIN) left = MARGIN;
35+
36+
if (top + rect.height > viewportHeight - MARGIN) {
37+
top = viewportHeight - MARGIN - rect.height;
38+
}
39+
if (top < MARGIN) top = MARGIN;
40+
41+
tooltip.style.left = `${left}px`;
42+
tooltip.style.top = `${top}px`;
43+
}
44+
45+
function showForElement(element, clientX, clientY) {
46+
const text = element.dataset.tooltip;
47+
if (!text) return;
48+
activeTarget = element;
49+
tooltip.textContent = text;
50+
if (typeof clientY === "number") {
51+
// Pointer hover: place down-right of the cursor, then clamp on-screen.
52+
positionTooltip(clientX + GAP, clientY + GAP);
53+
} else {
54+
// Keyboard focus: anchor just below the element's left edge, then clamp.
55+
const box = element.getBoundingClientRect();
56+
positionTooltip(box.left, box.bottom + GAP);
57+
}
58+
tooltip.classList.add("is-visible");
59+
}
60+
61+
function hide() {
62+
activeTarget = null;
63+
tooltip.classList.remove("is-visible");
64+
}
65+
66+
document.addEventListener("pointerover", (event) => {
67+
const element = event.target.closest("[data-tooltip]");
68+
if (!element || !element.dataset.tooltip) return;
69+
showForElement(element, event.clientX, event.clientY);
70+
});
71+
72+
document.addEventListener("pointermove", (event) => {
73+
if (!activeTarget) return;
74+
const element = event.target.closest("[data-tooltip]");
75+
if (element !== activeTarget) return;
76+
positionTooltip(event.clientX + GAP, event.clientY + GAP);
77+
});
78+
79+
document.addEventListener("pointerout", (event) => {
80+
const element = event.target.closest("[data-tooltip]");
81+
if (!element) return;
82+
// Ignore moves between the element and its own descendants.
83+
if (event.relatedTarget && element.contains(event.relatedTarget)) return;
84+
// Moving straight to another tooltip target is handled by its pointerover,
85+
// so don't hide here (prevents a flicker between adjacent targets).
86+
if (
87+
event.relatedTarget &&
88+
typeof event.relatedTarget.closest === "function" &&
89+
event.relatedTarget.closest("[data-tooltip]")
90+
) {
91+
return;
92+
}
93+
hide();
94+
});
95+
96+
document.addEventListener("focusin", (event) => {
97+
const element = event.target.closest("[data-tooltip]");
98+
if (!element || !element.dataset.tooltip) return;
99+
showForElement(element);
100+
});
101+
102+
document.addEventListener("focusout", (event) => {
103+
const element = event.target.closest("[data-tooltip]");
104+
if (!element) return;
105+
hide();
106+
});
107+
})();

0 commit comments

Comments
 (0)