+
{children}
diff --git a/packages/ui/components/ui/tooltip.tsx b/packages/ui/components/ui/tooltip.tsx
index 80e7546f9..9ae4e6e95 100644
--- a/packages/ui/components/ui/tooltip.tsx
+++ b/packages/ui/components/ui/tooltip.tsx
@@ -2,16 +2,54 @@
import { Tooltip as TooltipPrimitive } from "@base-ui/react/tooltip"
+import { useEffect } from "react"
+
import { cn } from "@multica/ui/lib/utils"
+/** Hover time before a tooltip opens, shared by every tooltip in the app. */
+const TOOLTIP_OPEN_DELAY = 500
+
+/**
+ * After a tooltip closes, hovering another trigger within this window opens it
+ * at once, so moving along a toolbar only waits on the first tooltip.
+ */
+const TOOLTIP_INSTANT_WINDOW = 400
+
+// Tooltip activity across every tooltip popup, including ones Base UI's delay
+// group doesn't see (DeferredTooltip drives a controlled Tooltip with no
+// Trigger). Updated by the popup itself as it mounts and unmounts.
+let openPopups = 0
+let lastPopupClosedAt = Number.NEGATIVE_INFINITY
+
+/** True while a tooltip is showing or just closed, i.e. the next one should skip its delay. */
+function isTooltipInstantPhase(): boolean {
+ return (
+ openPopups > 0 ||
+ Date.now() - lastPopupClosedAt < TOOLTIP_INSTANT_WINDOW
+ )
+}
+
+function TooltipActivity() {
+ useEffect(() => {
+ openPopups += 1
+ return () => {
+ openPopups -= 1
+ lastPopupClosedAt = Date.now()
+ }
+ }, [])
+ return null
+}
+
function TooltipProvider({
- delay = 200,
+ delay = TOOLTIP_OPEN_DELAY,
+ timeout = TOOLTIP_INSTANT_WINDOW,
...props
}: TooltipPrimitive.Provider.Props) {
return (
)
@@ -61,6 +99,7 @@ function TooltipContent({
)}
{...props}
>
+
{children}
@@ -68,4 +107,12 @@ function TooltipContent({
)
}
-export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
+export {
+ Tooltip,
+ TooltipTrigger,
+ TooltipContent,
+ TooltipProvider,
+ TOOLTIP_OPEN_DELAY,
+ TOOLTIP_INSTANT_WINDOW,
+ isTooltipInstantPhase,
+}
diff --git a/packages/views/issues/components/issue-runs-dialog.test.tsx b/packages/views/issues/components/issue-runs-dialog.test.tsx
index 0ee3d8503..2bfcc95b5 100644
--- a/packages/views/issues/components/issue-runs-dialog.test.tsx
+++ b/packages/views/issues/components/issue-runs-dialog.test.tsx
@@ -424,7 +424,7 @@ describe("IssueRunsDialog", () => {
vi.useFakeTimers();
fireEvent.pointerEnter(line, { pointerType: "mouse" });
act(() => {
- vi.advanceTimersByTime(250);
+ vi.advanceTimersByTime(550);
});
expect(document.querySelector("[data-slot='tooltip-content']")).toHaveTextContent(`“${long}”`);
});
diff --git a/packages/views/issues/components/pickers/property-picker.tsx b/packages/views/issues/components/pickers/property-picker.tsx
index 8f3d84ee4..8e9e5f032 100644
--- a/packages/views/issues/components/pickers/property-picker.tsx
+++ b/packages/views/issues/components/pickers/property-picker.tsx
@@ -256,7 +256,7 @@ export function PickerItem({
emptyValue?: boolean;
/** Design-system tooltip for the row — useful when truncated content needs
* the full string, or when the row carries metadata that doesn't fit on
- * a single line. Wrapped in a real Tooltip component (200ms delay,
+ * a single line. Wrapped in a real Tooltip component (shared delay,
* styled), not a native `title` attribute. */
tooltip?: React.ReactNode;
/** Full text of a row label that may be cut off. Unlike `tooltip`, it only
diff --git a/packages/views/modals/create-issue.tsx b/packages/views/modals/create-issue.tsx
index 7e35e89dc..7646f3c69 100644
--- a/packages/views/modals/create-issue.tsx
+++ b/packages/views/modals/create-issue.tsx
@@ -48,7 +48,7 @@ import {
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@multica/ui/components/ui/dropdown-menu";
-import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from "@multica/ui/components/ui/tooltip";
+import { Tooltip, TooltipTrigger, TooltipContent } from "@multica/ui/components/ui/tooltip";
import { TooltipHint } from "@multica/ui/components/common/tooltip-hint";
import { Button } from "@multica/ui/components/ui/button";
import { Switch } from "@multica/ui/components/ui/switch";
@@ -1469,7 +1469,7 @@ export function ManualCreatePanel({
{/* Footer — same 2x2-grid-on-phones / single-row-from-`sm` shape
as the agent panel; see the note on AgentCreatePanel's footer
- for why (MUL-6236). TooltipProvider/Tooltip render no DOM and
+ for why (MUL-6236). Tooltip renders no DOM and
TooltipContent is portaled, so the Create button stays a direct
grid child in both branches below. */}
@@ -1503,15 +1503,13 @@ export function ManualCreatePanel({
{t(($) => $.create_issue.create_another)}
{submitState === "missing_title" ? (
-
-
- {/* No `` wrapper needed now: aria-disabled leaves the
- button focusable and hoverable, so it can anchor its own
- tooltip. */}
-
- {t(($) => $.create_issue.title_required)}
-
-
+
+ {/* No `` wrapper needed now: aria-disabled leaves the
+ button focusable and hoverable, so it can anchor its own
+ tooltip. */}
+
+ {t(($) => $.create_issue.title_required)}
+
) : (
createButton
)}