I'm building an assessable nav. Each menu item has a button which toggles the sub-menu.
I've got this working, but I'm having trouble removing the previous state when another button is clicked.
For example, if you click the "Food" button followed by the "Drink" button, the "Food" sub-menu remains visible.
As you can see, I've added a for loop at the start of my function. I was expecting this to remove any previous classes.
What am I doing wrong here and how can I get this working to reset the aria attribute too?
function dropdownButton() {
var buttons = document.querySelectorAll('.header__button');
for (var i = 0; i < buttons.length; i ) {
buttons[i].addEventListener('click', function () {
// Remove class
var removeClass = document.querySelectorAll('.header__button');
for (var j = 0; j < removeClass.length; j ) {
removeClass[j].classList.remove('.header__button--toggled');
}
// Toggle button class
this.classList.toggle('header__button--toggled');
// Toggle class on next sibling (.header__sub-menu)
this.nextElementSibling.classList.toggle('header__sub-menu--toggled');
// Toggle Aria
var ariaExpanded = this.getAttribute('aria-expanded');
ariaExpanded === 'true' ? ariaExpanded = 'false' : ariaExpanded = 'true';
this.setAttribute('aria-expanded', ariaExpanded);
});
}
}
dropdownButton();
.header__nav-items {
display: grid;
grid-auto-flow: column;
justify-content: start;
column-gap: 12px;
margin: 0;
padding: 0;
list-style: none;
}
.header__nav-item {
position: relative;
}
.header__sub-menu {
display: none;
position: absolute;
top: 24px;
left: 0;
background-color: #fff;
border: 1px solid #dedede;
padding: 12px;
list-style: none;
}
.header__sub-menu--toggled {
display: block;
}
<header class="header">
<nav>
<ul class="header__nav-items">
<li class="header__nav-item">
<button aria-expanded="false" aria-controls="food" class="header__button">Food</button>
<ul id="food" class="header__sub-menu">
<li><a href="/pizza">Pizza</a></li>
<li><a href="/fries">Fries</a></li>
<li><a href="/hamburger">Hamburger</a></li>
<li><a href="/waffles">Waffles</a></li>
</ul>
</li>
<li class="header__nav-item">
<button aria-expanded="false" aria-controls="drink" class="header__button">Drink</button>
<ul id="drink" class="header__sub-menu">
<li><a href="/coke">Coke</a></li>
<li><a href="/lemonade">Lemonade</a></li>
<li><a href="/beer">Beer</a></li>
<li><a href="/water">Water</a></li>
</ul>
</li>
</ul>
</nav>
</header>
<iframe name="sif1" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>
CodePudding user response:
As Kinglish mentioned you need to remove the .
inside removeClass[j].classList.remove('.header__button--toggled');
and also add
removeClass[j].nextElementSibling.classList.remove('header__sub-menu--toggled');
after it in the loop
you also dont need to use toggle
function since you already removed the class names, you can use add
class name instead
function dropdownButton() {
var buttons = document.querySelectorAll('.header__button');
for (var i = 0; i < buttons.length; i ) {
buttons[i].addEventListener('click', function () {
// Remove class
var removeClass = document.querySelectorAll('.header__button');
for (var j = 0; j < removeClass.length; j ) {
removeClass[j].classList.remove('header__button--toggled');
removeClass[j].nextElementSibling.classList.remove('header__sub-menu--toggled');
}
// Toggle button class
this.classList.add('header__button--toggled');
// Toggle class on next sibling (.header__sub-menu)
this.nextElementSibling.classList.add('header__sub-menu--toggled');
// Toggle Aria
var ariaExpanded = this.getAttribute('aria-expanded');
ariaExpanded === 'true' ? ariaExpanded = 'false' : ariaExpanded = 'true';
this.setAttribute('aria-expanded', ariaExpanded);
});
}
}
dropdownButton();
.header__nav-items {
display: grid;
grid-auto-flow: column;
justify-content: start;
column-gap: 12px;
margin: 0;
padding: 0;
list-style: none;
}
.header__nav-item {
position: relative;
}
.header__sub-menu {
display: none;
position: absolute;
top: 24px;
left: 0;
background-color: #fff;
border: 1px solid #dedede;
padding: 12px;
list-style: none;
}
.header__sub-menu--toggled {
display: block;
}
<header class="header">
<nav>
<ul class="header__nav-items">
<li class="header__nav-item">
<button aria-expanded="false" aria-controls="food" class="header__button">Food</button>
<ul id="food" class="header__sub-menu">
<li><a href="/pizza">Pizza</a></li>
<li><a href="/fries">Fries</a></li>
<li><a href="/hamburger">Hamburger</a></li>
<li><a href="/waffles">Waffles</a></li>
</ul>
</li>
<li class="header__nav-item">
<button aria-expanded="false" aria-controls="drink" class="header__button">Drink</button>
<ul id="drink" class="header__sub-menu">
<li><a href="/coke">Coke</a></li>
<li><a href="/lemonade">Lemonade</a></li>
<li><a href="/beer">Beer</a></li>
<li><a href="/water">Water</a></li>
</ul>
</li>
</ul>
</nav>
</header>
<iframe name="sif2" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>
based on your comment request if you want to keep the toggle functionality you can modify your code to check if the current expaned button is same as the clicked button. If so to add the class to it
let me know if the modifcation is not clear but basically by var ariaExpanded = this.getAttribute('aria-expanded') == 'true';
we know if the current button is already expaned so that we can remove the expaned property from it as well otherwise we add to it in the if statment below
function dropdownButton() {
var buttons = document.querySelectorAll('.header__button');
for (var i = 0; i < buttons.length; i ) {
buttons[i].addEventListener('click', function () {
// Remove class
var ariaExpanded = this.getAttribute('aria-expanded') == 'true';
var removeClass = document.querySelectorAll('.header__button');
for (var j = 0; j < removeClass.length; j ) {
removeClass[j].classList.remove('header__button--toggled');
removeClass[j].nextElementSibling.classList.remove('header__sub-menu--toggled');
removeClass[j].setAttribute('aria-expanded', 'false');
}
if(!ariaExpanded){
// Toggle button class
this.classList.add('header__button--toggled');
// Toggle class on next sibling (.header__sub-menu)
this.nextElementSibling.classList.add('header__sub-menu--toggled');
ariaExpanded = 'true';
}else{
ariaExpanded = 'false'
}
this.setAttribute('aria-expanded', ariaExpanded);
});
}
}
dropdownButton();
.header__nav-items {
display: grid;
grid-auto-flow: column;
justify-content: start;
column-gap: 12px;
margin: 0;
padding: 0;
list-style: none;
}
.header__nav-item {
position: relative;
}
.header__sub-menu {
display: none;
position: absolute;
top: 24px;
left: 0;
background-color: #fff;
border: 1px solid #dedede;
padding: 12px;
list-style: none;
}
.header__sub-menu--toggled {
display: block;
}
<header class="header">
<nav>
<ul class="header__nav-items">
<li class="header__nav-item">
<button aria-expanded="false" aria-controls="food" class="header__button">Food</button>
<ul id="food" class="header__sub-menu">
<li><a href="/pizza">Pizza</a></li>
<li><a href="/fries">Fries</a></li>
<li><a href="/hamburger">Hamburger</a></li>
<li><a href="/waffles">Waffles</a></li>
</ul>
</li>
<li class="header__nav-item">
<button aria-expanded="false" aria-controls="drink" class="header__button">Drink</button>
<ul id="drink" class="header__sub-menu">
<li><a href="/coke">Coke</a></li>
<li><a href="/lemonade">Lemonade</a></li>
<li><a href="/beer">Beer</a></li>
<li><a href="/water">Water</a></li>
</ul>
</li>
</ul>
</nav>
</header>
<iframe name="sif3" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>
CodePudding user response:
Very quick post; based on your requirement and the code.
- Fix the selectors to have periods when needed
- Fix the aria/remove if not the button clicked
- Aria adjust the code to simplify the condition to toggle the true/false strings
- Smaller number of variables using
.forEach()
- not a performance issue with this small a number
function setup() {
const subMenu = 'header__sub-menu--toggled';
const buttonToggle = 'header__button--toggled';
document.querySelectorAll('.header__button')
.forEach(function(button) {
button.addEventListener('click', function() {
// is it me?
let hasClass = this.classList.contains(buttonToggle);
if (!hasClass) {
// Remove class
document.querySelector(".header__nav-items")
.querySelectorAll('.' buttonToggle)
.forEach(function(navItem) {
navItem.nextElementSibling.classList.remove(subMenu);
navItem.setAttribute('aria-expanded', "false");
navItem.classList.remove(buttonToggle);
});
}
// Toggle button class
this.classList.toggle(buttonToggle);
// Toggle class on next sibling - subMenu
this.nextElementSibling.classList.toggle(subMenu);
// Toggle Aria
const ariaExpanded = this.getAttribute('aria-expanded') === 'true' ? 'false' : 'true';
this.setAttribute('aria-expanded', ariaExpanded);
});
});
}
setup();
.header__nav-items {
display: grid;
grid-auto-flow: column;
justify-content: start;
column-gap: 12px;
margin: 0;
padding: 0;
list-style: none;
}
.header__nav-item {
position: relative;
}
.header__sub-menu {
display: none;
position: absolute;
top: 24px;
left: 0;
background-color: #fff;
border: 1px solid #dedede;
padding: 12px;
list-style: none;
}
.header__sub-menu--toggled {
display: block;
}
<header class="header">
<nav>
<ul class="header__nav-items">
<li class="header__nav-item">
<button aria-expanded="false" aria-controls="food" class="header__button">Food</button>
<ul id="food" class="header__sub-menu">
<li><a href="/pizza">Pizza</a></li>
<li><a href="/fries">Fries</a></li>
<li><a href="/hamburger">Hamburger</a></li>
<li><a href="/waffles">Waffles</a></li>
</ul>
</li>
<li class="header__nav-item">
<button aria-expanded="false" aria-controls="drink" class="header__button">Drink</button>
<ul id="drink" class="header__sub-menu">
<li><a href="/coke">Coke</a></li>
<li><a href="/lemonade">Lemonade</a></li>
<li><a href="/beer">Beer</a></li>
<li><a href="/water">Water</a></li>
</ul>
</li>
</ul>
</nav>
</header>
<iframe name="sif4" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>