Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Allow more lines in hidden textarea to improve screen reader experience on Windows #5225

Merged
merged 25 commits into from
Jul 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about linux?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question, I don't have quick access to a Linux desktop so I couldn't test this so I didn't want to change anything for Linux. The most used screen reader on Linux seems to be orca.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tried on Ubuntu with orca, and to be honest, it's not great with either option but it's better without the extra lines in the textarea (like it is currently in this PR).

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 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;

Check warning on line 64 in src/keyboard/textinput.js

View check run for this annotation

Codecov / codecov/patch

src/keyboard/textinput.js#L63-L64

Added lines #L63 - L64 were not covered by tests
}

numberOfExtraLines = number;
};
this.setAriaOptions = function(options) {
if (options.activeDescendant) {
text.setAttribute("aria-haspopup", "true");
Expand All @@ -63,21 +79,16 @@
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 @@
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 @@
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 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 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
Loading