Repository: powerhome/playbook
Pull request #4417: [PLAY-1938] Transition Rails Tooltip to @floating-ui/dom
Author: login: kangaree
Labels: name: milano
description: 20 MAX - Deploy this PR to a review environment via Milano
name: Ready for Release
description: merged to master, ready for a versioned released
name: improvement
description: This is used when your PR contains library upgrades or doc/site improvements. (USED IN CHANGELOG))
name: alpha
name: Product Approved
description: pending technical review, OK to merge to master
name: Code Approved
description: Approved by a Playbook Admin
name: minor
description: Semver Target
Created: 2025-03-21T19:49:35Z | Merged: 2025-03-27T11:57:48Z

**What does this PR do?**
- Complete moving Rails tooltip to floating-ui (from popperjs)
- Replace popperjs file with floating-ui file
- Combine selectors

**Screenshots:** Screenshots to visualize your addition/change
<img width="618" alt="Screenshot 2025-03-21 at 3 44 11 PM" src="https://github.com/user-attachments/assets/00f47d60-8a20-4f98-93cb-101ce6576c88" />

**How to test?** Steps to confirm the desired behavior:
1. Go through tooltip doc examples and make sure nothing changed (Rails)
2. Briefly go through popover docs and make sure nothing changed

#### Checklist:
- [x] **LABELS** Add a label: `enhancement`, `bug`, `improvement`, `new kit`, `deprecated`, or `breaking`. See [Changelog & Labels](https://github.com/powerhome/playbook/wiki/Changelog-&-Labels) for details.
- [x] **DEPLOY** I have added the `milano` label to show I'm ready for a review.


Diff:
diff --git a/playbook/app/entrypoints/playbook-rails.js b/playbook/app/entrypoints/playbook-rails.js
index 4dde675f40..b6f3ef2110 100644
--- a/playbook/app/entrypoints/playbook-rails.js
+++ b/playbook/app/entrypoints/playbook-rails.js
@@ -37,9 +37,6 @@ PbPopover.start()
 import PbTooltip from 'kits/pb_tooltip'
 PbTooltip.start()
 
-import PbTooltipFLoatingUi from 'kits/pb_tooltip/floating_ui'
-PbTooltipFLoatingUi.start()
-
 import PbFixedConfirmationToast from 'kits/pb_fixed_confirmation_toast'
 PbFixedConfirmationToast.start()
 
diff --git a/playbook/app/pb_kits/playbook/pb_tooltip/floating_ui.js b/playbook/app/pb_kits/playbook/pb_tooltip/floating_ui.js
deleted file mode 100644
index 400ddc0868..0000000000
--- a/playbook/app/pb_kits/playbook/pb_tooltip/floating_ui.js
+++ /dev/null
@@ -1,282 +0,0 @@
-import PbEnhancedElement from '../pb_enhanced_element'
-import { computePosition, offset, flip, shift, arrow, autoUpdate } from '@floating-ui/dom'
-
-const TOOLTIP_OFFSET = 20
-const TOOLTIP_TIMEOUT = 250
-const SAFE_ZONE_MARGIN = 1
-
-export default class PbTooltipFloatingUi extends PbEnhancedElement {
-  static get selector() {
-    return '[data-pb-tooltip-kit="true"][data-pb-tooltip-delay-open], [data-pb-tooltip-kit="true"][data-pb-tooltip-delay-close], [data-pb-tooltip-kit="true"][data-pb-tooltip-interaction="true"]'
-  }
-
-  connect() {
-    if (this.tooltipInteraction) {
-      document.addEventListener('mousemove', (e) => {
-        this.lastMouseX = e.clientX
-        this.lastMouseY = e.clientY
-      })
-    }
-    
-    this.triggerElements.forEach((trigger) => {
-      const method = this.triggerMethod
-      const interactionEnabled = this.tooltipInteraction
-
-      if (method === 'click') {
-        trigger.addEventListener('click', () => {
-          this.showTooltip(trigger)
-        })
-      } else {
-        trigger.addEventListener('mouseenter', () => {
-          clearSafeZoneListener(this)
-          clearTimeout(this.mouseleaveTimeout)
-          this.currentTrigger = trigger
-          const delayOpen = this.delayOpen ? parseInt(this.delayOpen) : TOOLTIP_TIMEOUT
-          this.mouseenterTimeout = setTimeout(() => {
-            this.showTooltip(trigger)
-            if (interactionEnabled) {
-              this.checkCloseTooltip(trigger)
-            }
-          }, delayOpen)
-        })
-
-        trigger.addEventListener('mouseleave', () => {
-          clearTimeout(this.mouseenterTimeout)
-          if (this.delayClose) {
-            const delayClose = parseInt(this.delayClose)
-            this.mouseleaveTimeout = setTimeout(() => {
-              if (interactionEnabled) {
-                this.attachSafeZoneListener()
-              } else {
-                this.hideTooltip()
-              }
-            }, delayClose)
-          } else {
-            if (interactionEnabled) {
-              this.attachSafeZoneListener()
-            } else {
-              this.hideTooltip()
-            }
-          }
-        })
-
-        if (interactionEnabled) {
-          this.tooltip.addEventListener('mouseenter', () => {
-            clearSafeZoneListener(this)
-          })
-
-          this.tooltip.addEventListener('mouseleave', () => {
-            this.attachSafeZoneListener()
-          })
-        }
-      }
-    })
-  }
-
-  attachSafeZoneListener() {
-    clearSafeZoneListener(this)
-    this.safeZoneHandler = (e) => {
-      if (!this.currentTrigger) return
-      const triggerRect = this.currentTrigger.getBoundingClientRect()
-      const tooltipRect = this.tooltip.getBoundingClientRect()
-      const safeRect = getSafeZone(triggerRect, tooltipRect, this.position, SAFE_ZONE_MARGIN)
-      if (!isPointInsideRect(e.clientX, e.clientY, safeRect)) {
-        this.hideTooltip()
-        clearSafeZoneListener(this)
-      }
-    }
-    document.addEventListener('mousemove', this.safeZoneHandler)
-  }
-
-  checkCloseTooltip(trigger) {
-    document.querySelector('body').addEventListener('click', ({ target }) => {
-      const isTooltip = target.closest(`#${this.tooltipId}`) === this.tooltip
-      const isTrigger = target.closest(this.triggerElementSelector) === trigger
-      if (isTrigger || isTooltip) {
-        this.checkCloseTooltip(trigger)
-      } else {
-        this.hideTooltip()
-      }
-    }, { once: true })
-  }
-
-  showTooltip(trigger) {
-    if (this.shouldShowTooltip === 'false') return
-
-    clearSafeZoneListener(this)
-
-    this.tooltip.style.opacity = '1'
-    this.tooltip.style.visibility = 'visible'
-    this.tooltip.style.pointerEvents = 'auto'
-
-    if (this.cleanup) {
-      this.cleanup()
-    }
-
-    const arrowElement = document.querySelector(`#${this.tooltipId}-arrow`)
-
-    this.cleanup = autoUpdate(trigger, this.tooltip, () => {
-      computePosition(trigger, this.tooltip, {
-        placement: this.position,
-        strategy: 'fixed',
-        middleware: [
-          offset({ mainAxis: TOOLTIP_OFFSET, crossAxis: 0 }),
-          flip(),
-          shift(),
-          arrow({ element: arrowElement })
-        ],
-      }).then(({ x, y, placement, middlewareData }) => {
-        Object.assign(this.tooltip.style, {
-          left: `${x}px`,
-          top: `${y}px`,
-          position: 'fixed'
-        })
-        this.tooltip.setAttribute('data-popper-placement', placement)
-        if (arrowElement && middlewareData.arrow) {
-          const { x: arrowX, y: arrowY } = middlewareData.arrow
-          Object.assign(arrowElement.style, {
-            left: arrowX != null ? `${arrowX}px` : '',
-            top: arrowY != null ? `${arrowY}px` : '',
-            position: 'absolute'
-          })
-        }
-      })
-    })
-
-    this.tooltip.classList.add('show')
-
-    if (this.triggerMethod === 'click') {
-      clearTimeout(this.autoHideTimeout)
-      this.autoHideTimeout = setTimeout(() => {
-        this.hideTooltip()
-      }, 1000)
-    }
-  }
-
-  hideTooltip() {
-    if (!this.tooltip) return
-
-    this.tooltip.classList.add('fade_out')
-    setTimeout(() => {
-      if (this.cleanup) {
-        this.cleanup()
-        this.cleanup = null
-      }
-      this.tooltip.classList.remove('show')
-      this.tooltip.classList.remove('fade_out')
-      this.tooltip.style.opacity = '0'
-      this.tooltip.style.visibility = 'hidden'
-      this.tooltip.style.pointerEvents = 'none'
-      this.tooltip.style.position = ''
-      this.tooltip.style.top = ''
-      this.tooltip.style.left = ''
-      this.tooltip.style.transform = ''
-    }, TOOLTIP_TIMEOUT)
-  }
-
-  get triggerElements() {
-    let triggerEl
-    if (this.triggerElementId) {
-      triggerEl = document.querySelector(`#${this.triggerElementId}`)
-    } else if (this.triggerElementSelector) {
-      const selectorIsId = this.triggerElementSelector.indexOf('#') > -1
-      triggerEl = selectorIsId
-        ? document.querySelector(this.triggerElementSelector)
-        : document.querySelectorAll(this.triggerElementSelector)
-    } else {
-      triggerEl = this.element
-    }
-    if (!triggerEl) {
-      console.error('Tooltip Kit: No valid trigger element found!')
-      return []
-    }
-    if (triggerEl.length === undefined) {
-      triggerEl = [triggerEl]
-    }
-    return triggerEl
-  }
-
-  get tooltip() {
-    return (this._tooltip = this._tooltip || this.element.querySelector(`#${this.tooltipId}`))
-  }
-
-  get position() {
-    return this.element.dataset.pbTooltipPosition
-  }
-
-  get triggerElementId() {
-    return this.element.dataset.pbTooltipTriggerElementId
-  }
-
-  get tooltipId() {
-    return this.element.dataset.pbTooltipTooltipId
-  }
-
-  get triggerElementSelector() {
-    return this.element.dataset.pbTooltipTriggerElementSelector
-  }
-
-  get shouldShowTooltip() {
-    return this.element.dataset.pbTooltipShowTooltip
-  }
-
-  get triggerMethod() {
-    return this.element.dataset.pbTooltipTriggerMethod || 'hover'
-  }
-
-  get tooltipInteraction() {
-    return this.element.dataset.pbTooltipInteraction === 'true'
-  }
-  
-  get delayOpen() {
-    return this.element.dataset.pbTooltipDelayOpen
-  }
-  
-  get delayClose() {
-    return this.element.dataset.pbTooltipDelayClose
-  }
-}
-
-function clearSafeZoneListener(context) {
-  if (context.safeZoneHandler) {
-    document.removeEventListener('mousemove', context.safeZoneHandler)
-    context.safeZoneHandler = null
-  }
-}
-
-function getSafeZone(triggerRect, tooltipRect, placement, margin) {
-  let safeRect = {}
-  if (placement.startsWith('top')) {
-    safeRect.left = triggerRect.left - margin
-    safeRect.right = triggerRect.right + margin
-    safeRect.top = tooltipRect.bottom - margin
-    safeRect.bottom = triggerRect.top + margin
-  } else if (placement.startsWith('bottom')) {
-    safeRect.left = triggerRect.left - margin
-    safeRect.right = triggerRect.right + margin
-    safeRect.top = triggerRect.bottom - margin
-    safeRect.bottom = tooltipRect.top + margin
-  } else if (placement.startsWith('left')) {
-    safeRect.top = triggerRect.top - margin
-    safeRect.bottom = triggerRect.bottom + margin
-    safeRect.left = tooltipRect.right - margin
-    safeRect.right = triggerRect.left + margin
-  } else if (placement.startsWith('right')) {
-    safeRect.top = triggerRect.top - margin
-    safeRect.bottom = triggerRect.bottom + margin
-    safeRect.left = triggerRect.right - margin
-    safeRect.right = tooltipRect.left + margin
-  } else {
-    safeRect = {
-      left: triggerRect.left - margin,
-      right: triggerRect.right + margin,
-      top: triggerRect.top - margin,
-      bottom: triggerRect.bottom + margin,
-    }
-  }
-  return safeRect
-}
-
-function isPointInsideRect(x, y, rect) {
-  return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom
-}
diff --git a/playbook/app/pb_kits/playbook/pb_tooltip/index.js b/playbook/app/pb_kits/playbook/pb_tooltip/index.js
index bf7aad36b9..05b053e938 100644
--- a/playbook/app/pb_kits/playbook/pb_tooltip/index.js
+++ b/playbook/app/pb_kits/playbook/pb_tooltip/index.js
@@ -1,22 +1,26 @@
 import PbEnhancedElement from '../pb_enhanced_element'
-import {
-  createPopperLite as createPopper,
-  flip,
-  offset,
-  preventOverflow,
-} from '@popperjs/core'
-
-const TOOLTIP_OFFSET = [0, 20]
+import { computePosition, offset, flip, shift, arrow, autoUpdate } from '@floating-ui/dom'
+
+const TOOLTIP_OFFSET = 20
 const TOOLTIP_TIMEOUT = 250
+const SAFE_ZONE_MARGIN = 1
 
 export default class PbTooltip extends PbEnhancedElement {
   static get selector() {
-    return '[data-pb-tooltip-kit="true"][data-pb-tooltip-interaction="false"]:not([data-pb-tooltip-delay-open]):not([data-pb-tooltip-delay-close])'
+    return '[data-pb-tooltip-kit]'
   }
 
   connect() {
+    if (this.tooltipInteraction) {
+      document.addEventListener('mousemove', (e) => {
+        this.lastMouseX = e.clientX
+        this.lastMouseY = e.clientY
+      })
+    }
+    
     this.triggerElements.forEach((trigger) => {
       const method = this.triggerMethod
+      const interactionEnabled = this.tooltipInteraction
 
       if (method === 'click') {
         trigger.addEventListener('click', () => {
@@ -24,29 +28,66 @@ export default class PbTooltip extends PbEnhancedElement {
         })
       } else {
         trigger.addEventListener('mouseenter', () => {
+          clearSafeZoneListener(this)
+          clearTimeout(this.mouseleaveTimeout)
+          this.currentTrigger = trigger
+          const delayOpen = this.delayOpen ? parseInt(this.delayOpen) : TOOLTIP_TIMEOUT
           this.mouseenterTimeout = setTimeout(() => {
             this.showTooltip(trigger)
-            this.checkCloseTooltip(trigger)
-          }, TOOLTIP_TIMEOUT)
-
-          trigger.addEventListener('mouseleave', () => {
-            clearTimeout(this.mouseenterTimeout)
-            setTimeout(() => {
-              this.hideTooltip()
-            }, 0)
-          }, { once: true })
+            if (interactionEnabled) {
+              this.checkCloseTooltip(trigger)
+            }
+          }, delayOpen)
         })
 
-        this.tooltip.addEventListener('mouseenter', () => {
+        trigger.addEventListener('mouseleave', () => {
           clearTimeout(this.mouseenterTimeout)
+          if (this.delayClose) {
+            const delayClose = parseInt(this.delayClose)
+            this.mouseleaveTimeout = setTimeout(() => {
+              if (interactionEnabled) {
+                this.attachSafeZoneListener()
+              } else {
+                this.hideTooltip()
+              }
+            }, delayClose)
+          } else {
+            if (interactionEnabled) {
+              this.attachSafeZoneListener()
+            } else {
+              this.hideTooltip()
+            }
+          }
         })
-        this.tooltip.addEventListener('mouseleave', () => {
-          this.hideTooltip()
-        })
+
+        if (interactionEnabled) {
+          this.tooltip.addEventListener('mouseenter', () => {
+            clearSafeZoneListener(this)
+          })
+
+          this.tooltip.addEventListener('mouseleave', () => {
+            this.attachSafeZoneListener()
+          })
+        }
       }
     })
   }
 
+  attachSafeZoneListener() {
+    clearSafeZoneListener(this)
+    this.safeZoneHandler = (e) => {
+      if (!this.currentTrigger) return
+      const triggerRect = this.currentTrigger.getBoundingClientRect()
+      const tooltipRect = this.tooltip.getBoundingClientRect()
+      const safeRect = getSafeZone(triggerRect, tooltipRect, this.position, SAFE_ZONE_MARGIN)
+      if (!isPointInsideRect(e.clientX, e.clientY, safeRect)) {
+        this.hideTooltip()
+        clearSafeZoneListener(this)
+      }
+    }
+    document.addEventListener('mousemove', this.safeZoneHandler)
+  }
+
   checkCloseTooltip(trigger) {
     document.querySelector('body').addEventListener('click', ({ target }) => {
       const isTooltip = target.closest(`#${this.tooltipId}`) === this.tooltip
@@ -62,27 +103,46 @@ export default class PbTooltip extends PbEnhancedElement {
   showTooltip(trigger) {
     if (this.shouldShowTooltip === 'false') return
 
-    this.popper = createPopper(trigger, this.tooltip, {
-      placement: this.position,
-      strategy: 'fixed',
-      modifiers: [
-        {
-          name: 'offset',
-          options: {
-            offset: TOOLTIP_OFFSET,
-          },
-        },
-        {
-          name: 'arrow',
-          options: {
-            element: document.querySelector(`#${this.tooltipId}-arrow`),
-          },
-        },
-        offset,
-        preventOverflow,
-        flip,
-      ],
+    clearSafeZoneListener(this)
+
+    this.tooltip.style.opacity = '1'
+    this.tooltip.style.visibility = 'visible'
+    this.tooltip.style.pointerEvents = 'auto'
+
+    if (this.cleanup) {
+      this.cleanup()
+    }
+
+    const arrowElement = document.querySelector(`#${this.tooltipId}-arrow`)
+
+    this.cleanup = autoUpdate(trigger, this.tooltip, () => {
+      computePosition(trigger, this.tooltip, {
+        placement: this.position,
+        strategy: 'fixed',
+        middleware: [
+          offset({ mainAxis: TOOLTIP_OFFSET, crossAxis: 0 }),
+          flip(),
+          shift(),
+          arrow({ element: arrowElement })
+        ],
+      }).then(({ x, y, placement, middlewareData }) => {
+        Object.assign(this.tooltip.style, {
+          left: `${x}px`,
+          top: `${y}px`,
+          position: 'fixed'
+        })
+        this.tooltip.setAttribute('data-popper-placement', placement)
+        if (arrowElement && middlewareData.arrow) {
+          const { x: arrowX, y: arrowY } = middlewareData.arrow
+          Object.assign(arrowElement.style, {
+            left: arrowX != null ? `${arrowX}px` : '',
+            top: arrowY != null ? `${arrowY}px` : '',
+            position: 'absolute'
+          })
+        }
+      })
     })
+
     this.tooltip.classList.add('show')
 
     if (this.triggerMethod === 'click') {
@@ -94,39 +154,50 @@ export default class PbTooltip extends PbEnhancedElement {
   }
 
   hideTooltip() {
+    if (!this.tooltip) return
+
     this.tooltip.classList.add('fade_out')
     setTimeout(() => {
-      if (!this.popper) return
-      this.popper.destroy()
+      if (this.cleanup) {
+        this.cleanup()
+        this.cleanup = null
+      }
       this.tooltip.classList.remove('show')
       this.tooltip.classList.remove('fade_out')
+      this.tooltip.style.opacity = '0'
+      this.tooltip.style.visibility = 'hidden'
+      this.tooltip.style.pointerEvents = 'none'
+      this.tooltip.style.position = ''
+      this.tooltip.style.top = ''
+      this.tooltip.style.left = ''
+      this.tooltip.style.transform = ''
     }, TOOLTIP_TIMEOUT)
   }
 
   get triggerElements() {
     let triggerEl
-
     if (this.triggerElementId) {
       triggerEl = document.querySelector(`#${this.triggerElementId}`)
-    } else {
+    } else if (this.triggerElementSelector) {
       const selectorIsId = this.triggerElementSelector.indexOf('#') > -1
       triggerEl = selectorIsId
-        ? document.querySelector(`${this.triggerElementSelector}`)
-        : document.querySelectorAll(`${this.triggerElementSelector}`)
+        ? document.querySelector(this.triggerElementSelector)
+        : document.querySelectorAll(this.triggerElementSelector)
+    } else {
+      triggerEl = this.element
     }
-
     if (!triggerEl) {
-      console.error('Tooltip Kit: an invalid or unavailable DOM reference was provided!')
+      console.error('Tooltip Kit: No valid trigger element found!')
       return []
     }
-
-    if (!triggerEl.length) triggerEl = [triggerEl]
-    return (this._triggerElements = this._triggerElements || triggerEl)
+    if (triggerEl.length === undefined) {
+      triggerEl = [triggerEl]
+    }
+    return triggerEl
   }
 
   get tooltip() {
-    return (this._tooltip =
-      this._tooltip || this.element.querySelector(`#${this.tooltipId}`))
+    return (this._tooltip = this._tooltip || this.element.querySelector(`#${this.tooltipId}`))
   }
 
   get position() {
@@ -152,4 +223,60 @@ export default class PbTooltip extends PbEnhancedElement {
   get triggerMethod() {
     return this.element.dataset.pbTooltipTriggerMethod || 'hover'
   }
-}
\ No newline at end of file
+
+  get tooltipInteraction() {
+    return this.element.dataset.pbTooltipInteraction === 'true'
+  }
+  
+  get delayOpen() {
+    return this.element.dataset.pbTooltipDelayOpen
+  }
+  
+  get delayClose() {
+    return this.element.dataset.pbTooltipDelayClose
+  }
+}
+
+function clearSafeZoneListener(context) {
+  if (context.safeZoneHandler) {
+    document.removeEventListener('mousemove', context.safeZoneHandler)
+    context.safeZoneHandler = null
+  }
+}
+
+function getSafeZone(triggerRect, tooltipRect, placement, margin) {
+  let safeRect = {}
+  if (placement.startsWith('top')) {
+    safeRect.left = triggerRect.left - margin
+    safeRect.right = triggerRect.right + margin
+    safeRect.top = tooltipRect.bottom - margin
+    safeRect.bottom = triggerRect.top + margin
+  } else if (placement.startsWith('bottom')) {
+    safeRect.left = triggerRect.left - margin
+    safeRect.right = triggerRect.right + margin
+    safeRect.top = triggerRect.bottom - margin
+    safeRect.bottom = tooltipRect.top + margin
+  } else if (placement.startsWith('left')) {
+    safeRect.top = triggerRect.top - margin
+    safeRect.bottom = triggerRect.bottom + margin
+    safeRect.left = tooltipRect.right - margin
+    safeRect.right = triggerRect.left + margin
+  } else if (placement.startsWith('right')) {
+    safeRect.top = triggerRect.top - margin
+    safeRect.bottom = triggerRect.bottom + margin
+    safeRect.left = triggerRect.right - margin
+    safeRect.right = tooltipRect.left + margin
+  } else {
+    safeRect = {
+      left: triggerRect.left - margin,
+      right: triggerRect.right + margin,
+      top: triggerRect.top - margin,
+      bottom: triggerRect.bottom + margin,
+    }
+  }
+  return safeRect
+}
+
+function isPointInsideRect(x, y, rect) {
+  return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom
+}


Reviews:
user: login: nidaqg
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T12:35:33Z
user: login: kangaree
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T14:12:49Z
user: login: nidaqg
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T15:00:22Z

Review comments:
diff_hunk: @@ -1,52 +1,93 @@
 import PbEnhancedElement from '../pb_enhanced_element'
-import {
-  createPopperLite as createPopper,
-  flip,
-  offset,
-  preventOverflow,
-} from '@popperjs/core'
-
-const TOOLTIP_OFFSET = [0, 20]
+import { computePosition, offset, flip, shift, arrow, autoUpdate } from '@floating-ui/dom'
+
+const TOOLTIP_OFFSET = 20
 const TOOLTIP_TIMEOUT = 250
+const SAFE_ZONE_MARGIN = 1
 
-export default class PbTooltip extends PbEnhancedElement {
+export default class PbTooltipFloatingUi extends PbEnhancedElement {
path: playbook/app/pb_kits/playbook/pb_tooltip/index.js
user: login: nidaqg
body: @kangaree  Code looks good but shouldn't this be PbTooltip now instead of PbTooltipFloatingUi?
created_at: 2025-03-24T12:35:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,52 +1,93 @@
 import PbEnhancedElement from '../pb_enhanced_element'
-import {
-  createPopperLite as createPopper,
-  flip,
-  offset,
-  preventOverflow,
-} from '@popperjs/core'
-
-const TOOLTIP_OFFSET = [0, 20]
+import { computePosition, offset, flip, shift, arrow, autoUpdate } from '@floating-ui/dom'
+
+const TOOLTIP_OFFSET = 20
 const TOOLTIP_TIMEOUT = 250
+const SAFE_ZONE_MARGIN = 1
 
-export default class PbTooltip extends PbEnhancedElement {
+export default class PbTooltipFloatingUi extends PbEnhancedElement {
path: playbook/app/pb_kits/playbook/pb_tooltip/index.js
user: login: kangaree
body: Thanks! I've fixed this. 
created_at: 2025-03-24T14:12:49Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-03-24T12:22:01Z
body: 🎉 Congratulations on creating an Alpha Version! 

 Your Alpha for Ruby Gems is [14.16.0.pre.alpha.PLAY1938completetooltipfloatinguitransition6830](https://rubygems.org/gems/playbook_ui/versions/14.16.0.pre.alpha.PLAY1938completetooltipfloatinguitransition6830) 

 Your Alpha for NPM is [14.16.0-alpha.PLAY1938completetooltipfloatinguitransition6830](https://www.npmjs.com/package/playbook-ui/v/14.16.0-alpha.PLAY1938completetooltipfloatinguitransition6830) 
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-27T11:59:50Z
body: You merged this pr to master branch:
    - Ruby Gem: [14.17.0.pre.rc.1](https://rubygems.org/gems/playbook_ui/versions/14.17.0.pre.rc.1)
    - NPM: [14.17.0-rc.1](https://www.npmjs.com/package/playbook-ui/v/14.17.0-rc.1)
author_association: NONE