diff --git a/js/src/dropdown.js b/js/src/dropdown.js index 2fe707f152c777d9473060db26b6dada694b9021..21341f0fc3f1bffc15cd9a6e62287fec1b3f9df9 100644 --- a/js/src/dropdown.js +++ b/js/src/dropdown.js @@ -434,30 +434,50 @@ class Dropdown { return getElementFromSelector(element) || element.parentNode } + // If not input/textarea: + // - And not a key in REGEXP_KEYDOWN => not a dropdown command + // - And dropdown disabled or not active and escape key => not a dropdown command + // If input/textarea: + // - If space key => not a dropdown command + // - If key is other than escape + // - If key is not up or down => not a dropdown command + // - If trigger inside the menu => not a dropdown command + static _isDropDownCommand(event, dropdownElement) { + const isInputOrTextArea = /input|textarea/i.test(event.target.tagName) + const isSpaceKeyEvent = event.key === SPACE_KEY + const isEscapeKeyEvent = event.key === ESCAPE_KEY + const isUpOrDownKeyEvent = event.key === ARROW_DOWN_KEY || event.key === ARROW_UP_KEY + const isTriggerInsideMenu = SelectorEngine.closest(event.target, SELECTOR_MENU) + const isKeyInRegexpKeydown = REGEXP_KEYDOWN.test(event.key) + + const isActive = dropdownElement.classList.contains(CLASS_NAME_SHOW) + const isDisabled = dropdownElement.disabled || dropdownElement.classList.contains(CLASS_NAME_DISABLED) + + if (isInputOrTextArea) { + if (isSpaceKeyEvent) { + return false + } + + if (!isEscapeKeyEvent) { + return isUpOrDownKeyEvent && !isTriggerInsideMenu + } + } else if (isKeyInRegexpKeydown) { + if (isDisabled || (!isActive && isEscapeKeyEvent)) { + return false + } + } + + return true + } + static dataApiKeydownHandler(event) { - // If not input/textarea: - // - And not a key in REGEXP_KEYDOWN => not a dropdown command - // If input/textarea: - // - If space key => not a dropdown command - // - If key is other than escape - // - If key is not up or down => not a dropdown command - // - If trigger inside the menu => not a dropdown command - if (/input|textarea/i.test(event.target.tagName) ? - event.key === SPACE_KEY || (event.key !== ESCAPE_KEY && - ((event.key !== ARROW_DOWN_KEY && event.key !== ARROW_UP_KEY) || - SelectorEngine.closest(event.target, SELECTOR_MENU))) : - !REGEXP_KEYDOWN.test(event.key)) { + if (!Dropdown._isDropDownCommand(event, this)) { return } event.preventDefault() event.stopPropagation() - if (this.disabled || this.classList.contains(CLASS_NAME_DISABLED)) { - return - } - - const parent = Dropdown.getParentFromElement(this) const isActive = this.classList.contains(CLASS_NAME_SHOW) if (event.key === ESCAPE_KEY) { @@ -472,6 +492,7 @@ class Dropdown { return } + const parent = Dropdown.getParentFromElement(this) const items = SelectorEngine.find(SELECTOR_VISIBLE_ITEMS, parent) .filter(isVisible) diff --git a/js/tests/unit/dropdown.spec.js b/js/tests/unit/dropdown.spec.js index 4f5639db8fec3d82f7ba0f01c81ffa83622c5f66..7e5bb0a13c7c6b09ac7f52e1490c607ba1eca710 100644 --- a/js/tests/unit/dropdown.spec.js +++ b/js/tests/unit/dropdown.spec.js @@ -1557,6 +1557,64 @@ describe('Dropdown', () => { done() }, 20) }) + + it('should not stop key event propagation when dropdown is disabled', () => { + fixtureEl.innerHTML = [ + '<div id="dropdown-container">', + ' <div class="dropdown">', + ' <button class="btn dropdown-toggle" id="toggle" data-toggle="dropdown" disabled>Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" id="item" href="#">Menu item</a>', + ' </div>', + ' </div>', + '</div>' + ] + + const dropdownBtn = fixtureEl.querySelector('button[data-toggle="dropdown"]') + const dropDownContainer = fixtureEl.querySelector('#dropdown-container') + const eventHandlerSpy = jasmine.createSpy() + + dropDownContainer.addEventListener('keydown', eventHandlerSpy) + + const params = { bubbles: true, cancelable: false } + const keyDownEscape = createEvent('keydown', params) + keyDownEscape.key = 'Escape' + + // Key escape + dropdownBtn.focus() + dropdownBtn.dispatchEvent(keyDownEscape) + + expect(eventHandlerSpy).toHaveBeenCalled() + }) + + it('should not stop ESC key event propagation when dropdown is not active', () => { + fixtureEl.innerHTML = [ + '<div id="dropdown-container">', + ' <div class="dropdown">', + ' <button class="btn dropdown-toggle" id="toggle" data-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" id="item" href="#">Menu item</a>', + ' </div>', + ' </div>', + '</div>' + ] + + const dropdownBtn = fixtureEl.querySelector('button[data-toggle="dropdown"]') + const dropDownContainer = fixtureEl.querySelector('#dropdown-container') + const eventHandlerSpy = jasmine.createSpy() + + dropDownContainer.addEventListener('keydown', eventHandlerSpy) + + const params = { bubbles: true, cancelable: false } + const keyDownEscape = createEvent('keydown', params) + keyDownEscape.key = 'Escape' + + // Key escape + dropdownBtn.focus() + dropdownBtn.dispatchEvent(keyDownEscape) + + expect(eventHandlerSpy).toHaveBeenCalled() + }) }) describe('jQueryInterface', () => {