Focus Next Element In Tab Index

徘徊边缘 提交于 2019-11-26 19:57:55
Brian Glaz

Without jquery: First of all, on your tab-able elements, add class="tabable" this will let us select them later. (Do not forget the "." class selector prefix in the code below)

var lastTabIndex = 10;
function OnFocusOut()
{
    var currentElement = $get(currentElementId); // ID set by OnFOcusIn
    var curIndex = currentElement.tabIndex; //get current elements tab index
    if(curIndex == lastTabIndex) { //if we are on the last tabindex, go back to the beginning
        curIndex = 0;
    }
    var tabbables = document.querySelectorAll(".tabable"); //get all tabable elements
    for(var i=0; i<tabbables.length; i++) { //loop through each element
        if(tabbables[i].tabIndex == (curIndex+1)) { //check the tabindex to see if it's the element we want
            tabbables[i].focus(); //if it's the one we want, focus it and exit the loop
            break;
        }
    }
}

I've never implemented this, but I've looked into a similar problem, and here's what I would try.

Try this first

First, I would see if you could simply fire a keypress event for the Tab key on the element that currently has focus. There may be a different way of doing this for different browsers.

If that doesn't work, you'll have to work harder…

Referencing the jQuery implementation, you must:

  1. Listen for Tab and Shift+Tab
  2. Know which elements are tab-able
  3. Understand how tab order works

1. Listen for Tab and Shift+Tab

Listening for Tab and Shift+Tab are probably well-covered elsewhere on the web, so I'll skip that part.

2. Know which elements are tab-able

Knowing which elements are tab-able is trickier. Basically, an element is tab-able if it is focusable and does not have the attribute tabindex="-1" set. So then we must ask which elements are focusable. The following elements are focusable:

  • input, select, textarea, button, and object elements that aren't disabled.
  • a and area elements that have an href or have a numerical value for tabindex set.
  • any element that has a numerical value for tabindex set.

Furthermore, an element is focusable only if:

  • None of its ancestors are display: none.
  • The computed value of visibility is visible. This means that the nearest ancestor to have visibility set must have a value of visible. If no ancestor has visibility set, then the computed value is visible.

More details are in another Stack Overflow answer.

3. Understand how tab order works

The tab order of elements in a document is controlled by the tabindex attribute. If no value is set, the tabindex is effectively 0.

The tabindex order for the document is: 1, 2, 3, …, 0.

Initially, when the body element (or no element) has focus, the first element in the tab order is the lowest non-zero tabindex. If multiple elements have the same tabindex, you then go in document order until you reach the last element with that tabindex. Then you move to the next lowest tabindex and the process continues. Finally, finish with those elements with a zero (or empty) tabindex.

Mx.

Here's something I build for this purpose:

focusNextElement: function () {
    //add all elements we want to include in our selection
    var focussableElements = 'a:not([disabled]), button:not([disabled]), input[type=text]:not([disabled]), [tabindex]:not([disabled]):not([tabindex="-1"])';
    if (document.activeElement && document.activeElement.form) {
        var focussable = Array.prototype.filter.call(document.activeElement.form.querySelectorAll(focussableElements),
        function (element) {
            //check for visibility while always include the current activeElement 
            return element.offsetWidth > 0 || element.offsetHeight > 0 || element === document.activeElement
        });
        var index = focussable.indexOf(document.activeElement);
        if(index > -1) {
           var nextElement = focussable[index + 1] || focussable[0];
           nextElement.focus();
        }                    
    }
}

Features:

  • configurable set of focusable elements
  • no jQuery needed
  • works in all modern browsers
  • fast & lightweight

I created a simple jQuery plugin which does just this. It uses the ':tabbable' selector of jQuery UI to find the next 'tabbable' element and selects it.

Example usage:

// Simulate tab key when element is clicked 
$('.myElement').bind('click', function(event){
    $.tabNext();
    return false;
});

The core of the answer lies on finding the next element:

  function findNextTabStop(el) {
    var universe = document.querySelectorAll('input, button, select, textarea, a[href]');
    var list = Array.prototype.filter.call(universe, function(item) {return item.tabIndex >= "0"});
    var index = list.indexOf(el);
    return list[index + 1] || list[0];
  }

Usage:

var nextEl = findNextTabStop(element);
nextEl.focus();

Notice I don't care about prioritizing tabIndex.

