I have a simple dropdown and I want to have the possibility that the user can navigate via keyboard arrows (up/down) in the dropdown.
Here is the code:
function myFunction() {
document.getElementById("myDropdown").classList.toggle("show");
}
window.onclick = function(event) {
if (!event.target.matches('.dropbtn')) {
var dropdowns = document.getElementsByClassName("dropdown-content");
var i;
for (i = 0; i < dropdowns.length; i++) {
var openDropdown = dropdowns[i];
if (openDropdown.classList.contains('show')) {
openDropdown.classList.remove('show');
}
}
}
}
.dropbtn {
background-color: #3498DB;
color: white;
padding: 16px;
font-size: 16px;
border: none;
cursor: pointer;
}
.dropbtn:hover, .dropbtn:focus {
background-color: #2980B9;
}
.dropdown {
position: relative;
display: inline-block;
}
.dropdown-content {
display: none;
position: absolute;
background-color: #f1f1f1;
min-width: 160px;
overflow: auto;
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
z-index: 1;
}
.dropdown-content a {
color: black;
padding: 12px 16px;
text-decoration: none;
display: block;
}
.dropdown a:hover {background-color: #ddd;}
.show {display: block;}
<div class="dropdown">
<button onclick="myFunction()" class="dropbtn">Dropdown</button>
<div id="myDropdown" class="dropdown-content">
<a href="#home">Home</a>
<a href="#about">About</a>
<a href="#contact">Contact</a>
</div>
</div>
I have a simple dropdown and I want to have the possibility that the user can navigate via keyboard arrows (up/down) in the dropdown.
Here is the code:
function myFunction() {
document.getElementById("myDropdown").classList.toggle("show");
}
window.onclick = function(event) {
if (!event.target.matches('.dropbtn')) {
var dropdowns = document.getElementsByClassName("dropdown-content");
var i;
for (i = 0; i < dropdowns.length; i++) {
var openDropdown = dropdowns[i];
if (openDropdown.classList.contains('show')) {
openDropdown.classList.remove('show');
}
}
}
}
.dropbtn {
background-color: #3498DB;
color: white;
padding: 16px;
font-size: 16px;
border: none;
cursor: pointer;
}
.dropbtn:hover, .dropbtn:focus {
background-color: #2980B9;
}
.dropdown {
position: relative;
display: inline-block;
}
.dropdown-content {
display: none;
position: absolute;
background-color: #f1f1f1;
min-width: 160px;
overflow: auto;
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
z-index: 1;
}
.dropdown-content a {
color: black;
padding: 12px 16px;
text-decoration: none;
display: block;
}
.dropdown a:hover {background-color: #ddd;}
.show {display: block;}
<div class="dropdown">
<button onclick="myFunction()" class="dropbtn">Dropdown</button>
<div id="myDropdown" class="dropdown-content">
<a href="#home">Home</a>
<a href="#about">About</a>
<a href="#contact">Contact</a>
</div>
</div>
This works fine when I am using tab
, but I want to use arrows (up/down), can anybody try to help me with this?
5 Answers
Reset to default 2just add keydown event on dropdown ,play with the focus and cancel the scrolling
var pos = 0;
var maxpos = 0;
function myFunction() {
pos=0;
document.getElementById("myDropdown").classList.toggle("show");
maxpos = $("#myDropdown a").length - 1;
var x = window.scrollX, y = window.scrollY;
$("#myDropdown a").eq(pos).trigger("focus");
window.scrollTo(x, y);
}
$("#myDropdown, .dropbtn").on("keydown", function(e){
if(e.which == 40){//down
pos = pos == maxpos ? 0 : pos + 1;
$("#myDropdown a").eq(pos).trigger("focus");
}
if(e.which == 38){//up
pos = pos == 0 ? maxpos : pos - 1;
$("#myDropdown a").eq(pos).trigger("focus");
}
return false;//cancel scrolling
});
window.onclick = function(event) {
if (!event.target.matches('.dropbtn')) {
var dropdowns = document.getElementsByClassName("dropdown-content");
var i;
for (i = 0; i < dropdowns.length; i++) {
var openDropdown = dropdowns[i];
if (openDropdown.classList.contains('show')) {
openDropdown.classList.remove('show');
}
}
}
}
.dropbtn {
background-color: #3498DB;
color: white;
padding: 16px;
font-size: 16px;
border: none;
cursor: pointer;
}
.dropbtn:hover, .dropbtn:focus {
background-color: #2980B9;
}
.dropdown {
position: relative;
display: inline-block;
}
.dropdown-content {
display: none;
position: absolute;
background-color: #f1f1f1;
min-width: 160px;
overflow: auto;
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
z-index: 1;
}
.dropdown-content a {
color: black;
padding: 12px 16px;
text-decoration: none;
display: block;
}
.dropdown a:hover, .dropdown a:focus{
background-color: #ddd;
}
.show {display: block;}
<script src="https://cdnjs.cloudflare./ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class="dropdown">
<button onclick="myFunction()" class="dropbtn">Dropdown</button>
<div id="myDropdown" class="dropdown-content">
<a href="#home">Home</a>
<a href="#about">About</a>
<a href="#contact">Contact</a>
</div>
</div>
I would add the listeners to the dropdown and its content, rather than the entire page.
You can call focus()
on the previous/next child by storing a selected index using the drop-down's dataset
property.
const
ARROW_UP = 38,
ARROW_DOWN = 40;
const mod = (n, m) => ((n % m) + m) % m;
const navigateList = e => {
const
dropdown = e.target.closest('.dropdown'),
selectedIndex = parseInt(dropdown.dataset.selectedIndex, 10),
children = dropdown.querySelectorAll('.dropdown-content a');
switch (e.which) {
case ARROW_UP:
focusDropdownChild(dropdown, mod(selectedIndex - 1, children.length));
break;
case ARROW_DOWN:
focusDropdownChild(dropdown, mod(selectedIndex + 1, children.length));
break;
}
};
const toggleShow = (e) => {
const
button = e.target,
dropdown = button.closest('.dropdown'),
content = dropdown.querySelector('.dropdown-content');
content.classList.toggle('show');
focusDropdownChild(dropdown, 0);
};
const navigate = e => {
const
item = e.target,
content = item.closest('.dropdown-content');
console.log(`Navigating to... "${item.textContent}"`);
content.classList.toggle('show');
e.preventDefault();
e.stopImmediatePropagation();
};
const focusDropdownChild = (dropdown, index) => {
const children = dropdown.querySelectorAll('.dropdown-content a');
children.forEach(child => child.classList.remove('dropdown-item-focus'));
dropdown.dataset.selectedIndex = index;
children[index].focus();
children[index].classList.add('dropdown-item-focus');
};
const connectListeners = (dropdown) => {
const
button = dropdown.querySelector('.dropdown-button'),
content = dropdown.querySelector('.dropdown-content'),
children = content.querySelectorAll('a');
button.addEventListener('click', toggleShow);
content.addEventListener('keydown', navigateList);
children.forEach(child => child.addEventListener('click', navigate));
};
const disconnectListeners = (dropdown) => {
const
button = dropdown.querySelector('.dropdown-button'),
content = dropdown.querySelector('.dropdown-content'),
children = content.querySelectorAll('a');
button.removeEventListener('click', toggleShow);
content.removeEventListener('keydown', navigateList);
children.forEach(child => child.removeEventListener('click', navigate));
};
document.querySelectorAll('.dropdown').forEach(connectListeners);
.dropdown-button {
background-color: #3498DB;
color: white;
padding: 16px;
font-size: 16px;
border: none;
cursor: pointer;
}
.dropdown-button:hover,
.dropdown-button:focus {
background-color: #2980B9;
}
.dropdown {
position: relative;
display: inline-block;
}
.dropdown-content {
display: none;
position: absolute;
background-color: #f1f1f1;
min-width: 160px;
overflow: auto;
box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2);
z-index: 1;
}
.dropdown-content a {
color: black;
padding: 12px 16px;
text-decoration: none;
display: block;
}
.dropdown a:hover {
background-color: #ddd;
}
.show {
display: block
}
.dropdown-item-focus {
background: yellow;
}
<div class="dropdown">
<button class="dropdown-button">Dropdown</button>
<div class="dropdown-content">
<a href="#home">Home</a>
<a href="#about">About</a>
<a href="#contact">Contact</a>
</div>
</div>
You could write a function that listens for keystrokes (when the dropdown is open), determine the key and change the focus accordingly.
function myFunction() {
let i = 0; // iterate over children elements inside dropdown
const dropdown = document.getElementById("myDropdown");
const childs = dropdown.children; // get all dropdown elements
dropdown.classList.toggle("show");
// attach keyboard events
window.addEventListener("keydown", event => {
switch(event.code) {
case "ArrowDown":
for (let c of childs)
c.classList.remove('dropbtn-selected')
childs[Math.abs(i) % childs.length].classList.add('dropbtn-selected');
i++;
break;
case "ArrowUp":
for (let c of childs)
c.classList.remove('dropbtn-selected')
childs[Math.abs(i) % childs.length].classList.add('dropbtn-selected');
i--;
break;
}
if (event.isComposing || event.keyCode === 229) {
return;
}
});
}
.dropbtn {
background-color: #3498DB;
color: white;
padding: 16px;
font-size: 16px;
border: none;
cursor: pointer;
}
.dropbtn-selected {
background-color: #2980B9;
}
.dropbtn:hover, .dropbtn:focus {
background-color: #2980B9;
}
.dropdown {
position: relative;
display: inline-block;
}
.dropdown-content {
display: none;
position: absolute;
background-color: #f1f1f1;
min-width: 160px;
overflow: auto;
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
z-index: 1;
}
.dropdown-content a {
color: black;
padding: 12px 16px;
text-decoration: none;
display: block;
}
.dropdown a:hover {background-color: #ddd;}
.show {display: block;}
<div class="dropdown">
<button onclick="myFunction()" class="dropbtn">Dropdown</button>
<div id="myDropdown" class="dropdown-content">
<a href="#home">Home</a>
<a href="#about">About</a>
<a href="#contact">Contact</a>
</div>
</div>
Don't forget to remove eventListener as soon as you select a dropdown option and close
Mihalma,
So, by scrounging around on the internet (keywords "HTML5 keyboard navigable dropdown"), I found this: https://dev.to/emmabostian/creating-a-custom-accessible-drop-down-3gmo
You can use a element around your dropdown.
<select>
<option value="">Value 1</option>
<option value="">Value 2</option>
<option value="">Value 3</option>
<option value="">Value 4</option>
</select>
Sadly, this makes for some pretty trashy looking styling, which is what the rest of the article is for.