fix: tooltip positioning without rAF — measure synchronously via ready class
This commit is contained in:
@@ -272,7 +272,7 @@
|
|||||||
"id": "lengths-and-distance",
|
"id": "lengths-and-distance",
|
||||||
"label": "Lengths and Distance",
|
"label": "Lengths and Distance",
|
||||||
"baseUnitId": "mm",
|
"baseUnitId": "mm",
|
||||||
"toUniversal": 14.2332177486,
|
"toUniversal": 1,
|
||||||
"alwaysShowLabel": false,
|
"alwaysShowLabel": false,
|
||||||
"hidden": false
|
"hidden": false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -469,6 +469,10 @@
|
|||||||
.hu-tooltip--visible {
|
.hu-tooltip--visible {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
.hu-tooltip--ready {
|
||||||
|
opacity: 1;
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
[data-theme=dan] .hu-tooltip {
|
[data-theme=dan] .hu-tooltip {
|
||||||
background: #000;
|
background: #000;
|
||||||
color: #ffff00;
|
color: #ffff00;
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const TOOLTIP_CLASS = 'hu-tooltip';
|
const TOOLTIP_CLASS = 'hu-tooltip';
|
||||||
|
const TOOLTIP_VISIBLE_CLASS = 'hu-tooltip--visible';
|
||||||
|
const TOOLTIP_READY_CLASS = 'hu-tooltip--ready';
|
||||||
|
|
||||||
// Shared tooltip element — one per page, repositioned as needed.
|
// Shared tooltip element — one per page, repositioned as needed.
|
||||||
let tooltipEl: HTMLDivElement | null = null;
|
let tooltipEl: HTMLDivElement | null = null;
|
||||||
@@ -21,32 +23,16 @@ function getTooltipEl(): HTMLDivElement {
|
|||||||
return tooltipEl;
|
return tooltipEl;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function tooltip(node: HTMLElement, text: string) {
|
/**
|
||||||
let timer: ReturnType<typeof setTimeout> | null = null;
|
* Position the tooltip near (cx, cy), shifting up only as much as needed
|
||||||
let currentText = text;
|
* to keep it within the viewport. Requires the element to already be in
|
||||||
|
* the DOM with its content set so offsetWidth/offsetHeight are accurate.
|
||||||
function show(e: MouseEvent) {
|
*/
|
||||||
if (!currentText) return;
|
|
||||||
const el = getTooltipEl();
|
|
||||||
el.textContent = currentText;
|
|
||||||
|
|
||||||
// Position off-screen first so we can measure dimensions after render
|
|
||||||
el.style.left = '-9999px';
|
|
||||||
el.style.top = '-9999px';
|
|
||||||
el.classList.add('hu-tooltip--visible');
|
|
||||||
|
|
||||||
// Use rAF to let the browser lay out the element before measuring
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
position(el, e.clientX, e.clientY);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function position(el: HTMLDivElement, cx: number, cy: number) {
|
function position(el: HTMLDivElement, cx: number, cy: number) {
|
||||||
const margin = 8;
|
const margin = 8;
|
||||||
const w = el.offsetWidth;
|
const w = el.offsetWidth;
|
||||||
const h = el.offsetHeight;
|
const h = el.offsetHeight;
|
||||||
const x = Math.min(cx + 12, window.innerWidth - w - margin);
|
const x = Math.min(cx + 12, window.innerWidth - w - margin);
|
||||||
// Place below cursor; shift up only as much as needed to fit
|
|
||||||
let y = cy + 20;
|
let y = cy + 20;
|
||||||
const overflow = y + h + margin - window.innerHeight;
|
const overflow = y + h + margin - window.innerHeight;
|
||||||
if (overflow > 0) y -= overflow;
|
if (overflow > 0) y -= overflow;
|
||||||
@@ -55,14 +41,37 @@ export function tooltip(node: HTMLElement, text: string) {
|
|||||||
el.style.top = `${y}px`;
|
el.style.top = `${y}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function tooltip(node: HTMLElement, text: string) {
|
||||||
|
let timer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
let currentText = text;
|
||||||
|
|
||||||
|
function show(cx: number, cy: number) {
|
||||||
|
if (!currentText) return;
|
||||||
|
const el = getTooltipEl();
|
||||||
|
el.textContent = currentText;
|
||||||
|
|
||||||
|
// Make visible but suppress the opacity transition while we measure+position.
|
||||||
|
// --ready class sets opacity:1 with no transition; --visible adds the fade-in.
|
||||||
|
el.classList.remove(TOOLTIP_VISIBLE_CLASS);
|
||||||
|
el.classList.add(TOOLTIP_READY_CLASS);
|
||||||
|
|
||||||
|
// offsetWidth/offsetHeight are now accurate (element is in DOM, display not none)
|
||||||
|
position(el, cx, cy);
|
||||||
|
|
||||||
|
// Swap to the visible class to re-enable the fade transition
|
||||||
|
el.classList.remove(TOOLTIP_READY_CLASS);
|
||||||
|
el.classList.add(TOOLTIP_VISIBLE_CLASS);
|
||||||
|
}
|
||||||
|
|
||||||
function enter(e: MouseEvent) {
|
function enter(e: MouseEvent) {
|
||||||
if (!currentText) return;
|
if (!currentText) return;
|
||||||
timer = setTimeout(() => show(e), 500);
|
const cx = e.clientX;
|
||||||
|
const cy = e.clientY;
|
||||||
|
timer = setTimeout(() => show(cx, cy), 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
function move(e: MouseEvent) {
|
function move(e: MouseEvent) {
|
||||||
// Keep tooltip near cursor while hovering
|
if (!tooltipEl?.classList.contains(TOOLTIP_VISIBLE_CLASS)) return;
|
||||||
if (!tooltipEl?.classList.contains('hu-tooltip--visible')) return;
|
|
||||||
position(getTooltipEl(), e.clientX, e.clientY);
|
position(getTooltipEl(), e.clientX, e.clientY);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,7 +80,11 @@ export function tooltip(node: HTMLElement, text: string) {
|
|||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
timer = null;
|
timer = null;
|
||||||
}
|
}
|
||||||
tooltipEl?.classList.remove('hu-tooltip--visible');
|
const el = tooltipEl;
|
||||||
|
if (el) {
|
||||||
|
el.classList.remove(TOOLTIP_VISIBLE_CLASS);
|
||||||
|
el.classList.remove(TOOLTIP_READY_CLASS);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
node.addEventListener('mouseenter', enter);
|
node.addEventListener('mouseenter', enter);
|
||||||
@@ -81,7 +94,7 @@ export function tooltip(node: HTMLElement, text: string) {
|
|||||||
return {
|
return {
|
||||||
update(newText: string) {
|
update(newText: string) {
|
||||||
currentText = newText;
|
currentText = newText;
|
||||||
if (tooltipEl?.classList.contains('hu-tooltip--visible')) {
|
if (tooltipEl?.classList.contains(TOOLTIP_VISIBLE_CLASS)) {
|
||||||
tooltipEl.textContent = newText;
|
tooltipEl.textContent = newText;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -90,7 +103,11 @@ export function tooltip(node: HTMLElement, text: string) {
|
|||||||
node.removeEventListener('mouseenter', enter);
|
node.removeEventListener('mouseenter', enter);
|
||||||
node.removeEventListener('mousemove', move);
|
node.removeEventListener('mousemove', move);
|
||||||
node.removeEventListener('mouseleave', leave);
|
node.removeEventListener('mouseleave', leave);
|
||||||
tooltipEl?.classList.remove('hu-tooltip--visible');
|
const el = tooltipEl;
|
||||||
|
if (el) {
|
||||||
|
el.classList.remove(TOOLTIP_VISIBLE_CLASS);
|
||||||
|
el.classList.remove(TOOLTIP_READY_CLASS);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user