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

Implemented keyboard navigation to operations menu #1724

Open
wants to merge 25 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
65b058f
adding tab index to operations panel
j264415 Feb 9, 2024
0458941
Merge branch 'gchq:master' into tabbing-to-side-panel
j264415 Feb 14, 2024
c2df7be
add enter and spacebar functionality to open and close panels
j264415 Feb 14, 2024
2bade1c
remove unused imports
j264415 Feb 14, 2024
068811a
tidy up previous commit and added functionality to add operations to …
j264415 Feb 14, 2024
1b877ca
rewrite comments for handlers
j264415 Feb 14, 2024
ab816b6
made small change
j264415 Feb 14, 2024
80c1c6d
fixed classList error
j264415 Feb 15, 2024
3abd952
changed function names for easy readability
j264415 Feb 15, 2024
ce2e75e
Merge branch 'gchq:master' into tabbing-to-side-panel
j264415 Feb 16, 2024
55fbe28
removed console log
j264415 Feb 19, 2024
5d6544e
spelling change and removed console log
j264415 Feb 19, 2024
6b1086d
changes made
j264415 Feb 19, 2024
959aa70
fixed linting errors
j264415 Feb 19, 2024
76e72e9
Merge branch 'master' into tabbing-to-side-panel
j264415 Feb 20, 2024
939d925
Merge branch 'master' into tabbing-to-side-panel
j264415 Feb 22, 2024
c38bb8b
added functionality to open edit favourites with keyboard
j264415 Feb 22, 2024
213d834
reorder fav list with keyboard
e218736 Feb 26, 2024
69c23de
updae fav dialog reorder instructions
e218736 Feb 26, 2024
0d47aa6
formatting
e218736 Feb 26, 2024
3dc4b7b
delete on space
e218736 Feb 26, 2024
39bc09f
more detail to reorder instructions
e218736 Feb 27, 2024
f4e07c9
Merge branch 'master' into favourites-dialog-box
e218736 Feb 27, 2024
7c65878
Merge pull request #1 from e218736/favourites-dialog-box
e218736 Mar 1, 2024
03f5dc6
implemented edit favourites function to work with keyboard navigation
j264415 Mar 1, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/web/HTMLCategory.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class HTMLCategory {
*/
toHtml() {
const catName = "cat" + this.name.replace(/[\s/\-:_]/g, "");
let html = `<div class="panel category">
let html = `<div class="panel category" tabIndex="0">
<a class="category-title" data-toggle="collapse" data-target="#${catName}">
${this.name}
</a>
Expand Down
6 changes: 3 additions & 3 deletions src/web/HTMLOperation.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -46,20 +46,20 @@ class HTMLOperation {
* @returns {string}
*/
toStubHtml(removeIcon) {
let html = "<li class='operation'";
let html = "<li tabIndex='0' class='operation'";

if (this.description) {
const infoLink = this.infoURL ? `<hr>${titleFromWikiLink(this.infoURL)}` : "";

html += ` data-container='body' data-toggle='popover' data-placement='right'
data-content="${this.description}${infoLink}" data-html='true' data-trigger='hover'
data-content="${this.description}${infoLink}" data-html='true' data-trigger='hover focus'
data-boundary='viewport'`;
}

html += ">" + this.name;

if (removeIcon) {
html += "<i class='material-icons remove-icon op-icon'>delete</i>";
html += "<i class='material-icons remove-icon op-icon' tabindex='0'>delete</i>";
}

html += "</li>";
Expand Down
3 changes: 3 additions & 0 deletions src/web/Manager.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,9 @@ class Manager {
this.addDynamicListener(".op-list li.operation", "dblclick", this.ops.operationDblclick, this.ops);
document.getElementById("edit-favourites").addEventListener("click", this.ops.editFavouritesClick.bind(this.ops));
document.getElementById("save-favourites").addEventListener("click", this.ops.saveFavouritesClick.bind(this.ops));
document.getElementById("edit-favourites").addEventListener("keydown", this.ops.editFavouritesKeyPress.bind(this.ops));
document.getElementById("categories").addEventListener("keydown", this.ops.onKeyPress.bind(this.ops));
this.addDynamicListener(".op-list li.operation", "keydown", this.ops.keyboardPopulateRecipe.bind(this.ops));
document.getElementById("reset-favourites").addEventListener("click", this.ops.resetFavouritesClick.bind(this.ops));
this.addDynamicListener(".op-list", "oplistcreate", this.ops.opListCreate, this.ops);
this.addDynamicListener("li.operation", "operationadd", this.recipe.opAdd, this.recipe);
Expand Down
2 changes: 1 addition & 1 deletion src/web/html/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -536,7 +536,7 @@ <h5 class="modal-title">Edit Favourites</h5>
<div class="modal-body" id="favourites-body">
<ul>
<li><span style="font-weight: bold">To add:</span> drag the operation over the favourites category and drop it</li>
<li><span style="font-weight: bold">To reorder:</span> drag up and down in the list below</li>
<li><span style="font-weight: bold">To reorder:</span> drag up and down in the list below or focus on operation and press Ctrl + Up/Down Arrow to reorder using keyboard</li>
<li><span style="font-weight: bold">To remove:</span> hit the delete button or drag out of the list below</li>
</ul>
<br>
Expand Down
182 changes: 180 additions & 2 deletions src/web/waiters/OperationsWaiter.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import HTMLOperation from "../HTMLOperation.mjs";
import Sortable from "sortablejs";
import {fuzzyMatch, calcMatchRanges} from "../../core/lib/FuzzyMatch.mjs";


/**
* Waiter to handle events related to the operations.
*/
Expand Down Expand Up @@ -237,9 +236,16 @@ class OperationsWaiter {
}

const editFavouritesList = document.getElementById("edit-favourites-list");
const editFavouritesListElements = editFavouritesList.getElementsByTagName("li");
editFavouritesList.innerHTML = html;
this.removeIntent = false;

for (let i = 0; i < editFavouritesListElements.length; i++) {
editFavouritesListElements[i].setAttribute("tabindex", "0");
editFavouritesListElements[i].addEventListener("keydown", this.ArrowNavFavourites.bind(this), false);
editFavouritesListElements[i].firstElementChild.addEventListener("keydown", this.deleteFavourite.bind(this), false);
}

const editableList = Sortable.create(editFavouritesList, {
filter: ".remove-icon",
onFilter: function (evt) {
Expand Down Expand Up @@ -270,6 +276,66 @@ class OperationsWaiter {
}


/**
* Handler for navigation key press events.
* Navigates through the favourites list and corresponding delete buttons.
* Move favourites elements up and down with Ctrl + Arrow keys to imitate drag and drop mouse functionality.
*/
ArrowNavFavourites(event) {
const currentElement = event.target;
const nextElement = currentElement.nextElementSibling;
const prevElement = currentElement.previousElementSibling;
const favouritesList = currentElement.parentNode;

event.preventDefault();
event.stopPropagation();
if (event.key === "ArrowDown" && !event.ctrlKey) {
if (nextElement === null) {
currentElement.parentElement.firstElementChild.focus();
} else {
nextElement.focus();
}
} else if (event.key === "ArrowUp" && !event.ctrlKey) {
if (prevElement === null) {
currentElement.parentElement.lastElementChild.focus();
} else {
prevElement.focus();
}
} else if (event.key === "Tab") {
currentElement.parentElement.closest(".modal-body").nextElementSibling.getElementsByTagName("Button")[0].focus();
} else if (event.key === "ArrowRight") {
if (currentElement.firstElementChild !== null) {
currentElement.firstElementChild.focus();
}
} else if (event.key === "ArrowLeft" && (currentElement.classList.contains("remove-icon"))) {
currentElement.parentElement.focus();
} else if (event.ctrlKey && event.key === "ArrowDown") {
if (nextElement === null) {
favouritesList.insertBefore(currentElement, currentElement.parentElement.firstElementChild);
} else {
favouritesList.insertBefore(currentElement, nextElement.nextElementSibling);
}
currentElement.focus();
} else if (event.ctrlKey && event.key === "ArrowUp") {
favouritesList.insertBefore(currentElement, prevElement);
currentElement.focus();
}
}

/**
* Handler for delete favourites keydown events.
* delete the selected favourite from the list.
*/
deleteFavourite(event) {
if (event.key === "Enter" || event.key === " ") {
const el = event.target;
if (el && el.parentNode) {
el.parentNode.remove();
}
}
}


/**
* Handler for save favourites click events.
* Saves the selected favourites and reloads them.
Expand All @@ -284,7 +350,6 @@ class OperationsWaiter {
this.manager.recipe.initialiseOperationDragNDrop();
}


/**
* Handler for reset favourites click events.
* Resets favourites to their defaults.
Expand All @@ -293,6 +358,119 @@ class OperationsWaiter {
this.app.resetFavourites();
}

/**
* Handler that allows users to open favourite modal by "Enter/Space".
* This codes mimics editFavouritesClick event handler.
* @param {Event} ev
*/
editFavouritesKeyPress(ev) {
if (ev.key === "Enter" || ev.key === "Space" || ev.key === " ") {
ev.preventDefault();
ev.stopPropagation();
const favCat = this.app.categories.filter(function (c) {
return c.name === "Favourites";
})[0];

let html = "";
for (let i = 0; i < favCat.ops.length; i++) {
const opName = favCat.ops[i];
const operation = new HTMLOperation(opName, this.app.operations[opName], this.app, this.manager);
html += operation.toStubHtml(true);
}

const editFavouritesList = document.getElementById("edit-favourites-list");
const editFavouritesListElements = editFavouritesList.getElementsByTagName("li");
editFavouritesList.innerHTML = html;
this.removeIntent = false;

for (let i = 0; i < editFavouritesListElements.length; i++) {
editFavouritesListElements[i].setAttribute("tabindex", "0");
editFavouritesListElements[i].addEventListener("keydown", this.ArrowNavFavourites.bind(this), false);
editFavouritesListElements[i].firstElementChild.addEventListener("keydown", this.deleteFavourite.bind(this), false);
}

const editableList = Sortable.create(editFavouritesList, {
filter: ".remove-icon",
onFilter: function (evt) {
const el = editableList.closest(evt.item);
if (el && el.parentNode) {
$(el).popover("dispose");
el.parentNode.removeChild(el);
}
},
onEnd: function (evt) {
if (this.removeIntent) {
$(evt.item).popover("dispose");
evt.item.remove();
}
}.bind(this),
});

$("#edit-favourites-list [data-toggle=popover]").popover();
$("#favourites-modal").modal();

}
}

/**
* Handler for on key press events.
* Get the children of categories and add event listener to them.
*/
onKeyPress() {
const cat = document.getElementById("categories");
for (let i = 0; i < cat.children.length; i++) {
cat.children[i].addEventListener("keydown", this.keyboardEventHandler, false);
}
}

/**
* Handler for keyboard enter/space events.
* Uses "Enter" or "Space" to mimic the click function and open the operations panels .
* @param {Event} ev
*/
keyboardEventHandler(ev) {
if (ev.key === "Enter" || ev.key === "Space" || ev.key === " ") {
ev.preventDefault();
for (let i = 0; i < ev.target.childNodes.length; i++) {
const targetChild = ev.target.childNodes[i].classList;
if (targetChild !== undefined && targetChild.value.includes("panel-collapse collapse")) {
if (!targetChild.contains("show")) {
targetChild.add("show");
} else if (targetChild.contains("show")) {
targetChild.remove("show");
}

}

}
}

}

/**
* Handler to populate recipe.
* Get the children of op-list and add event listener to them.
*/
operationPopulateRecipe() {
const cat = document.querySelectorAll(".op-list li.operation");
for (let i = 0; i < cat.children.length; i++) {
cat.children[i].addEventListener("keydown", this.keyboardPopulateRecipe, false);
}

}

/**
* Handler to add operators to recipe with keyboard.
* Uses keyboard shortcut "CTRl + Enter" to mimic operationDblClick handler function
* @param {Event} ev
*/
keyboardPopulateRecipe(ev) {
if (ev.ctrlKey && ev.key === "Enter") {
const li = ev.target;
this.manager.recipe.addOperation(li.textContent);
}
}

}

export default OperationsWaiter;