diff --git a/.changeset/famous-jobs-applaud.md b/.changeset/famous-jobs-applaud.md
new file mode 100644
index 00000000000..5b3ea4feaf7
--- /dev/null
+++ b/.changeset/famous-jobs-applaud.md
@@ -0,0 +1,5 @@
+---
+"@primer/react": minor
+---
+
+Tooltip: Add delay functionality to tooltips with the options of `instant` (default), `medium`, `long`
diff --git a/packages/react/src/Button/IconButton.features.stories.tsx b/packages/react/src/Button/IconButton.features.stories.tsx
index 6c98391d520..674a3d7df2b 100644
--- a/packages/react/src/Button/IconButton.features.stories.tsx
+++ b/packages/react/src/Button/IconButton.features.stories.tsx
@@ -95,3 +95,10 @@ export const KeybindingHintOnDescription = () => (
)
export const KeybindingHint = () =>
+
+export const LongDelayedTooltip = () => (
+ // Ideal for cases where we don't want to show the tooltip immediately — for example, when the user is just passing over the element.
+
+
+
+)
diff --git a/packages/react/src/TooltipV2/Tooltip.examples.stories.tsx b/packages/react/src/TooltipV2/Tooltip.examples.stories.tsx
index 37f0e359a1f..e6e55e43804 100644
--- a/packages/react/src/TooltipV2/Tooltip.examples.stories.tsx
+++ b/packages/react/src/TooltipV2/Tooltip.examples.stories.tsx
@@ -185,3 +185,61 @@ export const DialogTrigger = () => {
>
)
}
+
+export const EmojiPicker = () => {
+ // This example demonstrates a grid of emojis/icons with tooltips that appear after a long delay.
+ // This pattern is used in places like emoji reactions on comments and the icon picker in the issues dashboard's saved views on GitHub.
+ // The delay improves UX by preventing distraction when users move their cursor across multiple emojis/icons,
+ // especially since these icons are generally familiar and don't require immediate explanation.
+
+ const emojis = [
+ {emoji: '😀', name: 'Grinning Face'},
+ {emoji: '😍', name: 'Heart Eyes'},
+ {emoji: '🎉', name: 'Party Popper'},
+ {emoji: '👍', name: 'Thumbs Up'},
+ {emoji: '❤️', name: 'Red Heart'},
+ {emoji: '🔥', name: 'Fire'},
+ {emoji: '💯', name: 'Hundred Points'},
+ {emoji: '🚀', name: 'Rocket'},
+ {emoji: '⭐', name: 'Star'},
+ {emoji: '🎯', name: 'Direct Hit'},
+ {emoji: '💡', name: 'Light Bulb'},
+ {emoji: '🌟', name: 'Glowing Star'},
+ {emoji: '🎊', name: 'Confetti Ball'},
+ {emoji: '✨', name: 'Sparkles'},
+ {emoji: '🌈', name: 'Rainbow'},
+ ]
+
+ return (
+
+ {emojis.map((emojiItem, index) => (
+
+
+
+ ))}
+
+ )
+}
diff --git a/packages/react/src/TooltipV2/Tooltip.features.stories.tsx b/packages/react/src/TooltipV2/Tooltip.features.stories.tsx
index d72956c19eb..a8f1123a6e6 100644
--- a/packages/react/src/TooltipV2/Tooltip.features.stories.tsx
+++ b/packages/react/src/TooltipV2/Tooltip.features.stories.tsx
@@ -1,7 +1,15 @@
import {IconButton, Button, Link, ActionMenu, ActionList, VisuallyHidden} from '..'
import Octicon from '../Octicon'
import {Tooltip} from './Tooltip'
-import {SearchIcon, BookIcon, CheckIcon, TriangleDownIcon, GitBranchIcon, InfoIcon} from '@primer/octicons-react'
+import {
+ SearchIcon,
+ BookIcon,
+ CheckIcon,
+ TriangleDownIcon,
+ GitBranchIcon,
+ InfoIcon,
+ HeartIcon,
+} from '@primer/octicons-react'
import classes from './Tooltip.features.stories.module.css'
export default {
@@ -194,3 +202,19 @@ export const KeybindingHint = () => (
)
+
+export const WithMediumDelay = () => (
+
+
+
+
+
+)
+
+export const WithLongDelay = () => (
+
+
+
+
+
+)
diff --git a/packages/react/src/TooltipV2/Tooltip.tsx b/packages/react/src/TooltipV2/Tooltip.tsx
index a0479eb8f5c..a291ecc9e10 100644
--- a/packages/react/src/TooltipV2/Tooltip.tsx
+++ b/packages/react/src/TooltipV2/Tooltip.tsx
@@ -17,6 +17,13 @@ export type TooltipProps = React.PropsWithChildren<{
text: string
type?: 'label' | 'description'
keybindingHint?: KeybindingHintProps['keys']
+ /**
+ * Delay in milliseconds before showing the tooltip
+ * @default short (50ms)
+ * medium (400ms)
+ * long (1200ms)
+ */
+ delay?: 'short' | 'medium' | 'long'
}> &
React.HTMLAttributes
@@ -69,6 +76,14 @@ const interactiveElements = [
'textarea',
]
+// Map delay prop to actual time in ms
+// For context on delay times, see https://github.com/github/primer/issues/3313#issuecomment-3336696699
+const delayTimeMap = {
+ short: 50,
+ medium: 400,
+ long: 1200,
+}
+
const isInteractive = (element: HTMLElement) => {
return (
interactiveElements.some(selector => element.matches(selector)) ||
@@ -79,7 +94,17 @@ export const TooltipContext = React.createContext<{tooltipId?: string}>({})
export const Tooltip = React.forwardRef(
(
- {direction = 's', text, type = 'description', children, id, className, keybindingHint, ...rest}: TooltipProps,
+ {
+ direction = 's',
+ text,
+ type = 'description',
+ children,
+ id,
+ className,
+ keybindingHint,
+ delay = 'short',
+ ...rest
+ }: TooltipProps,
forwardedRef,
) => {
const tooltipId = useId(id)
@@ -280,14 +305,17 @@ export const Tooltip = React.forwardRef(
child.props.onFocus?.(event)
},
onMouseOverCapture: (event: React.MouseEvent) => {
+ const delayTime = delayTimeMap[delay] || 50
// We use a `capture` event to ensure this is called first before
// events that might cancel the opening timeout (like `onTouchEnd`)
- // show tooltip after mouse has been hovering for at least 50ms
+ // show tooltip after mouse has been hovering for the specified delay time
// (prevent showing tooltip when mouse is just passing through)
openTimeoutRef.current = safeSetTimeout(() => {
+ // if the mouse is already moved out, do not show the tooltip
+ if (!openTimeoutRef.current) return
openTooltip()
child.props.onMouseEnter?.(event)
- }, 50)
+ }, delayTime)
},
onMouseLeave: (event: React.MouseEvent) => {
closeTooltip()