Skip to content

Instantly share code, notes, and snippets.

@equinusocio
Last active June 4, 2019 20:36
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save equinusocio/69a584f8658d34ba2ac30ee1fd4dafb1 to your computer and use it in GitHub Desktop.
Save equinusocio/69a584f8658d34ba2ac30ee1fd4dafb1 to your computer and use it in GitHub Desktop.
Add a basic roving tabindex behaviour.

How to use

Import the utility function:

import rovingTabindex from './rovingTabindex';

Init the function by bassing the required parameter:

rovingTabindex(<container>, 'next event.key name', 'previous event.key name', 'elements classname or any selector')

Where:

<container>

type Node

The element containing the consecutive elements that should be navigable by keyboard

next event.key name

type String

The event.key name that navigate to the next element. For example ArrowUp. More info here.

previous event.key name

type String

The event.key name that navigate to the previous element. For example ArrowDown. More info here.

elements classname or any selector

type StringSelector

The selector that match the navigable list items inside the container.

Example

Taking this HTML structure:

<nav class="MyList">
  <h2>Menu</h2>
  <a href="#">List item 1</a>
  <a href="#">List item 2</a>  
  <a href="#">List item 3</a>  
</nav>

to make li navigable we can call rovingTabindex() as follow:

rovingTabindex(document.querySelector('.MyList'), 'ArrowRight', 'ArrowLeft', 'a')
/**
* Figure out if the current element has a next sibling.
* If so, moving focus to it.
* @param {HTMLElement} parent
* @param {string} target
*/
const focusNextItem = (parent, target) => {
const item = document.activeElement
const nextIsVisible = item.nextElementSibling.offsetWidth > 0 && item.nextElementSibling.offsetHeight > 0;
if (item.nextElementSibling.hasAttribute('tabindex') && item.nextElementSibling && !item.nextElementSibling.hasAttribute('disabled') && nextIsVisible) {
activate(parent, target, item.nextElementSibling)
}
}
/**
* Figure out if the current element has a previous sibling.
* If so, moving focus to it.
* @param {HTMLElement} parent
* @param {string} target
*/
const focusPreviousItem = (parent, target) => {
const item = document.activeElement
const prevIsVisible = item.previousElementSibling.offsetWidth > 0 && item.previousElementSibling.offsetHeight > 0;
if (item.previousElementSibling.hasAttribute('tabindex') && item.previousElementSibling && !item.previousElementSibling.hasAttribute('disabled') && prevIsVisible) {
activate(parent, target, item.previousElementSibling)
}
}
/**
* Here is where the roving tabindex magic happens!
* @param {HTMLElement} parent
* @param {string} target
* @param {HTMLElement} item
*/
const activate = (parent, target, elementSibling) => {
// Set all the target elements to tabindex -1
parent.querySelectorAll(target).forEach(el => (el.tabIndex = -1))
// Make the current target element to "active"
elementSibling.tabIndex = 0
elementSibling.focus()
}
/**
* Enable the roving tabindex and set the control keys
* @param {Node} parent The element node
* @param {String} nextKey The event.key name to select next item
* @param {String} prevKey The event.key name to select previous item
* @param {String} element The type of child elements to be selectable
*/
const rovingTabindex = (parent, nextKey, prevKey, element) => {
/**
* Auto set the tabindex order to prepare
* the roving tabindex
*/
const iterables = parent.querySelectorAll(element);
iterables.forEach((item, index) => {
item.setAttribute('tabindex', index === 0 ? '0' : '-1')
})
/**
* Activate the roving tabindex navigation
*/
parent.addEventListener('keydown', (event) => {
switch (event.key) {
case nextKey:
event.preventDefault()
focusNextItem(parent, element)
break
case prevKey:
event.preventDefault()
focusPreviousItem(parent, element)
break
default:
break
}
}, true)
}
export default rovingTabindex;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment