Skip to content

Commit 16bc47d

Browse files
caseyjholXhmikosR
authored andcommitted
Allow data-toggle="dropdown" and form click events to bubble
* 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
1 parent 9667438 commit 16bc47d

File tree

2 files changed

+68
-14
lines changed

2 files changed

+68
-14
lines changed

js/src/dropdown.js

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@ const CLASS_NAME_DROPSTART = 'dropstart'
5959
const CLASS_NAME_NAVBAR = 'navbar'
6060

6161
const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="dropdown"]'
62-
const SELECTOR_FORM_CHILD = '.dropdown form'
6362
const SELECTOR_MENU = '.dropdown-menu'
6463
const SELECTOR_NAVBAR_NAV = '.navbar-nav'
6564
const SELECTOR_VISIBLE_ITEMS = '.dropdown-menu .dropdown-item:not(.disabled):not(:disabled)'
@@ -253,7 +252,6 @@ class Dropdown extends BaseComponent {
253252
_addEventListeners() {
254253
EventHandler.on(this._element, EVENT_CLICK, event => {
255254
event.preventDefault()
256-
event.stopPropagation()
257255
this.toggle()
258256
})
259257
}
@@ -377,8 +375,14 @@ class Dropdown extends BaseComponent {
377375
}
378376

379377
static clearMenus(event) {
380-
if (event && (event.button === RIGHT_MOUSE_BUTTON || (event.type === 'keyup' && event.key !== TAB_KEY))) {
381-
return
378+
if (event) {
379+
if (event.button === RIGHT_MOUSE_BUTTON || (event.type === 'keyup' && event.key !== TAB_KEY)) {
380+
return
381+
}
382+
383+
if (/input|select|textarea|form/i.test(event.target.tagName)) {
384+
return
385+
}
382386
}
383387

384388
const toggles = SelectorEngine.find(SELECTOR_DATA_TOGGLE)
@@ -402,11 +406,16 @@ class Dropdown extends BaseComponent {
402406
continue
403407
}
404408

405-
if (event && ((event.type === 'click' &&
406-
/input|textarea/i.test(event.target.tagName)) ||
407-
(event.type === 'keyup' && event.key === TAB_KEY)) &&
408-
dropdownMenu.contains(event.target)) {
409-
continue
409+
if (event) {
410+
// Don't close the menu if the clicked element or one of its parents is the dropdown button
411+
if ([context._element].some(element => event.composedPath().includes(element))) {
412+
continue
413+
}
414+
415+
// Tab navigation through the dropdown menu shouldn't close the menu
416+
if (event.type === 'keyup' && event.key === TAB_KEY && dropdownMenu.contains(event.target)) {
417+
continue
418+
}
410419
}
411420

412421
const hideEvent = EventHandler.trigger(toggles[i], EVENT_HIDE, relatedTarget)
@@ -519,10 +528,8 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, Dropdown.clearMenus)
519528
EventHandler.on(document, EVENT_KEYUP_DATA_API, Dropdown.clearMenus)
520529
EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {
521530
event.preventDefault()
522-
event.stopPropagation()
523-
Dropdown.dropdownInterface(this, 'toggle')
531+
Dropdown.dropdownInterface(this)
524532
})
525-
EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_FORM_CHILD, e => e.stopPropagation())
526533

527534
/**
528535
* ------------------------------------------------------------------------

js/tests/unit/dropdown.spec.js

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1020,13 +1020,13 @@ describe('Dropdown', () => {
10201020
showEventTriggered = true
10211021
})
10221022

1023-
btnDropdown.addEventListener('shown.bs.dropdown', e => {
1023+
btnDropdown.addEventListener('shown.bs.dropdown', e => setTimeout(() => {
10241024
expect(btnDropdown.classList.contains('show')).toEqual(true)
10251025
expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true')
10261026
expect(showEventTriggered).toEqual(true)
10271027
expect(e.relatedTarget).toEqual(btnDropdown)
10281028
document.body.click()
1029-
})
1029+
}))
10301030

10311031
btnDropdown.addEventListener('hide.bs.dropdown', () => {
10321032
hideEventTriggered = true
@@ -1783,4 +1783,51 @@ describe('Dropdown', () => {
17831783

17841784
triggerDropdown.dispatchEvent(keydown)
17851785
})
1786+
1787+
it('should allow `data-bs-toggle="dropdown"` click events to bubble up', () => {
1788+
fixtureEl.innerHTML = [
1789+
'<div class="dropdown">',
1790+
' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>',
1791+
' <div class="dropdown-menu">',
1792+
' <a class="dropdown-item" href="#">Secondary link</a>',
1793+
' </div>',
1794+
'</div>'
1795+
].join('')
1796+
1797+
const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
1798+
const clickListener = jasmine.createSpy('clickListener')
1799+
const delegatedClickListener = jasmine.createSpy('delegatedClickListener')
1800+
1801+
btnDropdown.addEventListener('click', clickListener)
1802+
document.addEventListener('click', delegatedClickListener)
1803+
1804+
btnDropdown.click()
1805+
1806+
expect(clickListener).toHaveBeenCalled()
1807+
expect(delegatedClickListener).toHaveBeenCalled()
1808+
})
1809+
1810+
it('should open the dropdown when clicking the child element inside `data-bs-toggle="dropdown"`', done => {
1811+
fixtureEl.innerHTML = [
1812+
'<div class="container">',
1813+
' <div class="dropdown">',
1814+
' <button class="btn dropdown-toggle" data-bs-toggle="dropdown"><span id="childElement">Dropdown</span></button>',
1815+
' <div class="dropdown-menu">',
1816+
' <a class="dropdown-item" href="#subMenu">Sub menu</a>',
1817+
' </div>',
1818+
' </div>',
1819+
'</div>'
1820+
].join('')
1821+
1822+
const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
1823+
const childElement = fixtureEl.querySelector('#childElement')
1824+
1825+
btnDropdown.addEventListener('shown.bs.dropdown', () => setTimeout(() => {
1826+
expect(btnDropdown.classList.contains('show')).toEqual(true)
1827+
expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true')
1828+
done()
1829+
}))
1830+
1831+
childElement.click()
1832+
})
17861833
})

0 commit comments

Comments
 (0)