Skip to content

Commit fb6aa30

Browse files
committed
Skip disabled submenu triggers in keyboard navigation
Apply the same :not(.disabled):not(:disabled) filter used for direct menu items when collecting submenu triggers, so arrow keys do not land on disabled nested menu entries.
1 parent 8a0db38 commit fb6aa30

2 files changed

Lines changed: 47 additions & 1 deletion

File tree

js/src/menu.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -787,8 +787,9 @@ class Menu extends BaseComponent {
787787
_getItemsInMenu(menu) {
788788
// Items may be direct children of `.menu`, or submenu triggers nested as
789789
// `.menu > .submenu > .menu-item` (see menu-submenu visual tests / docs).
790+
// Always use SELECTOR_VISIBLE_ITEMS so disabled triggers are skipped too.
790791
return SelectorEngine.find(
791-
`:scope > ${SELECTOR_VISIBLE_ITEMS}, :scope > ${SELECTOR_SUBMENU_TOGGLE}`,
792+
`:scope > ${SELECTOR_VISIBLE_ITEMS}, :scope > ${SELECTOR_SUBMENU} > ${SELECTOR_VISIBLE_ITEMS}`,
792793
menu
793794
).filter(element => isVisible(element))
794795
}

js/tests/unit/menu.spec.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2862,6 +2862,51 @@ describe('Menu', () => {
28622862
})
28632863
})
28642864

2865+
it('should skip disabled submenu triggers during keyboard navigation', () => {
2866+
return new Promise(resolve => {
2867+
fixtureEl.innerHTML = [
2868+
'<div>',
2869+
' <button class="btn" data-bs-toggle="menu">Menu</button>',
2870+
' <div class="menu">',
2871+
' <div class="submenu" id="submenu1">',
2872+
' <button class="menu-item" type="button">Submenu 1</button>',
2873+
' <div class="menu"><a class="menu-item" href="#">Action 1</a></div>',
2874+
' </div>',
2875+
' <div class="submenu" id="submenuDisabled">',
2876+
' <button class="menu-item" type="button" disabled>Disabled submenu</button>',
2877+
' <div class="menu"><a class="menu-item" href="#">Hidden</a></div>',
2878+
' </div>',
2879+
' <div class="submenu" id="submenu2">',
2880+
' <button class="menu-item" type="button">Submenu 2</button>',
2881+
' <div class="menu"><a class="menu-item" href="#">Action 2</a></div>',
2882+
' </div>',
2883+
' </div>',
2884+
'</div>'
2885+
].join('')
2886+
2887+
const btnMenu = fixtureEl.querySelector('[data-bs-toggle="menu"]')
2888+
const submenu1Trigger = fixtureEl.querySelector('#submenu1 > .menu-item')
2889+
const submenu2Trigger = fixtureEl.querySelector('#submenu2 > .menu-item')
2890+
const disabledTrigger = fixtureEl.querySelector('#submenuDisabled > .menu-item')
2891+
2892+
btnMenu.addEventListener('shown.bs.menu', () => {
2893+
submenu1Trigger.focus()
2894+
2895+
const keydown = createEvent('keydown', { bubbles: true })
2896+
keydown.key = 'ArrowDown'
2897+
submenu1Trigger.dispatchEvent(keydown)
2898+
2899+
expect(document.activeElement).toEqual(submenu2Trigger)
2900+
expect(document.activeElement).not.toEqual(disabledTrigger)
2901+
resolve()
2902+
})
2903+
2904+
// eslint-disable-next-line no-new
2905+
new Menu(btnMenu)
2906+
btnMenu.click()
2907+
})
2908+
})
2909+
28652910
it('should open submenu with ArrowRight key', () => {
28662911
return new Promise(resolve => {
28672912
fixtureEl.innerHTML = [

0 commit comments

Comments
 (0)