Skip to content

Commit 5103727

Browse files
authored
fix: Tooltips are no longer triggered by programmatic focus() moves (#29791)
Use the keyborg:focusin event to detect when focus was programmatically moved to the trigger element. Since this is not a React event, use a callback ref to manage attaching and detaching the event listener.
1 parent 88efc19 commit 5103727

File tree

6 files changed

+62
-9
lines changed

6 files changed

+62
-9
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "patch",
3+
"comment": "chore: Export KeyborgFocusInEvent and KEYBORG_FOCUSIN",
4+
"packageName": "@fluentui/react-tabster",
5+
"email": "[email protected]",
6+
"dependentChangeType": "patch"
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "minor",
3+
"comment": "fix: Tooltips are no longer triggered by programmatic focus() moves.",
4+
"packageName": "@fluentui/react-tooltip",
5+
"email": "[email protected]",
6+
"dependentChangeType": "patch"
7+
}

packages/react-components/react-tabster/etc/react-tabster.api.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
```ts
66

77
import type { GriffelStyle } from '@griffel/react';
8+
import { KEYBORG_FOCUSIN } from 'keyborg';
9+
import { KeyborgFocusInEvent } from 'keyborg';
810
import { makeResetStyles } from '@griffel/react';
911
import * as React_2 from 'react';
1012
import type { RefObject } from 'react';
@@ -45,6 +47,10 @@ export type FocusOutlineStyleOptions = {
4547
outlineOffset?: string | FocusOutlineOffset;
4648
};
4749

50+
export { KEYBORG_FOCUSIN }
51+
52+
export { KeyborgFocusInEvent }
53+
4854
// @public (undocumented)
4955
export type TabsterDOMAttribute = Types.TabsterDOMAttribute;
5056

packages/react-components/react-tabster/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,6 @@ export { applyFocusVisiblePolyfill } from './focus/index';
3232
import { Types as TabsterTypes } from 'tabster';
3333

3434
export type TabsterDOMAttribute = TabsterTypes.TabsterDOMAttribute;
35+
36+
export type { KeyborgFocusInEvent } from 'keyborg';
37+
export { KEYBORG_FOCUSIN } from 'keyborg';

packages/react-components/react-tooltip/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"@fluentui/react-portal": "^9.4.1",
3939
"@fluentui/react-positioning": "^9.10.0",
4040
"@fluentui/react-shared-contexts": "^9.12.0",
41+
"@fluentui/react-tabster": "^9.14.5",
4142
"@fluentui/react-theme": "^9.1.16",
4243
"@fluentui/react-utilities": "^9.15.2",
4344
"@griffel/react": "^1.5.14",

packages/react-components/react-tooltip/src/components/Tooltip/useTooltip.tsx

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import {
44
useTooltipVisibility_unstable as useTooltipVisibility,
55
useFluent_unstable as useFluent,
66
} from '@fluentui/react-shared-contexts';
7+
import type { KeyborgFocusInEvent } from '@fluentui/react-tabster';
8+
import { KEYBORG_FOCUSIN } from '@fluentui/react-tabster';
79
import {
810
applyTriggerPropsToChildren,
911
useControllableState,
@@ -149,11 +151,8 @@ export const useTooltip_unstable = (props: TooltipProps): TooltipState => {
149151
}
150152
}, [context, targetDocument, visible, setVisible]);
151153

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.
157156
const ignoreNextFocusEventRef = React.useRef(false);
158157

159158
// Listener for onPointerEnter and onFocus on the trigger element
@@ -176,6 +175,29 @@ export const useTooltip_unstable = (props: TooltipProps): TooltipState => {
176175
[setDelayTimeout, setVisible, state.showDelay, context],
177176
);
178177

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+
179201
// Listener for onPointerLeave and onBlur on the trigger element
180202
const onLeaveTrigger = React.useCallback(
181203
(ev: React.PointerEvent<HTMLElement> | React.FocusEvent<HTMLElement>) => {
@@ -185,6 +207,11 @@ export const useTooltip_unstable = (props: TooltipProps): TooltipState => {
185207
// Hide immediately when losing focus
186208
delay = 0;
187209

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
188215
ignoreNextFocusEventRef.current = targetDocument?.activeElement === ev.target;
189216
}
190217

@@ -228,14 +255,16 @@ export const useTooltip_unstable = (props: TooltipProps): TooltipState => {
228255
state.shouldRenderTooltip = false;
229256
}
230257

231-
const childTargetRef = useMergedRefs(child?.ref, targetRef);
232-
233258
// Apply the trigger props to the child, either by calling the render function, or cloning with the new props
234259
state.children = applyTriggerPropsToChildren(children, {
235260
...triggerAriaProps,
236261
...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+
),
239268
onPointerEnter: useEventCallback(mergeCallbacks(child?.props?.onPointerEnter, onEnterTrigger)),
240269
onPointerLeave: useEventCallback(mergeCallbacks(child?.props?.onPointerLeave, onLeaveTrigger)),
241270
onFocus: useEventCallback(mergeCallbacks(child?.props?.onFocus, onEnterTrigger)),

0 commit comments

Comments
 (0)