Search code examples
jqueryhyperlinkhtml-listsarrow-keys

jQuery 2.1 | Navigate through non-contiguous links within a list with arrow keys


I want to navigate with arrow keys within a list that contains same-class links enclosed within some li, something like the following:

<ul>
<li class="linkTitle">LINKS BELOW</li>
<li class="linkHolder"><a class="link">LINK</a></li>
<li class="linkHolder"><a class="link">LINK</a></li>
<li class="linkTitle">LINKS BELOW</li>
<li class="linkHolder"><a class="link">LINK</a></li>
<li class="linkHolder"><a class="link">LINK</a></li>
<li class="linkHolder"><a class="link">LINK</a></li>
<li class="linkHolder"><a class="link">LINK</a></li>
</ul>

The code below works great for contiguous links and cycles up or down within the list, however, it breaks when confronted with an li that does not contain a link.

$(function() {

    var $li = $('li'),
        
    $move = $(".move").click(function () {
        this.focus();
    });
    
    $(document).keydown(function(e) {
        if (e.keyCode == 40 || e.keyCode == 38) {
            var inc = e.keyCode == 40 ? 1 : -1,
                move = $move.filter(":focus").parent('li').index() + inc;
            $li.eq(move % $li.length).find('.move').focus();
        }
    });
    
    $move.filter(':first').focus();
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script>
<ul>
    <li>
        <a href="#" class='move'>Link</a>
    </li>
    <li>
        <a href="#" class='move'>Link</a>
    </li>
    <li>
        <a href="#" class='move'>Link</a>
    </li>
    <li>
        <a href="#" class='move'>Link</a>
    </li>
    <li>
        <a href="#" class='move'>Link</a>
    </li>
    <li>
        <a href="#" class='move'>Link</a>
    </li>
    
</ul>

I tried but can't seem to make it work with non-contiguous links. A solution where you can only navigate (and cycle) within same-class links regardless of amount and position of non-linked li within a list would make my day!

UPDATE

Here's a DEMO I'm working on which kind of works, the UP arrow key works as intended but stops focus once at top of list. The DOWN arrow key jumps focus to bottom of list. Cycling through links doesn't work either. In this sample, nextAll() and prevAll() seems to do the trick when encountering li with no link.


Solution

  • Here is a SOLUTION I concocted based on this answer from Stackoverflow. The non-linked li criteria mentioned above is simulated with a block element inserted between unordered lists. Menu navigation is based on accesskey functionality (accesskey + z); arrow keys; Tab key (partial); or mouse. Once any UL is highlighted (focused), left and right arrow keys allow navigating through ULs while up and down arrow keys navigate through each element within a highlighted UL. I added CSS to help distinguish which UL / li are focused:

    CSS

    body {-webkit-touch-callout: none;
    -webkit-user-select: none;
    -khtml-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    user-select: none;}
    
    p {margin-top:10px;margin-left:4px;}
    .ulholder{width:160px;height:200px;background:#AAA}
    ul {background:#AAA}
    ul.focused {background:#999}
    ul:hover, ul:focus, ul:active {background:#999;}
    
    li.selected {background:darkblue}
    li:hover, li:focus, li:active {background:orange;}
    li:hover a, li:focus a, li:active a {color:white;}
    .list{cursor:pointer;}
    .link {margin-left:10px;font-weight:bold;text-decoration:none;color:darkblue}
    .link:hover, .link:focus, .link:active {color:white;}
    .linkfocused {color:white;}
    

    HTML

    <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script>
    
    <div class="ulholder"><p class="label">List 1</p>
    <ul accesskey="z" tabindex="0">
    <li class="list"><a href="#" tabindex="-1" class="link">Item 1</a></li>
    <li class="list"><a href="#" tabindex="-1" class="link">Item 2</a></li>
    <li class="list"><a href="#" tabindex="-1" class="link">Item 3</a></li>
    </ul>
    <p class="label">List 2</p>
    <ul tabindex="0">
    <li class="list"><a href="#" tabindex="-1" class="link">Item 1</a></li>
    <li class="list"><a href="#" tabindex="-1" class="link">Item 2</a></li>
    <li class="list"><a href="#" tabindex="-1" class="link">Item 3</a></li>
    </ul>
    </div>
    

    JQUERY

    var $liSelected;
    var $ulSelected;
    
    $(function(){
    
    $(document).keydown(function(e) {
     // Make sure we have a ul selected
    
    if($ulSelected) {
    
       if(e.which === 37) { // left arrow
       $('ul').prev().prev().addClass('focused').focus();
       }
       if(e.which === 39) { // right arrow
       $('ul').next().next().addClass('focused').focus();
       }
    
       if(e.which === 40) { // down arrow
       if($liSelected) {
       $liSelected.removeClass('selected');
       $liSelected.children('.link').blur();
       var $next = $liSelected.next('.list');
    
       if($next.length) {
       $liSelected = $next.addClass('selected');
       $next.children('.link').focus();
    
       } else {
       $liSelected = $ulSelected.children('li').first('.list').addClass('selected'); 
       $ulSelected.children('li').first('.list').children('.link').focus();
         }
    
       } else {
       $liSelected = $ulSelected.children('li').first('.list').addClass('selected');
       $ulSelected.children('li').first('.list').children('.link').focus();
         }
    
       } else if(e.which === 38) { // up arrow
    
       if($liSelected) {
       $liSelected.removeClass('selected');
       $liSelected.children('.link').blur();
       var $prev = $liSelected.prev('.list');
    
       if($prev.length) {
       $liSelected = $prev.addClass('selected');
       $prev.children('.link').focus();
    
       } else {
    
       $liSelected = $ulSelected.children('li').last('.list').addClass('selected');
       $ulSelected.children('li').last('.list').children('.link').focus();
         }
    
       } else {
       $liSelected = $ulSelected.children('li').last('.list').addClass('selected');
       $ulSelected.children('li').last('.list').children('.link').focus();
          }
       }
    
      }
    });
    
    $('ul .link').on('click focus', function() {
    $('ul').removeClass('focused');
    $('ul').find('.list').removeClass('selected');
    $('ul').find('.link').removeClass('listfocused');
    $(this).parent().addClass('selected');
    $(this).parent().parent('ul').addClass('focused');
    });
    
    $('ul .list').on('click', function(){
    $('.list').removeClass('selected');
    $('.link').removeClass('linkfocused');
    $(this).addClass('selected');
    $(this).children('.link').focus().click().addClass('linkfocused');
    });
    
    $('ul').on('focus', function(e) {
    $('ul .link').removeClass('linkfocused');
    $('ul .list').removeClass('selected');
    $('ul').removeClass('focused');
    $ulSelected = $(this);
    $(this).addClass('focused');
    $liSelected = false;
    });
    
    });