From 894af15679e60dc37562dcfd9b7e3610e346c4f5 Mon Sep 17 00:00:00 2001 From: Ion Babalau Date: Tue, 4 Feb 2025 12:45:30 +0100 Subject: [PATCH 1/2] fix: improve aria attributes of popup elements --- src/autocomplete/popup.js | 31 +++++++++++++++++++-------- src/autocomplete_test.js | 44 ++++++++++++++++++++------------------- src/layer/text.js | 10 ++++++++- 3 files changed, 54 insertions(+), 31 deletions(-) diff --git a/src/autocomplete/popup.js b/src/autocomplete/popup.js index 98bdb3e111..816232f621 100644 --- a/src/autocomplete/popup.js +++ b/src/autocomplete/popup.js @@ -133,6 +133,20 @@ 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 ariaLabel = `${popup.getData(row).caption || popup.getData(row).value}${popup.getData(row).meta ? `, ${popup.getData(row).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); + } + }); popup.renderer.on("afterRender", function () { var row = popup.getRow(); var t = popup.renderer.$textLayer; @@ -140,24 +154,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"); } }); @@ -443,6 +451,11 @@ dom.importCssString(` .ace_editor.ace_autocomplete .ace_completion-highlight{ color: #2d69c7; } +.ace_editor.ace_autocomplete .ace_completion-mark { + background-color: transparent; + color: inherit; + padding: 0; +} .ace_dark.ace_editor.ace_autocomplete .ace_completion-highlight{ color: #93ca12; } diff --git a/src/autocomplete_test.js b/src/autocomplete_test.js index e1f0f4f38d..3848a320ae 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) { diff --git a/src/layer/text.js b/src/layer/text.js index f0c1a4f6fe..dad9a6f9f5 100644 --- a/src/layer/text.js +++ b/src/layer/text.js @@ -424,7 +424,15 @@ class Text { span.className = classes; span.appendChild(valueFragment); - parent.appendChild(span); + if (token.type === "completion-highlight") { + var mark = this.dom.createElement("mark"); + mark.className = "ace_completion-mark"; + mark.appendChild(span); + parent.appendChild(mark); + } + else { + parent.appendChild(span); + } } else { parent.appendChild(valueFragment); From f7032e2eb2aae914c7baccdad8f1dd7d614d1630 Mon Sep 17 00:00:00 2001 From: Ion Babalau Date: Tue, 4 Feb 2025 16:38:13 +0100 Subject: [PATCH 2/2] use role=mark instead of mark element --- src/autocomplete/popup.js | 14 ++++++++------ src/autocomplete_test.js | 8 ++++---- src/layer/text.js | 10 +--------- 3 files changed, 13 insertions(+), 19 deletions(-) diff --git a/src/autocomplete/popup.js b/src/autocomplete/popup.js index 816232f621..b92d6dc339 100644 --- a/src/autocomplete/popup.js +++ b/src/autocomplete/popup.js @@ -138,13 +138,20 @@ class AcePopup { 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 ariaLabel = `${popup.getData(row).caption || popup.getData(row).value}${popup.getData(row).meta ? `, ${popup.getData(row).meta}` : ''}`; + 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 () { @@ -451,11 +458,6 @@ dom.importCssString(` .ace_editor.ace_autocomplete .ace_completion-highlight{ color: #2d69c7; } -.ace_editor.ace_autocomplete .ace_completion-mark { - background-color: transparent; - color: inherit; - padding: 0; -} .ace_dark.ace_editor.ace_autocomplete .ace_completion-highlight{ color: #93ca12; } diff --git a/src/autocomplete_test.js b/src/autocomplete_test.js index 3848a320ae..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(); diff --git a/src/layer/text.js b/src/layer/text.js index dad9a6f9f5..f0c1a4f6fe 100644 --- a/src/layer/text.js +++ b/src/layer/text.js @@ -424,15 +424,7 @@ class Text { span.className = classes; span.appendChild(valueFragment); - if (token.type === "completion-highlight") { - var mark = this.dom.createElement("mark"); - mark.className = "ace_completion-mark"; - mark.appendChild(span); - parent.appendChild(mark); - } - else { - parent.appendChild(span); - } + parent.appendChild(span); } else { parent.appendChild(valueFragment);