Skip to content
5 changes: 5 additions & 0 deletions .changeset/chilly-jars-jog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@primer/react": minor
---

Hide `TooltipV2` tooltips on `touchend` event
45 changes: 29 additions & 16 deletions packages/react/src/TooltipV2/Tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -146,14 +146,18 @@ export type TooltipProps = React.PropsWithChildren<
> &
React.HTMLAttributes<HTMLElement>

type TriggerPropsType = {
'aria-describedby'?: string
'aria-labelledby'?: string
'aria-label'?: string
onBlur?: React.FocusEventHandler
onFocus?: React.FocusEventHandler
onMouseEnter?: React.MouseEventHandler
onMouseLeave?: React.MouseEventHandler
type TriggerPropsType = Pick<
React.HTMLAttributes<HTMLElement>,
| 'aria-describedby'
| 'aria-labelledby'
| 'onBlur'
| 'onTouchEnd'
| 'onFocus'
| 'onMouseOverCapture'
| 'onMouseLeave'
| 'onTouchCancel'
| 'onTouchEnd'
> & {
ref?: React.RefObject<HTMLElement>
}

Expand Down Expand Up @@ -214,7 +218,7 @@ export const Tooltip = React.forwardRef(

const [isPopoverOpen, setIsPopoverOpen] = useState(false)

const timeoutRef = React.useRef<number | null>(null)
const openTimeoutRef = React.useRef<number | null>(null)

const {safeSetTimeout, safeClearTimeout} = useSafeTimeout()

Expand Down Expand Up @@ -261,6 +265,10 @@ export const Tooltip = React.forwardRef(
}
}
const closeTooltip = () => {
if (openTimeoutRef.current) {
safeClearTimeout(openTimeoutRef.current)
openTimeoutRef.current = null
}
try {
if (
tooltipElRef.current &&
Expand Down Expand Up @@ -362,6 +370,13 @@ export const Tooltip = React.forwardRef(
closeTooltip()
child.props.onBlur?.(event)
},
onTouchEnd: (event: React.TouchEvent) => {
child.props.onTouchEnd?.(event)

// Hide tooltips on tap to essentially disable them on touch devices;
// this still allows viewing the tooltip on tap-and-hold
safeSetTimeout(() => closeTooltip(), 10)
},
onFocus: (event: React.FocusEvent) => {
// only show tooltip on :focus-visible, not on :focus
try {
Expand All @@ -374,19 +389,17 @@ export const Tooltip = React.forwardRef(
openTooltip()
child.props.onFocus?.(event)
},
onMouseEnter: (event: React.MouseEvent) => {
// show tooltip after mosue has been hovering for at least 50ms
onMouseOverCapture: (event: React.MouseEvent) => {
// 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
// (prevent showing tooltip when mouse is just passing through)
timeoutRef.current = safeSetTimeout(() => {
openTimeoutRef.current = safeSetTimeout(() => {
openTooltip()
child.props.onMouseEnter?.(event)
}, 50)
},
onMouseLeave: (event: React.MouseEvent) => {
if (timeoutRef.current) {
safeClearTimeout(timeoutRef.current)
timeoutRef.current = null
}
closeTooltip()
child.props.onMouseLeave?.(event)
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3325,8 +3325,9 @@ exports[`TextInput renders trailingAction icon button 1`] = `
onBlur={[Function]}
onClick={[MockFunction]}
onFocus={[Function]}
onMouseEnter={[Function]}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if this will be problematic in integrations in dotcom?

onMouseLeave={[Function]}
onMouseOverCapture={[Function]}
onTouchEnd={[Function]}
type="button"
>
<svg
Expand Down Expand Up @@ -4090,8 +4091,9 @@ exports[`TextInput renders trailingAction text button with a tooltip 1`] = `
onBlur={[Function]}
onClick={[MockFunction]}
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
onMouseOverCapture={[Function]}
onTouchEnd={[Function]}
style={
{
"--button-color": "fg.subtle",
Expand Down
Loading