]> git.ipfire.org Git - thirdparty/bootstrap.git/commitdiff
Fix: Popover with hover and click triggers closes on mouseleave (#41511)
authorAmit Rathiesh <31440869+amitrathiesh@users.noreply.github.com>
Tue, 3 Jun 2025 19:19:24 +0000 (15:19 -0400)
committerGitHub <noreply@github.com>
Tue, 3 Jun 2025 19:19:24 +0000 (12:19 -0700)
* Fix: Popover with hover and click triggers closes on mouseleave

When a popover is configured with `trigger: 'hover click'`,
if you open it by a click, it would incorrectly close when the
mouse pointer leaves the trigger element. This was because the
`mouseleave` event (part of the hover trigger) would hide the
popover without adequately respecting the click trigger's intent
to keep it open.

This commit modifies the click event listener within `Tooltip.js`
(which Popover extends) to explicitly manage the `_activeTrigger[TRIGGER_CLICK]`
state:
- When a click opens the popover or makes a hover-opened popover
  sticky, `_activeTrigger[TRIGGER_CLICK]` is set to `true`.
- When a click closes an already click-activated popover,
  `_activeTrigger[TRIGGER_CLICK]` is set to `false`.

The `_leave()` method, called by `mouseleave`, already checks
`_isWithActiveTrigger()`. With `_activeTrigger[TRIGGER_CLICK]`
now accurately reflecting the click state, `_leave()` will not
hide a click-activated popover when the mouse leaves the trigger
element. The popover will now correctly remain open until a
subsequent click closes it.

* Removed `test-popover.html`

* Fix linting issues

* Add unit test

---------

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
Co-authored-by: Julien Déramond <juderamond@gmail.com>
js/src/tooltip.js
js/tests/unit/popover.spec.js

index 097477f7a1a813387caa92b33f585fb9cc3d07c7..1ad5615c21740b74b5436805c2411770c9841c15 100644 (file)
@@ -448,6 +448,7 @@ class Tooltip extends BaseComponent {
       if (trigger === 'click') {
         EventHandler.on(this._element, this.constructor.eventName(EVENT_CLICK), this._config.selector, event => {
           const context = this._initializeOnDelegatedTarget(event)
+          context._activeTrigger[TRIGGER_CLICK] = !(context._isShown() && context._activeTrigger[TRIGGER_CLICK])
           context.toggle()
         })
       } else if (trigger !== TRIGGER_MANUAL) {
index da5821e760d66a51224a51a906d915a3d88a0302..1338821bc86ddae1e1c5642fb1d0054d6f064790 100644 (file)
@@ -1,6 +1,8 @@
 import EventHandler from '../../src/dom/event-handler.js'
 import Popover from '../../src/popover.js'
-import { clearFixture, getFixture, jQueryMock } from '../helpers/fixture.js'
+import {
+  clearFixture, getFixture, jQueryMock, createEvent
+} from '../helpers/fixture.js'
 
 describe('Popover', () => {
   let fixtureEl
@@ -313,6 +315,28 @@ describe('Popover', () => {
         popover.show()
       })
     })
+
+    it('should keep popover open when mouse leaves after click trigger', () => {
+      return new Promise(resolve => {
+        fixtureEl.innerHTML = '<a href="#" title="Popover" data-bs-content="https://x.com/getbootstrap" data-bs-trigger="hover click">BS X</a>'
+
+        const popoverEl = fixtureEl.querySelector('a')
+        new Popover(popoverEl) // eslint-disable-line no-new
+
+        popoverEl.addEventListener('shown.bs.popover', () => {
+          popoverEl.dispatchEvent(createEvent('mouseout'))
+
+          popoverEl.addEventListener('hide.bs.popover', () => {
+            throw new Error('Popover should not hide when mouse leaves after click')
+          })
+
+          expect(document.querySelector('.popover')).not.toBeNull()
+          resolve()
+        })
+
+        popoverEl.click()
+      })
+    })
   })
 
   describe('hide', () => {