Commit 79aa8793 authored by Akihiro Ota's avatar Akihiro Ota Committed by Commit Bot

UserActionMonitor can stop propagation of an action, even when matched.

This change adds the ability to stop the propagation of an action, even
if it's matched by UserActionMonitor.

The motivating example for this change is
the interactive tutorial lesson that requests users to press Control.
After successfully pressing Control, the tutorial automatically moves
the user to the next lesson and reads the text. However, there are
cases where the text is never read because ChromeVox cancels speech
when Control is pressed.

The solution is to add a boolean parameter that can specify whether
or not the action should propagate, even if matched.
This ensures that speech for the next lesson is not cancelled
after completing the interactive Control key lesson.

Fixed: 1132944
AX-Relnotes: N/A
Change-Id: I6e63f7612b6fdf9aa398d46ff7746d5273aee683
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2469436Reviewed-by: default avatarDavid Tseng <dtseng@chromium.org>
Commit-Queue: Akihiro Ota <akihiroota@chromium.org>
Cr-Commit-Position: refs/heads/master@{#817264}
parent 1c7c38c5
...@@ -55,15 +55,16 @@ UserActionMonitor = class { ...@@ -55,15 +55,16 @@ UserActionMonitor = class {
this.actions_.push( this.actions_.push(
UserActionMonitor.Action.fromActionInfo(actionInfos[i])); UserActionMonitor.Action.fromActionInfo(actionInfos[i]));
} }
if (this.actions_[0].opt_beforeActionCallback) { if (this.actions_[0].beforeActionCallback) {
this.actions_[0].opt_beforeActionCallback(); this.actions_[0].beforeActionCallback();
} }
} }
// Public methods. // Public methods.
/** /**
* Returns true if the key sequence was matched. Returns false otherwise. * Returns true if the key sequence should be allowed to propagate to other
* handlers. Returns false otherwise.
* @param {!KeySequence} actualSequence * @param {!KeySequence} actualSequence
* @return {boolean} * @return {boolean}
*/ */
...@@ -85,7 +86,7 @@ UserActionMonitor = class { ...@@ -85,7 +86,7 @@ UserActionMonitor = class {
} }
this.expectedActionMatched_(); this.expectedActionMatched_();
return true; return expectedAction.shouldPropagate;
} }
// Private methods. // Private methods.
...@@ -93,8 +94,8 @@ UserActionMonitor = class { ...@@ -93,8 +94,8 @@ UserActionMonitor = class {
/** @private */ /** @private */
expectedActionMatched_() { expectedActionMatched_() {
const action = this.getExpectedAction_(); const action = this.getExpectedAction_();
if (action.opt_afterActionCallback) { if (action.afterActionCallback) {
action.opt_afterActionCallback(); action.afterActionCallback();
} }
this.nextAction_(); this.nextAction_();
...@@ -114,8 +115,8 @@ UserActionMonitor = class { ...@@ -114,8 +115,8 @@ UserActionMonitor = class {
} }
const action = this.getExpectedAction_(); const action = this.getExpectedAction_();
if (action.opt_beforeActionCallback) { if (action.beforeActionCallback) {
action.opt_beforeActionCallback(); action.beforeActionCallback();
} }
} }
...@@ -155,8 +156,9 @@ UserActionMonitor.CLOSE_CHROMEVOX_KEY_SEQUENCE_ = KeySequence.deserialize( ...@@ -155,8 +156,9 @@ UserActionMonitor.CLOSE_CHROMEVOX_KEY_SEQUENCE_ = KeySequence.deserialize(
* @typedef {{ * @typedef {{
* type: ActionType, * type: ActionType,
* value: (string|Object), * value: (string|Object),
* shouldPropagate: (boolean|undefined),
* beforeActionMsg: (string|undefined), * beforeActionMsg: (string|undefined),
* afterActionMsg: (string|undefined) * afterActionMsg: (string|undefined),
* }} * }}
*/ */
UserActionMonitor.ActionInfo; UserActionMonitor.ActionInfo;
...@@ -164,38 +166,49 @@ UserActionMonitor.ActionInfo; ...@@ -164,38 +166,49 @@ UserActionMonitor.ActionInfo;
// Represents an expected action. // Represents an expected action.
UserActionMonitor.Action = class { UserActionMonitor.Action = class {
/** /**
* @param {ActionType} type * Please see below for more information on arguments:
* @param {string|!KeySequence} value * type: The type of action.
* @param {(function():void)=} opt_beforeActionCallback Runs once before this * value: The action value.
* action is seen. * shouldPropagate: Whether or not this action should propagate to other
* @param {(function():void)=} opt_afterActionCallback Runs once after this * handlers e.g. CommandHandler.
* action is seen. * beforeActionCallback: A callback that runs once before this action is seen.
* afterActionCallback: A callback that runs once after this action is seen.
* @param {!{
* type: ActionType,
* value: (string|!KeySequence),
* shouldPropagate: (boolean|undefined),
* beforeActionCallback: (function(): void|undefined),
* afterActionCallback: (function(): void|undefined)
* }} params
*/ */
constructor(type, value, opt_beforeActionCallback, opt_afterActionCallback) { constructor(params) {
switch (type) { /** @type {ActionType} */
this.type = params.type;
/** @type {string|!KeySequence} */
this.value = params.value;
/** @type {boolean} */
this.shouldPropagate =
(params.shouldPropagate !== undefined) ? params.shouldPropagate : true;
/** @type {(function():void)|undefined} */
this.beforeActionCallback = params.beforeActionCallback;
/** @type {(function():void)|undefined} */
this.afterActionCallback = params.afterActionCallback;
switch (this.type) {
case ActionType.KEY_SEQUENCE: case ActionType.KEY_SEQUENCE:
if (!(value instanceof KeySequence)) { if (!(this.value instanceof KeySequence)) {
throw new Error( throw new Error(
'UserActionMonitor: Must provide a KeySequence value for ' + 'UserActionMonitor: Must provide a KeySequence value for ' +
'Actions of type ActionType.KEY_SEQUENCE'); 'Actions of type ActionType.KEY_SEQUENCE');
} }
break; break;
default: default:
if (typeof value !== 'string') { if (typeof this.value !== 'string') {
throw new Error( throw new Error(
'UserActionMonitor: Must provide a string value for Actions ' + 'UserActionMonitor: Must provide a string value for Actions ' +
'if type is other than ActionType.KEY_SEQUENCE'); 'if type is other than ActionType.KEY_SEQUENCE');
} }
} }
/** @type {ActionType} */
this.type = type;
/** @type {string|!KeySequence} */
this.value = value;
/** @type {(function():void)|undefined} */
this.opt_beforeActionCallback = opt_beforeActionCallback;
/** @type {(function():void)|undefined} */
this.opt_afterActionCallback = opt_afterActionCallback;
} }
/** /**
...@@ -225,6 +238,7 @@ UserActionMonitor.Action = class { ...@@ -225,6 +238,7 @@ UserActionMonitor.Action = class {
const value = (typeof info.value === 'object') ? const value = (typeof info.value === 'object') ?
KeySequence.deserialize(info.value) : KeySequence.deserialize(info.value) :
info.value; info.value;
const shouldPropagate = info.shouldPropagate;
const beforeActionMsg = info.beforeActionMsg; const beforeActionMsg = info.beforeActionMsg;
const afterActionMsg = info.afterActionMsg; const afterActionMsg = info.afterActionMsg;
...@@ -244,8 +258,13 @@ UserActionMonitor.Action = class { ...@@ -244,8 +258,13 @@ UserActionMonitor.Action = class {
UserActionMonitor.Action.output_(afterActionMsg); UserActionMonitor.Action.output_(afterActionMsg);
}; };
return new UserActionMonitor.Action( return new UserActionMonitor.Action({
type, value, beforeActionCallback, afterActionCallback); type,
value,
shouldPropagate,
beforeActionCallback,
afterActionCallback
});
} }
/** /**
......
...@@ -68,13 +68,14 @@ TEST_F('ChromeVoxUserActionMonitorTest', 'ActionUnitTest', function() { ...@@ -68,13 +68,14 @@ TEST_F('ChromeVoxUserActionMonitorTest', 'ActionUnitTest', function() {
this.runWithLoadedTree(this.simpleDoc, function() { this.runWithLoadedTree(this.simpleDoc, function() {
const keySequenceActionOne = UserActionMonitor.Action.fromActionInfo( const keySequenceActionOne = UserActionMonitor.Action.fromActionInfo(
{type: 'key_sequence', value: {keys: {keyCode: [KeyCode.SPACE]}}}); {type: 'key_sequence', value: {keys: {keyCode: [KeyCode.SPACE]}}});
const keySequenceActionTwo = new UserActionMonitor.Action( const keySequenceActionTwo = new UserActionMonitor.Action({
'key_sequence', type: 'key_sequence',
new KeySequence(TestUtils.createMockKeyEvent(KeyCode.A))); value: new KeySequence(TestUtils.createMockKeyEvent(KeyCode.A))
});
const gestureActionOne = UserActionMonitor.Action.fromActionInfo( const gestureActionOne = UserActionMonitor.Action.fromActionInfo(
{type: 'gesture', value: 'swipeUp1'}); {type: 'gesture', value: 'swipeUp1'});
const gestureActionTwo = const gestureActionTwo =
new UserActionMonitor.Action('gesture', 'swipeUp2'); new UserActionMonitor.Action({type: 'gesture', value: 'swipeUp2'});
assertFalse(keySequenceActionOne.equals(keySequenceActionTwo)); assertFalse(keySequenceActionOne.equals(keySequenceActionTwo));
assertFalse(keySequenceActionOne.equals(gestureActionOne)); assertFalse(keySequenceActionOne.equals(gestureActionOne));
...@@ -86,7 +87,7 @@ TEST_F('ChromeVoxUserActionMonitorTest', 'ActionUnitTest', function() { ...@@ -86,7 +87,7 @@ TEST_F('ChromeVoxUserActionMonitorTest', 'ActionUnitTest', function() {
const cloneKeySequenceActionOne = UserActionMonitor.Action.fromActionInfo( const cloneKeySequenceActionOne = UserActionMonitor.Action.fromActionInfo(
{type: 'key_sequence', value: {keys: {keyCode: [KeyCode.SPACE]}}}); {type: 'key_sequence', value: {keys: {keyCode: [KeyCode.SPACE]}}});
const cloneGestureActionOne = const cloneGestureActionOne =
new UserActionMonitor.Action('gesture', 'swipeUp1'); new UserActionMonitor.Action({type: 'gesture', value: 'swipeUp1'});
assertTrue(keySequenceActionOne.equals(cloneKeySequenceActionOne)); assertTrue(keySequenceActionOne.equals(cloneKeySequenceActionOne));
assertTrue(gestureActionOne.equals(cloneGestureActionOne)); assertTrue(gestureActionOne.equals(cloneGestureActionOne));
}); });
...@@ -119,7 +120,7 @@ TEST_F('ChromeVoxUserActionMonitorTest', 'Errors', function() { ...@@ -119,7 +120,7 @@ TEST_F('ChromeVoxUserActionMonitorTest', 'Errors', function() {
} }
assertCaughtAndReset(); assertCaughtAndReset();
try { try {
new UserActionMonitor.Action('key_sequence', 'invalid'); new UserActionMonitor.Action({type: 'key_sequence', value: 'invalid'});
assertTrue(false); // Shouldn't execute assertTrue(false); // Shouldn't execute
} catch (error) { } catch (error) {
assertEquals( assertEquals(
...@@ -417,3 +418,30 @@ TEST_F('ChromeVoxUserActionMonitorTest', 'CloseChromeVox', function() { ...@@ -417,3 +418,30 @@ TEST_F('ChromeVoxUserActionMonitorTest', 'CloseChromeVox', function() {
assertFalse(finished); assertFalse(finished);
}); });
}); });
// Tests that we can stop propagation of an action, even if it is matched.
// In this test, we stop propagation of the Control key to avoid executing the
// stopSpeech command.
TEST_F('ChromeVoxUserActionMonitorTest', 'StopPropagation', function() {
this.runWithLoadedTree(this.simpleDoc, function() {
const keyboardHandler = ChromeVoxState.instance.keyboardHandler_;
let finished = false;
let executedCommand = false;
const actions = [{
type: 'key_sequence',
value: {keys: {keyCode: [KeyCode.CONTROL]}},
shouldPropagate: false
}];
const onFinished = () => finished = true;
ChromeVoxState.instance.createUserActionMonitor(actions, onFinished);
ChromeVoxKbHandler.commandHandler = function(command) {
executedCommand = true;
};
assertFalse(finished);
assertFalse(executedCommand);
keyboardHandler.onKeyDown(TestUtils.createMockKeyEvent(KeyCode.CONTROL));
keyboardHandler.onKeyUp(TestUtils.createMockKeyEvent(KeyCode.CONTROL));
assertFalse(executedCommand);
assertTrue(finished);
});
});
...@@ -89,9 +89,8 @@ ChromeVoxKbHandler.basicKeyDownActionsListener = function(evt) { ...@@ -89,9 +89,8 @@ ChromeVoxKbHandler.basicKeyDownActionsListener = function(evt) {
const chromeVoxState = ChromeVoxState.instance; const chromeVoxState = ChromeVoxState.instance;
const monitor = chromeVoxState ? chromeVoxState.getUserActionMonitor() : null; const monitor = chromeVoxState ? chromeVoxState.getUserActionMonitor() : null;
if (monitor && !monitor.onKeySequence(keySequence)) { if (monitor && !monitor.onKeySequence(keySequence)) {
// UserActionMonitor returns true if this key sequence was matched. If a // UserActionMonitor returns true if this key sequence should propagate.
// key sequence is matched by the UserActionMonitor, allow it to process. // Prevent the default action if it returns false.
// Otherwise, prevent the default action.
return false; return false;
} }
......
...@@ -170,9 +170,11 @@ Polymer({ ...@@ -170,9 +170,11 @@ Polymer({
key.`], key.`],
medium: InteractionMedium.KEYBOARD, medium: InteractionMedium.KEYBOARD,
curriculums: [Curriculum.QUICK_ORIENTATION], curriculums: [Curriculum.QUICK_ORIENTATION],
actions: [ actions: [{
{type: 'key_sequence', value: {keys: {keyCode: [17 /* Ctrl */]}}} type: 'key_sequence',
], value: {keys: {keyCode: [17 /* Ctrl */]}},
shouldPropagate: false
}],
autoInteractive: true, autoInteractive: true,
}, },
......
...@@ -537,6 +537,13 @@ TEST_F('ChromeVoxTutorialTest', 'QuickOrientationLessonTest', function() { ...@@ -537,6 +537,13 @@ TEST_F('ChromeVoxTutorialTest', 'QuickOrientationLessonTest', function() {
assertNotEquals(firstLessonNode, getRangeStart()); assertNotEquals(firstLessonNode, getRangeStart());
assertEquals(1, tutorial.activeLessonNum); assertEquals(1, tutorial.activeLessonNum);
}) })
// Pressing control, which is the desired key sequence, should move us
// to the next lesson.
.call(simulateKeyPress.bind(this, KeyCode.CONTROL, {}))
.expectSpeech('Essential Keys: Shift')
.call(() => {
assertEquals(2, tutorial.activeLessonNum);
})
.replay(); .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