2
0
Fork 0
mirror of https://github.com/gchq/CyberChef synced 2025-03-14 05:46:56 +00:00

Add initial support for screenreaders in search

This commit is contained in:
Matt C 2024-05-30 09:23:53 +01:00
parent 18159ce806
commit bdeec065bc
3 changed files with 39 additions and 16 deletions

View file

@ -43,17 +43,23 @@ class HTMLOperation {
/** /**
* Renders the operation in HTML as a stub operation with no ingredients. * Renders the operation in HTML as a stub operation with no ingredients.
* *
* @param {boolean} removeIcon - show icon for removing operation
* @param {string} elementId - element ID for aria usage
* @returns {string} * @returns {string}
*/ */
toStubHtml(removeIcon) { toStubHtml(removeIcon = false, elementId = null) {
let html = "<li class='operation'"; let html = "<li class='operation'";
if (elementId) {
html += ` id='${elementId}'`;
}
if (this.description) { if (this.description) {
const infoLink = this.infoURL ? `<hr>${titleFromWikiLink(this.infoURL)}` : ""; const infoLink = this.infoURL ? `<hr>${titleFromWikiLink(this.infoURL)}` : "";
html += ` data-container='body' data-toggle='popover' data-placement='right' 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'
data-boundary='viewport'`; data-boundary='viewport' role='button'`;
} }
html += ">" + this.name; html += ">" + this.name;

View file

@ -173,7 +173,7 @@
Operations Operations
<span class="op-count"></span> <span class="op-count"></span>
</div> </div>
<input id="search" type="search" class="form-control" placeholder="Search..." autocomplete="off" tabindex="2" data-help-title="Searching for operations" data-help="<p>Use the search box to find useful operations.</p><p>Both operation names and descriptions are queried using a fuzzy matching algorithm.</p>"> <input id="search" type="search" class="form-control" placeholder="Search..." autocomplete="off" tabindex="2" data-help-title="Searching for operations" data-help="<p>Use the search box to find useful operations.</p><p>Both operation names and descriptions are queried using a fuzzy matching algorithm.</p>" aria-owns="search-results">
<ul id="search-results" class="op-list"></ul> <ul id="search-results" class="op-list"></ul>
<div id="categories" class="panel-group no-select"></div> <div id="categories" class="panel-group no-select"></div>
</div> </div>

View file

@ -28,17 +28,16 @@ class OperationsWaiter {
this.removeIntent = false; this.removeIntent = false;
} }
/** /**
* Handler for search events. * Handler for search events.
* Finds operations which match the given search term and displays them under the search box. * Finds operations which match the given search term and displays them under the search box.
* *
* @param {event} e * @param {KeyboardEvent | ClipboardEvent | Event} e
*/ */
searchOperations(e) { searchOperations(e) {
let ops, selected; let ops, selected;
if (e.type === "search" || e.keyCode === 13) { // Search or Return if ((e.type === "search" && e.target.value !== "") || e.keyCode === 13) { // Search (non-empty) or Return
e.preventDefault(); e.preventDefault();
ops = document.querySelectorAll("#search-results li"); ops = document.querySelectorAll("#search-results li");
if (ops.length) { if (ops.length) {
@ -49,27 +48,43 @@ class OperationsWaiter {
} }
} }
/**
* Sets up the operation element with the correct attributes when selected
* @param {HTMLElement} element
*/
const _selectOperation = (element) => {
element.classList.add("selected-op");
element.scrollIntoView({block: "nearest"});
$(element).popover("show");
e.target.setAttribute("aria-activedescendant", element.id);
};
/**
* Sets up the operation element with the correct attributes when deselected
* @param {HTMLElement} element
*/
const _deselectOperation = (element) => {
element.classList.remove("selected-op");
$(element).popover("hide");
};
if (e.keyCode === 40) { // Down if (e.keyCode === 40) { // Down
e.preventDefault(); e.preventDefault();
ops = document.querySelectorAll("#search-results li"); ops = document.querySelectorAll("#search-results li");
if (ops.length) { if (ops.length) {
selected = this.getSelectedOp(ops); selected = this.getSelectedOp(ops);
if (selected > -1) { if (selected > -1) _deselectOperation(ops[selected]);
ops[selected].classList.remove("selected-op");
}
if (selected === ops.length-1) selected = -1; if (selected === ops.length-1) selected = -1;
ops[selected+1].classList.add("selected-op"); _selectOperation(ops[selected+1]);
} }
} else if (e.keyCode === 38) { // Up } else if (e.keyCode === 38) { // Up
e.preventDefault(); e.preventDefault();
ops = document.querySelectorAll("#search-results li"); ops = document.querySelectorAll("#search-results li");
if (ops.length) { if (ops.length) {
selected = this.getSelectedOp(ops); selected = this.getSelectedOp(ops);
if (selected > -1) { if (selected > -1) _deselectOperation(ops[selected]);
ops[selected].classList.remove("selected-op");
}
if (selected === 0) selected = ops.length; if (selected === 0) selected = ops.length;
ops[selected-1].classList.add("selected-op"); _selectOperation(ops[selected-1]);
} }
} else { } else {
const searchResultsEl = document.getElementById("search-results"); const searchResultsEl = document.getElementById("search-results");
@ -83,11 +98,13 @@ class OperationsWaiter {
searchResultsEl.removeChild(searchResultsEl.firstChild); searchResultsEl.removeChild(searchResultsEl.firstChild);
} }
document.querySelector("#search").removeAttribute("aria-activedescendant");
$("#categories .show").collapse("hide"); $("#categories .show").collapse("hide");
if (str) { if (str) {
const matchedOps = this.filterOperations(str, true); const matchedOps = this.filterOperations(str, true);
const matchedOpsHtml = matchedOps const matchedOpsHtml = matchedOps
.map(v => v.toStubHtml()) .map((operation, idx) => operation.toStubHtml(false, `search-result-${idx}`))
.join(""); .join("");
searchResultsEl.innerHTML = matchedOpsHtml; searchResultsEl.innerHTML = matchedOpsHtml;
@ -103,7 +120,7 @@ class OperationsWaiter {
* @param {string} searchStr * @param {string} searchStr
* @param {boolean} highlight - Whether or not to highlight the matching string in the operation * @param {boolean} highlight - Whether or not to highlight the matching string in the operation
* name and description * name and description
* @returns {string[]} * @returns {HTMLOperation[]}
*/ */
filterOperations(inStr, highlight) { filterOperations(inStr, highlight) {
const matchedOps = []; const matchedOps = [];