Skip to content

Commit

Permalink
Inline preview/ghost text screen reader a11y (#5327)
Browse files Browse the repository at this point in the history
* add initial inline screen reader

* change to use aria-details

* moving code around to make tests pass

* fix semicolons

* add test inline screen reader

* fix dangling commas

* small cleanup inline screen reader

* rename CSS class inline screen reader

* remove optional chaining

* add comments inline screen reader

* add constructor parameter to comment

* switch inline screenreader to describedby

* don't remove children before destroying div

* fix: wrong comment text in autocomplete_test

* add GH issue link to todo

---------

Co-authored-by: Alice Koreman <[email protected]>
  • Loading branch information
akoreman and Alice Koreman authored Oct 9, 2023
1 parent 899ea3e commit e2eed5f
Show file tree
Hide file tree
Showing 4 changed files with 141 additions and 0 deletions.
12 changes: 12 additions & 0 deletions src/autocomplete/inline.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use strict";

var snippetManager = require("../snippets").snippetManager;
var AceInlineScreenReader = require("./inline_screenreader").AceInlineScreenReader;

/**
* This object is used to manage inline code completions rendered into an editor with ghost text.
Expand All @@ -25,15 +26,22 @@ class AceInline {
if (editor && this.editor && this.editor !== editor) {
this.hide();
this.editor = null;
this.inlineScreenReader = null;
}
if (!editor || !completion) {
return false;
}
if (!this.inlineScreenReader) {
this.inlineScreenReader = new AceInlineScreenReader(editor);
}
var displayText = completion.snippet ? snippetManager.getDisplayTextForSnippet(editor, completion.snippet) : completion.value;
if (completion.hideInlinePreview || !displayText || !displayText.startsWith(prefix)) {
return false;
}
this.editor = editor;

this.inlineScreenReader.setScreenReaderContent(displayText);

displayText = displayText.slice(prefix.length);
if (displayText === "") {
editor.removeGhostText();
Expand Down Expand Up @@ -61,6 +69,10 @@ class AceInline {
destroy() {
this.hide();
this.editor = null;
if (this.inlineScreenReader) {
this.inlineScreenReader.destroy();
this.inlineScreenReader = null;
}
}
}

Expand Down
78 changes: 78 additions & 0 deletions src/autocomplete/inline_screenreader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"use strict";

/**
* This object is used to communicate inline code completions rendered into an editor with ghost text to screen reader users.
*/
class AceInlineScreenReader {
/**
* Creates the off-screen div in which the ghost text content in redered and which the screen reader reads.
* @param {import("../editor").Editor} editor
*/
constructor(editor) {
this.editor = editor;

this.screenReaderDiv = document.createElement("div");
this.screenReaderDiv.classList.add("ace_screenreader-only");
this.editor.container.appendChild(this.screenReaderDiv);
}

/**
* Set the ghost text content to the screen reader div
* @param {string} content
*/
setScreenReaderContent(content) {
// Path for when inline preview is used with 'normal' completion popup.
if (!this.popup && this.editor.completer && this.editor.completer.popup) {
this.popup = this.editor.completer.popup;

this.popup.renderer.on("afterRender", function() {
let row = this.popup.getRow();
let t = this.popup.renderer.$textLayer;
let selected = t.element.childNodes[row - t.config.firstRow];
if (selected) {
let idString = "doc-tooltip ";
for (let lineIndex = 0; lineIndex < this._lines.length; lineIndex++) {
idString += `ace-inline-screenreader-line-${lineIndex} `;
}
selected.setAttribute("aria-describedby", idString);
}
}.bind(this));
}

// TODO: Path for when special inline completion popup is used.
// https://github.com/ajaxorg/ace/issues/5348

// Remove all children of the div
while (this.screenReaderDiv.firstChild) {
this.screenReaderDiv.removeChild(this.screenReaderDiv.firstChild);
}
this._lines = content.split(/\r\n|\r|\n/);
const codeElement = this.createCodeBlock();
this.screenReaderDiv.appendChild(codeElement);
}

destroy() {
this.screenReaderDiv.remove();
}

/**
* Take this._lines, render it as <code> blocks and add those to the screen reader div.
*/
createCodeBlock() {
const container = document.createElement("pre");
container.setAttribute("id", "ace-inline-screenreader");

for (let lineIndex = 0; lineIndex < this._lines.length; lineIndex++) {
const codeElement = document.createElement("code");
codeElement.setAttribute("id", `ace-inline-screenreader-line-${lineIndex}`);
const line = document.createTextNode(this._lines[lineIndex]);

codeElement.appendChild(line);
container.appendChild(codeElement);
}

return container;
}
}

exports.AceInlineScreenReader = AceInlineScreenReader;
42 changes: 42 additions & 0 deletions src/autocomplete_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -778,6 +778,48 @@ module.exports = {
// Should filter using the value instead.
user.type(" value");
assert.equal(completer.popup.isOpen, true);
},
"test: should add inline preview content to aria-describedby": function(done) {
var editor = initEditor("fun");

editor.completers = [
{
getCompletions: function (editor, session, pos, prefix, callback) {
var completions = [
{
caption: "function",
value: "function\nthat does something\ncool"
}
];
callback(null, completions);
}
}
];

var completer = Autocomplete.for(editor);
completer.inlineEnabled = true;

user.type("Ctrl-Space");
var inline = completer.inlineRenderer;

// Popup should be open, with inline text renderered.
assert.equal(editor.completer.popup.isOpen, true);
assert.equal(completer.popup.getRow(), 0);
assert.strictEqual(inline.isOpen(), true);
assert.strictEqual(editor.renderer.$ghostText.text, "function\nthat does something\ncool");

editor.completer.popup.renderer.$loop._flush();
var popupTextLayer = completer.popup.renderer.$textLayer;

// aria-describedby of selected popup item should have aria-describedby set to the offscreen inline screen reader div and doc-tooltip.
assert.strictEqual(popupTextLayer.selectedNode.getAttribute("aria-describedby"), "doc-tooltip ace-inline-screenreader-line-0 ace-inline-screenreader-line-1 ace-inline-screenreader-line-2 ");

// The elements with these IDs should have the correct content.
assert.strictEqual(document.getElementById("ace-inline-screenreader-line-0").textContent,"function");
assert.strictEqual(document.getElementById("ace-inline-screenreader-line-1").textContent,"that does something");
assert.strictEqual(document.getElementById("ace-inline-screenreader-line-2").textContent,"cool");

done();
}
};

Expand Down
9 changes: 9 additions & 0 deletions src/css/editor-css.js
Original file line number Diff line number Diff line change
Expand Up @@ -651,4 +651,13 @@ module.exports = `
opacity: 0.5;
font-style: italic;
white-space: pre;
}
.ace_screenreader-only {
position:absolute;
left:-10000px;
top:auto;
width:1px;
height:1px;
overflow:hidden;
}`;

0 comments on commit e2eed5f

Please sign in to comment.