As mentioned in a comment above, I don't think that any browsers expose tab order information. Here a simplified approximation of what the browser does to get the next element in tab order:

var allowedTags = {input: true, textarea: true, button: true};

var walker = document.createTreeWalker(
  document.body,
  NodeFilter.SHOW_ELEMENT,
  {
    acceptNode: function(node)
    {
      if (node.localName in allowedTags)
        return NodeFilter.FILTER_ACCEPT;
      else
        NodeFilter.FILTER_SKIP;
    }
  },
  false
);
walker.currentNode = currentElement;
if (!walker.nextNode())
{
  // Restart search from the start of the document
  walker.currentNode = walker.root;
  walker.nextNode();
}
if (walker.currentNode && walker.currentNode != walker.root)
  walker.currentNode.focus();

This only considers some tags and ignores tabindex attribute but might be enough depending on what you are trying to achieve.

It seems that you can check the tabIndex property of an element to determine if it is focusable. An element that is not focusable has a tabindex of "-1".

Then you just need to know the rules for tab stops:

  • tabIndex="1" has the highest priorty.
  • tabIndex="2" has the next highest priority.
  • tabIndex="3" is next, and so on.
  • tabIndex="0" (or tabbable by default) has the lowest priority.
  • tabIndex="-1" (or not tabbable by default) does not act as a tab stop.
  • For two elements that have the same tabIndex, the one that appears first in the DOM has the higher priority.

Here is an example of how to build the list of tab stops, in sequence, using pure Javascript:

function getTabStops(o, a, el) {
    // Check if this element is a tab stop
    if (el.tabIndex > 0) {
        if (o[el.tabIndex]) {
            o[el.tabIndex].push(el);
        } else {
            o[el.tabIndex] = [el];
        }
    } else if (el.tabIndex === 0) {
        // Tab index "0" comes last so we accumulate it seperately
        a.push(el);
    }
    // Check if children are tab stops
    for (var i = 0, l = el.children.length; i < l; i++) {
        getTabStops(o, a, el.children[i]);
    }
}

var o = [],
    a = [],
    stops = [],
    active = document.activeElement;

getTabStops(o, a, document.body);

// Use simple loops for maximum browser support
for (var i = 0, l = o.length; i < l; i++) {
    if (o[i]) {
        for (var j = 0, m = o[i].length; j < m; j++) {
            stops.push(o[i][j]);
        }
    }
}
for (var i = 0, l = a.length; i < l; i++) {
    stops.push(a[i]);
}

We first walk the DOM, collecting up all tab stops in sequence with their index. We then assemble the final list. Notice that we add the items with tabIndex="0" at the very end of the list, after the items with a tabIndex of 1, 2, 3, etc.

For a fully working example, where you can tab around using the "enter" key, check out this fiddle.

Tabbable is a small JS package that gives you a list of all tabbable elements in tab order. So you could find your element within that list, then focus on the next list entry.

The package correctly handles the complicated edge cases mentioned in other answers (e.g., no ancestor can be display: none). And it doesn't depend on jQuery!

As of this writing (version 1.1.1), it has the caveats that it doesn't support IE8, and that browser bugs prevent it from handling contenteditable correctly.

This is my first post on SO, so I don't have enough reputation to comment the accepted answer, but I had to modify the code to the following:

export function focusNextElement () {
  //add all elements we want to include in our selection
  const focussableElements = 
    'a:not([disabled]), button:not([disabled]), input[type=text]:not([disabled])'
  if (document.activeElement && document.activeElement.form) {
      var focussable = Array.prototype.filter.call(
        document.activeElement.form.querySelectorAll(focussableElements),
      function (element) {
          // if element has tabindex = -1, it is not focussable
          if ( element.hasAttribute('tabindex') && element.tabIndex === -1 ){
            return false
          }
          //check for visibility while always include the current activeElement 
          return (element.offsetWidth > 0 || element.offsetHeight > 0 || 
            element === document.activeElement)
      });
      console.log(focussable)
      var index = focussable.indexOf(document.activeElement);
      if(index > -1) {
         var nextElement = focussable[index + 1] || focussable[0];
         console.log(nextElement)
         nextElement.focus()
      }                    
  }
}

The changing of var to constant is non-critical. The main change is that we get rid of the selector that checks tabindex != "-1". Then later, if the element has the attribute tabindex AND it is set to "-1", we do NOT consider it focussable.

