Skip to content

Commit 09fba2e

Browse files
authored
fix: improve aria attributes of popup elements (#5739)
- set aria attributes to all visible elements of the popup - add the meta text to the aria-label if present - add role="mark" to ace_completion-highlight spans
1 parent ef8b1f0 commit 09fba2e

File tree

2 files changed

+47
-30
lines changed

2 files changed

+47
-30
lines changed

src/autocomplete/popup.js

+24-9
Original file line numberDiff line numberDiff line change
@@ -133,31 +133,46 @@ class AcePopup {
133133
setHoverMarker(row, true);
134134
}
135135
});
136+
// set aria attributes on all visible elements of the popup
137+
popup.renderer.on("afterRender", function () {
138+
var t = popup.renderer.$textLayer;
139+
for (var row = t.config.firstRow, l = t.config.lastRow; row <= l; row++) {
140+
const popupRowElement = /** @type {HTMLElement|null} */(t.element.childNodes[row - t.config.firstRow]);
141+
const rowData = popup.getData(row);
142+
const ariaLabel = `${rowData.caption || rowData.value}${rowData.meta ? `, ${rowData.meta}` : ''}`;
143+
144+
popupRowElement.setAttribute("role", optionAriaRole);
145+
popupRowElement.setAttribute("aria-roledescription", nls("autocomplete.popup.item.aria-roledescription", "item"));
146+
popupRowElement.setAttribute("aria-label", ariaLabel);
147+
popupRowElement.setAttribute("aria-setsize", popup.data.length);
148+
popupRowElement.setAttribute("aria-describedby", "doc-tooltip");
149+
popupRowElement.setAttribute("aria-posinset", row + 1);
150+
151+
const highlightedSpans = popupRowElement.querySelectorAll(".ace_completion-highlight");
152+
highlightedSpans.forEach(span => {
153+
span.setAttribute("role", "mark");
154+
});
155+
}
156+
});
136157
popup.renderer.on("afterRender", function () {
137158
var row = popup.getRow();
138159
var t = popup.renderer.$textLayer;
139160
var selected = /** @type {HTMLElement|null} */(t.element.childNodes[row - t.config.firstRow]);
140161
var el = document.activeElement; // Active element is textarea of main editor
141162
if (selected !== popup.selectedNode && popup.selectedNode) {
142163
dom.removeCssClass(popup.selectedNode, "ace_selected");
143-
el.removeAttribute("aria-activedescendant");
144164
popup.selectedNode.removeAttribute(ariaActiveState);
145-
popup.selectedNode.removeAttribute("aria-posinset");
146165
popup.selectedNode.removeAttribute("id");
147166
}
167+
el.removeAttribute("aria-activedescendant");
168+
148169
popup.selectedNode = selected;
149170
if (selected) {
150-
dom.addCssClass(selected, "ace_selected");
151171
var ariaId = getAriaId(row);
172+
dom.addCssClass(selected, "ace_selected");
152173
selected.id = ariaId;
153174
t.element.setAttribute("aria-activedescendant", ariaId);
154175
el.setAttribute("aria-activedescendant", ariaId);
155-
selected.setAttribute("role", optionAriaRole);
156-
selected.setAttribute("aria-roledescription", nls("autocomplete.popup.item.aria-roledescription", "item"));
157-
selected.setAttribute("aria-label", popup.getData(row).caption || popup.getData(row).value);
158-
selected.setAttribute("aria-setsize", popup.data.length);
159-
selected.setAttribute("aria-posinset", row + 1);
160-
selected.setAttribute("aria-describedby", "doc-tooltip");
161176
selected.setAttribute(ariaActiveState, "true");
162177
}
163178
});

src/autocomplete_test.js

