Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for quick keys #5

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions build/assets/js/modules/quick-key-manager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/**
* @class
* Class to manage quick keys in the current application.
*/
"use strict";

import { QuickKey } from "./quick-key.js";

export class QuickKeyManager {

quickKeys = new Map();

/**
* @constructor
* @param {Object} quickKeyData - Set of key/value pairs mapping keyboard characters to CSS selectors.
* @param {Node} rootNode - Root node to use for finding quick key matches.
* @returns {QuickKeyManager} - A new instance of the QuickKeyManager class.
*/
constructor(quickKeyData, rootNode) {
// Create a new QuickKey object for each key/selector pair.
for (const key in quickKeyData) {
if (Object.hasOwnProperty.call(quickKeyData, key)) {
const selector = quickKeyData[key];
const qk = new QuickKey(key, selector, rootNode);
if (qk.nodes.length > 0) {
this.quickKeys.set(key, new QuickKey(key, selector, rootNode));
}
}
}
}

/**
* Binds the specified function to the node returned
* by the pressed quick key. The lowercase quick key yields
* the next matching node, and the uppercase quick key yields
* the previous matching node.
*
* The specified function must take two parameters:
* - The node returned by the quick key.
* - The event processed by the "keydown" listener.
* @method
* @param {func} - The function to call on the node returned by pressing the quick key.
*/
bindQuickKeysToFunction(func) {
document.addEventListener( 'keydown', function (e) {
// If the lowercase quick key is pressed...
// use the next matching node.
var node = null;
if (this.quickKeys.has(e.key)) {
node = this.quickKeys.get(e.key).nextNode();
}
// If the uppercase quick key is pressed,
// use the previous matching node.
else if (e.key === e.key.toUpperCase() && this.quickKeys.has(e.key.toLowerCase)) {
node = this.quickKeys.get(e.key.toLowerCase()).previousNode();
}
if (node) {
func(node, e);
}
});
}
}
120 changes: 120 additions & 0 deletions build/assets/js/modules/quick-key.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/**
* @class
* Class to represent a screen reader quick key to iterate through a set of
* tags in the DOM when a certain key is pressed.
*/

"use strict";

export class QuickKey {

// Key to press to advance to the next node.
key = null;

// CSS selector for DOM nodes matched by this quick key.
selector = '';

// List of nodes that that match this quick key.
nodes = [];

// Index of current node in list of nodes.
currentNodeIndex = -1;

/**
* @constructor
*
* @param {String} key - Key to press to advance to the next node.
* @param {String} selector - CSS selector for DOM nodes matched by this quick key
* @param {Node} rootNode - Root node against which selector is run (optional).
* @returns {QuickKey} - A new instance of the QuickKey class.
*/
constructor(key, selector, rootNode = null) {
this.key = key;
this.selector = selector;
if (rootNode) {
this.nodes = this.findNodes(rootNode);
}
}

/**
* Finds and returns a list of nodes which matches the object's selector property.
* @method
* @param {NodeListOf} rootNode - Root node against which selector is run.
* @returns {Array[Node]} - List of nodes matching the object's selector.
*/
findNodes(rootNode) {
var nodes = [];
if (rootNode) {
nodes = rootNode.querySelectorAll(this.selector);
}
return nodes;
}

/**
* Returns the current node matched by this quick key.
* @method
* @returns {Node} - The current matching node or undefined if there are none
* or the list of nodes has not been traversed yet.
*/
currentNode() {
if (!this.nodes.length || this.currentNodeIndex < 0) {
return;
}
return this.nodes[this.currentNodeIndex];
}

/**
* Returns the next node in the DOM matched by this quick key.
* @method
* @returns {Node} - The next matching node (which could be the first one
* if we are at the end of the list), or undefined if there are no matching nodes.
*/
nextNode() {
// Make sure there are matching nodes for this quick key.
if (!this.nodes.length) {
return;
}

// Return this node if it's the only one in the list.
if (this.nodes.length == 1) {
this.currentNodeIndex = 0;
}
// Get the next node in the list.
else if (this.currentNodeIndex < this.nodes.length - 1) {
this.currentNodeIndex += 1;
}
// Or loop around to to the start of the list if we are at the end.
else {
this.currentNodeIndex = 0;
}

return this.nodes[this.currentNodeIndex];
}

/**
* Returns the previous node in the DOM matched by this quick key.
* @method
* @returns {Node} - The previous matching node (which could be the last one
* if we are at the start of the list), or null if there are no matching nodes.
*/
previousNode() {
// Make sure there are matching nodes for this quick key.
if (!this.nodes.length) {
return;
}

// Return this node if it's the only one in the list.
if (this.nodes.length == 1) {
this.currentNodeIndex = 0;
}
// Get the previous node in the list.
else if (this.currentNodeIndex > 0) {
this.currentNodeIndex -= 1;
}
// Or loop around to to the start of the list if we are at the end.
else {
this.currentNodeIndex = this.nodes.length - 1;
}
return this.nodes[this.currentNodeIndex];
}
}
41 changes: 41 additions & 0 deletions build/assets/js/quick-key-setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"use strict";

import { QuickKeyManager } from './modules/quick-key-manager.js';

// Keys/values for quick keys and the CSS selectors they match.
let keyData = {
// Press h/H to move forward/backward through headings.
'h': 'h1, h2, h3, h4, h5, h6',
// Press k/K to move forward/backward through links.
'k': 'a, [role="link"]',
// Press r/R to move forward/backward through page regions and landmarks.
'r': ' header, nav, main, footer, [role="region"], [role="banner"], [role="navigation"], [role="main"], [role="contentinfo"], [role="search"]',
// Press f/F to move forward/backward through form controls.
'f': 'input, select, button, [role="form"], [role="textbox"], [role="checkbox"], [role="button"]',
// Press b/B to move forward/backward through buttons.
'b': 'button, input[type="button"], input[type="submit"], input[type="reset"], [role="button"]',
// Press l/L to move forward/backward through lists.
'l': 'ul, ol, dl, [role="list"]'
};

let qkm = new QuickKeyManager(keyData, document.getElementById('content'));

// Check if the pressed key is a quick key.
document.addEventListener( 'keydown', function (e) {
// If the pressed key is the lowercase version of a quick key,
// target the next matching node.
var node = null;
if (qkm.quickKeys.has(e.key)) {
node = qkm.quickKeys.get(e.key).nextNode();
}
// If the pressed key is the uppercase version of a quick key,
// target the next matching node.
else if (e.key === e.key.toUpperCase() && qkm.quickKeys.has(e.key.toLowerCase())) {
node = qkm.quickKeys.get(e.key.toLowerCase()).previousNode();
}
// Move keyboard focus to the matching node (if any).
if (node) {
// alert( e.key + " ==> " + node.nodeName + ": " + node.textContent );
node.focus();
}
});
Loading