diff --git a/js/src/dom/selector-engine.js b/js/src/dom/selector-engine.js index a8c14ae4c8..c18ec4136f 100644 --- a/js/src/dom/selector-engine.js +++ b/js/src/dom/selector-engine.js @@ -66,6 +66,20 @@ const SelectorEngine = { previous = previous.previousElementSibling } + return [] + }, + + next(element, selector) { + let next = element.nextElementSibling + + while (next) { + if (this.matches(next, selector)) { + return [next] + } + + next = next.nextElementSibling + } + return [] } } diff --git a/js/src/dropdown.js b/js/src/dropdown.js index 9d6f8a3296..b8f8c22b8c 100644 --- a/js/src/dropdown.js +++ b/js/src/dropdown.js @@ -129,7 +129,7 @@ class Dropdown { return } - const isActive = this._menu.classList.contains(CLASS_NAME_SHOW) + const isActive = this._element.classList.contains(CLASS_NAME_SHOW) Dropdown.clearMenus() @@ -150,7 +150,7 @@ class Dropdown { relatedTarget: this._element } - const showEvent = EventHandler.trigger(parent, EVENT_SHOW, relatedTarget) + const showEvent = EventHandler.trigger(this._element, EVENT_SHOW, relatedTarget) if (showEvent.defaultPrevented) { return @@ -199,7 +199,7 @@ class Dropdown { this._element.setAttribute('aria-expanded', true) Manipulator.toggleClass(this._menu, CLASS_NAME_SHOW) - Manipulator.toggleClass(parent, CLASS_NAME_SHOW) + Manipulator.toggleClass(this._element, CLASS_NAME_SHOW) EventHandler.trigger(parent, EVENT_SHOWN, relatedTarget) } @@ -224,7 +224,7 @@ class Dropdown { } Manipulator.toggleClass(this._menu, CLASS_NAME_SHOW) - Manipulator.toggleClass(parent, CLASS_NAME_SHOW) + Manipulator.toggleClass(this._element, CLASS_NAME_SHOW) EventHandler.trigger(parent, EVENT_HIDDEN, relatedTarget) } @@ -273,9 +273,7 @@ class Dropdown { } _getMenuElement() { - const parent = Dropdown.getParentFromElement(this._element) - - return SelectorEngine.findOne(SELECTOR_MENU, parent) + return SelectorEngine.next(this._element, SELECTOR_MENU)[0] } _getPlacement() { @@ -397,14 +395,14 @@ class Dropdown { } const dropdownMenu = context._menu - if (!parent.classList.contains(CLASS_NAME_SHOW)) { + if (!toggles[i].classList.contains(CLASS_NAME_SHOW)) { continue } if (event && ((event.type === 'click' && /input|textarea/i.test(event.target.tagName)) || (event.type === 'keyup' && event.which === TAB_KEYCODE)) && - parent.contains(event.target)) { + dropdownMenu.contains(event.target)) { continue } @@ -427,7 +425,7 @@ class Dropdown { } dropdownMenu.classList.remove(CLASS_NAME_SHOW) - parent.classList.remove(CLASS_NAME_SHOW) + toggles[i].classList.remove(CLASS_NAME_SHOW) EventHandler.trigger(parent, EVENT_HIDDEN, relatedTarget) } } @@ -460,13 +458,16 @@ class Dropdown { } const parent = Dropdown.getParentFromElement(this) - const isActive = parent.classList.contains(CLASS_NAME_SHOW) + const isActive = this.classList.contains(CLASS_NAME_SHOW) - if (!isActive || (isActive && (event.which === ESCAPE_KEYCODE || event.which === SPACE_KEYCODE))) { - if (event.which === ESCAPE_KEYCODE) { - SelectorEngine.findOne(SELECTOR_DATA_TOGGLE, parent).focus() - } + if (event.which === ESCAPE_KEYCODE) { + const button = this.matches(SELECTOR_DATA_TOGGLE) ? this : SelectorEngine.prev(this, SELECTOR_DATA_TOGGLE)[0] + button.focus() + Dropdown.clearMenus() + return + } + if (!isActive || event.which === SPACE_KEYCODE) { Dropdown.clearMenus() return } @@ -478,7 +479,7 @@ class Dropdown { return } - let index = items.indexOf(event.target) + let index = items.indexOf(event.target) || 0 if (event.which === ARROW_UP_KEYCODE && index > 0) { // Up index-- @@ -488,10 +489,6 @@ class Dropdown { index++ } - if (index < 0) { - index = 0 - } - items[index].focus() } diff --git a/js/tests/unit/dom/selector-engine.spec.js b/js/tests/unit/dom/selector-engine.spec.js index e140c6a3e8..727f106214 100644 --- a/js/tests/unit/dom/selector-engine.spec.js +++ b/js/tests/unit/dom/selector-engine.spec.js @@ -126,5 +126,44 @@ describe('SelectorEngine', () => { expect(SelectorEngine.prev(btn, '.test')).toEqual([divTest]) }) }) + + describe('next', () => { + it('should return next element', () => { + fixtureEl.innerHTML = '
' + + const btn = fixtureEl.querySelector('.btn') + const divTest = fixtureEl.querySelector('.test') + + expect(SelectorEngine.next(divTest, '.btn')).toEqual([btn]) + }) + + it('should return next element with an extra element between', () => { + fixtureEl.innerHTML = [ + '', + '', + '' + ].join('') + + const btn = fixtureEl.querySelector('.btn') + const divTest = fixtureEl.querySelector('.test') + + expect(SelectorEngine.next(divTest, '.btn')).toEqual([btn]) + }) + + it('should return next element with comments or text nodes between', () => { + fixtureEl.innerHTML = [ + '', + '', + 'Text', + '', + '' + ].join('') + + const btn = fixtureEl.querySelector('.btn') + const divTest = fixtureEl.querySelector('.test') + + expect(SelectorEngine.next(divTest, '.btn')).toEqual([btn]) + }) + }) }) diff --git a/js/tests/unit/dropdown.spec.js b/js/tests/unit/dropdown.spec.js index 2762ad21b9..e8a7e66ba9 100644 --- a/js/tests/unit/dropdown.spec.js +++ b/js/tests/unit/dropdown.spec.js @@ -139,7 +139,7 @@ describe('Dropdown', () => { const dropdown = new Dropdown(btnDropdown) dropdownEl.addEventListener('shown.bs.dropdown', () => { - expect(dropdownEl.classList.contains('show')).toEqual(true) + expect(btnDropdown.classList.contains('show')).toEqual(true) expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') done() }) @@ -171,7 +171,7 @@ describe('Dropdown', () => { const dropdown2 = new Dropdown(btnDropdown2) firstDropdownEl.addEventListener('shown.bs.dropdown', () => { - expect(firstDropdownEl.classList.contains('show')).toEqual(true) + expect(btnDropdown1.classList.contains('show')).toEqual(true) spyOn(dropdown1._popper, 'destroy') dropdown2.toggle() }) @@ -204,7 +204,7 @@ describe('Dropdown', () => { spyOn(EventHandler, 'off') dropdownEl.addEventListener('shown.bs.dropdown', () => { - expect(dropdownEl.classList.contains('show')).toEqual(true) + expect(btnDropdown.classList.contains('show')).toEqual(true) expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') expect(EventHandler.on).toHaveBeenCalled() @@ -212,7 +212,7 @@ describe('Dropdown', () => { }) dropdownEl.addEventListener('hidden.bs.dropdown', () => { - expect(dropdownEl.classList.contains('show')).toEqual(false) + expect(btnDropdown.classList.contains('show')).toEqual(false) expect(btnDropdown.getAttribute('aria-expanded')).toEqual('false') expect(EventHandler.off).toHaveBeenCalled() @@ -238,7 +238,7 @@ describe('Dropdown', () => { const dropdown = new Dropdown(btnDropdown) dropdownEl.addEventListener('shown.bs.dropdown', () => { - expect(dropdownEl.classList.contains('show')).toEqual(true) + expect(btnDropdown.classList.contains('show')).toEqual(true) expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') done() }) @@ -261,7 +261,7 @@ describe('Dropdown', () => { const dropdown = new Dropdown(btnDropdown) dropupEl.addEventListener('shown.bs.dropdown', () => { - expect(dropupEl.classList.contains('show')).toEqual(true) + expect(btnDropdown.classList.contains('show')).toEqual(true) expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') done() }) @@ -284,7 +284,7 @@ describe('Dropdown', () => { const dropdown = new Dropdown(btnDropdown) dropupEl.addEventListener('shown.bs.dropdown', () => { - expect(dropupEl.classList.contains('show')).toEqual(true) + expect(btnDropdown.classList.contains('show')).toEqual(true) expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') done() }) @@ -307,7 +307,7 @@ describe('Dropdown', () => { const dropdown = new Dropdown(btnDropdown) droprightEl.addEventListener('shown.bs.dropdown', () => { - expect(droprightEl.classList.contains('show')).toEqual(true) + expect(btnDropdown.classList.contains('show')).toEqual(true) expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') done() }) @@ -330,7 +330,7 @@ describe('Dropdown', () => { const dropdown = new Dropdown(btnDropdown) dropleftEl.addEventListener('shown.bs.dropdown', () => { - expect(dropleftEl.classList.contains('show')).toEqual(true) + expect(btnDropdown.classList.contains('show')).toEqual(true) expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') done() }) @@ -355,7 +355,7 @@ describe('Dropdown', () => { }) dropdownEl.addEventListener('shown.bs.dropdown', () => { - expect(dropdownEl.classList.contains('show')).toEqual(true) + expect(btnDropdown.classList.contains('show')).toEqual(true) expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') done() }) @@ -380,7 +380,7 @@ describe('Dropdown', () => { }) dropdownEl.addEventListener('shown.bs.dropdown', () => { - expect(dropdownEl.classList.contains('show')).toEqual(true) + expect(btnDropdown.classList.contains('show')).toEqual(true) expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') done() }) @@ -405,7 +405,7 @@ describe('Dropdown', () => { }) dropdownEl.addEventListener('shown.bs.dropdown', () => { - expect(dropdownEl.classList.contains('show')).toEqual(true) + expect(btnDropdown.classList.contains('show')).toEqual(true) expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') done() }) @@ -538,7 +538,7 @@ describe('Dropdown', () => { const dropdown = new Dropdown(btnDropdown) dropdownEl.addEventListener('shown.bs.dropdown', () => { - expect(dropdownEl.classList.contains('show')).toEqual(true) + expect(btnDropdown.classList.contains('show')).toEqual(true) done() }) @@ -983,7 +983,7 @@ describe('Dropdown', () => { }) dropdownEl.addEventListener('shown.bs.dropdown', e => { - expect(dropdownEl.classList.contains('show')).toEqual(true) + expect(btnDropdown.classList.contains('show')).toEqual(true) expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') expect(showEventTriggered).toEqual(true) expect(e.relatedTarget).toEqual(btnDropdown) @@ -995,7 +995,7 @@ describe('Dropdown', () => { }) dropdownEl.addEventListener('hidden.bs.dropdown', e => { - expect(dropdownEl.classList.contains('show')).toEqual(false) + expect(btnDropdown.classList.contains('show')).toEqual(false) expect(btnDropdown.getAttribute('aria-expanded')).toEqual('false') expect(hideEventTriggered).toEqual(true) expect(e.relatedTarget).toEqual(btnDropdown) @@ -1066,7 +1066,7 @@ describe('Dropdown', () => { const dropdownEl = fixtureEl.querySelector('.dropdown') dropdownEl.addEventListener('shown.bs.dropdown', () => { - expect(dropdownEl.classList.contains('show')).toEqual(true) + expect(btnDropdown.classList.contains('show')).toEqual(true) const keyUp = createEvent('keyup') @@ -1075,7 +1075,7 @@ describe('Dropdown', () => { }) dropdownEl.addEventListener('hidden.bs.dropdown', () => { - expect(dropdownEl.classList.contains('show')).toEqual(false) + expect(btnDropdown.classList.contains('show')).toEqual(false) done() }) @@ -1111,7 +1111,7 @@ describe('Dropdown', () => { const btnGroup = last.parentNode dropdownTestMenu.addEventListener('shown.bs.dropdown', () => { - expect(dropdownTestMenu.classList.contains('show')).toEqual(true) + expect(first.classList.contains('show')).toEqual(true) expect(fixtureEl.querySelectorAll('.dropdown-menu.show').length).toEqual(1) document.body.click() }) @@ -1122,7 +1122,7 @@ describe('Dropdown', () => { }) btnGroup.addEventListener('shown.bs.dropdown', () => { - expect(btnGroup.classList.contains('show')).toEqual(true) + expect(last.classList.contains('show')).toEqual(true) expect(fixtureEl.querySelectorAll('.dropdown-menu.show').length).toEqual(1) document.body.click() }) @@ -1162,7 +1162,7 @@ describe('Dropdown', () => { const btnGroup = last.parentNode dropdownTestMenu.addEventListener('shown.bs.dropdown', () => { - expect(dropdownTestMenu.classList.contains('show')).toEqual(true, '"show" class added on click') + expect(first.classList.contains('show')).toEqual(true, '"show" class added on click') expect(fixtureEl.querySelectorAll('.dropdown-menu.show').length).toEqual(1, 'only one dropdown is shown') const keyUp = createEvent('keyup') @@ -1177,7 +1177,7 @@ describe('Dropdown', () => { }) btnGroup.addEventListener('shown.bs.dropdown', () => { - expect(btnGroup.classList.contains('show')).toEqual(true, '"show" class added on click') + expect(last.classList.contains('show')).toEqual(true, '"show" class added on click') expect(fixtureEl.querySelectorAll('.dropdown-menu.show').length).toEqual(1, 'only one dropdown is shown') const keyUp = createEvent('keyup') @@ -1382,12 +1382,12 @@ describe('Dropdown', () => { const input = fixtureEl.querySelector('input') input.addEventListener('click', () => { - expect(dropdown.classList.contains('show')).toEqual(true, 'dropdown menu is shown') + expect(triggerDropdown.classList.contains('show')).toEqual(true, 'dropdown menu is shown') done() }) dropdown.addEventListener('shown.bs.dropdown', () => { - expect(dropdown.classList.contains('show')).toEqual(true, 'dropdown menu is shown') + expect(triggerDropdown.classList.contains('show')).toEqual(true, 'dropdown menu is shown') input.dispatchEvent(createEvent('click')) }) @@ -1409,12 +1409,12 @@ describe('Dropdown', () => { const textarea = fixtureEl.querySelector('textarea') textarea.addEventListener('click', () => { - expect(dropdown.classList.contains('show')).toEqual(true, 'dropdown menu is shown') + expect(triggerDropdown.classList.contains('show')).toEqual(true, 'dropdown menu is shown') done() }) dropdown.addEventListener('shown.bs.dropdown', () => { - expect(dropdown.classList.contains('show')).toEqual(true, 'dropdown menu is shown') + expect(triggerDropdown.classList.contains('show')).toEqual(true, 'dropdown menu is shown') textarea.dispatchEvent(createEvent('click')) }) @@ -1492,7 +1492,7 @@ describe('Dropdown', () => { input.focus() input.dispatchEvent(keyDownEscape) - expect(dropdown.classList.contains('show')).toEqual(false, 'dropdown menu is not shown') + expect(triggerDropdown.classList.contains('show')).toEqual(false, 'dropdown menu is not shown') done() }) @@ -1529,7 +1529,7 @@ describe('Dropdown', () => { setTimeout(() => { expect(dropdown.toggle).not.toHaveBeenCalled() - expect(triggerDropdown.parentNode.classList.contains('show')).toEqual(false) + expect(triggerDropdown.classList.contains('show')).toEqual(false) done() }, 20) }) diff --git a/scss/mixins/_buttons.scss b/scss/mixins/_buttons.scss index 92fe214d14..acf6b450c5 100644 --- a/scss/mixins/_buttons.scss +++ b/scss/mixins/_buttons.scss @@ -90,7 +90,7 @@ &:active, &.active, - .show > &.dropdown-toggle { + &.dropdown-toggle.show { color: $active-color; background-color: $active-background; border-color: $active-border;