From 2960896ce4c98cdc7a1503c030be45c22fe9d768 Mon Sep 17 00:00:00 2001
From: alpadev <2838324+alpadev@users.noreply.github.com>
Date: Sun, 31 Aug 2025 08:43:30 +0200
Subject: [PATCH 1/5] test: focus dropdown trigger after item has been selected
---
js/tests/unit/dropdown.spec.js | 28 ++++++++++++++++++++++++++++
1 file changed, 28 insertions(+)
diff --git a/js/tests/unit/dropdown.spec.js b/js/tests/unit/dropdown.spec.js
index 63ae4bd102bc..5011a8fc6f94 100644
--- a/js/tests/unit/dropdown.spec.js
+++ b/js/tests/unit/dropdown.spec.js
@@ -2097,6 +2097,34 @@ describe('Dropdown', () => {
})
})
+ it('should focus the dropdown trigger when an item is selected (and the dropdown is hidden)', () => {
+ return new Promise(resolve => {
+ fixtureEl.innerHTML = [
+ '
',
+ ' ',
+ ' ',
+ '
'
+ ].join('')
+
+ const toggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
+ const item = fixtureEl.querySelector('.dropdown-item')
+
+ toggle.addEventListener('shown.bs.dropdown', () => {
+ item.focus()
+ item.click()
+ })
+
+ toggle.addEventListener('hidden.bs.dropdown', () => setTimeout(() => {
+ expect(document.activeElement).toEqual(toggle)
+ resolve()
+ }))
+
+ toggle.click()
+ })
+ })
+
it('should close dropdown (only) by clicking inside the dropdown menu when it has data-attribute `data-bs-auto-close="inside"`', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = [
From a7eaa6a8f394a832851d3fd89d7f978f6dc15317 Mon Sep 17 00:00:00 2001
From: alpadev <2838324+alpadev@users.noreply.github.com>
Date: Sun, 31 Aug 2025 08:46:22 +0200
Subject: [PATCH 2/5] feat(a11y): focus dropdown trigger after item has been
selected
---
js/src/dropdown.js | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/js/src/dropdown.js b/js/src/dropdown.js
index 96094a3e6577..a9f22a617abe 100644
--- a/js/src/dropdown.js
+++ b/js/src/dropdown.js
@@ -202,6 +202,10 @@ class Dropdown extends BaseComponent {
this._popper.destroy()
}
+ if (this._menu.contains(document.activeElement)) {
+ this._element.focus()
+ }
+
this._menu.classList.remove(CLASS_NAME_SHOW)
this._element.classList.remove(CLASS_NAME_SHOW)
this._element.setAttribute('aria-expanded', 'false')
From 200d26e7923f8e621e811ea2634a3d03e2059e03 Mon Sep 17 00:00:00 2001
From: alpadev <2838324+alpadev@users.noreply.github.com>
Date: Mon, 1 Sep 2025 00:28:36 +0200
Subject: [PATCH 3/5] test: add additional tests to the dropdown spec
Allow users to move the focus in the hidden event, without interfering.
Make sure if dispose is called in the hidden event, no error is thrown.
---
js/tests/unit/dropdown.spec.js | 66 ++++++++++++++++++++++++++++++++++
1 file changed, 66 insertions(+)
diff --git a/js/tests/unit/dropdown.spec.js b/js/tests/unit/dropdown.spec.js
index 5011a8fc6f94..225071760377 100644
--- a/js/tests/unit/dropdown.spec.js
+++ b/js/tests/unit/dropdown.spec.js
@@ -2125,6 +2125,72 @@ describe('Dropdown', () => {
})
})
+ it('should not focus the dropdown trigger when an item is selected and something else is focused in the hidden event', () => {
+ return new Promise(resolve => {
+ fixtureEl.innerHTML = [
+ '',
+ '',
+ ' ',
+ ' ',
+ '
'
+ ].join('')
+
+ const toggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
+ const item = fixtureEl.querySelector('.dropdown-item')
+ const focusTarget = fixtureEl.querySelector('.focus-target')
+
+ toggle.addEventListener('shown.bs.dropdown', () => {
+ item.focus()
+ item.click()
+ })
+
+ toggle.addEventListener('hidden.bs.dropdown', () => {
+ focusTarget.focus()
+ setTimeout(() => {
+ expect(document.activeElement).toEqual(focusTarget)
+ expect(document.activeElement).not.toEqual(toggle)
+ resolve()
+ })
+ })
+
+ toggle.click()
+ })
+ })
+
+ it('should not throw an error when the dropdown was disposed in the hidden event', () => {
+ return new Promise(resolve => {
+ fixtureEl.innerHTML = [
+ '',
+ ' ',
+ ' ',
+ '
'
+ ].join('')
+
+ const toggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
+ const item = fixtureEl.querySelector('.dropdown-item')
+
+ toggle.addEventListener('shown.bs.dropdown', () => {
+ item.click()
+ })
+
+ toggle.addEventListener('hidden.bs.dropdown', () => {
+ const dropdown = Dropdown.getInstance(toggle)
+ dropdown.dispose()
+
+ setTimeout(() => {
+ expect(dropdown._menu).toBeNull()
+ resolve()
+ })
+ })
+
+ toggle.click()
+ })
+ })
+
it('should close dropdown (only) by clicking inside the dropdown menu when it has data-attribute `data-bs-auto-close="inside"`', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = [
From d9c14fb359775668a26cbdc4d1b1cba86e19378a Mon Sep 17 00:00:00 2001
From: alpadev <2838324+alpadev@users.noreply.github.com>
Date: Mon, 1 Sep 2025 00:34:22 +0200
Subject: [PATCH 4/5] refactor: Focus dropdown after hidden event
Allow users to move the focus manually in the hidden event without interfering.
---
js/src/dropdown.js | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/js/src/dropdown.js b/js/src/dropdown.js
index a9f22a617abe..e0715408efc9 100644
--- a/js/src/dropdown.js
+++ b/js/src/dropdown.js
@@ -202,15 +202,15 @@ class Dropdown extends BaseComponent {
this._popper.destroy()
}
- if (this._menu.contains(document.activeElement)) {
- this._element.focus()
- }
-
this._menu.classList.remove(CLASS_NAME_SHOW)
this._element.classList.remove(CLASS_NAME_SHOW)
this._element.setAttribute('aria-expanded', 'false')
Manipulator.removeDataAttribute(this._menu, 'popper')
EventHandler.trigger(this._element, EVENT_HIDDEN, relatedTarget)
+
+ if (this._menu?.contains(document.activeElement)) {
+ this._element?.focus()
+ }
}
_getConfig(config) {
From a9b3acc165314e36d5840264132d46af2ae9a30a Mon Sep 17 00:00:00 2001
From: alpadev <2838324+alpadev@users.noreply.github.com>
Date: Mon, 1 Sep 2025 17:02:07 +0200
Subject: [PATCH 5/5] refactor: don't use optional chaining
Because of transpiling using optional chaining is suboptimal
---
js/src/dropdown.js | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/js/src/dropdown.js b/js/src/dropdown.js
index e0715408efc9..7623e790af3e 100644
--- a/js/src/dropdown.js
+++ b/js/src/dropdown.js
@@ -208,8 +208,8 @@ class Dropdown extends BaseComponent {
Manipulator.removeDataAttribute(this._menu, 'popper')
EventHandler.trigger(this._element, EVENT_HIDDEN, relatedTarget)
- if (this._menu?.contains(document.activeElement)) {
- this._element?.focus()
+ if (this._menu && this._menu.contains(document.activeElement)) {
+ this._element.focus()
}
}