The reason I needed to change this was because when adding tabindex="-1" to an <input>, this element was still considered focussable because it matches the "input[type=text]:not([disabled])" selector. My change is equivalent to "if we are a non-disabled text input, and we have a tabIndex attribute, and the value of that attribute is -1, then we should not be considered focussable.

I believe that when the author of the accepted answer edited their answer to account for the tabIndex attribute, they did not do so correctly. Please let me know if this is not the case

Manpreet Singh Dhillon

I checked above solutions and found them quite lengthy. It can be accomplished with just one line of code:

currentElement.nextElementSibling.focus();

or

currentElement.previousElementSibling.focus();

here currentElement may be any i.e. document.activeElement or this if current element is in function's context.

I tracked tab and shift-tab events with keydown event

let cursorDirection = ''
$(document).keydown(function (e) {
    let key = e.which || e.keyCode;
    if (e.shiftKey) {
        //does not matter if user has pressed tab key or not.
        //If it matters for you then compare it with 9
        cursorDirection = 'prev';
    }
    else if (key == 9) {
        //if tab key is pressed then move next.
        cursorDirection = 'next';
    }
    else {
        cursorDirection == '';
    }
});

once you have cursor direction then you can use nextElementSibling.focus or previousElementSibling.focus methods

Did you specify your own tabIndex values for each element you want to cycle through? if so, you can try this:

var lasTabIndex = 10; //Set this to the highest tabIndex you have
function OnFocusOut()
{
    var currentElement = $get(currentElementId); // ID set by OnFocusIn 

    var curIndex = $(currentElement).attr('tabindex'); //get the tab index of the current element
    if(curIndex == lastTabIndex) { //if we are on the last tabindex, go back to the beginning
        curIndex = 0;
    }
    $('[tabindex=' + (curIndex + 1) + ']').focus(); //set focus on the element that has a tab index one greater than the current tab index
}

You are using jquery, right?

Shafiqul Islam

Hope this is helpful.

<input size="2" tabindex="1" id="one"
  maxlength="2" onkeyup="toUnicode(this)" />

<input size="2" tabindex="2" id="two"
  maxlength="2" onkeyup="toUnicode(this)" />

<input size="2" tabindex="3" id="three"
 maxlength="2" onkeyup="toUnicode(this)" />

then use simple javascript

function toUnicode(elmnt)
{
  var next;
 if (elmnt.value.length==elmnt.maxLength)
{
next=elmnt.tabIndex + 1;
//look for the fields with the next tabIndex
var f = elmnt.form;
for (var i = 0; i < f.elements.length; i++)
{
  if (next<=f.elements[i].tabIndex)
  {
    f.elements[i].focus();
    break;
    }
   }
  }
}

Here is a more complete version of focusing on the next element. It follows the spec guidelines and sorts the list of elements correctly by using tabindex. Also a reverse variable is defined if you want to get the previous element.

function focusNextElement( reverse, activeElem ) {
  /*check if an element is defined or use activeElement*/
  activeElem = activeElem instanceof HTMLElement ? activeElem : document.activeElement;

  let queryString = [
      'a:not([disabled]):not([tabindex="-1"])',
      'button:not([disabled]):not([tabindex="-1"])',
      'input:not([disabled]):not([tabindex="-1"])',
      'select:not([disabled]):not([tabindex="-1"])',
      '[tabindex]:not([disabled]):not([tabindex="-1"])'
      /* add custom queries here */
    ].join(','),
    queryResult = Array.prototype.filter.call(document.querySelectorAll(queryString), elem => {
      /*check for visibility while always include the current activeElement*/
      return elem.offsetWidth > 0 || elem.offsetHeight > 0 || elem === activeElem;
    }),
    indexedList = queryResult.slice().filter(elem => {
      /* filter out all indexes not greater than 0 */
      return elem.tabIndex == 0 || elem.tabIndex == -1 ? false : true;
    }).sort((a, b) => {
      /* sort the array by index from smallest to largest */
      return a.tabIndex != 0 && b.tabIndex != 0 
        ? (a.tabIndex < b.tabIndex ? -1 : b.tabIndex < a.tabIndex ? 1 : 0) 
        : a.tabIndex != 0 ? -1 : b.tabIndex != 0 ? 1 : 0;
    }),
    focusable = [].concat(indexedList, queryResult.filter(elem => {
      /* filter out all indexes above 0 */
      return elem.tabIndex == 0 || elem.tabIndex == -1 ? true : false;
    }));

  /* if reverse is true return the previous focusable element
     if reverse is false return the next focusable element */
  return reverse ? (focusable[focusable.indexOf(activeElem) - 1] || focusable[focusable.length - 1]) 
    : (focusable[focusable.indexOf(activeElem) + 1] || focusable[0]);
}
DavB.cs

