Keyboard Navigator
Keyboard Navigator is a simple javascript library through which developers can implement tabbing in their web applications either by their own strategy or by utilizing that which is provided by library by configuring it to any extent.
Keyboard Navigator provides navigation strategy on using Tab key
,Arrow Keys
,Enter Key
,Escape Key
.
Web Applications developed using frontend JS libraries/frameworks tend to update DOM frequently through template bindings, such applications tend to loose focus if DOM updates. Keyboard Navigator provides a strategy to persist focus in such scenarios by exploiting the XPath
reference to elements.
Keyboard Navigator is available as a Node Package.
npm install keyboard-navigator
For non node usage, developers can include keyboard-navigator.js from this repo into their project
or can use the below CDN link.
<script src="https://cdn.jsdelivr.net/npm/keyboard-navigator@latest/lib/keyboard-navigator.js"></script>
angular apps can import the js file from node_modules by adding the path to scripts array in angular.json
"scripts": [
...
"node_modules/keyboard-navigator/lib/keyboard-navigator.js"
]
example Angular app using keyboard-navigator
Table of Contents
Start Navigation
Navigation can be contained inside desired element or set it on window by calling this setNavigationOnContainingElement function.
Also calling this function Keyboard Navigator starts listening on target element.
keyBoardNavigator.setNavigationOnContainingElement("targetElementID")
calling setNavigationOnContainingElement()
without parameter sets navigation on window
object
Navigation can be paused by setting
keyBoardNavigator.pause = true
the same can be used to resume
Tabbing
General Tabbing order would be the HTML Source code order of tabbable elements which can be altered by setting tabindex attribute, but it might get complex.
Keyboard Navigator doesnot suggest/provide any tabbing order, but can be configured to follow tabbing order by passing array of HTML elements in desired order or the developer can opt for a custom tabbing logic. It is better to have custom tabbing logic to satisfy your needs.
Developer can input the list of HTML elements in custom order to make it the default tabbing logic/order of Keyboard Navigator(meaning Keyboard Navigator tabs through elements in the order that the developer inputs).
Keyboard Navigator by default assumes that developer has opted for custom tabbing logic.
Default HTML source code tabbing order is also considered custom tabbing logic in such case no need to configure anything in KeyBoardNavigator for tabbing.
keyBoardNavigator.customTabLogic = true;
but if developer has a tabbing logic and wants to integrate it with Keyboard Navigator then set
keyBoardNavigator.customTabLogic = function(keyBoardNavigatorScope, event){
};
also if the developer doesnot want any complex tabbing logic but need to implement custom tabbing order that Keyboard Navigator has to follow, then set
keyBoardNavigator.customTabLogic = false
keyBoardNavigator.listOfTabbingElementsInOrder = [Array of HTML Elements in custom order];
in few cases if Developer has to stop tabbing through Elements that was passed to Keyboard Navigator as above then set
keyBoardNavigator.additionalCustomTabLogic = function(keyBoardNavigatorScope, event){
}
Trapping Focus in Modals:
if Developer needs to trap focus in modals, then the containing element of modal must be given a class name: kbn-modal
and set:
keyBoardNavigator.customModalFocusTrapLogic = false
Keyboard Navigatore defaults customModalFocusTrapLogic = false
so if the modal shows up, Keyboard Navigator traps focus inside modal
but if developer needs to implement custom focus trap logic inside modals and replace Keyboard Navigator's modal focus trap logic then set:
keyBoardNavigator.customModalFocusTrapLogic = function(keyBoardNavigatorScope, event){
}
Tabbing logical flow(refer source code if not clear):
if(this.customTabLogic == false){
var executeDefaultTabbingLogic = true;
executeDefaultTabbingLogic = this.additionalCustomTabLogic.call(this,event);
if(executeDefaultTabbingLogic && this.listOfTabbingElementsInOrder){
}
}
else if(typeof(this.customTabLogic) == "function"){
this.customTabLogic.call(this,event)
}
if(this.customModalFocusTrapLogic == false){
}
else if(typeof(this.customModalFocusTrapLogic) == "function"){
this.customModalFocusTrapLogic.call(this,event)
}
List of all Default Values is mention at the last
Arrow Key Navigation
The preferred and easy way to navigate across similar elements such as list elements,custom dropdown list,multiple similar blocks... is via arrow keys.
In such custom implementaions navigation must be manually handled, if those components were default HTML components, then navigation is by handled by default.
Keyboard Navigator provides a strategy to navigate among such elements by comparing the coordinates of next directional element with active element.
Arrow key navigation can be triggered from:
1)Containing Element/Triggering Element
2)Active Element which is one of similar elements
Containing Element/Triggering Element:
User will be reaching the containing or triggering element by tabbing through previous tabbable elements, now on press of arrow keys, Keyboard Navigator shifts the focus to first option/block(first element among all similar element).
Also developer can constrain the listener to only listen on particular arrow keys.
once the focus gets onto any of the similar elements, user can move in any direction that the developer sets on these elements too.
So developer must ensure the following to implement arrow navigation:
keyBoardNavigator.arrowKeyNavigationConfig.push({
"containingOrTriggeringElementClass" : "arrowTrigger",
"containingOrTriggeringElementArrowKeys" : [this.keys["up"],this.keys["down"]],
"arrowNavigableElementClass" : "arrowNavigableElement",
"arrowNavigableElementArrowKeys" : [this.keys["up"],this.keys["down"],this.keys["left"],this.keys["right"]],
"additionalFilterOnArrowNavigableElements" : function(arr) {return arr}
})
Persisting Focus on DOM updates
Web applications built using moder JS frameworks refresh their DOM nodes to update the page with latest data,in that process focus is lost if active element is part of DOM update or active element gets removed in DOM update. So if the focus is lost(shifts to HTML body) user will have to re-tab all the elements to reach the current one which is frustrating. Also the references of DOM nodes gets lost if we keep track of them.
Keyboard Navigator uses XPath references to keep track and persist focus.
1)To persist focus on elements on DOM-updates due to store updates(ngrx store, redux store)/template bindings, call this function inside store subscriber which is that last step before DOM updates or where template bindings are written.
keyBoardNavigator.persistFocus()
examples:
in case of store subscriptions:
this.store.subscribe(data => {
this.keyBoardNavigator.persistFocus();
})
in case of simple template-binding updates:
function updatePrducts(newData){
this.products = newData;
this.keyBoardNavigator.persistFocus();
}
this function stores the Xpath of active element before DOM update, and Asynchronously focuses the same element after DOM update using the same Xpath.
2)User clicking on element(pressing enter)
Developer can opt out of this step so that Keyboard Navigator doesnot handle enter clicks by setting
keyBoardNavigator.listenOnEnterKey = false
Keyboard Navigator provides a strategy for handling such case where elements get removed/disappeared from DOM on clicking enter.
Developers can have their own strategy and can integrate it with Keyboard Navigator by setting:
keyBoardNavigator.useDefaultFallbackFocusLogic = false
keyBoardNavigator.customFallbackFocusLogic = function(keyBoardNavigatorScope,event){
}
Default strategy that Keboard Navigator provides:
Keyboard Navigator keeps track of fallback focus elements in a queue(first in first out) when user clicks enter key on elements.
Fallback focus elements are those elements to be focused when their child elements are being clicked(pressing enter key),resulting in removal of that child node from DOM.
To register a node/element as Fallback Focus Node assign a class navigable-fallbackFocusNode
to that node/element.
Keyboard Navigator before executing click on element via script, queries and stores the Xpath of Parent Fallback Focus Element
to the current element
if any into a Fallback Focus Elements queue
.
Keyboard Navigator checks if the clicked(enter key pressed) element still exists on DOM after click is executed, not by node reference but by Xpath reference, if that element doesnot exists on DOM, then it focuses the Fallback Focus Element
that is fetched from Fallback Focus Elements queue
which satisfy below conditions:
1)recently added Falback elements are given priority.
2)Falback elements must exist on DOM, which is ensured by checking if DOM contains any element matching the Xpath reference.
Developers might add non-interactive elements to DOM and expect users to interact with them(example: using an image of delete instead of delete button).
In such cases pressing enter on such elements will yeild nothing, click listeners wont fire.
Keyboard Navigator patches this action by listening on enter clicks and programatically click elements from script.
if Developer dont need any fallback focus strategy and just needs programatical clicks for non-interactive elements, set:
keyBoardNavigator.useDefaultFallbackFocusLogic = false
this will ensure to execute only programatic clicks.
Enter Listener Flow:
if(this.listenOnEnterKey){
if(this.useDefaultFallbackFocusLogic){
}
else if(this.customFallbackFocusLogic){
}
else{
event.target.click();
}
}
List of Defaults
this.masterNavigationElement = window
this.pause = false;
this.listenOnEnterKey = true;
this.listenOnEscapeKey = false;
this.listenOnTabKey = true;
this.listOfTabbingElementsInOrder = [];
this.customTabLogic = true;
this.additionalCustomTabLogic = null;
this.customModalFocusTrapLogic = false;
this.useDefaultFallbackFocusLogic = true;
this.customFallbackFocusLogic = false;
this.arrowKeyNavigationConfig = [{
"containingOrTriggeringElementClass" : "arrowTrigger",
"containingOrTriggeringElementArrowKeys" : [this.keys["up"],this.keys["down"]],
"arrowNavigableElementClass" : "arrowNavigableElement",
"arrowNavigableElementArrowKeys" : [this.keys["up"],this.keys["down"],this.keys["left"],this.keys["right"]],
"additionalFilterOnArrowNavigableElements" : function(arr) {return arr}
}];
this.setAutomaticTabIndexOnArrowNavigableElements = false;