From f36f8344533d3179b8d82af96e005b3106d9ab46 Mon Sep 17 00:00:00 2001 From: alpadev <2838324+alpadev@users.noreply.github.com> Date: Thu, 1 Apr 2021 20:44:04 +0200 Subject: [PATCH] Fix dropdown escape propagation (#33479) --- js/src/dropdown.js | 103 ++++++++++++++++++--------------- js/tests/unit/dropdown.spec.js | 33 +++++++++++ 2 files changed, 88 insertions(+), 48 deletions(-) diff --git a/js/src/dropdown.js b/js/src/dropdown.js index 97bf6e1099..605cbc64db 100644 --- a/js/src/dropdown.js +++ b/js/src/dropdown.js @@ -443,54 +443,7 @@ class Dropdown extends BaseComponent { } } - static getParentFromElement(element) { - return getElementFromSelector(element) || element.parentNode - } - - 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) || - event.target.closest(SELECTOR_MENU))) : - !REGEXP_KEYDOWN.test(event.key)) { - return - } - - event.preventDefault() - event.stopPropagation() - - if (isDisabled(this)) { - return - } - - const parent = Dropdown.getParentFromElement(this) - const isActive = this.classList.contains(CLASS_NAME_SHOW) - - if (event.key === ESCAPE_KEY) { - const button = this.matches(SELECTOR_DATA_TOGGLE) ? this : SelectorEngine.prev(this, SELECTOR_DATA_TOGGLE)[0] - button.focus() - Dropdown.clearMenus() - return - } - - if (!isActive && (event.key === ARROW_UP_KEY || event.key === ARROW_DOWN_KEY)) { - const button = this.matches(SELECTOR_DATA_TOGGLE) ? this : SelectorEngine.prev(this, SELECTOR_DATA_TOGGLE)[0] - button.click() - return - } - - if (!isActive || event.key === SPACE_KEY) { - Dropdown.clearMenus() - return - } - + static selectMenuItem(parent, event) { const items = SelectorEngine.find(SELECTOR_VISIBLE_ITEMS, parent).filter(isVisible) if (!items.length) { @@ -514,6 +467,60 @@ class Dropdown extends BaseComponent { items[index].focus() } + + static getParentFromElement(element) { + return getElementFromSelector(element) || element.parentNode + } + + 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) || + event.target.closest(SELECTOR_MENU))) : + !REGEXP_KEYDOWN.test(event.key)) { + return + } + + const isActive = this.classList.contains(CLASS_NAME_SHOW) + + if (!isActive && event.key === ESCAPE_KEY) { + return + } + + event.preventDefault() + event.stopPropagation() + + if (isDisabled(this)) { + return + } + + const getToggleButton = () => this.matches(SELECTOR_DATA_TOGGLE) ? this : SelectorEngine.prev(this, SELECTOR_DATA_TOGGLE)[0] + + if (event.key === ESCAPE_KEY) { + getToggleButton().focus() + Dropdown.clearMenus() + return + } + + if (!isActive && (event.key === ARROW_UP_KEY || event.key === ARROW_DOWN_KEY)) { + getToggleButton().click() + return + } + + if (!isActive || event.key === SPACE_KEY) { + Dropdown.clearMenus() + return + } + + Dropdown.selectMenuItem(Dropdown.getParentFromElement(this), event) + } } /** diff --git a/js/tests/unit/dropdown.spec.js b/js/tests/unit/dropdown.spec.js index ad51d487bf..03532256a3 100644 --- a/js/tests/unit/dropdown.spec.js +++ b/js/tests/unit/dropdown.spec.js @@ -1671,6 +1671,39 @@ describe('Dropdown', () => { done() }, 20) }) + + it('should propagate escape key events if dropdown is closed', done => { + fixtureEl.innerHTML = [ + '
', + ' ', + '
' + ] + + const parent = fixtureEl.querySelector('.parent') + const toggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + + const parentKeyHandler = jasmine.createSpy('parentKeyHandler') + + parent.addEventListener('keydown', parentKeyHandler) + parent.addEventListener('keyup', () => { + expect(parentKeyHandler).toHaveBeenCalled() + done() + }) + + const keydownEscape = createEvent('keydown', { bubbles: true }) + keydownEscape.key = 'Escape' + const keyupEscape = createEvent('keyup', { bubbles: true }) + keyupEscape.key = 'Escape' + + toggle.focus() + toggle.dispatchEvent(keydownEscape) + toggle.dispatchEvent(keyupEscape) + }) }) describe('jQueryInterface', () => {