This is a potential enhancement to the great solution that @Kano and @Mx offered. If you want to preserve TabIndex ordering, add this sort in the middle:

// Sort by explicit Tab Index, if any
var sort_by_TabIndex = function (elementA, elementB) {
    let a = elementA.tabIndex || 1;
    let b = elementB.tabIndex || 1;
    if (a < b) { return -1; }
    if (a > b) { return 1; }
    return 0;
}
focussable.sort(sort_by_TabIndex);

You can call this:

Tab:

$.tabNext();

Shift+Tab:

$.tabPrev();

<!DOCTYPE html>
<html>
<body>
<script src="https://code.jquery.com/jquery-3.3.1.js" integrity="sha256-2Kok7MbOyxpgUVvAk/HJ2jigOSYS2auK4Pfzbm7uH60=" crossorigin="anonymous"></script>
<script>
(function($){
	'use strict';

	/**
	 * Focusses the next :focusable element. Elements with tabindex=-1 are focusable, but not tabable.
	 * Does not take into account that the taborder might be different as the :tabbable elements order
	 * (which happens when using tabindexes which are greater than 0).
	 */
	$.focusNext = function(){
		selectNextTabbableOrFocusable(':focusable');
	};

	/**
	 * Focusses the previous :focusable element. Elements with tabindex=-1 are focusable, but not tabable.
	 * Does not take into account that the taborder might be different as the :tabbable elements order
	 * (which happens when using tabindexes which are greater than 0).
	 */
	$.focusPrev = function(){
		selectPrevTabbableOrFocusable(':focusable');
	};

	/**
	 * Focusses the next :tabable element.
	 * Does not take into account that the taborder might be different as the :tabbable elements order
	 * (which happens when using tabindexes which are greater than 0).
	 */
	$.tabNext = function(){
		selectNextTabbableOrFocusable(':tabbable');
	};

	/**
	 * Focusses the previous :tabbable element
	 * Does not take into account that the taborder might be different as the :tabbable elements order
	 * (which happens when using tabindexes which are greater than 0).
	 */
	$.tabPrev = function(){
		selectPrevTabbableOrFocusable(':tabbable');
	};

    function tabIndexToInt(tabIndex){
        var tabIndexInded = parseInt(tabIndex);
        if(isNaN(tabIndexInded)){
            return 0;
        }else{
            return tabIndexInded;
        }
    }

    function getTabIndexList(elements){
        var list = [];
        for(var i=0; i<elements.length; i++){
            list.push(tabIndexToInt(elements.eq(i).attr("tabIndex")));
        }
        return list;
    }

    function selectNextTabbableOrFocusable(selector){
        var selectables = $(selector);
        var current = $(':focus');

        // Find same TabIndex of remainder element
        var currentIndex = selectables.index(current);
        var currentTabIndex = tabIndexToInt(current.attr("tabIndex"));
        for(var i=currentIndex+1; i<selectables.length; i++){
            if(tabIndexToInt(selectables.eq(i).attr("tabIndex")) === currentTabIndex){
                selectables.eq(i).focus();
                return;
            }
        }

        // Check is last TabIndex
        var tabIndexList = getTabIndexList(selectables).sort(function(a, b){return a-b});
        if(currentTabIndex === tabIndexList[tabIndexList.length-1]){
            currentTabIndex = -1;// Starting from 0
        }

        // Find next TabIndex of all element
        var nextTabIndex = tabIndexList.find(function(element){return currentTabIndex<element;});
        for(var i=0; i<selectables.length; i++){
            if(tabIndexToInt(selectables.eq(i).attr("tabIndex")) === nextTabIndex){
                selectables.eq(i).focus();
                return;
            }
        }
    }

	function selectPrevTabbableOrFocusable(selector){
		var selectables = $(selector);
		var current = $(':focus');

		// Find same TabIndex of remainder element
        var currentIndex = selectables.index(current);
        var currentTabIndex = tabIndexToInt(current.attr("tabIndex"));
        for(var i=currentIndex-1; 0<=i; i--){
            if(tabIndexToInt(selectables.eq(i).attr("tabIndex")) === currentTabIndex){
                selectables.eq(i).focus();
                return;
            }
        }

        // Check is last TabIndex
        var tabIndexList = getTabIndexList(selectables).sort(function(a, b){return b-a});
        if(currentTabIndex <= tabIndexList[tabIndexList.length-1]){
            currentTabIndex = tabIndexList[0]+1;// Starting from max
        }

        // Find prev TabIndex of all element
        var prevTabIndex = tabIndexList.find(function(element){return element<currentTabIndex;});
        for(var i=selectables.length-1; 0<=i; i--){
            if(tabIndexToInt(selectables.eq(i).attr("tabIndex")) === prevTabIndex){
                selectables.eq(i).focus();
                return;
            }
        }
	}

	/**
	 * :focusable and :tabbable, both taken from jQuery UI Core
	 */
	$.extend($.expr[ ':' ], {
		data: $.expr.createPseudo ?
			$.expr.createPseudo(function(dataName){
				return function(elem){
					return !!$.data(elem, dataName);
				};
			}) :
			// support: jQuery <1.8
			function(elem, i, match){
				return !!$.data(elem, match[ 3 ]);
			},

		focusable: function(element){
			return focusable(element, !isNaN($.attr(element, 'tabindex')));
		},

		tabbable: function(element){
			var tabIndex = $.attr(element, 'tabindex'),
				isTabIndexNaN = isNaN(tabIndex);
			return ( isTabIndexNaN || tabIndex >= 0 ) && focusable(element, !isTabIndexNaN);
		}
	});

	/**
	 * focussable function, taken from jQuery UI Core
	 * @param element
	 * @returns {*}
	 */
	function focusable(element){
		var map, mapName, img,
			nodeName = element.nodeName.toLowerCase(),
			isTabIndexNotNaN = !isNaN($.attr(element, 'tabindex'));
		if('area' === nodeName){
			map = element.parentNode;
			mapName = map.name;
			if(!element.href || !mapName || map.nodeName.toLowerCase() !== 'map'){
				return false;
			}
			img = $('img[usemap=#' + mapName + ']')[0];
			return !!img && visible(img);
		}
		return ( /^(input|select|textarea|button|object)$/.test(nodeName) ?
			!element.disabled :
			'a' === nodeName ?
				element.href || isTabIndexNotNaN :
				isTabIndexNotNaN) &&
			// the element and all of its ancestors must be visible
			visible(element);

		function visible(element){
			return $.expr.filters.visible(element) && !$(element).parents().addBack().filter(function(){
				return $.css(this, 'visibility') === 'hidden';
			}).length;
		}
	}
})(jQuery);
</script>

