@@ -98,6 +98,8 @@ export interface UseFloatingTooltipOptions {
9898 getFocusTarget ?: ( ) => HTMLElement | null
9999 /** Semantic value whose changes should revalidate a visible tooltip. */
100100 revalidateKey ?: unknown
101+ /** Dismiss visible help with Escape until the next hover or focus gesture. Defaults to false. */
102+ dismissOnEscape ?: boolean
101103}
102104
103105export function useFloatingTooltip (
@@ -106,6 +108,8 @@ export function useFloatingTooltip(
106108) : {
107109 state : FloatingTooltipState
108110 handlers : FloatingTooltipHandlers
111+ /** Hides help until a fresh hover or focus gesture, including while the pointer stays on its trigger. */
112+ dismiss : ( ) => void
109113} {
110114 const canShowRef = React . useRef ( canShow )
111115 canShowRef . current = canShow
@@ -115,6 +119,7 @@ export function useFloatingTooltip(
115119 const lastPointerRef = React . useRef < PointerSnapshot | null > ( null )
116120 const velocityRef = React . useRef ( { x : 0 , magnitude : 0 } )
117121 const triggerRef = React . useRef < HTMLElement | null > ( null )
122+ const dismissedRef = React . useRef ( false )
118123 const [ state , setState ] = React . useState < FloatingTooltipState > ( HIDDEN_STATE )
119124
120125 const reset = React . useCallback ( ( ) => {
@@ -129,6 +134,11 @@ export function useFloatingTooltip(
129134 setState ( ( current ) => ( current . visible ? HIDDEN_STATE : current ) )
130135 } , [ reset ] )
131136
137+ const dismiss = React . useCallback ( ( ) => {
138+ dismissedRef . current = true
139+ hide ( )
140+ } , [ hide ] )
141+
132142 const apply = React . useCallback ( ( clientX : number , clientY : number , motion : TooltipMotion ) => {
133143 const next = { ...getTooltipPosition ( clientX , clientY , preferAboveRef . current ) , ...motion }
134144 setState ( ( current ) =>
@@ -175,12 +185,13 @@ export function useFloatingTooltip(
175185 return {
176186 onPointerEnter : ( event ) => {
177187 if ( event . pointerType === 'touch' ) return
188+ dismissedRef . current = false
178189 if ( ! canShowRef . current ( event . currentTarget ) ) return
179190 triggerRef . current = event . currentTarget
180191 showFromPointer ( event . clientX , event . clientY )
181192 } ,
182193 onPointerMove : ( event ) => {
183- if ( event . pointerType === 'touch' ) return
194+ if ( event . pointerType === 'touch' || dismissedRef . current ) return
184195 if ( ! canShowRef . current ( event . currentTarget ) ) return
185196 triggerRef . current = event . currentTarget
186197 const now = performance . now ( )
@@ -210,6 +221,7 @@ export function useFloatingTooltip(
210221 onPointerDown : hide ,
211222 onFocus : ( event ) => {
212223 const target = event . currentTarget
224+ dismissedRef . current = false
213225 if ( ! canShowRef . current ( target ) ) return
214226 if ( ! isFocusVisible ( target ) ) return
215227 triggerRef . current = target
@@ -223,6 +235,7 @@ export function useFloatingTooltip(
223235 const target = options . getFocusTarget ?.( )
224236 if ( ! target ) return undefined
225237 const show = ( ) => {
238+ dismissedRef . current = false
226239 if ( ! canShowRef . current ( target ) || ! isFocusVisible ( target ) ) return
227240 triggerRef . current = target
228241 showFromElement ( target )
@@ -240,6 +253,18 @@ export function useFloatingTooltip(
240253 if ( state . visible && ( ! trigger || ! canShowRef . current ( trigger ) ) ) hide ( )
241254 } , [ hide , options . revalidateKey , state . visible ] )
242255
256+ React . useEffect ( ( ) => {
257+ if ( ! options . dismissOnEscape || ! state . visible ) return undefined
258+ const onKeyDown = ( event : KeyboardEvent ) => {
259+ if ( event . key !== 'Escape' ) return
260+ event . preventDefault ( )
261+ event . stopPropagation ( )
262+ dismiss ( )
263+ }
264+ window . addEventListener ( 'keydown' , onKeyDown , true )
265+ return ( ) => window . removeEventListener ( 'keydown' , onKeyDown , true )
266+ } , [ dismiss , options . dismissOnEscape , state . visible ] )
267+
243268 /**
244269 * A keyboard- or script-driven UI change can hide the trigger with no pointer or focus event —
245270 * browsers don't re-dispatch boundary events until the pointer next moves (e.g. an editor bubble
@@ -255,7 +280,7 @@ export function useFloatingTooltip(
255280 return ( ) => window . clearInterval ( intervalId )
256281 } , [ state . visible , hide ] )
257282
258- return { state, handlers }
283+ return { state, handlers, dismiss }
259284}
260285
261286const overflowMeasureByElement = new WeakMap < Element , ( ) => void > ( )
0 commit comments