Search code examples
javascripthtmlcssdomaccessibility

Why does setAttribute 'false' work on another button but not on self in Javascript?


I have a menu that uses buttons and vanilla Javascript. When a button is clicked, it adds the class opened to the button and changes aria-expanded="false" to aria-expanded="true".

If you click another button, the original button's attribute reverts to aria-expanded="false" and the class opened is removed.

However, if you click the original button a second, consecutive time, whilst the class opened is removed, aria-expanded="true" does not change back to aria-expanded="false" and I am really struggling to understand why.

Here is the script and a mini-menu with minimum CSS (obviously, the browser's inspector is required to view the changes). NB The original JS below was written for a multi-level menu, obviously not used in this instance.

for (const selector of [".btn-level-1", ".btn-level-2", ".btn-level-3", ".btn-level-4", ".btn-level-5", ".btn-level-6",]) {
    const buttons = [...document.querySelectorAll(selector)];
    for (const button of buttons) {
        button.addEventListener('click', () => {
        buttons.filter(b => b !== button).forEach(b => {
            b.classList.remove('opened'); // this works on self and also if other button is clicked
            b.setAttribute("aria-expanded", "false"); //this doesn't work on self
        });
        button.classList.toggle('opened');
        button.setAttribute("aria-expanded", "true");
    })
}}
button + ul {
  display: none;
}
button.opened + ul {
  display: block;
}
li {
  padding: 10px;
  list-style: none;
}
.sidenav {
  width: 560px;
  background: #eee;
}
.visually-hidden { 
  border: 0;
  padding: 0;
  margin: 0;
  position: absolute;
  height: 1px; 
  width: 0;
  overflow: hidden;
  white-space: nowrap;
}
.menu-toggle {
  height: 44px;
  width: 44px;
  display: inline-block;
  line-height: 44px;
  font-size: 25px;
  text-align: center;
  text-decoration: none;
  margin: 0;
}
.icon {
  padding-left: 1rem;
}
<nav aria-label="Main Navigation">
    <ul>
        <li class="has-submenu">
            <a href="https://example.com/some-category">Some Category</a>
            <button class="btn-level-1 menu-toggle" type="button" aria-haspopup="true" aria-expanded="false">
            <span class="christmas-red"></span>
            <span class="icon">+</span>
            <span class="visually-hidden">
            <span class="toggleText">show submenu</span> for "Some Category"</span>
            </button>
            <ul>
                <li><a href="https://example.com/some-category/item-1">Some Category Item 1</a></li>
                <li><a href="https://example.com/some-category/item-2">Some Category Item 2</a></li>
            </ul>
        </li>
        <li class="has-submenu">
            <a href="https://example.com/some-category">Another Category</a>
            <button class="btn-level-1 menu-toggle"  type="button" aria-haspopup="true" aria-expanded="false">
            <span class="christmas-red"></span>
            <span class="icon">+</span>
            <span class="visually-hidden">
            <span class="toggleText">show submenu</span> for "Some Category"</span>
            </button>
            <ul>
                <li><a href="https://example.com/another-category/item-1">Some Category Item 1</a></li>
                <li><a href="https://example.com/another-category/item-2">Some Category Item 2</a></li>
            </ul>
        </li>
    </ul>
</nav>


Solution

  • After toggling the opened class on button you need to check whether you actually added or removed it when setting the aria-expanded attribute. You can do this using classList.contains. Updated working example:

    for (const selector of [".btn-level-1", ".btn-level-2", ".btn-level-3", ".btn-level-4", ".btn-level-5", ".btn-level-6",]) {
        const buttons = [...document.querySelectorAll(selector)];
        for (const button of buttons) {
            button.addEventListener('click', () => {
            buttons.filter(b => b !== button).forEach(b => {
                b.classList.remove('opened');
                b.setAttribute("aria-expanded", "false");
            });
            button.classList.toggle('opened');
            // check which aria-expanded value we should set below
            let ariaExpanded = button.classList.contains('opened').toString();
            button.setAttribute("aria-expanded", ariaExpanded);
        })
    }}
    button+ul{
      display: none;
    }
    button.opened+ul{
      display: block;
    }
    li{
      padding: 10px;
      list-style: none;
    }
    .sidenav {
      width: 560px;
      background: #eee;
    }
    .visually-hidden { 
      border: 0;
      padding: 0;
      margin: 0;
      position: absolute;
      height: 1px; 
      width: 0;
      overflow: hidden;
      white-space: nowrap;
    }
    .menu-toggle{
      height: 44px;
      width: 44px;
      display: inline-block;
      line-height: 44px;
      font-size: 25px;
      text-align: center;
      text-decoration: none;
      margin: 0;
    }
    .icon {
      padding-left: 1rem;
    }
    <nav aria-label="Main Navigation">
        <ul>
            <li class="has-submenu">
                <a href="https://example.com/some-category">Some Category</a>
                <button class="btn-level-1 menu-toggle" type="button" aria-haspopup="true" aria-expanded="false">
                <span class="christmas-red"></span>
                <span class="icon">+</span>
                <span class="visually-hidden">
                <span class="toggleText">show submenu</span> for "Some Category"</span>
                </button>
                <ul>
                    <li><a href="https://example.com/some-category/item-1">Some Category Item 1</a></li>
                    <li><a href="https://example.com/some-category/item-2">Some Category Item 2</a></li>
                </ul>
            </li>
            <li class="has-submenu">
                <a href="https://example.com/some-category">Another Category</a>
                <button class="btn-level-1 menu-toggle"  type="button" aria-haspopup="true" aria-expanded="false">
                <span class="christmas-red"></span>
                <span class="icon">+</span>
                <span class="visually-hidden">
                <span class="toggleText">show submenu</span> for "Some Category"</span>
                </button>
                <ul>
                    <li><a href="https://example.com/another-category/item-1">Some Category Item 1</a></li>
                    <li><a href="https://example.com/another-category/item-2">Some Category Item 2</a></li>
                </ul>
            </li>
        </ul>
    </nav>