]> git.ipfire.org Git - thirdparty/bootstrap.git/commitdiff
Allow data-toggle="dropdown" and form click events to bubble
authorCasey Holzer <caseyh@snapappointments.com>
Wed, 6 Jan 2021 01:07:43 +0000 (03:07 +0200)
committerXhmikosR <xhmikosr@gmail.com>
Tue, 23 Mar 2021 13:56:28 +0000 (15:56 +0200)
* remove stopPropagation from button click event

* test for delegated click events

* ensure button children can open menu

* test to ensure clicking button opens the menu

* check current element and parents

* allow dropdown form click events to bubble

js/src/dropdown.js
js/tests/unit/dropdown.spec.js

index eeec9076d52aaf1b77cce1dbb43c5d72e3d5cb0f..ae1aacb6bf4284357e4944cfcdaae9442822e19d 100644 (file)
@@ -59,7 +59,6 @@ const CLASS_NAME_DROPSTART = 'dropstart'
 const CLASS_NAME_NAVBAR = 'navbar'
 
 const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="dropdown"]'
-const SELECTOR_FORM_CHILD = '.dropdown form'
 const SELECTOR_MENU = '.dropdown-menu'
 const SELECTOR_NAVBAR_NAV = '.navbar-nav'
 const SELECTOR_VISIBLE_ITEMS = '.dropdown-menu .dropdown-item:not(.disabled):not(:disabled)'
@@ -253,7 +252,6 @@ class Dropdown extends BaseComponent {
   _addEventListeners() {
     EventHandler.on(this._element, EVENT_CLICK, event => {
       event.preventDefault()
-      event.stopPropagation()
       this.toggle()
     })
   }
@@ -377,8 +375,14 @@ class Dropdown extends BaseComponent {
   }
 
   static clearMenus(event) {
-    if (event && (event.button === RIGHT_MOUSE_BUTTON || (event.type === 'keyup' && event.key !== TAB_KEY))) {
-      return
+    if (event) {
+      if (event.button === RIGHT_MOUSE_BUTTON || (event.type === 'keyup' && event.key !== TAB_KEY)) {
+        return
+      }
+
+      if (/input|select|textarea|form/i.test(event.target.tagName)) {
+        return
+      }
     }
 
     const toggles = SelectorEngine.find(SELECTOR_DATA_TOGGLE)
@@ -402,11 +406,16 @@ class Dropdown extends BaseComponent {
         continue
       }
 
-      if (event && ((event.type === 'click' &&
-          /input|textarea/i.test(event.target.tagName)) ||
-          (event.type === 'keyup' && event.key === TAB_KEY)) &&
-          dropdownMenu.contains(event.target)) {
-        continue
+      if (event) {
+        // Don't close the menu if the clicked element or one of its parents is the dropdown button
+        if ([context._element].some(element => event.composedPath().includes(element))) {
+          continue
+        }
+
+        // Tab navigation through the dropdown menu shouldn't close the menu
+        if (event.type === 'keyup' && event.key === TAB_KEY && dropdownMenu.contains(event.target)) {
+          continue
+        }
       }
 
       const hideEvent = EventHandler.trigger(toggles[i], EVENT_HIDE, relatedTarget)
@@ -519,10 +528,8 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, Dropdown.clearMenus)
 EventHandler.on(document, EVENT_KEYUP_DATA_API, Dropdown.clearMenus)
 EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {
   event.preventDefault()
-  event.stopPropagation()
-  Dropdown.dropdownInterface(this, 'toggle')
+  Dropdown.dropdownInterface(this)
 })
-EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_FORM_CHILD, e => e.stopPropagation())
 
 /**
  * ------------------------------------------------------------------------
index d6fdaca5efbb9ed6f7ba157aa9be1d93d6f11f68..ad51d487bf12c0e6015c23e1a5c28f32efdec80f 100644 (file)
@@ -1020,13 +1020,13 @@ describe('Dropdown', () => {
         showEventTriggered = true
       })
 
-      btnDropdown.addEventListener('shown.bs.dropdown', e => {
+      btnDropdown.addEventListener('shown.bs.dropdown', e => setTimeout(() => {
         expect(btnDropdown.classList.contains('show')).toEqual(true)
         expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true')
         expect(showEventTriggered).toEqual(true)
         expect(e.relatedTarget).toEqual(btnDropdown)
         document.body.click()
-      })
+      }))
 
       btnDropdown.addEventListener('hide.bs.dropdown', () => {
         hideEventTriggered = true
@@ -1783,4 +1783,51 @@ describe('Dropdown', () => {
 
     triggerDropdown.dispatchEvent(keydown)
   })
+
+  it('should allow `data-bs-toggle="dropdown"` click events to bubble up', () => {
+    fixtureEl.innerHTML = [
+      '<div class="dropdown">',
+      '  <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>',
+      '  <div class="dropdown-menu">',
+      '    <a class="dropdown-item" href="#">Secondary link</a>',
+      '  </div>',
+      '</div>'
+    ].join('')
+
+    const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
+    const clickListener = jasmine.createSpy('clickListener')
+    const delegatedClickListener = jasmine.createSpy('delegatedClickListener')
+
+    btnDropdown.addEventListener('click', clickListener)
+    document.addEventListener('click', delegatedClickListener)
+
+    btnDropdown.click()
+
+    expect(clickListener).toHaveBeenCalled()
+    expect(delegatedClickListener).toHaveBeenCalled()
+  })
+
+  it('should open the dropdown when clicking the child element inside `data-bs-toggle="dropdown"`', done => {
+    fixtureEl.innerHTML = [
+      '<div class="container">',
+      '  <div class="dropdown">',
+      '    <button class="btn dropdown-toggle" data-bs-toggle="dropdown"><span id="childElement">Dropdown</span></button>',
+      '    <div class="dropdown-menu">',
+      '      <a class="dropdown-item" href="#subMenu">Sub menu</a>',
+      '    </div>',
+      '  </div>',
+      '</div>'
+    ].join('')
+
+    const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
+    const childElement = fixtureEl.querySelector('#childElement')
+
+    btnDropdown.addEventListener('shown.bs.dropdown', () => setTimeout(() => {
+      expect(btnDropdown.classList.contains('show')).toEqual(true)
+      expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true')
+      done()
+    }))
+
+    childElement.click()
+  })
 })