From 3b7bb5e4afbad0f2bdbc7f8487442a5cb78b8284 Mon Sep 17 00:00:00 2001 From: aoyku <116817988+aoyku@users.noreply.github.com> Date: Fri, 9 Dec 2022 12:04:23 +0100 Subject: [PATCH] feat: Autocomplete accessibility features (#5008) * Accessible autocomplete * remove ace_line_id and fix autocompletion tests --- src/autocomplete.js | 2 ++ src/autocomplete/popup.js | 23 +++++++++++++++++++++-- src/autocomplete_test.js | 8 ++++---- src/keyboard/textinput.js | 18 +++++++++++++++++- src/layer/text.js | 1 + 5 files changed, 45 insertions(+), 7 deletions(-) diff --git a/src/autocomplete.js b/src/autocomplete.js index b9ba51ca51..546c3538c3 100644 --- a/src/autocomplete.js +++ b/src/autocomplete.js @@ -2,6 +2,7 @@ var HashHandler = require("./keyboard/hash_handler").HashHandler; var AcePopup = require("./autocomplete/popup").AcePopup; +var getAriaId = require("./autocomplete/popup").getAriaId; var util = require("./autocomplete/util"); var lang = require("./lib/lang"); var dom = require("./lib/dom"); @@ -54,6 +55,7 @@ var Autocomplete = function() { this.popup.autoSelect = this.autoSelect; this.popup.setData(this.completions.filtered, this.completions.filterText); + this.editor.textInput.setAriaOptions({activeDescendant: getAriaId(this.popup.getRow())}); editor.keyBinding.addKeyboardHandler(this.keyboardHandler); diff --git a/src/autocomplete/popup.js b/src/autocomplete/popup.js index ed48d279c3..494166fa97 100644 --- a/src/autocomplete/popup.js +++ b/src/autocomplete/popup.js @@ -7,6 +7,10 @@ var event = require("../lib/event"); var lang = require("../lib/lang"); var dom = require("../lib/dom"); +var getAriaId = function(index) { + return `suggest-aria-id:${index}`; +}; + var $singleLineEditor = function(el) { var renderer = new Renderer(el); @@ -45,6 +49,10 @@ var AcePopup = function(parentNode) { popup.renderer.content.style.cursor = "default"; popup.renderer.setStyle("ace_autocomplete"); + // Set aria attributes for the popup + popup.renderer.container.setAttribute("role", "listbox"); + popup.renderer.container.setAttribute("aria-label", "Autocomplete suggestions"); + popup.setOption("displayIndentGuides", false); popup.setOption("dragDelay", 150); @@ -114,11 +122,21 @@ var AcePopup = function(parentNode) { var row = popup.getRow(); var t = popup.renderer.$textLayer; var selected = t.element.childNodes[row - t.config.firstRow]; - if (selected !== t.selectedNode && t.selectedNode) + var el = document.activeElement; // Active element is textarea of main editor + if (selected !== t.selectedNode && t.selectedNode) { dom.removeCssClass(t.selectedNode, "ace_selected"); + el.removeAttribute("aria-activedescendant"); + t.selectedNode.removeAttribute("id"); + } t.selectedNode = selected; - if (selected) + if (selected) { dom.addCssClass(selected, "ace_selected"); + var ariaId = getAriaId(row); + selected.id = ariaId; + popup.renderer.container.setAttribute("aria-activedescendant", ariaId); + el.setAttribute("aria-activedescendant", ariaId); + selected.setAttribute("aria-label", popup.getData(row).value); + } }); var hideHoverMarker = function() { setHoverMarker(-1); }; var setHoverMarker = function(row, suppressRedraw) { @@ -350,3 +368,4 @@ dom.importCssString(` exports.AcePopup = AcePopup; exports.$singleLineEditor = $singleLineEditor; +exports.getAriaId = getAriaId; diff --git a/src/autocomplete_test.js b/src/autocomplete_test.js index e060d532af..8352f52f7e 100644 --- a/src/autocomplete_test.js +++ b/src/autocomplete_test.js @@ -26,16 +26,16 @@ module.exports = { editor.renderer.$themeId = "./theme/textmate"; editor.execCommand("insertstring", "a"); - checkInnerHTML('arraysortlocalalooooooooooooooooooooooooooooong_wordlocal', function() { + checkInnerHTML('arraysortlocalalooooooooooooooooooooooooooooong_wordlocal', function() { editor.execCommand("insertstring", "rr"); - checkInnerHTML('arraysortlocal', function() { + checkInnerHTML('arraysortlocal', function() { editor.execCommand("insertstring", "r"); - checkInnerHTML('arraysortlocal', function() { + checkInnerHTML('arraysortlocal', function() { editor.onCommandKey(null, 0, 13); assert.equal(editor.getValue(), "arraysort\narraysort alooooooooooooooooooooooooooooong_word"); editor.execCommand("insertstring", " looooooooooooooooooooooooooooong_"); - checkInnerHTML('alooooooooooooooooooooooooooooong_wordlocal', function() { + checkInnerHTML('alooooooooooooooooooooooooooooong_wordlocal', function() { editor.onCommandKey(null, 0, 13); editor.destroy(); editor.container.remove(); diff --git a/src/keyboard/textinput.js b/src/keyboard/textinput.js index d11bef5930..1ee56031e2 100644 --- a/src/keyboard/textinput.js +++ b/src/keyboard/textinput.js @@ -48,7 +48,23 @@ var TextInput = function(parentNode, host) { // FOCUS // ie9 throws error if document.activeElement is accessed too soon try { var isFocused = document.activeElement === text; } catch(e) {} - + + this.setAriaOptions = function(options) { + if (options.activeDescendant) { + text.setAttribute("aria-haspopup", "true"); + text.setAttribute("aria-autocomplete", "list"); + text.setAttribute("aria-activedescendant", options.activeDescendant); + } else { + text.setAttribute("aria-haspopup", "false"); + text.setAttribute("aria-autocomplete", "both"); + text.removeAttribute("aria-activedescendant"); + } + if (options.role) { + text.setAttribute("role", options.role); + } + }; + this.setAriaOptions({role: "textbox"}); + event.addListener(text, "blur", function(e) { if (ignoreFocusEvents) return; host.onBlur(e); diff --git a/src/layer/text.js b/src/layer/text.js index 3e94d48f63..87f50ece9c 100644 --- a/src/layer/text.js +++ b/src/layer/text.js @@ -293,6 +293,7 @@ var Text = function(parentEl) { lineEl.className = "ace_line_group"; } else { lineEl.className = "ace_line"; + lineEl.setAttribute("role", "option"); } fragment.push(line);