Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion dist/css/bootstrap-grid.min.css.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/css/bootstrap-grid.rtl.min.css.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/css/bootstrap-reboot.min.css.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/css/bootstrap-reboot.rtl.min.css.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/css/bootstrap-utilities.min.css.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/css/bootstrap-utilities.rtl.min.css.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/css/bootstrap.min.css.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/css/bootstrap.rtl.min.css.map

Large diffs are not rendered by default.

12 changes: 11 additions & 1 deletion dist/js/bootstrap.bundle.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion dist/js/bootstrap.bundle.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/js/bootstrap.bundle.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/js/bootstrap.bundle.min.js.map

Large diffs are not rendered by default.

12 changes: 11 additions & 1 deletion dist/js/bootstrap.esm.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion dist/js/bootstrap.esm.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/js/bootstrap.esm.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/js/bootstrap.esm.min.js.map

Large diffs are not rendered by default.

12 changes: 11 additions & 1 deletion dist/js/bootstrap.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion dist/js/bootstrap.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/js/bootstrap.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/js/bootstrap.min.js.map

Large diffs are not rendered by default.

12 changes: 11 additions & 1 deletion js/dist/dropdown.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion js/dist/dropdown.js.map

Large diffs are not rendered by default.

22 changes: 18 additions & 4 deletions js/src/dropdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -419,12 +419,26 @@ class Dropdown extends BaseComponent {
const instance = Dropdown.getOrCreateInstance(getToggleButton)

if (isUpOrDownEvent) {
event.stopPropagation()
instance.show()
instance._selectMenuItem(event)
return
event.stopPropagation();

// Prevent reopening menus that should auto-close
const nextMenu = event.target.nextElementSibling;

// If navigating down into a submenu, open it (optional)
if (event.key === 'ArrowDown' && nextMenu && !nextMenu.classList.contains('show')) {
const dropdown = new Dropdown(
event.target.closest('.dropdown').querySelector('[data-bs-toggle="dropdown"]')
);
dropdown.show();
}

// Move focus within menu items
instance._selectMenuItem(event);

return;
}


if (instance._isShown()) { // else is escape and we check if it is shown
event.stopPropagation()
instance.hide()
Expand Down
68 changes: 68 additions & 0 deletions js/tests/unit/dropdown.submenu.keyboard.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import Dropdown from '../../src/dropdown.js'
import { getFixture, clearFixture } from '../helpers/fixture.js'

describe('Dropdown submenu keyboard behavior', () => {
beforeEach(() => {
document.body.innerHTML = ''
})

afterEach(() => {
clearFixture()
})

const getMenu = () => {
return getFixture(`
<ul class="dropdown-menu show" id="menu">
<li class="dropend">
<a class="dropdown-item dropdown-toggle" href="#" id="submenu1" data-bs-toggle="dropdown" data-bs-auto-close="outside">Submenu 1</a>
<ul class="dropdown-menu dropdown-menu-end" id="menu1">
<li class="dropdown-item">1A</li>
</ul>
</li>
<li class="dropend">
<a class="dropdown-item dropdown-toggle" href="#" id="submenu2" data-bs-toggle="dropdown" data-bs-auto-close="outside">Submenu 2</a>
<ul class="dropdown-menu dropdown-menu-end" id="menu2">
<li class="dropdown-item">2A</li>
</ul>
</li>
</ul>
`)
}

const pressKey = (element, key) => {
element.dispatchEvent(
new KeyboardEvent('keydown', { key, bubbles: true })
)
}

it('should close previous submenu when navigating upward', () => {
const fixture = getMenu()

const submenu1Toggle = fixture.querySelector('#submenu1')
const submenu2Toggle = fixture.querySelector('#submenu2')

// Create dropdown instances (prefix with _ to avoid "unused var" lint errors)
const _dd1 = Dropdown.getOrCreateInstance(submenu1Toggle)
const _dd2 = Dropdown.getOrCreateInstance(submenu2Toggle)

// Open submenu 1
submenu1Toggle.click()
const menu1 = fixture.querySelector('#menu1')
expect(menu1.classList.contains('show')).toBeTrue()

// Move down to submenu 2
submenu2Toggle.focus()
pressKey(submenu2Toggle, 'ArrowDown')
submenu2Toggle.click()

const menu2 = fixture.querySelector('#menu2')
expect(menu2.classList.contains('show')).toBeTrue()

// Navigate upward
pressKey(submenu2Toggle, 'ArrowUp')

// Expected behavior: submenu2 closed, submenu1 still open
expect(menu2.classList.contains('show')).toBeFalse()
expect(menu1.classList.contains('show')).toBeTrue()
})
})
39 changes: 39 additions & 0 deletions js/tests/visual/dropdown-submenu-keyboard.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Bootstrap Dropdown Submenu Keyboard Test</title>
<link rel="stylesheet" href="../../dist/css/bootstrap.css">
</head>
<body class="p-5">

<ul class="dropdown-menu show" style="position:static;">
<li class="dropend">
<a class="dropdown-item dropdown-toggle" data-bs-toggle="dropdown" data-bs-auto-close="outside" href="#" id="submenu1">Submenu 1</a>
<ul class="dropdown-menu dropdown-menu-end">
<li class="dropdown-item">1A</li>
<li class="dropdown-item">1B</li>
</ul>
</li>

<li class="dropend">
<a class="dropdown-item dropdown-toggle" data-bs-toggle="dropdown" data-bs-auto-close="outside" href="#" id="submenu2">Submenu 2</a>
<ul class="dropdown-menu dropdown-menu-end">
<li class="dropdown-item">2A</li>
<li class="dropdown-item">2B</li>
</ul>
</li>

<li class="dropend">
<a class="dropdown-item dropdown-toggle" data-bs-toggle="dropdown" data-bs-auto-close="outside" href="#" id="submenu3">Submenu 3</a>
<ul class="dropdown-menu dropdown-menu-end">
<li class="dropdown-item">3A</li>
<li class="dropdown-item">3B</li>
</ul>
</li>
</ul>

<script src="../../dist/js/bootstrap.bundle.js"></script>

</body>
</html>
63 changes: 63 additions & 0 deletions test-dropdown.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Bootstrap Dropdown Test</title>

<!-- Use your freshly built Bootstrap -->
<link rel="stylesheet" href="dist/css/bootstrap.css">
<script src="dist/js/bootstrap.bundle.js"></script>

</head>
<body class="p-5">

<ul class="nav">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button"
data-bs-toggle="dropdown" data-bs-auto-close="outside"
aria-expanded="false" id="menuDemo">
Demo
</a>

<ul class="dropdown-menu">

<li class="dropend">
<a class="dropdown-item dropdown-toggle" data-bs-toggle="dropdown"
data-bs-auto-close="outside" href="#">
Submenu 1
</a>
<ul class="dropdown-menu dropdown-menu-end">
<li class="dropdown-item">Menu Item 1A</li>
<li class="dropdown-item">Menu Item 1B</li>
</ul>
</li>

<li class="dropend">
<a class="dropdown-item dropdown-toggle" data-bs-toggle="dropdown"
data-bs-auto-close="outside" href="#">
Submenu 2
</a>
<ul class="dropdown-menu dropdown-menu-end">
<li class="dropdown-item">Menu Item 2A</li>
<li class="dropdown-item">Menu Item 2B</li>
<li class="dropdown-item">Menu Item 2C</li>
</ul>
</li>

<li class="dropend">
<a class="dropdown-item dropdown-toggle" data-bs-toggle="dropdown"
data-bs-auto-close="outside" href="#">
Submenu 3
</a>
<ul class="dropdown-menu dropdown-menu-end">
<li class="dropdown-item">Menu Item 3A</li>
<li class="dropdown-item">Menu Item 3B</li>
</ul>
</li>

</ul>
</li>
</ul>

</body>
</html>