Commit 2b424064 authored by Akihiro Ota's avatar Akihiro Ota Committed by Commit Bot

ChromeVox OOBE Tutorial

This change:
1. Fills in content for the tutorial given in the OOBE.
2. Automatically opens the OOBE tutorial if ChromeVox is started from
the OOBE. Only do this if the
--enable-experimental-accessibility-chromevox-tutorial flag is enabled.
3. Adds a Panel method for creating the interactive tutorial. The logic
for this function existed prior to this change; we add this method for
better cleanliness and readability.

Bug: 1075752
Change-Id: I3aaefb2a934cde078747bfee105c1427af305400
AX-Relnotes: N/A
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2330048
Commit-Queue: Akihiro Ota <akihiroota@chromium.org>
Reviewed-by: default avatarDavid Tseng <dtseng@chromium.org>
Cr-Commit-Position: refs/heads/master@{#796205}
parent 81b4743d
...@@ -183,6 +183,24 @@ Background = class extends ChromeVoxState { ...@@ -183,6 +183,24 @@ Background = class extends ChromeVoxState {
onTtsInterrupted() {} onTtsInterrupted() {}
}; };
new ProgressPlayer(); new ProgressPlayer();
chrome.commandLinePrivate.hasSwitch(
'enable-experimental-accessibility-chromevox-tutorial', (enabled) => {
if (!enabled) {
return;
}
chrome.loginState.getSessionState((sessionState) => {
// If starting ChromeVox from OOBE, start the ChromeVox tutorial.
// Use a timeout to allow ChromeVox to initialize first.
if (sessionState ===
chrome.loginState.SessionState.IN_OOBE_SCREEN) {
setTimeout(() => {
(new PanelCommand(PanelCommandType.TUTORIAL)).send();
}, 1000);
}
});
});
} }
/** /**
......
...@@ -101,7 +101,6 @@ h1 { ...@@ -101,7 +101,6 @@ h1 {
hidden$="[[ shouldHideMainMenu(activeScreen) ]]"> hidden$="[[ shouldHideMainMenu(activeScreen) ]]">
<h1 id="mainMenuHeader" tabindex="-1">[[ chooseYourExperience ]]</h1> <h1 id="mainMenuHeader" tabindex="-1">[[ chooseYourExperience ]]</h1>
<div id="mainMenuButtons"> <div id="mainMenuButtons">
<cr-button id="oobeButton" on-click="chooseCurriculum">OOBE</cr-button>
<cr-button id="newUserButton" on-click="chooseCurriculum"> <cr-button id="newUserButton" on-click="chooseCurriculum">
[[ newUser ]] [[ newUser ]]
</cr-button> </cr-button>
...@@ -125,8 +124,8 @@ h1 { ...@@ -125,8 +124,8 @@ h1 {
hidden$="[[ shouldHideLessonContainer(activeScreen) ]]"> hidden$="[[ shouldHideLessonContainer(activeScreen) ]]">
<!-- Use lessonData object to create all lessons --> <!-- Use lessonData object to create all lessons -->
<template is="dom-repeat" items="[[ lessonData ]]" as="lesson" <template id="lessonTemplate" is="dom-repeat" items="[[ lessonData ]]"
index-as="index"> as="lesson" index-as="index">
<tutorial-lesson <tutorial-lesson
lesson-num="[[ index ]]" lesson-num="[[ index ]]"
title="[[ lesson.title ]]" title="[[ lesson.title ]]"
...@@ -161,13 +160,18 @@ h1 { ...@@ -161,13 +160,18 @@ h1 {
[[ nextLesson ]] [[ nextLesson ]]
</cr-button> </cr-button>
<cr-button on-click="showLessonMenu" <cr-button on-click="showLessonMenu"
hidden$="[[ !shouldHideLessonMenu(activeScreen) ]]"> hidden$="[[ shouldHideLessonMenuButton(activeScreen) ]]">
[[ lessonMenu ]] [[ lessonMenu ]]
</cr-button> </cr-button>
<cr-button on-click="showMainMenu" <cr-button on-click="showMainMenu"
hidden$="[[ !shouldHideMainMenu(activeScreen) ]]"> hidden$="[[ shouldHideMainMenuButton(activeScreen) ]]">
[[ mainMenu ]] [[ mainMenu ]]
</cr-button> </cr-button>
<cr-button on-click="showFirstLesson"
hidden$="[[
shouldHideRestartBasicOrientationButton(activeLessonIndex) ]]">
[[ restartBasicOrientation ]]
</cr-button>
<cr-button on-click="exit">[[ exitTutorial ]]</cr-button> <cr-button on-click="exit">[[ exitTutorial ]]</cr-button>
</div> </div>
</div> </div>
......
...@@ -30,6 +30,7 @@ const InteractionMedium = { ...@@ -30,6 +30,7 @@ const InteractionMedium = {
* @enum {string} * @enum {string}
*/ */
const Screen = { const Screen = {
NONE: 'none',
MAIN_MENU: 'main_menu', MAIN_MENU: 'main_menu',
LESSON_MENU: 'lesson_menu', LESSON_MENU: 'lesson_menu',
LESSON: 'lesson', LESSON: 'lesson',
...@@ -79,6 +80,8 @@ Polymer({ ...@@ -79,6 +80,8 @@ Polymer({
continue: {type: String, value: 'Continue where I left off'}, continue: {type: String, value: 'Continue where I left off'},
restartBasicOrientation: {type: String, value: 'Restart basic orientation'},
previousLesson: {type: String, value: 'Previous lesson'}, previousLesson: {type: String, value: 'Previous lesson'},
nextLesson: {type: String, value: 'Next lesson'}, nextLesson: {type: String, value: 'Next lesson'},
...@@ -93,11 +96,14 @@ Polymer({ ...@@ -93,11 +96,14 @@ Polymer({
type: Array, type: Array,
value: [ value: [
{ {
content: content: [`Welcome to the ChromeVox tutorial. We noticed this might be
['Welcome to the ChromeVox tutorial. We noticed this might be ' + your first time using ChromeVox, so let's quickly cover the
'your first time using ChromeVox, so let\'s quickly cover the ' + basics. To exit this tutorial at any time, press the Escape
`basics. When you're ready, use the space bar to move to the ` + key, which is located in the top left corner of the
'next lesson.'], keyboard. To turn off ChromeVox, exit this tutorial and press
Control, Alt, and Z
at the same time. When you're ready, use the space bar to move
to the next lesson.`],
medium: InteractionMedium.KEYBOARD, medium: InteractionMedium.KEYBOARD,
curriculums: [Curriculum.OOBE], curriculums: [Curriculum.OOBE],
actions: [ actions: [
...@@ -107,19 +113,49 @@ Polymer({ ...@@ -107,19 +113,49 @@ Polymer({
}, },
{ {
content: [ content:
`Let's learn how to navigate the tutorial first. ` + [`Let's start with the keyboard layout. There are a few keys that
'In ChromeVox, the Search key is the modifier key. ' + are crucial to know when using ChromeVox: Control, Shift,
'Most ChromeVox shortcuts start with the Search key. ' + Search, and the Arrow keys. To continue to the next lesson,
'On the Chromebook, the Search key is immediately above the ' + find and press the Control key, which is located in the bottom
`left Shift key. When you're ready, try finding and ` + left corner of the keyboard`],
'pressing the search key on your keyboard', medium: InteractionMedium.KEYBOARD,
curriculums: [Curriculum.OOBE],
actions: [
{type: 'key_sequence', value: {keys: {keyCode: [17 /* Ctrl */]}}}
],
autoInteractive: true,
},
{
content:
[`The Control key can be used at any time to silence any current
speech; however, it does not prevent any new speech from being
announced. To continue to the next lesson, find and press
the left Shift key, which is directly above the Control key.`],
medium: InteractionMedium.KEYBOARD,
curriculums: [Curriculum.OOBE],
actions: [
{type: 'key_sequence', value: {keys: {keyCode: [16 /* Shift */]}}}
], ],
autoInteractive: true,
},
{
content:
[`We'll talk more about the shift key in later lessons, as it is
used in many ChromeVox commands. Let's
move on to the Search Key, which is arguably the most important
key to know when using ChromeVox. In ChromeVox, the Search key
is the modifier key. Most ChromeVox shortcuts start with the
Search key. On a Chromebook, the Search key is immediately
above the left Shift key. To continue to the next lesson,
find and press the Search key.`],
medium: InteractionMedium.KEYBOARD, medium: InteractionMedium.KEYBOARD,
curriculums: [Curriculum.OOBE], curriculums: [Curriculum.OOBE],
actions: [{ actions: [{
type: 'key_sequence', type: 'key_sequence',
value: {skipStripping: false, keys: {keyCode: [91 /* Search*/]}}, value: {skipStripping: false, keys: {keyCode: [91 /* Search */]}},
afterActionMsg: 'You found the search key!', afterActionMsg: 'You found the search key!',
}], }],
autoInteractive: true, autoInteractive: true,
...@@ -127,14 +163,15 @@ Polymer({ ...@@ -127,14 +163,15 @@ Polymer({
{ {
content: [ content: [
`Now that you know where the search key is, let's learn ` + `Now that you know where the search key is, let's learn
'some basic navigation. While holding search, use the arrow ' + some basic navigation. While holding search, use the arrow
'keys to move ChromeVox around the screen. Press Search + ' + keys to move ChromeVox around the screen. Press Search +
'right arrow to move to the next instruction', right arrow to move to the next instruction`,
'You can use Search + right arrow and search + left arrow to ' + `You can use Search + right arrow and search + left arrow to
'navigate the tutorial. Now press Search + right arrow again.', move ChromeVox around the screen. Try pressing Search +
'If you reach an item you want to click, press Search + Space. ' + right arrow again.`,
'Try doing so now to move to the next lesson', `If you reach an item you want to click, press Search + Space.
Try doing so now to move to the next lesson`,
], ],
medium: InteractionMedium.KEYBOARD, medium: InteractionMedium.KEYBOARD,
curriculums: [Curriculum.OOBE], curriculums: [Curriculum.OOBE],
...@@ -160,10 +197,11 @@ Polymer({ ...@@ -160,10 +197,11 @@ Polymer({
{ {
title: 'Basic orientation complete!', title: 'Basic orientation complete!',
content: [ content: [
'Well done! You have learned the basics of ChromeVox. You can ' + `Well done! You have learned the basics of ChromeVox. You can
`now continue browsing the tutorial with what you've ` + replay the basic orientation or exit this tutorial by
'learned, or exit by finding and pressing the Quit Tutorial ' + finding and clicking on a button below.`,
'button', `After you setup your device, you can come back to this tutorial
and view more lessons by pressing Search + O, then T.`,
], ],
medium: InteractionMedium.KEYBOARD, medium: InteractionMedium.KEYBOARD,
curriculums: [Curriculum.OOBE], curriculums: [Curriculum.OOBE],
...@@ -341,7 +379,7 @@ Polymer({ ...@@ -341,7 +379,7 @@ Polymer({
], ],
medium: InteractionMedium.KEYBOARD, medium: InteractionMedium.KEYBOARD,
curriculums: [ curriculums: [
Curriculum.OOBE, Curriculum.NEW_USER, Curriculum.DEVELOPER, Curriculum.NEW_USER, Curriculum.DEVELOPER,
Curriculum.EXPERIENCED_USER Curriculum.EXPERIENCED_USER
], ],
} }
...@@ -351,7 +389,24 @@ Polymer({ ...@@ -351,7 +389,24 @@ Polymer({
/** @override */ /** @override */
ready() { ready() {
document.addEventListener('keydown', this.onKeyDown.bind(this));
this.hideAllScreens();
this.$.lessonTemplate.addEventListener('dom-change', (evt) => {
// Executes once all lessons have been added to the dom.
this.show();
});
},
/** Shows the tutorial */
show() {
if (this.curriculum === Curriculum.OOBE) {
// If opening the tutorial from the OOBE, automatically show the first
// lesson.
this.updateIncludedLessons();
this.showLesson(0);
} else {
this.showMainMenu(); this.showMainMenu();
}
}, },
/** /**
...@@ -366,15 +421,12 @@ Polymer({ ...@@ -366,15 +421,12 @@ Polymer({
this.curriculum = Curriculum.EXPERIENCED_USER; this.curriculum = Curriculum.EXPERIENCED_USER;
} else if (id === 'developerButton') { } else if (id === 'developerButton') {
this.curriculum = Curriculum.DEVELOPER; this.curriculum = Curriculum.DEVELOPER;
} else if (id === 'oobeButton') {
this.curriculum = Curriculum.OOBE;
} else { } else {
throw new Error('Invalid target for chooseCurriculum: ' + evt.target.id); throw new Error('Invalid target for chooseCurriculum: ' + evt.target.id);
} }
this.showLessonMenu(); this.showLessonMenu();
}, },
/** @private */
showNextLesson() { showNextLesson() {
this.showLesson(this.activeLessonIndex + 1); this.showLesson(this.activeLessonIndex + 1);
}, },
...@@ -384,6 +436,11 @@ Polymer({ ...@@ -384,6 +436,11 @@ Polymer({
this.showLesson(this.activeLessonIndex - 1); this.showLesson(this.activeLessonIndex - 1);
}, },
/** @private */
showFirstLesson() {
this.showLesson(0);
},
/** /**
* @param {number} index * @param {number} index
* @private * @private
...@@ -403,12 +460,18 @@ Polymer({ ...@@ -403,12 +460,18 @@ Polymer({
const lesson = this.includedLessons[this.activeLessonIndex]; const lesson = this.includedLessons[this.activeLessonIndex];
if (lesson.autoInteractive) { if (lesson.autoInteractive) {
this.startInteractiveMode(lesson.actions); this.startInteractiveMode(lesson.actions);
} else {
this.stopInteractiveMode();
} }
}, },
// Methods for hiding and showing screens. // Methods for hiding and showing screens.
/** @private */
hideAllScreens() {
this.activeScreen = Screen.NONE;
},
/** @private */ /** @private */
showMainMenu() { showMainMenu() {
this.activeScreen = Screen.MAIN_MENU; this.activeScreen = Screen.MAIN_MENU;
...@@ -476,12 +539,8 @@ Polymer({ ...@@ -476,12 +539,8 @@ Polymer({
* @private * @private
*/ */
shouldHideNextLessonButton(activeLessonIndex, activeScreen) { shouldHideNextLessonButton(activeLessonIndex, activeScreen) {
if (activeLessonIndex === this.numLessons - 1 || return activeLessonIndex === this.numLessons - 1 ||
activeScreen !== Screen.LESSON) { activeScreen !== Screen.LESSON;
return true;
}
return false;
}, },
/** /**
...@@ -491,11 +550,17 @@ Polymer({ ...@@ -491,11 +550,17 @@ Polymer({
* @private * @private
*/ */
shouldHidePreviousLessonButton(activeLessonIndex, activeScreen) { shouldHidePreviousLessonButton(activeLessonIndex, activeScreen) {
if (activeLessonIndex === 0 || activeScreen !== Screen.LESSON) { return activeLessonIndex === 0 || activeScreen !== Screen.LESSON;
return true; },
}
return false; /**
* @param {Screen} activeScreen
* @return {boolean}
* @private
*/
shouldHideLessonMenuButton(activeScreen) {
return !this.curriculum || this.curriculum === Curriculum.OOBE ||
activeScreen === Screen.LESSON_MENU;
}, },
/** /**
...@@ -503,12 +568,21 @@ Polymer({ ...@@ -503,12 +568,21 @@ Polymer({
* @return {boolean} * @return {boolean}
* @private * @private
*/ */
shouldHideMainMenu(activeScreen) { shouldHideMainMenuButton(activeScreen) {
if (activeScreen === Screen.MAIN_MENU) { return this.curriculum === Curriculum.OOBE ||
return false; activeScreen === Screen.MAIN_MENU;
} },
return true; /**
* @param {number} activeLessonIndex
* @return {boolean}
* @private
*/
shouldHideRestartBasicOrientationButton(activeLessonIndex) {
// Only show when the user has completed the basic orientation.
return !(
this.curriculum === Curriculum.OOBE &&
activeLessonIndex === this.numLessons - 1);
}, },
/** /**
...@@ -516,12 +590,17 @@ Polymer({ ...@@ -516,12 +590,17 @@ Polymer({
* @return {boolean} * @return {boolean}
* @private * @private
*/ */
shouldHideLessonContainer(activeScreen) { shouldHideMainMenu(activeScreen) {
if (activeScreen === Screen.LESSON) { return activeScreen !== Screen.MAIN_MENU;
return false; },
}
return true; /**
* @param {Screen} activeScreen
* @return {boolean}
* @private
*/
shouldHideLessonContainer(activeScreen) {
return activeScreen !== Screen.LESSON;
}, },
/** /**
...@@ -530,11 +609,7 @@ Polymer({ ...@@ -530,11 +609,7 @@ Polymer({
* @private * @private
*/ */
shouldHideLessonMenu(activeScreen) { shouldHideLessonMenu(activeScreen) {
if (activeScreen === Screen.LESSON_MENU) { return activeScreen !== Screen.LESSON_MENU;
return false;
}
return true;
}, },
/** /**
...@@ -554,8 +629,35 @@ Polymer({ ...@@ -554,8 +629,35 @@ Polymer({
// Interactive mode. // Interactive mode.
/**
* @param {!Array<{
* type: string,
* value: (string|Object),
* beforeActionMsg: (string|undefined),
* afterActionMsg: (string|undefined)}>} actions
* @private
*/
startInteractiveMode(actions) { startInteractiveMode(actions) {
this.dispatchEvent(new CustomEvent( this.dispatchEvent(new CustomEvent(
'startinteractivemode', {composed: true, detail: {actions}})); 'startinteractivemode', {composed: true, detail: {actions}}));
}, },
/** @private */
stopInteractiveMode() {
this.dispatchEvent(
new CustomEvent('stopinteractivemode', {composed: true}));
},
/**
* @param {Event} evt
* @private
*/
onKeyDown(evt) {
const key = evt.key;
if (key === 'Escape') {
this.exit();
evt.preventDefault();
evt.stopPropagation();
}
},
}); });
...@@ -84,6 +84,10 @@ export const TutorialLesson = Polymer({ ...@@ -84,6 +84,10 @@ export const TutorialLesson = Polymer({
'A lesson must have an element which specifies tabindex.'); 'A lesson must have an element which specifies tabindex.');
} }
focus.focus(); focus.focus();
if (!focus.isEqualNode(this.shadowRoot.activeElement)) {
// Call show() again if we weren't able to focus the target element.
setTimeout(this.show.bind(this), 500);
}
}, },
/** @private */ /** @private */
......
...@@ -189,7 +189,6 @@ Panel = class { ...@@ -189,7 +189,6 @@ Panel = class {
/** @private {boolean} */ /** @private {boolean} */
Panel.iTutorialEnabled_ = false; Panel.iTutorialEnabled_ = false;
chrome.commandLinePrivate.hasSwitch( chrome.commandLinePrivate.hasSwitch(
'enable-experimental-accessibility-chromevox-tutorial', (enabled) => { 'enable-experimental-accessibility-chromevox-tutorial', (enabled) => {
Panel.iTutorialEnabled_ = enabled; Panel.iTutorialEnabled_ = enabled;
...@@ -1136,11 +1135,37 @@ Panel = class { ...@@ -1136,11 +1135,37 @@ Panel = class {
* @param {string=} opt_page Show a specific page. * @param {string=} opt_page Show a specific page.
*/ */
static onTutorial(opt_page) { static onTutorial(opt_page) {
// Change the url fragment to 'fullscreen', which signals the native
// host code to make the window fullscreen, revealing the menus.
if (Panel.iTutorialEnabled_) { if (Panel.iTutorialEnabled_) {
if ($('i-tutorial') === null) { if (!$('i-tutorial')) {
// Load resources if this is the first time opening the tutorial. const curriculum = Panel.sessionState ===
chrome.loginState.SessionState.IN_OOBE_SCREEN ?
'oobe' :
null;
Panel.createITutorial(curriculum);
}
Panel.setMode(Panel.Mode.FULLSCREEN_I_TUTORIAL);
if (Panel.iTutorial.show) {
Panel.iTutorial.show();
}
return;
}
Panel.setMode(Panel.Mode.FULLSCREEN_TUTORIAL);
switch (opt_page) {
case 'updateNotes':
Panel.tutorial_.updateNotes();
break;
default:
Panel.tutorial_.lastViewedPage();
}
}
/**
* Creates an <i-tutorial> element and adds it to the dom.
* @param {(string|null)} curriculum
*/
static createITutorial(curriculum) {
const tutorialScript = document.createElement('script'); const tutorialScript = document.createElement('script');
tutorialScript.src = '../i_tutorial/i_tutorial.js'; tutorialScript.src = '../i_tutorial/i_tutorial.js';
tutorialScript.setAttribute('type', 'module'); tutorialScript.setAttribute('type', 'module');
...@@ -1156,6 +1181,9 @@ Panel = class { ...@@ -1156,6 +1181,9 @@ Panel = class {
tutorialContainer.hidden = true; tutorialContainer.hidden = true;
const tutorialElement = document.createElement('i-tutorial'); const tutorialElement = document.createElement('i-tutorial');
tutorialElement.setAttribute('id', 'i-tutorial'); tutorialElement.setAttribute('id', 'i-tutorial');
if (curriculum) {
tutorialElement.curriculum = curriculum;
}
tutorialContainer.appendChild(tutorialElement); tutorialContainer.appendChild(tutorialElement);
document.body.appendChild(tutorialContainer); document.body.appendChild(tutorialContainer);
Panel.iTutorial = tutorialElement; Panel.iTutorial = tutorialElement;
...@@ -1164,8 +1192,7 @@ Panel = class { ...@@ -1164,8 +1192,7 @@ Panel = class {
$('i-tutorial').addEventListener('closetutorial', (evt) => { $('i-tutorial').addEventListener('closetutorial', (evt) => {
// Ensure UserActionMonitor is destroyed before closing tutorial. // Ensure UserActionMonitor is destroyed before closing tutorial.
const background = const background =
chrome.extension chrome.extension.getBackgroundPage()['ChromeVoxState']['instance'];
.getBackgroundPage()['ChromeVoxState']['instance'];
background.destroyUserActionMonitor(); background.destroyUserActionMonitor();
Panel.onCloseTutorial(); Panel.onCloseTutorial();
}); });
...@@ -1174,33 +1201,22 @@ Panel = class { ...@@ -1174,33 +1201,22 @@ Panel = class {
const background = chrome.extension.getBackgroundPage(); const background = chrome.extension.getBackgroundPage();
const cvox = background['ChromeVox']; const cvox = background['ChromeVox'];
cvox.tts.speak( cvox.tts.speak(
text, background.QueueMode.FLUSH, {'doNotInterrupt': true}); text, background.QueueMode.INTERJECT, {'doNotInterrupt': true});
}); });
$('i-tutorial').addEventListener('startinteractivemode', (evt) => { $('i-tutorial').addEventListener('startinteractivemode', (evt) => {
const actions = evt.detail.actions; const actions = evt.detail.actions;
const background = const background =
chrome.extension chrome.extension.getBackgroundPage()['ChromeVoxState']['instance'];
.getBackgroundPage()['ChromeVoxState']['instance'];
background.createUserActionMonitor(actions, () => { background.createUserActionMonitor(actions, () => {
background.destroyUserActionMonitor(); background.destroyUserActionMonitor();
Panel.iTutorial.showNextLesson(); Panel.iTutorial.showNextLesson();
}); });
}); });
} $('i-tutorial').addEventListener('stopinteractivemode', (evt) => {
const background =
Panel.setMode(Panel.Mode.FULLSCREEN_I_TUTORIAL); chrome.extension.getBackgroundPage()['ChromeVoxState']['instance'];
return; background.destroyUserActionMonitor();
} });
Panel.setMode(Panel.Mode.FULLSCREEN_TUTORIAL);
switch (opt_page) {
case 'updateNotes':
Panel.tutorial_.updateNotes();
break;
default:
Panel.tutorial_.lastViewedPage();
}
} }
/** /**
......
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