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) {