diff --git a/packages/react/src/TooltipV2/Tooltip.dev.stories.tsx b/packages/react/src/TooltipV2/Tooltip.dev.stories.tsx
deleted file mode 100644
index 60285231c40..00000000000
--- a/packages/react/src/TooltipV2/Tooltip.dev.stories.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import {Button} from '../Button'
-import {Tooltip} from './Tooltip'
-import classes from './Tooltip.dev.stories.module.css'
-
-export default {
- title: 'Components/TooltipV2/Dev',
- component: Tooltip,
-}
-
-// Description type, north direction by default
-export const Default = () => (
-
-
-
-
-
-)
diff --git a/packages/react/src/TooltipV2/Tooltip.dev.stories.module.css b/packages/react/src/TooltipV2/Tooltip.examples.stories.module.css
similarity index 100%
rename from packages/react/src/TooltipV2/Tooltip.dev.stories.module.css
rename to packages/react/src/TooltipV2/Tooltip.examples.stories.module.css
diff --git a/packages/react/src/TooltipV2/Tooltip.repro.stories.module.css b/packages/react/src/TooltipV2/Tooltip.repro.stories.module.css
new file mode 100644
index 00000000000..de38b5fcf50
--- /dev/null
+++ b/packages/react/src/TooltipV2/Tooltip.repro.stories.module.css
@@ -0,0 +1,10 @@
+/* TooltipV2.dev.stories.module.css */
+.PaddedContainer {
+ padding: var(--base-size-40);
+}
+
+.Popover[popover] {
+ /* stylelint-disable-next-line color-named */
+ background: red;
+ font-size: var(--text-title-size-small);
+}
diff --git a/packages/react/src/TooltipV2/Tooltip.repro.stories.tsx b/packages/react/src/TooltipV2/Tooltip.repro.stories.tsx
new file mode 100644
index 00000000000..f639b734360
--- /dev/null
+++ b/packages/react/src/TooltipV2/Tooltip.repro.stories.tsx
@@ -0,0 +1,174 @@
+import React from 'react'
+import {flushSync} from 'react-dom'
+import {Button} from '../Button'
+import Checkbox from '../Checkbox'
+import {Stack} from '../Stack'
+import {Tooltip} from './Tooltip'
+import classes from './Tooltip.repro.stories.module.css'
+
+export default {
+ title: 'Components/TooltipV2/Repro',
+ component: Tooltip,
+}
+
+// Description type, north direction by default
+export const Default = () => (
+
+
+
+
+
+)
+
+function describeActiveElement() {
+ const el = document.activeElement
+ if (!el || el === document.body) return 'body'
+
+ const tag = el.tagName.toLowerCase()
+ const text = el.textContent ? el.textContent.trim() : ''
+ return text ? `${tag} "${text}"` : tag
+}
+
+export const ConditionalTooltipWrap = () => {
+ const [inactive, setInactive] = React.useState(false)
+ const [currentFocus, setCurrentFocus] = React.useState(describeActiveElement)
+ const buttonRef = React.useRef(null)
+
+ React.useEffect(function printCurrentActiveElement() {
+ const interval = setInterval(() => {
+ setCurrentFocus(describeActiveElement())
+ }, 100)
+ return () => clearInterval(interval)
+ }, [])
+
+ // Buggy flow: same toggle, but no focus restoration.
+ const saveWithoutRestoringFocus = () => {
+ setInactive(true)
+ window.setTimeout(() => setInactive(false), 2000)
+ }
+
+ // Imperative save flow for the fixed buttons: start, then after 2s flip back,
+ // restoring focus after each swap. flushSync forces React to commit the
+ // remount synchronously so buttonRef points at the new node before focus().
+ const saveAndRestoreFocus = () => {
+ flushSync(() => {
+ setInactive(true)
+ })
+ if (document.activeElement === document.body) buttonRef.current?.focus()
+ window.setTimeout(() => {
+ flushSync(() => {
+ setInactive(false)
+ })
+ if (document.activeElement === document.body) buttonRef.current?.focus()
+ }, 2000)
+ }
+
+ return (
+ <>
+
+ Current focus: {currentFocus}
+
+
Buggy
+
+ The element type at this slot swaps between Button and Tooltip, so React unmounts and
+ recreates the button. Focusing the button and activating it drops focus to body.
+
+
+ {inactive ? (
+
+
+
+ ) : (
+
+ )}
+
+
+
+
+
Fix, Option A: stable tree
+
+ The Tooltip is always mounted and only its text changes, so the button is never
+ remounted and focus is preserved. Tradeoff: the active button also gets a tooltip.
+
+
+
+
+
+
+
+
+
+
Fix, Option B: restore focus via ref
+
+ Accept the remount, then restore focus with a ref (see saveAndRestoreFocus). The ref goes on the{' '}
+ Tooltip in the inactive branch, because Tooltip overrides its child's ref via{' '}
+ cloneElement.
+