Compare commits

...

7 Commits

Author SHA1 Message Date
alpadev
91cd2c687c
Merge 903707da41febfcddfa8c89db48d80b5dbf49a93 into 380a1d738b221fecc964260add053997399be4d4 2025-09-27 16:38:36 +01:00
alpadev
903707da41
Merge branch 'main' into fix-dropdown-focus 2025-09-02 02:46:00 +02:00
alpadev
a9b3acc165 refactor: don't use optional chaining
Because of transpiling using optional chaining is suboptimal
2025-09-01 17:02:07 +02:00
alpadev
d9c14fb359 refactor: Focus dropdown after hidden event
Allow users to move the focus manually in the hidden event without interfering.
2025-09-01 00:34:22 +02:00
alpadev
200d26e792 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.
2025-09-01 00:28:36 +02:00
alpadev
a7eaa6a8f3 feat(a11y): focus dropdown trigger after item has been selected 2025-08-31 10:41:01 +02:00
alpadev
2960896ce4 test: focus dropdown trigger after item has been selected 2025-08-31 10:39:56 +02:00
2 changed files with 98 additions and 0 deletions

View File

@ -207,6 +207,10 @@ class Dropdown extends BaseComponent {
this._element.setAttribute('aria-expanded', 'false')
Manipulator.removeDataAttribute(this._menu, 'popper')
EventHandler.trigger(this._element, EVENT_HIDDEN, relatedTarget)
if (this._menu && this._menu.contains(document.activeElement)) {
this._element.focus()
}
}
_getConfig(config) {

View File

@ -2097,6 +2097,100 @@ describe('Dropdown', () => {
})
})
it('should focus the dropdown trigger when an item is selected (and the dropdown is hidden)', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = [
'<div class="dropdown">',
' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>',
' <div class="dropdown-menu">',
' <a class="dropdown-item" href="#">Some Item</a>',
' </div>',
'</div>'
].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 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 = [
'<button class="focus-target"></button>',
'<div class="dropdown">',
' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>',
' <div class="dropdown-menu">',
' <a class="dropdown-item" href="#">Some Item</a>',
' </div>',
'</div>'
].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 = [
'<div class="dropdown">',
' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>',
' <div class="dropdown-menu">',
' <a class="dropdown-item" href="#">Some Item</a>',
' </div>',
'</div>'
].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 = [