fix(frontend): dismiss tooltips during drawer activation

This commit is contained in:
rcourtman
2026-08-25 17:55:48 +01:00
parent 389ff2825e
commit 07e85acc6d
3 changed files with 87 additions and 27 deletions
+15 -23
View File
@@ -1,30 +1,23 @@
{
"version": 1,
"base_sha": "f5eb87437a396048959a851584d3104326e0d671",
"verified_at": "2026-08-25T16:41:20Z",
"base_sha": "389ff2825e5228a9113921cd2e3ca6c1d2118962",
"verified_at": "2026-08-25T16:54:23Z",
"result": "passed",
"changed_paths": [
"frontend-modern/src/components/Infrastructure/ResourceOperatorStateSection.tsx",
"frontend-modern/src/components/Workloads/StackedMemoryBar.tsx",
"frontend-modern/src/components/shared/InlineDetailTableRow.tsx",
"frontend-modern/src/components/shared/Subtabs.tsx",
"frontend-modern/src/features/platformPage/PlatformWindowedRows.tsx"
"frontend-modern/src/hooks/useTooltip.ts"
],
"content_sha256": {
"frontend-modern/src/components/Infrastructure/ResourceOperatorStateSection.tsx": "1a01f6e6c8db585c32b37afae33ed7b6671dc76e40e8f49c364ee78659e8a961",
"frontend-modern/src/components/Workloads/StackedMemoryBar.tsx": "3266fa75221643199d41b7a87f8e07181e643171c744ac0e32e2ac9c6773e3bf",
"frontend-modern/src/components/shared/InlineDetailTableRow.tsx": "0e169eb088c0bb0754dda5936b52baecc102f1a50853263517d76931729a1673",
"frontend-modern/src/components/shared/Subtabs.tsx": "5106f14ec387c72bd2d3014200678f342e28b04d5ff372aa95410695fe0dc544",
"frontend-modern/src/features/platformPage/PlatformWindowedRows.tsx": "3f5d02c126379a44803bea3efcc803965d872eb188ec7e0305c973a82fa80924"
"frontend-modern/src/hooks/useTooltip.ts": "5cd07282857599694979567df14cbfdb54f853b73f6d5d17b29ab1fffcc51c17"
},
"routes": [
"/docker/overview",
"/truenas/overview",
"/kubernetes/overview"
"/vmware/overview"
],
"viewports": [
{
"width": 1280,
"height": 720
"height": 800
},
{
"width": 390,
@@ -32,16 +25,15 @@
}
],
"states": [
"TrueNAS system summary with memory-capacity label scrim",
"TrueNAS system drawer Manage tab at desktop width",
"TrueNAS system drawer Manage tab at phone width",
"Kubernetes cluster drawer with five-tab overflow rail before and after scrolling"
"Docker host table before expansion with metric hover targets",
"Docker auth-service-01 host drawer open without a lingering metric tooltip",
"TrueNAS truenas-main system drawer open at phone width",
"vSphere etl-batch-01 workload drawer open at phone width"
],
"interactions": [
"opened the TrueNAS system inline drawer and selected Manage",
"verified Manage remained selected through repeated realtime refresh samples and after the final main rebase",
"inspected wrapped operator-policy help text and the stacked phone maintenance action",
"opened a Kubernetes cluster inline drawer at 390px and used its right-edge tab scroll control",
"verified the left-edge control appeared after scrolling and restored the default viewport"
"activated the auth-service-01 metric-heavy row at 390px and verified the memory and CPU tooltip portals dismissed before the drawer reflow",
"repeated the auth-service-01 row activation at desktop width and verified the drawer remained unobscured with no lingering metric tooltip",
"opened TrueNAS and vSphere specialized drawers at 390px and inspected placement, clipping, stacking, scrolling, and tab usability",
"restored the browser viewport after responsive verification"
]
}
@@ -41,6 +41,49 @@ describe('useTooltip', () => {
});
});
it('dismisses an active hover tooltip when the pointer activates the page', () => {
createRoot((dispose) => {
const tip = useTooltip();
tip.onMouseEnter({
clientX: 120,
clientY: 80,
currentTarget: {
ownerDocument: document,
getBoundingClientRect: () => ({ left: 10, top: 20, width: 40, height: 16 }),
},
} as unknown as MouseEvent);
expect(tip.show()).toBe(true);
document.dispatchEvent(new Event('pointerdown', { bubbles: true }));
expect(tip.show()).toBe(false);
dispose();
});
});
it('re-arms pointer dismissal after a subsequent hover', () => {
createRoot((dispose) => {
const tip = useTooltip();
const enter = () =>
tip.onMouseEnter({
clientX: 120,
clientY: 80,
currentTarget: {
ownerDocument: document,
getBoundingClientRect: () => ({ left: 10, top: 20, width: 40, height: 16 }),
},
} as unknown as MouseEvent);
enter();
document.dispatchEvent(new Event('pointerdown', { bubbles: true }));
enter();
expect(tip.show()).toBe(true);
document.dispatchEvent(new Event('pointerdown', { bubbles: true }));
expect(tip.show()).toBe(false);
dispose();
});
});
it('ignores synthesized mouse hover on a coarse touch pointer', () => {
vi.stubGlobal(
'matchMedia',
+29 -4
View File
@@ -1,4 +1,4 @@
import { createSignal } from 'solid-js';
import { createSignal, onCleanup } from 'solid-js';
import { supportsHoverTooltips } from '@/components/shared/hoverCapability';
@@ -24,22 +24,47 @@ export interface TooltipPos {
export function useTooltip() {
const [show, setShow] = createSignal(false);
const [pos, setPos] = createSignal<TooltipPos>({ x: 0, y: 0 });
let pointerDismissDocument: Document | undefined;
const disarmPointerDismissal = () => {
pointerDismissDocument?.removeEventListener('pointerdown', dismissTooltip, true);
pointerDismissDocument = undefined;
};
const dismissTooltip = () => {
setShow(false);
disarmPointerDismissal();
};
const armPointerDismissal = (ownerDocument?: Document) => {
disarmPointerDismissal();
pointerDismissDocument =
ownerDocument ?? (typeof document === 'undefined' ? undefined : document);
pointerDismissDocument?.addEventListener('pointerdown', dismissTooltip, {
capture: true,
once: true,
});
};
const onMouseEnter = (e: MouseEvent) => {
if (!supportsHoverTooltips()) {
setShow(false);
dismissTooltip();
return;
}
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
const currentTarget = e.currentTarget as HTMLElement;
const rect = currentTarget.getBoundingClientRect();
const hasPointerPosition = Number.isFinite(e.clientX) && Number.isFinite(e.clientY);
setPos({
x: hasPointerPosition ? e.clientX : rect.left + rect.width / 2,
y: hasPointerPosition ? e.clientY : rect.top,
});
setShow(true);
armPointerDismissal(currentTarget.ownerDocument);
};
const onMouseLeave = () => setShow(false);
const onMouseLeave = dismissTooltip;
onCleanup(disarmPointerDismissal);
return { show, setShow, pos, onMouseEnter, onMouseLeave } as const;
}