+23-21
Original file line numberDiff line numberDiff line change
@@ -68,16 +68,16 @@ module.exports = {
6868
assert.ok(!editor.container.querySelector("style"));
6969

7070
sendKey("a");
71-
checkInnerHTML('<d "ace_line ace_selected" id="suggest-aria-id:0" role="option" aria-roledescription="item" aria-label="arraysort" aria-setsize="2" aria-posinset="1" aria-describedby="doc-tooltip" aria-selected="true"><s "ace_completion-highlight">a</s><s "ace_">rraysort</s><s "ace_completion-spacer"> </s><s "ace_completion-meta">local</s></d><d "ace_line"><s "ace_completion-highlight">a</s><s "ace_">looooooooooooooooooooooooooooong_word</s><s "ace_completion-spacer"> </s><s "ace_completion-meta">local</s></d>', function() {
71+
checkInnerHTML('<d "ace_line ace_selected" role="option" aria-roledescription="item" aria-label="arraysort, local" aria-setsize="2" aria-describedby="doc-tooltip" aria-posinset="1" id="suggest-aria-id:0" aria-selected="true"><s "ace_completion-highlight" role="mark">a</s><s "ace_">rraysort</s><s "ace_completion-spacer"> </s><s "ace_completion-meta">local</s></d><d "ace_line" role="option" aria-roledescription="item" aria-label="alooooooooooooooooooooooooooooong_word, local" aria-setsize="2" aria-describedby="doc-tooltip" aria-posinset="2"><s "ace_completion-highlight" role="mark">a</s><s "ace_">looooooooooooooooooooooooooooong_word</s><s "ace_completion-spacer"> </s><s "ace_completion-meta">local</s></d>', function() {
7272
sendKey("rr");
73-
checkInnerHTML('<d "ace_line ace_selected" id="suggest-aria-id:0" role="option" aria-roledescription="item" aria-label="arraysort" aria-setsize="1" aria-posinset="1" aria-describedby="doc-tooltip" aria-selected="true"><s "ace_completion-highlight">arr</s><s "ace_">aysort</s><s "ace_completion-spacer"> </s><s "ace_completion-meta">local</s></d>', function() {
73+
checkInnerHTML('<d "ace_line ace_selected" role="option" aria-roledescription="item" aria-label="arraysort, local" aria-setsize="1" aria-describedby="doc-tooltip" aria-posinset="1" id="suggest-aria-id:0" aria-selected="true"><s "ace_completion-highlight" role="mark">arr</s><s "ace_">aysort</s><s "ace_completion-spacer"> </s><s "ace_completion-meta">local</s></d>', function() {
7474
sendKey("r");
75-
checkInnerHTML('<d "ace_line ace_selected" id="suggest-aria-id:0" role="option" aria-roledescription="item" aria-label="arraysort" aria-setsize="1" aria-posinset="1" aria-describedby="doc-tooltip" aria-selected="true"><s "ace_completion-highlight">arr</s><s "ace_">ayso</s><s "ace_completion-highlight">r</s><s "ace_">t</s><s "ace_completion-spacer"> </s><s "ace_completion-meta">local</s></d>', function() {
75+
checkInnerHTML('<d "ace_line ace_selected" role="option" aria-roledescription="item" aria-label="arraysort, local" aria-setsize="1" aria-describedby="doc-tooltip" aria-posinset="1" id="suggest-aria-id:0" aria-selected="true"><s "ace_completion-highlight" role="mark">arr</s><s "ace_">ayso</s><s "ace_completion-highlight" role="mark">r</s><s "ace_">t</s><s "ace_completion-spacer"> </s><s "ace_completion-meta">local</s></d>', function() {
7676

7777
sendKey("Return");
7878
assert.equal(editor.getValue(), "arraysort\narraysort alooooooooooooooooooooooooooooong_word");
7979
editor.execCommand("insertstring", " looooooooooooooooooooooooooooong_");
80-
checkInnerHTML('<d "ace_line ace_selected" id="suggest-aria-id:0" role="option" aria-roledescription="item" aria-label="alooooooooooooooooooooooooooooong_word" aria-setsize="1" aria-posinset="1" aria-describedby="doc-tooltip" aria-selected="true"><s "ace_">a</s><s "ace_completion-highlight">looooooooooooooooooooooooooooong_</s><s "ace_">word</s><s "ace_completion-spacer"> </s><s "ace_completion-meta">local</s></d>', function() {
80+
checkInnerHTML('<d "ace_line ace_selected" role="option" aria-roledescription="item" aria-label="alooooooooooooooooooooooooooooong_word, local" aria-setsize="1" aria-describedby="doc-tooltip" aria-posinset="1" id="suggest-aria-id:0" aria-selected="true"><s "ace_">a</s><s "ace_completion-highlight" role="mark">looooooooooooooooooooooooooooong_</s><s "ace_">word</s><s "ace_completion-spacer"> </s><s "ace_completion-meta">local</s></d>', function() {
8181
sendKey("Return");
8282
editor.destroy();
8383
editor.container.remove();
@@ -217,7 +217,7 @@ module.exports = {
217217
done();
218218
});
219219
},
220-
"test: should set aria labels for currently selected item": function(done) {
220+
"test: should set correct aria attributes for popup items": function(done) {
221221
var editor = initEditor("");
222222
var newLineCharacter = editor.session.doc.getNewLineCharacter();
223223
editor.completers = [
@@ -233,22 +233,26 @@ module.exports = {
233233
var popup = editor.completer.popup;
234234
check(function () {
235235
assert.equal(popup.data.length, 10);
236-
assert.ok(checkAria('<d id="suggest-aria-id:0" role="option" aria-roledescription="item" aria-label="0" aria-setsize="10" aria-posinset="1" aria-describedby="doc-tooltip" aria-selected="true"><s >0</s><s > </s></d><d ><s >1</s><s > </s></d><d ><s >2</s><s > </s></d><d ><s >3</s><s > </s></d><d ><s >4</s><s > </s></d><d ><s >5</s><s > </s></d><d ><s >6</s><s > </s></d><d ><s >7</s><s > </s></d><d ><s >8</s><s > </s></d>'));
236+
// 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
237+
assert.ok(checkAria(popup.renderer.$textLayer.element.innerHTML, '<d role="option" aria-roledescription="item" aria-label="0" aria-setsize="10" aria-describedby="doc-tooltip" aria-posinset="1" id="suggest-aria-id:0" aria-selected="true"><s >0</s><s > </s></d>' +
238+
'<d role="option" aria-roledescription="item" aria-label="1" aria-setsize="10" aria-describedby="doc-tooltip" aria-posinset="2"><s >1</s><s > </s></d>' +
239+
'<d role="option" aria-roledescription="item" aria-label="2" aria-setsize="10" aria-describedby="doc-tooltip" aria-posinset="3"><s >2</s><s > </s></d>' +
240+
'<d role="option" aria-roledescription="item" aria-label="3" aria-setsize="10" aria-describedby="doc-tooltip" aria-posinset="4"><s >3</s><s > </s></d>' +
241+
'<d role="option" aria-roledescription="item" aria-label="4" aria-setsize="10" aria-describedby="doc-tooltip" aria-posinset="5"><s >4</s><s > </s></d>' +
242+
'<d role="option" aria-roledescription="item" aria-label="5" aria-setsize="10" aria-describedby="doc-tooltip" aria-posinset="6"><s >5</s><s > </s></d>' +
243+
'<d role="option" aria-roledescription="item" aria-label="6" aria-setsize="10" aria-describedby="doc-tooltip" aria-posinset="7"><s >6</s><s > </s></d>' +
244+
'<d role="option" aria-roledescription="item" aria-label="7" aria-setsize="10" aria-describedby="doc-tooltip" aria-posinset="8"><s >7</s><s > </s></d>' +
245+
'<d role="option" aria-roledescription="item" aria-label="8" aria-setsize="10" aria-describedby="doc-tooltip" aria-posinset="9"><s >8</s><s > </s></d>'));
246+
const prevSelected = popup.selectedNode;
237247
sendKey('Down');
238248
check(function () {
239-
assert.ok(checkAria('<d role="option" aria-roledescription="item" aria-label="0" aria-setsize="10" aria-describedby="doc-tooltip"><s >0</s><s > </s></d><d id="suggest-aria-id:1" role="option" aria-roledescription="item" aria-label="1" aria-setsize="10" aria-posinset="2" aria-describedby="doc-tooltip" aria-selected="true"><s >1</s><s > </s></d><d ><s >2</s><s > </s></d><d ><s >3</s><s > </s></d><d ><s >4</s><s > </s></d><d ><s >5</s><s > </s></d><d ><s >6</s><s > </s></d><d ><s >7</s><s > </s></d><d ><s >8</s><s > </s></d>'));
249+
assert.ok(checkAria(popup.selectedNode.outerHTML, '<d role="option" aria-roledescription="item" aria-label="1" aria-setsize="10" aria-describedby="doc-tooltip" aria-posinset="2" id="suggest-aria-id:1" aria-selected="true"><s >1</s><s > </s></d>'));
250+
// check that the aria selected attributes have been removed from the previously selected element
251+
assert.ok(checkAria(prevSelected.outerHTML, '<d role="option" aria-roledescription="item" aria-label="0" aria-setsize="10" aria-describedby="doc-tooltip" aria-posinset="1"><s >0</s><s > </s></d>'));
240252
sendKey('Down');
241253
check(function () {
242-
assert.ok(checkAria('<d role="option" aria-roledescription="item" aria-label="0" aria-setsize="10" aria-describedby="doc-tooltip"><s >0</s><s > </s></d><d role="option" aria-roledescription="item" aria-label="1" aria-setsize="10" aria-describedby="doc-tooltip"><s >1</s><s > </s></d><d id="suggest-aria-id:2" role="option" aria-roledescription="item" aria-label="2" aria-setsize="10" aria-posinset="3" aria-describedby="doc-tooltip" aria-selected="true"><s >2</s><s > </s></d><d ><s >3</s><s > </s></d><d ><s >4</s><s > </s></d><d ><s >5</s><s > </s></d><d ><s >6</s><s > </s></d><d ><s >7</s><s > </s></d><d ><s >8</s><s > </s></d>'));
243-
sendKey('Down');
244-
check(function () {
245-
sendKey('Down');
246-
assert.ok(checkAria('<d role="option" aria-roledescription="item" aria-label="0" aria-setsize="10" aria-describedby="doc-tooltip"><s >0</s><s > </s></d><d role="option" aria-roledescription="item" aria-label="1" aria-setsize="10" aria-describedby="doc-tooltip"><s >1</s><s > </s></d><d role="option" aria-roledescription="item" aria-label="2" aria-setsize="10" aria-describedby="doc-tooltip"><s >2</s><s > </s></d><d id="suggest-aria-id:3" role="option" aria-roledescription="item" aria-label="3" aria-setsize="10" aria-posinset="4" aria-describedby="doc-tooltip" aria-selected="true"><s >3</s><s > </s></d><d ><s >4</s><s > </s></d><d ><s >5</s><s > </s></d><d ><s >6</s><s > </s></d><d ><s >7</s><s > </s></d><d ><s >8</s><s > </s></d>'));
247-
check(function () {
248-
assert.ok(checkAria('<d role="option" aria-roledescription="item" aria-label="0" aria-setsize="10" aria-describedby="doc-tooltip"><s >0</s><s > </s></d><d role="option" aria-roledescription="item" aria-label="1" aria-setsize="10" aria-describedby="doc-tooltip"><s >1</s><s > </s></d><d role="option" aria-roledescription="item" aria-label="2" aria-setsize="10" aria-describedby="doc-tooltip"><s >2</s><s > </s></d><d role="option" aria-roledescription="item" aria-label="3" aria-setsize="10" aria-describedby="doc-tooltip"><s >3</s><s > </s></d><d id="suggest-aria-id:4" role="option" aria-roledescription="item" aria-label="4" aria-setsize="10" aria-posinset="5" aria-describedby="doc-tooltip" aria-selected="true"><s >4</s><s > </s></d><d ><s >5</s><s > </s></d><d ><s >6</s><s > </s></d><d ><s >7</s><s > </s></d><d ><s >8</s><s > </s></d>'));
254+
assert.ok(checkAria(popup.selectedNode.outerHTML, '<d role="option" aria-roledescription="item" aria-label="2" aria-setsize="10" aria-describedby="doc-tooltip" aria-posinset="3" id="suggest-aria-id:2" aria-selected="true"><s >2</s><s > </s></d>'));
249255
done();
250-
});
251-
});
252256
});
253257
});
254258
});
@@ -259,11 +263,9 @@ module.exports = {
259263
callback();
260264
});
261265
}
262-
function checkAria(expected) {
263-
var popup = editor.completer.popup;
264-
var innerHTML = popup.renderer.$textLayer.element.innerHTML
265-
.replace(/\s*style="[^"]+"|class="[^"]+"|(d)iv|(s)pan/g, "$1$2");
266-
return innerHTML === expected;
266+
function checkAria(htmlElement, expected) {
267+
var actual = htmlElement.replace(/\s*style="[^"]+"|class="[^"]+"|(d)iv|(s)pan/g, "$1$2");
268+
return actual === expected;
267269
}
268270
},
269271
"test: different completers tooltips": function (done) {

0 commit comments

Comments
 (0)