Skip to content

Commit

Permalink
feat: Allow more lines in hidden textarea to improve screen reader ex…
Browse files Browse the repository at this point in the history
…perience on Windows (#5225)

Some Windows browsers and screen readers have trouble reading the content in the hidden textarea when navigating up/down.

This change allows more lines of text into the textarea to allow to improve the screen reader experience (without changing the default behavior).

This allows the cursor in the textarea to move lines up/down which makes the behavior in the hidden textarea closer to a 'normal' textarea. Monaco uses a similar approach and is generally considered to be ahead of Ace when it comes to screen reader compatibility.
  • Loading branch information
akoreman committed Jul 24, 2023
1 parent c731164 commit bccff5a
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 29 deletions.
11 changes: 9 additions & 2 deletions src/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -2889,13 +2889,19 @@ config.defineOptions(Editor.prototype, "editor", {

var gutterKeyboardHandler;

// Prevent focus to be captured when tabbing through the page. When focus is set to the content div,
// press Enter key to give focus to Ace and press Esc to again allow to tab through the page.
// If keyboard a11y mode is enabled we:
// - Enable keyboard operability gutter.
// - Prevent tab-trapping.
// - Hide irrelevant elements from assistive technology.
// - On Windows, set more lines to the textarea.
if (value){
this.renderer.enableKeyboardAccessibility = true;
this.renderer.keyboardFocusClassName = "ace_keyboard-focus";

this.textInput.getElement().setAttribute("tabindex", -1);
// VoiceOver on Mac OS works best with single line in the textarea, the screen readers on
// Windows work best with multiple lines in the textarea.
this.textInput.setNumberOfExtraLines(useragent.isWin ? 3 : 0);
this.renderer.scroller.setAttribute("tabindex", 0);
this.renderer.scroller.setAttribute("role", "group");
this.renderer.scroller.setAttribute("aria-roledescription", nls("editor"));
Expand Down Expand Up @@ -2926,6 +2932,7 @@ config.defineOptions(Editor.prototype, "editor", {
this.renderer.enableKeyboardAccessibility = false;

this.textInput.getElement().setAttribute("tabindex", 0);
this.textInput.setNumberOfExtraLines(0);
this.renderer.scroller.setAttribute("tabindex", -1);
this.renderer.scroller.removeAttribute("role");
this.renderer.scroller.removeAttribute("aria-roledescription");
Expand Down
102 changes: 75 additions & 27 deletions src/keyboard/textinput.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,27 @@ var TextInput = function(parentNode, host) {
var lastSelectionStart = 0;
var lastSelectionEnd = 0;
var lastRestoreEnd = 0;
var rowStart = Number.MAX_SAFE_INTEGER;
var rowEnd = Number.MIN_SAFE_INTEGER;
var numberOfExtraLines = 0;

// FOCUS
// ie9 throws error if document.activeElement is accessed too soon
try { var isFocused = document.activeElement === text; } catch(e) {}

// Set number of extra lines in textarea, some screenreaders
// perform better with extra lines above and below in the textarea.
this.setNumberOfExtraLines = function(number) {
rowStart = Number.MAX_SAFE_INTEGER;
rowEnd = Number.MIN_SAFE_INTEGER;

if (number < 0) {
numberOfExtraLines = 0;
return;
}

numberOfExtraLines = number;
};
this.setAriaOptions = function(options) {
if (options.activeDescendant) {
text.setAttribute("aria-haspopup", "true");
Expand All @@ -63,21 +79,16 @@ var TextInput = function(parentNode, host) {
if (options.role) {
text.setAttribute("role", options.role);
}
};
this.setAriaLabel = function() {
if(host.session && host.renderer.enableKeyboardAccessibility) {
var row = host.session.selection.cursor.row;

if (options.setLabel) {
text.setAttribute("aria-roledescription", nls("editor"));
text.setAttribute("aria-label", nls("Cursor at row $0", [row + 1]));
} else {
text.removeAttribute("aria-roledescription");
text.removeAttribute("aria-label");
if(host.session) {
var row = host.session.selection.cursor.row;
text.setAttribute("aria-label", nls("Cursor at row $0", [row + 1]));
}
}
};

this.setAriaOptions({role: "textbox"});
this.setAriaLabel();
this.setAriaOptions({role: "textbox"});

event.addListener(text, "blur", function(e) {
if (ignoreFocusEvents) return;
Expand All @@ -103,7 +114,9 @@ var TextInput = function(parentNode, host) {
this.$focusScroll = false;
this.focus = function() {
// On focusing on the textarea, read active row number to assistive tech.
this.setAriaLabel();
this.setAriaOptions({
setLabel: host.renderer.enableKeyboardAccessibility
});

if (tempStyle || HAS_FOCUS_ARGS || this.$focusScroll == "browser")
return text.focus({ preventScroll: true });
Expand Down Expand Up @@ -163,6 +176,17 @@ var TextInput = function(parentNode, host) {
resetSelection();
});

// Convert from row,column position to the linear position with respect to the current
// block of lines in the textarea.
var positionToSelection = function(row, column) {
var selection = column;

for (var i = 1; i <= row - rowStart && i < 2*numberOfExtraLines + 1; i++) {
selection += host.session.getLine(row - i).length + 1;
}
return selection;
};

var resetSelection = isIOS
? function(value) {
if (!isFocused || (copied && !value) || sendingText) return;
Expand Down Expand Up @@ -199,19 +223,43 @@ var TextInput = function(parentNode, host) {
var selection = host.selection;
var range = selection.getRange();
var row = selection.cursor.row;
selectionStart = range.start.column;
selectionEnd = range.end.column;
line = host.session.getLine(row);

if (range.start.row != row) {
var prevLine = host.session.getLine(row - 1);
selectionStart = range.start.row < row - 1 ? 0 : selectionStart;
// We keep 2*numberOfExtraLines + 1 lines in the textarea, if the new active row
// is within the current block of lines in the textarea we do nothing. If the new row
// is one row above or below the current block, move up or down to the next block of lines.
// If the new row is further than 1 row away from the current block grab a new block centered
// around the new row.
if (row === rowEnd + 1) {
rowStart = rowEnd + 1;
rowEnd = rowStart + 2*numberOfExtraLines;
} else if (row === rowStart - 1) {
rowEnd = rowStart - 1;
rowStart = rowEnd - 2*numberOfExtraLines;
} else if (row < rowStart - 1 || row > rowEnd + 1) {
rowStart = row > numberOfExtraLines ? row - numberOfExtraLines : 0;
rowEnd = row > numberOfExtraLines ? row + numberOfExtraLines : 2*numberOfExtraLines;
}

var lines = [];

for (var i = rowStart; i <= rowEnd; i++) {
lines.push(host.session.getLine(i));
}

line = lines.join('\n');

selectionStart = positionToSelection(range.start.row, range.start.column);
selectionEnd = positionToSelection(range.end.row, range.end.column);

if (range.start.row < rowStart) {
var prevLine = host.session.getLine(rowStart - 1);
selectionStart = range.start.row < rowStart - 1 ? 0 : selectionStart;
selectionEnd += prevLine.length + 1;
line = prevLine + "\n" + line;
}
else if (range.end.row != row) {
var nextLine = host.session.getLine(row + 1);
selectionEnd = range.end.row > row + 1 ? nextLine.length : selectionEnd;
else if (range.end.row > rowEnd) {
var nextLine = host.session.getLine(rowEnd + 1);
selectionEnd = range.end.row > rowEnd + 1 ? nextLine.length : range.end.column;
selectionEnd += line.length + 1;
line = line + "\n" + nextLine;
}
Expand All @@ -235,12 +283,12 @@ var TextInput = function(parentNode, host) {
}
}
}
}

var newValue = line + "\n\n";
if (newValue != lastValue) {
text.value = lastValue = newValue;
lastSelectionStart = lastSelectionEnd = newValue.length;

var newValue = line + "\n\n";
if (newValue != lastValue) {
text.value = lastValue = newValue;
lastSelectionStart = lastSelectionEnd = newValue.length;
}
}

// contextmenu on mac may change the selection
Expand Down
34 changes: 34 additions & 0 deletions src/keyboard/textinput_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,40 @@ module.exports = {
assert.equal([textarea.value.length, textarea.selectionStart, textarea.selectionEnd].join(","), "3,0,1");
},

"test: selection synchronization with extra lines enabled": function() {
editor.textInput.setNumberOfExtraLines(1);
editor.session.setValue("line1\nline2\nline3\nline4\nline5\nline6\n");
[
{ _: "keydown", range: [1,1], value: "line1\nline2\nline3\n\n", key: { code: "ArrowRight", key: "ArrowRight", keyCode: 39}},
{ _: "keydown", range: [2,2], value: "line1\nline2\nline3\n\n", key: { code: "ArrowRight", key: "ArrowRight", keyCode: 39}},
{ _: "keydown", range: [2,2], value: "line1\nline2\nline3\n\n", key: { code: "ShiftLeft", key: "Shift", keyCode: 16}, modifier: "shift-"},
{ _: "keydown", range: [2,3], value: "line1\nline2\nline3\n\n", key: { code: "ArrowRight", key: "ArrowRight", keyCode: 39}, modifier: "shift-"},
{ _: "keydown", range: [2,4], value: "line1\nline2\nline3\n\n", key: { code: "ArrowRight", key: "ArrowRight", keyCode: 39}, modifier: "shift-"},
{ _: "keydown", range: [2,5], value: "line1\nline2\nline3\n\n", key: { code: "ArrowRight", key: "ArrowRight", keyCode: 39}, modifier: "shift-"},
{ _: "keydown", range: [2,6], value: "line1\nline2\nline3\n\n", key: { code: "ArrowRight", key: "ArrowRight", keyCode: 39}, modifier: "shift-"},
{ _: "keydown", range: [2,7], value: "line1\nline2\nline3\n\n", key: { code: "ArrowRight", key: "ArrowRight", keyCode: 39}, modifier: "shift-"},
{ _: "keydown", range: [2,8], value: "line1\nline2\nline3\n\n", key: { code: "ArrowRight", key: "ArrowRight", keyCode: 39}, modifier: "shift-"},
{ _: "keydown", range: [2,14], value: "line1\nline2\nline3\n\n", key: { code: "ArrowDown", key: "ArrowDown", keyCode: 40}, modifier: "shift-"},
{ _: "keydown", range: [2,2], value: "line4\nline5\nline6\n\n", key: { code: "ArrowDown", key: "ArrowDown", keyCode: 40}},
{ _: "keydown", range: [2,2], value: "line4\nline5\nline6\n\n", key: { code: "ShiftLeft", key: "Shift", keyCode: 16}, modifier: "shift-"},
{ _: "keydown", range: [14,20], value: "line1\nline2\nline3\nline4\n\n", key: { code: "ArrowUp", key: "ArrowUp", keyCode: 38}, modifier: "shift-"},
{ _: "keydown", range: [8,8], value: "line1\nline2\nline3\n\n", key: { code: "ArrowUp", key: "ArrowUp", keyCode: 38}},
{ _: "keydown", range: [14,14], value: "line1\nline2\nline3\n\n", key: { code: "ArrowDown", key: "ArrowDown", keyCode: 40}},
{ _: "keydown", range: [2,8], value: "line3\nline4\nline5\nline6\n\n", key: { code: "ArrowDown", key: "ArrowDown", keyCode: 40}, modifier: "shift-"}
].forEach(function(data) {
sendEvent(data._, data);
});
// test overflow
editor.session.setValue("0123456789".repeat(80));
editor.execCommand("gotoright");
editor.execCommand("selectright");
assert.equal([textarea.value.length, textarea.selectionStart, textarea.selectionEnd].join(","), "402,1,2");
editor.execCommand("gotolineend");
assert.equal([textarea.value.length, textarea.selectionStart, textarea.selectionEnd].join(","), "3,0,0");
editor.execCommand("selectleft");
assert.equal([textarea.value.length, textarea.selectionStart, textarea.selectionEnd].join(","), "3,0,1");
},

"test: chinese ime on ie": function() {
editor.setOption("useTextareaForIME", false);
[
Expand Down

0 comments on commit bccff5a

Please sign in to comment.