Skip to content

Commit 3b7bb5e

Browse files
author
aoyku
authored
feat: Autocomplete accessibility features (#5008)
* Accessible autocomplete * remove ace_line_id and fix autocompletion tests
1 parent 5016e90 commit 3b7bb5e

File tree

5 files changed

+45
-7
lines changed

5 files changed

+45
-7
lines changed

src/autocomplete.js

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
var HashHandler = require("./keyboard/hash_handler").HashHandler;
44
var AcePopup = require("./autocomplete/popup").AcePopup;
5+
var getAriaId = require("./autocomplete/popup").getAriaId;
56
var util = require("./autocomplete/util");
67
var lang = require("./lib/lang");
78
var dom = require("./lib/dom");
@@ -54,6 +55,7 @@ var Autocomplete = function() {
5455
this.popup.autoSelect = this.autoSelect;
5556

5657
this.popup.setData(this.completions.filtered, this.completions.filterText);
58+
this.editor.textInput.setAriaOptions({activeDescendant: getAriaId(this.popup.getRow())});
5759

5860
editor.keyBinding.addKeyboardHandler(this.keyboardHandler);
5961

src/autocomplete/popup.js

+21-2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ var event = require("../lib/event");
77
var lang = require("../lib/lang");
88
var dom = require("../lib/dom");
99

10+
var getAriaId = function(index) {
11+
return `suggest-aria-id:${index}`;
12+
};
13+
1014
var $singleLineEditor = function(el) {
1115
var renderer = new Renderer(el);
1216

@@ -45,6 +49,10 @@ var AcePopup = function(parentNode) {
4549
popup.renderer.content.style.cursor = "default";
4650
popup.renderer.setStyle("ace_autocomplete");
4751

52+
// Set aria attributes for the popup
53+
popup.renderer.container.setAttribute("role", "listbox");
54+
popup.renderer.container.setAttribute("aria-label", "Autocomplete suggestions");
55+
4856
popup.setOption("displayIndentGuides", false);
4957
popup.setOption("dragDelay", 150);
5058

@@ -114,11 +122,21 @@ var AcePopup = function(parentNode) {
114122
var row = popup.getRow();
115123
var t = popup.renderer.$textLayer;
116124
var selected = t.element.childNodes[row - t.config.firstRow];
117-
if (selected !== t.selectedNode && t.selectedNode)
125+
var el = document.activeElement; // Active element is textarea of main editor
126+
if (selected !== t.selectedNode && t.selectedNode) {
118127
dom.removeCssClass(t.selectedNode, "ace_selected");
128+
el.removeAttribute("aria-activedescendant");
129+
t.selectedNode.removeAttribute("id");
130+
}
119131
t.selectedNode = selected;
120-
if (selected)
132+
if (selected) {
121133
dom.addCssClass(selected, "ace_selected");
134+
var ariaId = getAriaId(row);
135+
selected.id = ariaId;
136+
popup.renderer.container.setAttribute("aria-activedescendant", ariaId);
137+
el.setAttribute("aria-activedescendant", ariaId);
138+
selected.setAttribute("aria-label", popup.getData(row).value);
139+
}
122140
});
123141
var hideHoverMarker = function() { setHoverMarker(-1); };
124142
var setHoverMarker = function(row, suppressRedraw) {
@@ -350,3 +368,4 @@ dom.importCssString(`
350368

351369
exports.AcePopup = AcePopup;
352370
exports.$singleLineEditor = $singleLineEditor;
371+
exports.getAriaId = getAriaId;

src/autocomplete_test.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,16 @@ module.exports = {
2626
editor.renderer.$themeId = "./theme/textmate";
2727

2828
editor.execCommand("insertstring", "a");
29-
checkInnerHTML('<d "ace_line ace_selected"><s "ace_completion-highlight">a</s><s "ace_">rraysort</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-meta">local</s></d>', function() {
29+
checkInnerHTML('<d "ace_line ace_selected" role="option" id="suggest-aria-id:0" aria-label="arraysort"><s "ace_completion-highlight">a</s><s "ace_">rraysort</s><s "ace_completion-meta">local</s></d><d "ace_line" role="option"><s "ace_completion-highlight">a</s><s "ace_">looooooooooooooooooooooooooooong_word</s><s "ace_completion-meta">local</s></d>', function() {
3030
editor.execCommand("insertstring", "rr");
31-
checkInnerHTML('<d "ace_line ace_selected"><s "ace_completion-highlight">arr</s><s "ace_">aysort</s><s "ace_completion-meta">local</s></d>', function() {
31+
checkInnerHTML('<d "ace_line ace_selected" role="option" id="suggest-aria-id:0" aria-label="arraysort"><s "ace_completion-highlight">arr</s><s "ace_">aysort</s><s "ace_completion-meta">local</s></d>', function() {
3232
editor.execCommand("insertstring", "r");
33-
checkInnerHTML('<d "ace_line ace_selected"><s "ace_completion-highlight">arr</s><s "ace_">ayso</s><s "ace_completion-highlight">r</s><s "ace_">t</s><s "ace_completion-meta">local</s></d>', function() {
33+
checkInnerHTML('<d "ace_line ace_selected" role="option" id="suggest-aria-id:0" aria-label="arraysort"><s "ace_completion-highlight">arr</s><s "ace_">ayso</s><s "ace_completion-highlight">r</s><s "ace_">t</s><s "ace_completion-meta">local</s></d>', function() {
3434

3535
editor.onCommandKey(null, 0, 13);
3636
assert.equal(editor.getValue(), "arraysort\narraysort alooooooooooooooooooooooooooooong_word");
3737
editor.execCommand("insertstring", " looooooooooooooooooooooooooooong_");
38-
checkInnerHTML('<d "ace_line ace_selected"><s "ace_">a</s><s "ace_completion-highlight">looooooooooooooooooooooooooooong_</s><s "ace_">word</s><s "ace_completion-meta">local</s></d>', function() {
38+
checkInnerHTML('<d "ace_line ace_selected" role="option" id="suggest-aria-id:0" aria-label="alooooooooooooooooooooooooooooong_word"><s "ace_">a</s><s "ace_completion-highlight">looooooooooooooooooooooooooooong_</s><s "ace_">word</s><s "ace_completion-meta">local</s></d>', function() {
3939
editor.onCommandKey(null, 0, 13);
4040
editor.destroy();
4141
editor.container.remove();

src/keyboard/textinput.js

+17-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,23 @@ var TextInput = function(parentNode, host) {
4848
// FOCUS
4949
// ie9 throws error if document.activeElement is accessed too soon
5050
try { var isFocused = document.activeElement === text; } catch(e) {}
51-
51+
52+
this.setAriaOptions = function(options) {
53+
if (options.activeDescendant) {
54+
text.setAttribute("aria-haspopup", "true");
55+
text.setAttribute("aria-autocomplete", "list");
56+
text.setAttribute("aria-activedescendant", options.activeDescendant);
57+
} else {
58+
text.setAttribute("aria-haspopup", "false");
59+
text.setAttribute("aria-autocomplete", "both");
60+
text.removeAttribute("aria-activedescendant");
61+
}
62+
if (options.role) {
63+
text.setAttribute("role", options.role);
64+
}
65+
};
66+
this.setAriaOptions({role: "textbox"});
67+
5268
event.addListener(text, "blur", function(e) {
5369
if (ignoreFocusEvents) return;
5470
host.onBlur(e);

src/layer/text.js

+1
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,7 @@ var Text = function(parentEl) {
293293
lineEl.className = "ace_line_group";
294294
} else {
295295
lineEl.className = "ace_line";
296+
lineEl.setAttribute("role", "option");
296297
}
297298
fragment.push(line);
298299

0 commit comments

Comments
 (0)