Skip to content

Commit 98dbfe9

Browse files
committed
fix(web): avoid topbar tooltip overlap
1 parent 55c1bd0 commit 98dbfe9

2 files changed

Lines changed: 60 additions & 4 deletions

File tree

packages/web/src/components/ui/tooltip/index.test.tsx

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,52 @@ describe("Tooltip", () => {
176176
});
177177
});
178178

179+
it("places the tooltip below the trigger when there is not enough room above", () => {
180+
vi.spyOn(HTMLElement.prototype, "getBoundingClientRect").mockImplementation(function (
181+
this: HTMLElement
182+
) {
183+
if (this.getAttribute("role") === "tooltip") {
184+
return {
185+
x: 0,
186+
y: 0,
187+
top: 0,
188+
left: 0,
189+
right: 96,
190+
bottom: 24,
191+
width: 96,
192+
height: 24,
193+
toJSON: () => ({}),
194+
} as DOMRect;
195+
}
196+
197+
return {
198+
x: 120,
199+
y: 12,
200+
top: 12,
201+
left: 120,
202+
right: 160,
203+
bottom: 44,
204+
width: 40,
205+
height: 32,
206+
toJSON: () => ({}),
207+
} as DOMRect;
208+
});
209+
vi.stubGlobal("innerWidth", 1024);
210+
vi.stubGlobal("innerHeight", 768);
211+
212+
render(
213+
<Tooltip content="Settings">
214+
<button type="button">Trigger</button>
215+
</Tooltip>
216+
);
217+
218+
fireEvent.mouseEnter(screen.getByRole("button", { name: "Trigger" }));
219+
220+
expect(screen.getByRole("tooltip")).toHaveStyle({
221+
top: "52px",
222+
});
223+
});
224+
179225
it("becomes a no-op wrapper on mobile/coarse viewports", () => {
180226
viewportMock.value = "mobile";
181227

packages/web/src/components/ui/tooltip/index.tsx

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,14 +73,24 @@ export function Tooltip({ children, content, disabled = false }: TooltipProps) {
7373

7474
const triggerRect = triggerRef.current.getBoundingClientRect();
7575
const tooltipRect = tooltipRef.current.getBoundingClientRect();
76+
const viewportPadding = 8;
7677
const centeredLeft = triggerRect.left + triggerRect.width / 2 - tooltipRect.width / 2;
77-
const maxLeft = Math.max(8, window.innerWidth - tooltipRect.width - 8);
78-
const left = Math.min(maxLeft, Math.max(8, centeredLeft));
79-
const top = triggerRect.top - tooltipRect.height - 8;
78+
const maxLeft = Math.max(
79+
viewportPadding,
80+
window.innerWidth - tooltipRect.width - viewportPadding
81+
);
82+
const left = Math.min(maxLeft, Math.max(viewportPadding, centeredLeft));
83+
const aboveTop = triggerRect.top - tooltipRect.height - viewportPadding;
84+
const belowTop = triggerRect.bottom + viewportPadding;
85+
const maxTop = Math.max(
86+
viewportPadding,
87+
window.innerHeight - tooltipRect.height - viewportPadding
88+
);
89+
const top = aboveTop >= viewportPadding ? aboveTop : Math.min(maxTop, belowTop);
8090

8191
setPosition({
8292
left,
83-
top: Math.max(8, top),
93+
top: Math.max(viewportPadding, top),
8494
});
8595
}, [open]);
8696

0 commit comments

Comments
 (0)