4
4
useTooltipVisibility_unstable as useTooltipVisibility ,
5
5
useFluent_unstable as useFluent ,
6
6
} from '@fluentui/react-shared-contexts' ;
7
+ import type { KeyborgFocusInEvent } from '@fluentui/react-tabster' ;
8
+ import { KEYBORG_FOCUSIN } from '@fluentui/react-tabster' ;
7
9
import {
8
10
applyTriggerPropsToChildren ,
9
11
useControllableState ,
@@ -149,11 +151,8 @@ export const useTooltip_unstable = (props: TooltipProps): TooltipState => {
149
151
}
150
152
} , [ context , targetDocument , visible , setVisible ] ) ;
151
153
152
- // The focused element gets a blur event when the document loses focus
153
- // (e.g. switching tabs in the browser), but we don't want to show the
154
- // tooltip again when the document gets focus back. Handle this case by
155
- // checking if the blurred element is still the document's activeElement.
156
- // See https://github.com/microsoft/fluentui/issues/13541
154
+ // Used to skip showing the tooltip in certain situations when the trigger is focued.
155
+ // See comments where this is set for more info.
157
156
const ignoreNextFocusEventRef = React . useRef ( false ) ;
158
157
159
158
// Listener for onPointerEnter and onFocus on the trigger element
@@ -176,6 +175,29 @@ export const useTooltip_unstable = (props: TooltipProps): TooltipState => {
176
175
[ setDelayTimeout , setVisible , state . showDelay , context ] ,
177
176
) ;
178
177
178
+ // Callback ref that attaches a keyborg:focusin event listener.
179
+ const [ keyborgListenerCallbackRef ] = React . useState ( ( ) => {
180
+ const onKeyborgFocusIn = ( ( ev : KeyborgFocusInEvent ) => {
181
+ // Skip showing the tooltip if focus moved programmatically.
182
+ // For example, we don't want to show the tooltip when a dialog is closed
183
+ // and Tabster programmatically restores focus to the trigger button.
184
+ // See https://github.com/microsoft/fluentui/issues/27576
185
+ if ( ev . details ?. isFocusedProgrammatically ) {
186
+ ignoreNextFocusEventRef . current = true ;
187
+ }
188
+ } ) as EventListener ;
189
+
190
+ // Save the current element to remove the listener when the ref changes
191
+ let current : Element | null = null ;
192
+
193
+ // Callback ref that attaches the listener to the element
194
+ return ( element : Element | null ) => {
195
+ current ?. removeEventListener ( KEYBORG_FOCUSIN , onKeyborgFocusIn ) ;
196
+ element ?. addEventListener ( KEYBORG_FOCUSIN , onKeyborgFocusIn ) ;
197
+ current = element ;
198
+ } ;
199
+ } ) ;
200
+
179
201
// Listener for onPointerLeave and onBlur on the trigger element
180
202
const onLeaveTrigger = React . useCallback (
181
203
( ev : React . PointerEvent < HTMLElement > | React . FocusEvent < HTMLElement > ) => {
@@ -185,6 +207,11 @@ export const useTooltip_unstable = (props: TooltipProps): TooltipState => {
185
207
// Hide immediately when losing focus
186
208
delay = 0 ;
187
209
210
+ // The focused element gets a blur event when the document loses focus
211
+ // (e.g. switching tabs in the browser), but we don't want to show the
212
+ // tooltip again when the document gets focus back. Handle this case by
213
+ // checking if the blurred element is still the document's activeElement.
214
+ // See https://github.com/microsoft/fluentui/issues/13541
188
215
ignoreNextFocusEventRef . current = targetDocument ?. activeElement === ev . target ;
189
216
}
190
217
@@ -228,14 +255,16 @@ export const useTooltip_unstable = (props: TooltipProps): TooltipState => {
228
255
state . shouldRenderTooltip = false ;
229
256
}
230
257
231
- const childTargetRef = useMergedRefs ( child ?. ref , targetRef ) ;
232
-
233
258
// Apply the trigger props to the child, either by calling the render function, or cloning with the new props
234
259
state . children = applyTriggerPropsToChildren ( children , {
235
260
...triggerAriaProps ,
236
261
...child ?. props ,
237
- // If the target prop is not provided, attach targetRef to the trigger element's ref prop
238
- ref : positioningOptions . target === undefined ? childTargetRef : child ?. ref ,
262
+ ref : useMergedRefs (
263
+ child ?. ref ,
264
+ keyborgListenerCallbackRef ,
265
+ // If the target prop is not provided, attach targetRef to the trigger element's ref prop
266
+ positioningOptions . target === undefined ? targetRef : undefined ,
267
+ ) ,
239
268
onPointerEnter : useEventCallback ( mergeCallbacks ( child ?. props ?. onPointerEnter , onEnterTrigger ) ) ,
240
269
onPointerLeave : useEventCallback ( mergeCallbacks ( child ?. props ?. onPointerLeave , onLeaveTrigger ) ) ,
241
270
onFocus : useEventCallback ( mergeCallbacks ( child ?. props ?. onFocus , onEnterTrigger ) ) ,
0 commit comments