From 0fabd404532c4b6d8be993ab37973b98a82452a2 Mon Sep 17 00:00:00 2001 From: Michael Tran Date: Tue, 19 Nov 2024 11:01:47 +1100 Subject: [PATCH] Dropdowns: Modified Dropdown's _parent to use a new method called 'closest', to allow dropdown menus to not necessarily be the sibling of the trigger button element. So instead of trying to use dropdown wrapper as the direct parent, it can be an ancestor. --- js/src/dom/selector-engine.js | 4 ++++ js/src/dropdown.js | 3 ++- js/tests/unit/dom/selector-engine.spec.js | 22 ++++++++++++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/js/src/dom/selector-engine.js b/js/src/dom/selector-engine.js index a4d81f3b9127..78db38fdd4e1 100644 --- a/js/src/dom/selector-engine.js +++ b/js/src/dom/selector-engine.js @@ -57,6 +57,10 @@ const SelectorEngine = { return parents }, + closest(element, selector) { + return Element.prototype.closest.call(element, selector) + }, + prev(element, selector) { let previous = element.previousElementSibling diff --git a/js/src/dropdown.js b/js/src/dropdown.js index 96094a3e6577..95e9797b4573 100644 --- a/js/src/dropdown.js +++ b/js/src/dropdown.js @@ -95,7 +95,8 @@ class Dropdown extends BaseComponent { super(element, config) this._popper = null - this._parent = this._element.parentNode // dropdown wrapper + const wrapperSelector = `:has(${SELECTOR_MENU})` + this._parent = SelectorEngine.closest(element, wrapperSelector) // dropdown wrapper // TODO: v6 revert #37011 & change markup https://getbootstrap.com/docs/5.3/forms/input-group/ this._menu = SelectorEngine.next(this._element, SELECTOR_MENU)[0] || SelectorEngine.prev(this._element, SELECTOR_MENU)[0] || diff --git a/js/tests/unit/dom/selector-engine.spec.js b/js/tests/unit/dom/selector-engine.spec.js index 95d9bf8ec9d8..aee5daff5be0 100644 --- a/js/tests/unit/dom/selector-engine.spec.js +++ b/js/tests/unit/dom/selector-engine.spec.js @@ -81,6 +81,28 @@ describe('SelectorEngine', () => { }) }) + describe('closest', () => { + it('should return one element when element with selector is the direct parent', () => { + const testId = 'test' + fixtureEl.innerHTML = `
` + + const element = fixtureEl.querySelector('#element') + const parent = fixtureEl.querySelector(`#${testId}`) + + expect(SelectorEngine.closest(element, `#${testId}`)).toEqual(parent) + }) + + it('should return one element when element with selector is an ancestor', () => { + const testId = 'test' + fixtureEl.innerHTML = `
` + + const element = fixtureEl.querySelector('#element') + const ancestor = fixtureEl.querySelector(`#${testId}`) + + expect(SelectorEngine.closest(element, `#${testId}`)).toEqual(ancestor) + }) + }) + describe('prev', () => { it('should return previous element', () => { fixtureEl.innerHTML = '
'