diff --git a/js/src/dropdown.js b/js/src/dropdown.js index 590c748012c9d134a3be426510e7d785f25132ac..bbae240fc8d633f19c9f65f34599e44c64235d9e 100644 --- a/js/src/dropdown.js +++ b/js/src/dropdown.js @@ -255,7 +255,6 @@ class Dropdown extends BaseComponent { _addEventListeners() { EventHandler.on(this._element, EVENT_CLICK, event => { event.preventDefault() - event.stopPropagation() this.toggle() }) } @@ -518,12 +517,15 @@ class Dropdown extends BaseComponent { EventHandler.on(document, EVENT_KEYDOWN_DATA_API, SELECTOR_DATA_TOGGLE, Dropdown.dataApiKeydownHandler) EventHandler.on(document, EVENT_KEYDOWN_DATA_API, SELECTOR_MENU, Dropdown.dataApiKeydownHandler) -EventHandler.on(document, EVENT_CLICK_DATA_API, Dropdown.clearMenus) +EventHandler.on(document, EVENT_CLICK_DATA_API, event => { + if (!event.target.matches(SELECTOR_DATA_TOGGLE) && !SelectorEngine.parents(event.target, SELECTOR_DATA_TOGGLE)[0]) { + 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()) diff --git a/js/tests/unit/dropdown.spec.js b/js/tests/unit/dropdown.spec.js index 658cb65b04ad0697235fe7b337261d0f38bf54be..589cfd52647e0a0e074541d743e97473484a1be5 100644 --- a/js/tests/unit/dropdown.spec.js +++ b/js/tests/unit/dropdown.spec.js @@ -1765,4 +1765,66 @@ describe('Dropdown', () => { triggerDropdown.dispatchEvent(keydown) }) + + it('should bubble up the event from dropdown toggle to the parent elements', done => { + fixtureEl.innerHTML = [ + '<div class="container">', + ' <div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#subMenu">Sub menu</a>', + ' </div>', + ' </div>', + '</div>' + ].join('') + + const container = fixtureEl.querySelector('.container') + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdown = new Dropdown(triggerDropdown) + + spyOn(dropdown, 'show').and.callThrough() + + container.addEventListener('click', event => { + expect(triggerDropdown.classList.contains('show')).toEqual(true) + expect(dropdown.show).toHaveBeenCalled() + expect(event.target).toEqual(triggerDropdown) + done() + }) + + triggerDropdown.click() + }) + + 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 triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const childElement = fixtureEl.querySelector('#childElement') + + spyOn(Dropdown, 'clearMenus').and.callThrough() + spyOn(triggerDropdown.classList, 'remove') + + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + setTimeout(() => { + expect(Dropdown.clearMenus).toHaveBeenCalledTimes(1) + expect(triggerDropdown.classList.remove).not.toHaveBeenCalled() + expect(triggerDropdown.classList.contains('show')).toEqual(true) + done() + }, 20) + }) + + triggerDropdown.addEventListener('hidden.bs.dropdown', () => { + throw new Error('should not throw hidden.bs.dropdown event') + }) + + childElement.click() + }) })