<a tabindex="5">5</a><br>
<a tabindex="20">20</a><br>
<a tabindex="3">3</a><br>
<a tabindex="7">7</a><br>
<a tabindex="20">20</a><br>
<a tabindex="0">0</a><br>

<script>
var timer;
function tab(){
    window.clearTimeout(timer)
    timer = window.setInterval(function(){$.tabNext();}, 1000);
}
function shiftTab(){
    window.clearTimeout(timer)
    timer = window.setInterval(function(){$.tabPrev();}, 1000);
}
</script>
<button tabindex="-1" onclick="tab()">Tab</button>
<button tabindex="-1" onclick="shiftTab()">Shift+Tab</button>

</body>
</html>

I modify jquery.tabbable PlugIn to complete.

function focusNextElement(){
  var focusable = [].slice.call(document.querySelectorAll("a, button, input, select, textarea, [tabindex], [contenteditable]")).filter(function($e){
    if($e.disabled || ($e.getAttribute("tabindex") && parseInt($e.getAttribute("tabindex"))<0)) return false;
    return true;
  }).sort(function($a, $b){
    return (parseFloat($a.getAttribute("tabindex") || 99999) || 99999) - (parseFloat($b.getAttribute("tabindex") || 99999) || 99999);
  });
  var focusIndex = focusable.indexOf(document.activeElement);
  if(focusable[focusIndex+1]) focusable[focusIndex+1].focus();
};
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!