javascripthtmltwitter-bootstrapdrop-down-menubootstrap-5

How to allow up and down arrow keys to wrap in menu


In bootstrap 5 menu, pressing up arrow key in first item or down allow key in last item does nothing.

How to make up arrow to move to last item in menu if pressed in first item and down arrow to move to first item if pressed in last item just like in normal windows desktop application menus?

To reproduce open page

<div class="btn-group">
  <button type="button" class="btn btn-danger dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
    Action
  </button>
  <ul class="dropdown-menu">
    <li><a class="dropdown-item" href="#"><u>A</u>ction</a></li>
    <li><a class="dropdown-item" href="#">A<u>n</u>other action</a></li>
    <li><a class="dropdown-item" href="#"><u>S</u>omething else here</a></li>
    <li><hr class="dropdown-divider"></li>
    <li><a class="dropdown-item" href="#">S<u>e</u>parated link</a></li>
  </ul>
</div>

https://jsfiddle.net/b6or2s5e/

Open Action menu. Pressing up arrow in first menu line or pressing down arrow in last menu line does nothi


Solution

  • There is no API for this. I am capturing the event keydown and focusing the first / last visible .menu-item .

    addEventListener('keydown', function(ev) {
    
      const SELECTOR_VISIBLE_ITEMS = '.dropdown-item:not(.disabled):not(:disabled)'
      
      var elem = ev.target;
      var menu = elem.closest(".dropdown-menu")
      if (menu) {
    
        var visibleChildren = [...menu.querySelectorAll(SELECTOR_VISIBLE_ITEMS)]
        var length = visibleChildren.length;
        var index = visibleChildren.indexOf(elem);
        if (length) {
          var first = visibleChildren[0]
          var last = visibleChildren[length - 1]
    
          if (ev.key === 'ArrowDown' && index == length - 1) {
            first.focus();
            ev.preventDefault()
            ev.stopPropagation();
            return;
          }
    
          if (ev.key === 'ArrowUp' && index == 0) {
            last.focus();
            ev.preventDefault()
            ev.stopPropagation();
            return;
          }
    
        }
      }
      // must use capture = true
    }, true)
    body {
      padding: 10px;
    }
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/css/bootstrap.min.css"></link>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/js/bootstrap.bundle.min.js"></script>
    
    <!-- Example single danger button -->
    <div class="btn-group">
      <button type="button" class="btn btn-danger dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
        Action
      </button>
      <ul class="dropdown-menu">
        <li><a class="dropdown-item" href="#"><u>A</u>ction</a></li>
        <li><a class="dropdown-item" href="#">A<u>n</u>other action</a></li>
        <li><a class="dropdown-item" href="#"><u>S</u>omething else here</a></li>
        <li>
          <hr class="dropdown-divider">
        </li>
        <li><a class="dropdown-item" href="#">S<u>e</u>parated link</a></li>
      </ul>
    </div>