diff --git a/src/ui/Tooltip.module.css b/src/ui/Tooltip.module.css index 48cd3c4a..d5717f3d 100644 --- a/src/ui/Tooltip.module.css +++ b/src/ui/Tooltip.module.css @@ -5,7 +5,7 @@ display: flex; align-items: center; gap: 8px; - max-width: 360px; + max-width: min(720px, calc(100vw - 32px)); padding: 6px 10px; border-radius: var(--radius-tooltip, 6px); background: var(--bg-tooltip); @@ -15,17 +15,23 @@ font-size: var(--fs-ui); line-height: 16px; pointer-events: none; - white-space: nowrap; opacity: 0; } .tooltip[data-placed] { opacity: 1; transition: opacity var(--dur-fast) var(--ease); } +/* Long paths wrap inside the bubble; the shortcut stays on one line. */ +.text { + min-width: 0; + overflow-wrap: anywhere; +} .shortcut { font-family: var(--font-keys); font-size: var(--fs-ui); color: color-mix(in srgb, var(--fg-tooltip) 62%, transparent); + white-space: nowrap; + flex-shrink: 0; } @media (prefers-reduced-motion: reduce) { diff --git a/src/ui/Tooltip.test.tsx b/src/ui/Tooltip.test.tsx new file mode 100644 index 00000000..813bd8a8 --- /dev/null +++ b/src/ui/Tooltip.test.tsx @@ -0,0 +1,67 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { act, cleanup, fireEvent, render, screen } from "@testing-library/react"; +import { TOOLTIP_DELAY, TooltipHost, tooltipAttrs } from "./Tooltip"; +import styles from "./Tooltip.module.css"; + +describe("tooltipAttrs", () => { + it("returns empty object when text is missing", () => { + expect(tooltipAttrs(undefined)).toEqual({}); + expect(tooltipAttrs("")).toEqual({}); + }); + + it("sets data-tooltip and optional shortcut", () => { + expect(tooltipAttrs("Close Tab")).toEqual({ "data-tooltip": "Close Tab" }); + expect(tooltipAttrs("Save All", "Ctrl+S")).toEqual({ + "data-tooltip": "Save All", + "data-tooltip-shortcut": "Ctrl+S", + }); + }); +}); + +describe("TooltipHost", () => { + beforeEach(() => { + vi.useFakeTimers(); + }); + + afterEach(() => { + cleanup(); + vi.useRealTimers(); + }); + + it("shows tooltip text with styles.text class and shortcut after delay", async () => { + render( +
+ + +
, + ); + + const button = screen.getByRole("button", { name: "Tab" }); + expect(screen.queryByRole("tooltip")).toBeNull(); + + fireEvent.pointerOver(button); + expect(screen.queryByRole("tooltip")).toBeNull(); + + act(() => { + vi.advanceTimersByTime(TOOLTIP_DELAY); + }); + + const tooltip = screen.getByRole("tooltip"); + expect(tooltip).toBeInTheDocument(); + expect(tooltip).toHaveClass(styles.tooltip!); + + const textSpan = tooltip.querySelector(`.${styles.text!}`); + expect(textSpan).toBeInTheDocument(); + expect(textSpan?.textContent).toBe("d:/project/src/main.rs"); + + const shortcutSpan = tooltip.querySelector(`.${styles.shortcut!}`); + expect(shortcutSpan).toBeInTheDocument(); + expect(shortcutSpan?.textContent).toBe("Ctrl+Shift+S"); + + // Pointer out hides the tooltip + fireEvent.pointerOut(button); + expect(screen.queryByRole("tooltip")).toBeNull(); + }); +}); diff --git a/src/ui/Tooltip.tsx b/src/ui/Tooltip.tsx index 893e2b54..0b970220 100644 --- a/src/ui/Tooltip.tsx +++ b/src/ui/Tooltip.tsx @@ -121,7 +121,7 @@ export function TooltipHost() { data-placed={shown.placed || undefined} style={{ left: shown.x, top: shown.y }} > - {shown.text} + {shown.text} {shown.shortcut && {shown.shortcut}} , document.body,