From 73aff3ba1445719ba6506300e97a7817ce067fb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Tomczak?= Date: Wed, 8 Apr 2020 00:36:35 +0200 Subject: [PATCH] Fix event propagation from inactive and disabled dropdowns (Fixes #30510) --- js/src/dropdown.js | 55 ++++++++++++++++++++++---------- js/tests/unit/dropdown.spec.js | 58 ++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 17 deletions(-) diff --git a/js/src/dropdown.js b/js/src/dropdown.js index aab1d6bd2a20..061f0ea4175e 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.which === SPACE_KEYCODE + const isEscapeKeyEvent = event.which === ESCAPE_KEYCODE + const isUpOrDownKeyEvent = event.which === ARROW_DOWN_KEYCODE || event.which === ARROW_UP_KEYCODE + const isTriggerInsideMenu = SelectorEngine.closest(event.target, SELECTOR_MENU) + const isKeyInRegexpKeydown = REGEXP_KEYDOWN.test(event.which) + + 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.which === SPACE_KEYCODE || (event.which !== ESCAPE_KEYCODE && - ((event.which !== ARROW_DOWN_KEYCODE && event.which !== ARROW_UP_KEYCODE) || - SelectorEngine.closest(event.target, SELECTOR_MENU))) : - !REGEXP_KEYDOWN.test(event.which)) { + 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.which === ESCAPE_KEYCODE) { @@ -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 e8a7e66ba9ee..182270820bf9 100644 --- a/js/tests/unit/dropdown.spec.js +++ b/js/tests/unit/dropdown.spec.js @@ -1533,6 +1533,64 @@ describe('Dropdown', () => { done() }, 20) }) + + it('should not stop key event propagation when dropdown is disabled', () => { + fixtureEl.innerHTML = [ + '' + ] + + 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.which = 27 + + // 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 = [ + '' + ] + + 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.which = 27 + + // Key escape + dropdownBtn.focus() + dropdownBtn.dispatchEvent(keyDownEscape) + + expect(eventHandlerSpy).toHaveBeenCalled() + }) }) describe('jQueryInterface', () => {