Commit 2db13360 authored by David Tseng's avatar David Tseng Committed by Commit Bot

Fall back to simpler implementation of editing feedback for <textarea>

A <textarea> currently exposes all text like so:
textarea node
  static text node
    inline text box node
    ...

i.e. all lines are under just one static text. Given 1000+ lines, this amounts to a fairly flat tree structure. The computation of necessary text attributes such as indexing into the lines for the purposes of finding the line with text selection becomes increasingly expensive.

In a similar content editable, the large static text would hvae been split into multiple static texts.

The user consequence is that in a textarea with just a few hundred lines becomes unusable. ~4 second latency between key press and speech output.

Bug: 936947:
Change-Id: I9772851ba34c8ec1945e83a7d0baa2676cedad3f
Reviewed-on: https://chromium-review.googlesource.com/c/1496299Reviewed-by: default avatarDominic Mazzoni <dmazzoni@chromium.org>
Commit-Queue: David Tseng <dtseng@chromium.org>
Cr-Commit-Position: refs/heads/master@{#636830}
parent a8a17087
...@@ -1539,30 +1539,11 @@ TEST_F('ChromeVoxBackgroundTest', 'NavigationEscapesEdit', function() { ...@@ -1539,30 +1539,11 @@ TEST_F('ChromeVoxBackgroundTest', 'NavigationEscapesEdit', function() {
.call(textArea.focus.bind(textArea)) .call(textArea.focus.bind(textArea))
.expectSpeech('Text area') .expectSpeech('Text area')
.call(assertBeginning.bind(this, true)) .call(assertBeginning.bind(this, true))
.call(assertEnd.bind(this, false))
.clearPendingOutput()
.call(press(40 /* ArrowDown */))
.expectSpeech('is')
.call(press(39 /* ArrowRight */))
.expectSpeech('s')
.call(assertBeginning.bind(this, false))
.call(assertEnd.bind(this, false))
.call(press(40 /* ArrowDown */))
.call(press(37 /* ArrowLeft */))
.expectSpeech('a')
.call(assertBeginning.bind(this, false))
.call(assertEnd.bind(this, false))
.call(press(40 /* ArrowDown */))
.expectSpeech('test')
.call(press(39 /* ArrowRight */))
.expectSpeech('e')
.call(assertBeginning.bind(this, false))
.call(assertEnd.bind(this, true)) .call(assertEnd.bind(this, true))
.replay(); .replay();
// TODO: soft line breaks currently won't work in <textarea>.
}.bind(this)); }.bind(this));
contentEditable.focus(); contentEditable.focus();
}); });
......
...@@ -54,8 +54,7 @@ editing.TextEditHandler = function(node) { ...@@ -54,8 +54,7 @@ editing.TextEditHandler = function(node) {
// //
// The only other editables we expect are all single line (including those // The only other editables we expect are all single line (including those
// from ARC++). // from ARC++).
var useRichText = var useRichText = node.state[StateType.RICHLY_EDITABLE];
node.state[StateType.RICHLY_EDITABLE] || node.htmlTag == 'textarea';
/** @private {!AutomationEditableText} */ /** @private {!AutomationEditableText} */
this.editableText_ = useRichText ? new AutomationRichEditableText(node) : this.editableText_ = useRichText ? new AutomationRichEditableText(node) :
...@@ -128,9 +127,12 @@ editing.TextEditHandler.prototype = { ...@@ -128,9 +127,12 @@ editing.TextEditHandler.prototype = {
function AutomationEditableText(node) { function AutomationEditableText(node) {
if (!node.state.editable) if (!node.state.editable)
throw Error('Node must have editable state set to true.'); throw Error('Node must have editable state set to true.');
var value = this.getProcessedValue_(node) || '';
/** @private {!Array<number>} */
this.lineBreaks_ = [];
this.updateLineBreaks_(value);
var start = node.textSelStart; var start = node.textSelStart;
var end = node.textSelEnd; var end = node.textSelEnd;
var value = this.getProcessedValue_(node) || '';
cvox.ChromeVoxEditableTextBase.call( cvox.ChromeVoxEditableTextBase.call(
this, value, Math.min(start, end, value.length), this, value, Math.min(start, end, value.length),
Math.min(Math.max(start, end), value.length), Math.min(Math.max(start, end), value.length),
...@@ -149,52 +151,77 @@ AutomationEditableText.prototype = { ...@@ -149,52 +151,77 @@ AutomationEditableText.prototype = {
* @param {string|undefined} eventFrom * @param {string|undefined} eventFrom
*/ */
onUpdate: function(eventFrom) { onUpdate: function(eventFrom) {
var oldValue = this.value;
var oldStart = this.start;
var oldEnd = this.end;
var newValue = this.getProcessedValue_(this.node_) || ''; var newValue = this.getProcessedValue_(this.node_) || '';
this.updateLineBreaks_(newValue);
var textChangeEvent = new cvox.TextChangeEvent( var textChangeEvent = new cvox.TextChangeEvent(
newValue, Math.min(this.node_.textSelStart || 0, newValue.length), newValue, Math.min(this.node_.textSelStart || 0, newValue.length),
Math.min(this.node_.textSelEnd || 0, newValue.length), Math.min(this.node_.textSelEnd || 0, newValue.length),
true /* triggered by user */); true /* triggered by user */);
this.changed(textChangeEvent); this.changed(textChangeEvent);
this.outputBraille_(); this.outputBraille_(oldValue, oldStart, oldEnd);
}, },
/** /**
* Returns true if selection starts on the first line. * Returns true if selection starts on the first line.
*/ */
isSelectionOnFirstLine: function() { isSelectionOnFirstLine: function() {
return true; return this.getLineIndex(this.start) == 0;
}, },
/** /**
* Returns true if selection ends on the last line. * Returns true if selection ends on the last line.
*/ */
isSelectionOnLastLine: function() { isSelectionOnLastLine: function() {
return true; return this.getLineIndex(this.end) >= this.lineBreaks_.length - 1;
}, },
/** @override */ /** @override */
getLineIndex: function(charIndex) { getLineIndex: function(charIndex) {
return 0; var lineIndex = 0;
while (charIndex > this.lineBreaks_[lineIndex])
lineIndex++;
return lineIndex;
}, },
/** @override */ /** @override */
getLineStart: function(lineIndex) { getLineStart: function(lineIndex) {
return 0; if (lineIndex == 0)
return 0;
// The start of this line is defined as the line break of the previous line
// + 1 (the hard line break).
return this.lineBreaks_[lineIndex - 1] + 1;
}, },
/** @override */ /** @override */
getLineEnd: function(lineIndex) { getLineEnd: function(lineIndex) {
return this.node_.value.length; return this.lineBreaks_[lineIndex];
}, },
/** @private */ /** @private */
outputBraille_: function() { outputBraille_: function(oldValue, oldStart, oldEnd) {
var output = new Output(); var lineIndex = this.getLineIndex(this.start);
var range; // Output braille at the end of the selection that changed, if start and end
range = Range.fromNode(this.node_); // differ.
output.withBraille(range, null, Output.EventType.NAVIGATE); if (this.start != this.end && this.start == oldStart)
output.go(); lineIndex = this.getLineIndex(this.end);
var lineStart = this.getLineStart(lineIndex);
var lineText =
this.value.substr(lineStart, this.getLineEnd(lineIndex) - lineStart);
if (lineIndex == 0)
lineText += ' ' +
Msgs.getMsg(this.multiline ? 'tag_textarea_brl' : 'role_textbox_brl');
cvox.ChromeVox.braille.write(new cvox.NavBraille({
text: lineText,
startIndex: this.start - lineStart,
endIndex: this.end - lineStart
}));
}, },
/** /**
...@@ -205,6 +232,24 @@ AutomationEditableText.prototype = { ...@@ -205,6 +232,24 @@ AutomationEditableText.prototype = {
getProcessedValue_: function(node) { getProcessedValue_: function(node) {
var value = node.value; var value = node.value;
return (value && node.inputType == 'tel') ? value['trimEnd']() : value; return (value && node.inputType == 'tel') ? value['trimEnd']() : value;
},
/**
* @private
*/
updateLineBreaks_: function(value) {
if (value == this.value)
return;
this.lineBreaks_ = [];
var lines = value.split('\n');
for (var i = 0, total = 0; i < lines.length; i++) {
total += lines[i].length;
this.lineBreaks_[i] = total;
// Account for the line break itself.
total++;
}
} }
}; };
......
...@@ -87,15 +87,15 @@ TEST_F('ChromeVoxEditingTest', 'Multiline', function() { ...@@ -87,15 +87,15 @@ TEST_F('ChromeVoxEditingTest', 'Multiline', function() {
{startIndex: 9, endIndex: 9}) {startIndex: 9, endIndex: 9})
.call(textarea.setSelection.bind(textarea, 1, 1)) .call(textarea.setSelection.bind(textarea, 1, 1))
.expectSpeech('i') .expectSpeech('i')
.expectBraille('Line 1\nmled', .expectBraille('Line 1 mled',
{startIndex: 1, endIndex: 1}) {startIndex: 1, endIndex: 1})
.call(textarea.setSelection.bind(textarea, 7, 7)) .call(textarea.setSelection.bind(textarea, 7, 7))
.expectSpeech('line 2') .expectSpeech('line 2')
.expectBraille('line 2\n', .expectBraille('line 2',
{startIndex: 0, endIndex: 0}) {startIndex: 0, endIndex: 0})
.call(textarea.setSelection.bind(textarea, 7, 13)) .call(textarea.setSelection.bind(textarea, 7, 13))
.expectSpeech('line 2', 'selected') .expectSpeech('line 2', 'selected')
.expectBraille('line 2\n', .expectBraille('line 2',
{startIndex: 0, endIndex: 6}); {startIndex: 0, endIndex: 6});
mockFeedback.replay(); mockFeedback.replay();
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment