Commit b310c0db authored by David Tseng's avatar David Tseng Committed by Commit Bot

Add hints to ChromeVox

Bug: 809083,806470, 806469
Cq-Include-Trybots: master.tryserver.chromium.linux:closure_compilation
Change-Id: I56b36811d6125f10ee042a317156d8dd96ac06d3
Reviewed-on: https://chromium-review.googlesource.com/902405
Commit-Queue: David Tseng <dtseng@chromium.org>
Reviewed-by: default avatarDominic Mazzoni <dmazzoni@chromium.org>
Cr-Commit-Position: refs/heads/master@{#534648}
parent 631176ab
......@@ -314,6 +314,8 @@ IN_PROC_BROWSER_TEST_P(SpokenFeedbackTest, FocusShelf) {
EXPECT_EQ("Shelf", speech_monitor_.GetNextUtterance());
EXPECT_EQ("Tool bar", speech_monitor_.GetNextUtterance());
EXPECT_EQ(", window", speech_monitor_.GetNextUtterance());
EXPECT_EQ("Press Search plus Space to activate.",
speech_monitor_.GetNextUtterance());
SendKeyPress(ui::VKEY_TAB);
EXPECT_TRUE(base::MatchPattern(speech_monitor_.GetNextUtterance(), "*"));
......
......@@ -46,18 +46,23 @@ AutomationPredicate.roles = function(roles) {
/**
* Constructs a predicate given a list of roles or predicates.
* @param {{anyRole: (Array<Role>|undefined),
* anyPredicate: (Array<AutomationPredicate.Unary>|undefined)}} params
* anyPredicate: (Array<AutomationPredicate.Unary>|undefined),
* anyAttribute: (Object|undefined)}} params
* @return {AutomationPredicate.Unary}
*/
AutomationPredicate.match = function(params) {
var anyRole = params.anyRole || [];
var anyPredicate = params.anyPredicate || [];
var anyAttribute = params.anyAttribute || {};
return function(node) {
return anyRole.some(function(role) {
return role == node.role;
}) ||
anyPredicate.some(function(p) {
return p(node);
}) ||
Object.keys(anyAttribute).some(function(key) {
return node[key] === anyAttribute[key];
});
};
};
......@@ -393,6 +398,16 @@ AutomationPredicate.checkable = AutomationPredicate.roles([
Role.MENU_ITEM_RADIO, Role.TREE_ITEM
]);
/**
* Returns if the node is clickable.
* @param {!AutomationNode} node
* @return {boolean}
*/
AutomationPredicate.clickable = AutomationPredicate.match({
anyPredicate: [AutomationPredicate.button, AutomationPredicate.link],
anyAttribute: {clickable: true}
});
// Table related predicates.
/**
* Returns if the node has a cell like role.
......
......@@ -139,16 +139,16 @@ TEST_F('BackgroundTest', 'ForwardBackwardNavigation', function() {
mockFeedback.call(doCmd('nextLink'))
.expectSpeech('alpha', 'Link')
.expectBraille('alpha lnk');
.expectBraille('alpha lnk Press Search+Space to activate.');
mockFeedback.call(doCmd('nextLink'))
.expectSpeech('beta', 'Link')
.expectBraille('beta lnk');
.expectBraille('beta lnk Press Search+Space to activate.');
mockFeedback.call(doCmd('nextLink'))
.expectSpeech('delta', 'Link')
.expectBraille('delta lnk');
.expectBraille('delta lnk Press Search+Space to activate.');
mockFeedback.call(doCmd('previousLink'))
.expectSpeech('beta', 'Link')
.expectBraille('beta lnk');
.expectBraille('beta lnk Press Search+Space to activate.');
mockFeedback.call(doCmd('nextHeading'))
.expectSpeech('charlie', 'Heading 1')
.expectBraille('charlie h1');
......@@ -161,10 +161,10 @@ TEST_F('BackgroundTest', 'ForwardBackwardNavigation', function() {
mockFeedback.call(doCmd('nextObject'))
.expectSpeech('delta', 'Link')
.expectBraille('delta lnk');
.expectBraille('delta lnk Press Search+Space to activate.');
mockFeedback.call(doCmd('nextObject'))
.expectSpeech('echo', 'Link')
.expectBraille('echo lnk');
.expectBraille('echo lnk Press Search+Space to activate.');
mockFeedback.call(doCmd('nextObject'))
.expectSpeech('foxtraut', 'Heading 2')
.expectBraille('foxtraut h2');
......@@ -265,7 +265,7 @@ TEST_F('BackgroundTest', 'SelectSingleBasic', function() {
var incrementSelectedIndex =
this.incrementSelectedIndex.bind(this, undefined, '#fruitSelect');
mockFeedback.expectSpeech('apple', 'has pop up', 'Collapsed')
.expectBraille('apple btn +popup +')
.expectBraille('apple btn +popup + Press Search+Space to activate.')
.call(incrementSelectedIndex)
.expectSpeech('grape', /2 of 3/)
.expectBraille('grape mnuitm 2/3 (x)')
......@@ -307,7 +307,8 @@ TEST_F('BackgroundTest', 'AriaLabel', function() {
rootNode.find({role: RoleType.LINK}).focus();
mockFeedback.expectSpeech('foo')
.expectSpeech('Link')
.expectBraille('foo lnk');
.expectSpeech('Press Search+Space to activate.')
.expectBraille('foo lnk Press Search+Space to activate.');
mockFeedback.replay();
}
);
......
......@@ -238,8 +238,10 @@ CommandHandler.onCommand = function(command) {
}
// Require a current range.
if (!ChromeVoxState.instance.currentRange_)
if (!ChromeVoxState.instance.currentRange_) {
new Output().format('@warning_no_current_range').go();
return true;
}
var current = ChromeVoxState.instance.currentRange_;
......@@ -514,6 +516,7 @@ CommandHandler.onCommand = function(command) {
newRange.select();
new Output()
.withoutHints()
.withRichSpeechAndBraille(
ChromeVoxState.instance.currentRange_, prevRange,
Output.EventType.NAVIGATE)
......
......@@ -97,6 +97,9 @@ Output = function() {
/** @private {!Object<string, boolean>} */
this.suppressions_ = {};
/** @private {boolean} */
this.enableHints_ = true;
};
/**
......@@ -783,6 +786,15 @@ Output.prototype = {
return this;
},
/**
* Don't include hints in subsequent output.
* @return {Output}
*/
withoutHints: function() {
this.enableHints_ = false;
return this;
},
/**
* Suppresses processing of a token for subsequent formatting commands.
* @param {string} token
......@@ -956,6 +968,8 @@ Output.prototype = {
this.subNode_(range, prevRange, type, buff);
else
this.range_(range, prevRange, type, buff);
this.hint_(range, uniqueAncestors, buff);
},
/**
......@@ -1708,6 +1722,39 @@ Output.prototype = {
this.locations_.push(loc);
},
hint_: function(range, uniqueAncestors, buff) {
if (!this.enableHints_ || localStorage['useVerboseMode'] != 'true')
return;
var node = range.start.node;
if (!node) {
this.append_(buff, Msgs.getMsg('warning_no_current_range'));
return;
}
// Prioritized hints.
if (node.state[StateType.EDITABLE] && cvox.ChromeVox.isStickyPrefOn)
this.format_(node, '@sticky_mode_enabled', buff);
if (AutomationPredicate.checkable(node))
this.format_(node, '@hint_checkable', buff);
if (AutomationPredicate.clickable(node))
this.format_(node, '@hint_clickable', buff);
if (node.autoComplete == 'list' || node.autoComplete == 'both')
this.format_(node, '@hint_autocomplete_list', buff);
if (node.autoComplete == 'inline' || node.autoComplete == 'both')
this.format_(node, '@hint_autocomplete_inline', buff);
if (node.accessKey)
this.append_(buff, Msgs.getMsg('access_key', [node.accessKey]));
// Ancestry based hints.
if (uniqueAncestors.find(AutomationPredicate.table))
this.format_(node, '@hint_table', buff);
if (uniqueAncestors.find(
AutomationPredicate.roles([RoleType.MENU, RoleType.MENU_BAR])))
this.format_(node, '@hint_menu', buff);
},
/**
* Appends output to the |buff|.
* @param {!Array<Spannable>} buff
......
......@@ -111,7 +111,8 @@ TEST_F('OutputE2ETest', 'Links', function() {
var el = root.firstChild.firstChild;
var range = cursors.Range.fromNode(el);
var o = new Output().withSpeechAndBraille(range, null, 'navigate');
assertEqualsJSON({string_: 'Click here|Link', 'spans_': [
assertEqualsJSON({string_:
'Click here|Link|Press Search+Space to activate.', 'spans_': [
// Attributes.
{value: 'name', start: 0, end: 10},
......@@ -121,7 +122,7 @@ TEST_F('OutputE2ETest', 'Links', function() {
{value: 'role', start: 11, end: 15}
]}, o.speechOutputForTest);
checkBrailleOutput(
'Click here lnk',
'Click here lnk Press Search+Space to activate.',
[{value: new Output.NodeSpan(el), start: 0, end: 14}],
o);
});
......@@ -133,14 +134,14 @@ TEST_F('OutputE2ETest', 'Checkbox', function() {
var el = root.firstChild.firstChild;
var range = cursors.Range.fromNode(el);
var o = new Output().withSpeechAndBraille(range, null, 'navigate');
checkSpeechOutput('|Check box|Not checked',
checkSpeechOutput('|Check box|Not checked|Press Search+Space to toggle.',
[
{value: new Output.EarconAction('CHECK_OFF'), start: 0, end: 0},
{value: 'role', start: 1, end: 10}
],
o);
checkBrailleOutput(
'chk ( )',
'chk ( ) Press Search+Space to toggle.',
[{value: new Output.NodeSpan(el), start: 0, end: 7}],
o);
});
......@@ -225,7 +226,9 @@ TEST_F('OutputE2ETest', 'Audio', function() {
function(root) {
var el = root.find({role: 'button'});
var range = cursors.Range.fromNode(el);
var o = new Output().withSpeechAndBraille(range, null, 'navigate');
var o = new Output()
.withoutHints()
.withSpeechAndBraille(range, null, 'navigate');
checkSpeechOutput('play|Button|begin playback|audio|Tool bar',
[
......@@ -245,7 +248,9 @@ TEST_F('OutputE2ETest', 'Audio', function() {
el = el.nextSibling.nextSibling;
var prevRange = range;
range = cursors.Range.fromNode(el);
var o = new Output().withSpeechAndBraille(range, prevRange, 'navigate');
var o = new Output()
.withoutHints()
.withSpeechAndBraille(range, prevRange, 'navigate');
checkSpeechOutput('0|Min 0|Max 0||Slider|audio time scrubber',
[
{value: 'valueForRange', start: 0, end: 1},
......@@ -329,7 +334,9 @@ TEST_F('OutputE2ETest', 'Input', function() {
var el = root.firstChild.firstChild;
expectedSpeechValues.forEach(function(expectedValue) {
var range = cursors.Range.fromNode(el);
var o = new Output().withSpeechAndBraille(range, null, 'navigate');
var o = new Output()
.withoutHints()
.withSpeechAndBraille(range, null, 'navigate');
if (typeof expectedValue == 'object') {
checkSpeechOutput(expectedValue[0], expectedValue[1], o);
} else {
......@@ -342,7 +349,9 @@ TEST_F('OutputE2ETest', 'Input', function() {
el = root.firstChild.firstChild;
expectedBrailleValues.forEach(function(expectedValue) {
var range = cursors.Range.fromNode(el);
var o = new Output().withBraille(range, null, 'navigate');
var o = new Output()
.withoutHints()
.withBraille(range, null, 'navigate');
if (typeof expectedValue === 'string') {
checkBrailleOutput(
expectedValue,
......@@ -630,7 +639,8 @@ TEST_F('OutputE2ETest', 'ToggleButton', function() {
function(root) {
var el = root.firstChild;
var o = new Output().withSpeech(cursors.Range.fromNode(el));
assertEqualsJSON({string_: '|Subscribe|Button|Pressed', spans_: [
assertEqualsJSON({string_: '|Subscribe|Button|Pressed|Press Search+Space to activate.',
spans_: [
{value: {earconId: 'CHECK_ON'}, start: 0, end: 0},
{value: 'name', start: 1, end:10},
{value: 'role', start: 11, end: 17}
......@@ -740,7 +750,9 @@ TEST_F('OutputE2ETest', 'RangeOutput', function() {
*/}, function(root) {
var obj = root.find({role: RoleType.SLIDER});
var o = new Output().withSpeech(cursors.Range.fromNode(obj));
var o = new Output()
.withoutHints()
.withSpeech(cursors.Range.fromNode(obj));
checkSpeechOutput('2|Min 1|Max 10|volume|Slider',
[
{value: 'valueForRange', start: 0, end: 1},
......@@ -751,7 +763,9 @@ TEST_F('OutputE2ETest', 'RangeOutput', function() {
o);
obj = root.find({role: RoleType.PROGRESS_INDICATOR});
o = new Output().withSpeech(cursors.Range.fromNode(obj));
o = new Output()
.withoutHints()
.withSpeech(cursors.Range.fromNode(obj));
checkSpeechOutput('2|Min 1|Max 10|volume|Progress indicator',
[
{value: 'valueForRange', start: 0, end: 1},
......@@ -761,7 +775,9 @@ TEST_F('OutputE2ETest', 'RangeOutput', function() {
o);
obj = root.find({role: RoleType.METER});
o = new Output().withSpeech(cursors.Range.fromNode(obj));
o = new Output()
.withoutHints()
.withSpeech(cursors.Range.fromNode(obj));
checkSpeechOutput('2|Min 1|Max 10|volume|Meter',
[
{value: 'valueForRange', start: 0, end: 1},
......@@ -771,7 +787,9 @@ TEST_F('OutputE2ETest', 'RangeOutput', function() {
o);
obj = root.find({role: RoleType.SPIN_BUTTON});
o = new Output().withSpeech(cursors.Range.fromNode(obj));
o = new Output()
.withoutHints()
.withSpeech(cursors.Range.fromNode(obj));
checkSpeechOutput('2|Min 1|Max 10|volume|Spin button',
[
{value: 'valueForRange', start: 0, end: 1},
......@@ -788,7 +806,9 @@ TEST_F('OutputE2ETest', 'RoleDescription', function() {
<div aria-label="hi" role="button" aria-roledescription="foo"></div>
*/}, function(root) {
var obj = root.find({role: RoleType.BUTTON});
var o = new Output().withSpeech(cursors.Range.fromNode(obj));
var o = new Output()
.withoutHints()
.withSpeech(cursors.Range.fromNode(obj));
checkSpeechOutput('hi|foo',
[
{value: 'name', start: 0, end: 2},
......
......@@ -316,6 +316,7 @@ PanelNodeMenu.prototype = {
if (this.pred_(node)) {
var output = new Output();
var range = cursors.Range.fromNode(node);
output.withoutHints();
output.withSpeech(range, range, Output.EventType.NAVIGATE);
var label = output.toString();
this.addMenuItem(label, '', '', (function() {
......
......@@ -2783,6 +2783,27 @@ If you're done with the tutorial, use ChromeVox to navigate to the Close button
<message desc="Shown to a user when they invoke the read current title command in a context without a title." name="IDS_CHROMEVOX_NO_TITLE">
No title
</message>
<message desc="Spoken when a user issues a command when nothing is focused." name="IDS_CHROMEVOX_WARNING_NO_CURRENT_RANGE">
No focus. Press Ctrl+T to open a new tab.
</message>
<message desc="A hint to the user that the current control is checkable." name="IDS_CHROMEVOX_HINT_CHECKABLE">
Press Search+Space to toggle.
</message>
<message desc="A hint to the user that the current control is clickable." name="IDS_CHROMEVOX_HINT_CLICKABLE">
Press Search+Space to activate.
</message>
<message desc="A hint to the user that the current control has a list of auto completions." name="IDS_CHROMEVOX_HINT_AUTOCOMPLETE_LIST">
Press up or down arrow for auto completions.
</message>
<message desc="A hint to the user that the current control has inline auto completions." name="IDS_CHROMEVOX_HINT_AUTOCOMPLETE_INLINE">
Type to auto complete.
</message>
<message desc="A hint to the user for interacting with the table control." name="IDS_CHROMEVOX_HINT_TABLE">
Press Search+Ctrl+Alt with arrows to navigate by cell.
</message>
<message desc="A hint to the user for interacting with the menu control." name="IDS_CHROMEVOX_HINT_MENU">
Press up or down arrow to navigate; enter to activate.
</message>
</messages>
</release>
</grit>
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