diff --git a/src/autocomplete/popup.js b/src/autocomplete/popup.js
index 98bdb3e111..b92d6dc339 100644
--- a/src/autocomplete/popup.js
+++ b/src/autocomplete/popup.js
@@ -133,6 +133,27 @@ class AcePopup {
setHoverMarker(row, true);
}
});
+ // set aria attributes on all visible elements of the popup
+ popup.renderer.on("afterRender", function () {
+ var t = popup.renderer.$textLayer;
+ for (var row = t.config.firstRow, l = t.config.lastRow; row <= l; row++) {
+ const popupRowElement = /** @type {HTMLElement|null} */(t.element.childNodes[row - t.config.firstRow]);
+ const rowData = popup.getData(row);
+ const ariaLabel = `${rowData.caption || rowData.value}${rowData.meta ? `, ${rowData.meta}` : ''}`;
+
+ popupRowElement.setAttribute("role", optionAriaRole);
+ popupRowElement.setAttribute("aria-roledescription", nls("autocomplete.popup.item.aria-roledescription", "item"));
+ popupRowElement.setAttribute("aria-label", ariaLabel);
+ popupRowElement.setAttribute("aria-setsize", popup.data.length);
+ popupRowElement.setAttribute("aria-describedby", "doc-tooltip");
+ popupRowElement.setAttribute("aria-posinset", row + 1);
+
+ const highlightedSpans = popupRowElement.querySelectorAll(".ace_completion-highlight");
+ highlightedSpans.forEach(span => {
+ span.setAttribute("role", "mark");
+ });
+ }
+ });
popup.renderer.on("afterRender", function () {
var row = popup.getRow();
var t = popup.renderer.$textLayer;
@@ -140,24 +161,18 @@ class AcePopup {
var el = document.activeElement; // Active element is textarea of main editor
if (selected !== popup.selectedNode && popup.selectedNode) {
dom.removeCssClass(popup.selectedNode, "ace_selected");
- el.removeAttribute("aria-activedescendant");
popup.selectedNode.removeAttribute(ariaActiveState);
- popup.selectedNode.removeAttribute("aria-posinset");
popup.selectedNode.removeAttribute("id");
}
+ el.removeAttribute("aria-activedescendant");
+
popup.selectedNode = selected;
if (selected) {
- dom.addCssClass(selected, "ace_selected");
var ariaId = getAriaId(row);
+ dom.addCssClass(selected, "ace_selected");
selected.id = ariaId;
t.element.setAttribute("aria-activedescendant", ariaId);
el.setAttribute("aria-activedescendant", ariaId);
- selected.setAttribute("role", optionAriaRole);
- selected.setAttribute("aria-roledescription", nls("autocomplete.popup.item.aria-roledescription", "item"));
- selected.setAttribute("aria-label", popup.getData(row).caption || popup.getData(row).value);
- selected.setAttribute("aria-setsize", popup.data.length);
- selected.setAttribute("aria-posinset", row + 1);
- selected.setAttribute("aria-describedby", "doc-tooltip");
selected.setAttribute(ariaActiveState, "true");
}
});
diff --git a/src/autocomplete_test.js b/src/autocomplete_test.js
index e1f0f4f38d..270a96ea4c 100644
--- a/src/autocomplete_test.js
+++ b/src/autocomplete_test.js
@@ -68,16 +68,16 @@ module.exports = {
assert.ok(!editor.container.querySelector("style"));
sendKey("a");
- checkInnerHTML('arraysort localalooooooooooooooooooooooooooooong_word local', function() {
+ checkInnerHTML('arraysort localalooooooooooooooooooooooooooooong_word local', function() {
sendKey("rr");
- checkInnerHTML('arraysort local', function() {
+ checkInnerHTML('arraysort local', function() {
sendKey("r");
- checkInnerHTML('arraysort local', function() {
+ checkInnerHTML('arraysort local', function() {
sendKey("Return");
assert.equal(editor.getValue(), "arraysort\narraysort alooooooooooooooooooooooooooooong_word");
editor.execCommand("insertstring", " looooooooooooooooooooooooooooong_");
- checkInnerHTML('alooooooooooooooooooooooooooooong_word local', function() {
+ checkInnerHTML('alooooooooooooooooooooooooooooong_word local', function() {
sendKey("Return");
editor.destroy();
editor.container.remove();
@@ -217,7 +217,7 @@ module.exports = {
done();
});
},
- "test: should set aria labels for currently selected item": function(done) {
+ "test: should set correct aria attributes for popup items": function(done) {
var editor = initEditor("");
var newLineCharacter = editor.session.doc.getNewLineCharacter();
editor.completers = [
@@ -233,22 +233,26 @@ module.exports = {
var popup = editor.completer.popup;
check(function () {
assert.equal(popup.data.length, 10);
- assert.ok(checkAria('0 1 2 3 4 5 6 7 8 '));
+ // check that the aria attributes have been set on all the elements of the popup and that aria selected attributes are set on the first item
+ assert.ok(checkAria(popup.renderer.$textLayer.element.innerHTML, '0 ' +
+ '1 ' +
+ '2 ' +
+ '3 ' +
+ '4 ' +
+ '5 ' +
+ '6 ' +
+ '7 ' +
+ '8 '));
+ const prevSelected = popup.selectedNode;
sendKey('Down');
check(function () {
- assert.ok(checkAria('0 1 2 3 4 5 6 7 8 '));
+ assert.ok(checkAria(popup.selectedNode.outerHTML, '1 '));
+ // check that the aria selected attributes have been removed from the previously selected element
+ assert.ok(checkAria(prevSelected.outerHTML, '0 '));
sendKey('Down');
check(function () {
- assert.ok(checkAria('0 1 2 3 4 5 6 7 8 '));
- sendKey('Down');
- check(function () {
- sendKey('Down');
- assert.ok(checkAria('0 1 2 3 4 5 6 7 8 '));
- check(function () {
- assert.ok(checkAria('0 1 2 3 4 5 6 7 8 '));
+ assert.ok(checkAria(popup.selectedNode.outerHTML, '2 '));
done();
- });
- });
});
});
});
@@ -259,11 +263,9 @@ module.exports = {
callback();
});
}
- function checkAria(expected) {
- var popup = editor.completer.popup;
- var innerHTML = popup.renderer.$textLayer.element.innerHTML
- .replace(/\s*style="[^"]+"|class="[^"]+"|(d)iv|(s)pan/g, "$1$2");
- return innerHTML === expected;
+ function checkAria(htmlElement, expected) {
+ var actual = htmlElement.replace(/\s*style="[^"]+"|class="[^"]+"|(d)iv|(s)pan/g, "$1$2");
+ return actual === expected;
}
},
"test: different completers tooltips": function (done) {