Skip to content

Commit bdeec06

Browse files
committed
Add initial support for screenreaders in search
1 parent 18159ce commit bdeec06

File tree

3 files changed

+39
-16
lines changed

3 files changed

+39
-16
lines changed

src/web/HTMLOperation.mjs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,17 +43,23 @@ class HTMLOperation {
4343
/**
4444
* Renders the operation in HTML as a stub operation with no ingredients.
4545
*
46+
* @param {boolean} removeIcon - show icon for removing operation
47+
* @param {string} elementId - element ID for aria usage
4648
* @returns {string}
4749
*/
48-
toStubHtml(removeIcon) {
50+
toStubHtml(removeIcon = false, elementId = null) {
4951
let html = "<li class='operation'";
5052

53+
if (elementId) {
54+
html += ` id='${elementId}'`;
55+
}
56+
5157
if (this.description) {
5258
const infoLink = this.infoURL ? `<hr>${titleFromWikiLink(this.infoURL)}` : "";
5359

5460
html += ` data-container='body' data-toggle='popover' data-placement='right'
5561
data-content="${this.description}${infoLink}" data-html='true' data-trigger='hover'
56-
data-boundary='viewport'`;
62+
data-boundary='viewport' role='button'`;
5763
}
5864

5965
html += ">" + this.name;

src/web/html/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@
173173
Operations
174174
<span class="op-count"></span>
175175
</div>
176-
<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>">
176+
<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">
177177
<ul id="search-results" class="op-list"></ul>
178178
<div id="categories" class="panel-group no-select"></div>
179179
</div>

src/web/waiters/OperationsWaiter.mjs

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -28,17 +28,16 @@ class OperationsWaiter {
2828
this.removeIntent = false;
2929
}
3030

31-
3231
/**
3332
* Handler for search events.
3433
* Finds operations which match the given search term and displays them under the search box.
3534
*
36-
* @param {event} e
35+
* @param {KeyboardEvent | ClipboardEvent | Event} e
3736
*/
3837
searchOperations(e) {
3938
let ops, selected;
4039

41-
if (e.type === "search" || e.keyCode === 13) { // Search or Return
40+
if ((e.type === "search" && e.target.value !== "") || e.keyCode === 13) { // Search (non-empty) or Return
4241
e.preventDefault();
4342
ops = document.querySelectorAll("#search-results li");
4443
if (ops.length) {
@@ -49,27 +48,43 @@ class OperationsWaiter {
4948
}
5049
}
5150

51+
/**
52+
* Sets up the operation element with the correct attributes when selected
53+
* @param {HTMLElement} element
54+
*/
55+
const _selectOperation = (element) => {
56+
element.classList.add("selected-op");
57+
element.scrollIntoView({block: "nearest"});
58+
$(element).popover("show");
59+
e.target.setAttribute("aria-activedescendant", element.id);
60+
};
61+
62+
/**
63+
* Sets up the operation element with the correct attributes when deselected
64+
* @param {HTMLElement} element
65+
*/
66+
const _deselectOperation = (element) => {
67+
element.classList.remove("selected-op");
68+
$(element).popover("hide");
69+
};
70+
5271
if (e.keyCode === 40) { // Down
5372
e.preventDefault();
5473
ops = document.querySelectorAll("#search-results li");
5574
if (ops.length) {
5675
selected = this.getSelectedOp(ops);
57-
if (selected > -1) {
58-
ops[selected].classList.remove("selected-op");
59-
}
76+
if (selected > -1) _deselectOperation(ops[selected]);
6077
if (selected === ops.length-1) selected = -1;
61-
ops[selected+1].classList.add("selected-op");
78+
_selectOperation(ops[selected+1]);
6279
}
6380
} else if (e.keyCode === 38) { // Up
6481
e.preventDefault();
6582
ops = document.querySelectorAll("#search-results li");
6683
if (ops.length) {
6784
selected = this.getSelectedOp(ops);
68-
if (selected > -1) {
69-
ops[selected].classList.remove("selected-op");
70-
}
85+
if (selected > -1) _deselectOperation(ops[selected]);
7186
if (selected === 0) selected = ops.length;
72-
ops[selected-1].classList.add("selected-op");
87+
_selectOperation(ops[selected-1]);
7388
}
7489
} else {
7590
const searchResultsEl = document.getElementById("search-results");
@@ -83,11 +98,13 @@ class OperationsWaiter {
8398
searchResultsEl.removeChild(searchResultsEl.firstChild);
8499
}
85100

101+
document.querySelector("#search").removeAttribute("aria-activedescendant");
102+
86103
$("#categories .show").collapse("hide");
87104
if (str) {
88105
const matchedOps = this.filterOperations(str, true);
89106
const matchedOpsHtml = matchedOps
90-
.map(v => v.toStubHtml())
107+
.map((operation, idx) => operation.toStubHtml(false, `search-result-${idx}`))
91108
.join("");
92109

93110
searchResultsEl.innerHTML = matchedOpsHtml;
@@ -103,7 +120,7 @@ class OperationsWaiter {
103120
* @param {string} searchStr
104121
* @param {boolean} highlight - Whether or not to highlight the matching string in the operation
105122
* name and description
106-
* @returns {string[]}
123+
* @returns {HTMLOperation[]}
107124
*/
108125
filterOperations(inStr, highlight) {
109126
const matchedOps = [];

0 commit comments

Comments
 (0)