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 {
onTtsInterrupted() {}
};
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 {
hidden$="[[ shouldHideMainMenu(activeScreen) ]]">
<h1 id="mainMenuHeader" tabindex="-1">[[ chooseYourExperience ]]</h1>
<div id="mainMenuButtons">
<cr-button id="oobeButton" on-click="chooseCurriculum">OOBE</cr-button>
<cr-button id="newUserButton" on-click="chooseCurriculum">
[[ newUser ]]
</cr-button>
......@@ -125,8 +124,8 @@ h1 {
hidden$="[[ shouldHideLessonContainer(activeScreen) ]]">
<!-- Use lessonData object to create all lessons -->
<template is="dom-repeat" items="[[ lessonData ]]" as="lesson"
index-as="index">
<template id="lessonTemplate" is="dom-repeat" items="[[ lessonData ]]"
as="lesson" index-as="index">
<tutorial-lesson
lesson-num="[[ index ]]"
title="[[ lesson.title ]]"
......@@ -161,13 +160,18 @@ h1 {
[[ nextLesson ]]
</cr-button>
<cr-button on-click="showLessonMenu"
hidden$="[[ !shouldHideLessonMenu(activeScreen) ]]">
hidden$="[[ shouldHideLessonMenuButton(activeScreen) ]]">
[[ lessonMenu ]]
</cr-button>
<cr-button on-click="showMainMenu"
hidden$="[[ !shouldHideMainMenu(activeScreen) ]]">
hidden$="[[ shouldHideMainMenuButton(activeScreen) ]]">
[[ mainMenu ]]
</cr-button>
<cr-button on-click="showFirstLesson"
hidden$="[[
shouldHideRestartBasicOrientationButton(activeLessonIndex) ]]">
[[ restartBasicOrientation ]]
</cr-button>
<cr-button on-click="exit">[[ exitTutorial ]]</cr-button>
</div>
</div>
......
......@@ -30,6 +30,7 @@ const InteractionMedium = {
* @enum {string}
*/
const Screen = {
NONE: 'none',
MAIN_MENU: 'main_menu',
LESSON_MENU: 'lesson_menu',
LESSON: 'lesson',
......@@ -79,6 +80,8 @@ Polymer({
continue: {type: String, value: 'Continue where I left off'},
restartBasicOrientation: {type: String, value: 'Restart basic orientation'},
previousLesson: {type: String, value: 'Previous lesson'},
nextLesson: {type: String, value: 'Next lesson'},
......@@ -93,11 +96,14 @@ Polymer({
type: Array,
value: [
{
content:
['Welcome to the ChromeVox tutorial. We noticed this might be ' +
'your first time using ChromeVox, so let\'s quickly cover the ' +
`basics. When you're ready, use the space bar to move to the ` +
'next lesson.'],
content: [`Welcome to the ChromeVox tutorial. We noticed this might be
your first time using ChromeVox, so let's quickly cover the
basics. To exit this tutorial at any time, press the Escape
key, which is located in the top left corner of the
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,
curriculums: [Curriculum.OOBE],
actions: [
......@@ -107,19 +113,49 @@ Polymer({
},
{
content: [
`Let's learn how to navigate the tutorial first. ` +
'In ChromeVox, the Search key is the modifier key. ' +
'Most ChromeVox shortcuts start with the Search key. ' +
'On the Chromebook, the Search key is immediately above the ' +
`left Shift key. When you're ready, try finding and ` +
'pressing the search key on your keyboard',
content:
[`Let's start with the keyboard layout. There are a few keys that
are crucial to know when using ChromeVox: Control, Shift,
Search, and the Arrow keys. To continue to the next lesson,
find and press the Control key, which is located in the bottom
left corner of the 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,
curriculums: [Curriculum.OOBE],
actions: [{
type: 'key_sequence',
value: {skipStripping: false, keys: {keyCode: [91 /* Search*/]}},
value: {skipStripping: false, keys: {keyCode: [91 /* Search */]}},
afterActionMsg: 'You found the search key!',
}],
autoInteractive: true,
......@@ -127,14 +163,15 @@ Polymer({
{
content: [
`Now that you know where the search key is, let's learn ` +
'some basic navigation. While holding search, use the arrow ' +
'keys to move ChromeVox around the screen. Press Search + ' +
'right arrow to move to the next instruction',
'You can use Search + right arrow and search + left arrow to ' +
'navigate the tutorial. Now press Search + right arrow again.',
'If you reach an item you want to click, press Search + Space. ' +
'Try doing so now to move to the next lesson',
`Now that you know where the search key is, let's learn
some basic navigation. While holding search, use the arrow
keys to move ChromeVox around the screen. Press Search +
right arrow to move to the next instruction`,
`You can use Search + right arrow and search + left arrow to
move ChromeVox around the screen. Try pressing Search +
right arrow again.`,
`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,
curriculums: [Curriculum.OOBE],
......@@ -160,10 +197,11 @@ Polymer({
{
title: 'Basic orientation complete!',
content: [
'Well done! You have learned the basics of ChromeVox. You can ' +
`now continue browsing the tutorial with what you've ` +
'learned, or exit by finding and pressing the Quit Tutorial ' +
'button',
`Well done! You have learned the basics of ChromeVox. You can
replay the basic orientation or exit this tutorial by
finding and clicking on a button below.`,
`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,
curriculums: [Curriculum.OOBE],
......@@ -341,7 +379,7 @@ Polymer({
],
medium: InteractionMedium.KEYBOARD,
curriculums: [
Curriculum.OOBE, Curriculum.NEW_USER, Curriculum.DEVELOPER,
Curriculum.NEW_USER, Curriculum.DEVELOPER,
Curriculum.EXPERIENCED_USER
],
}
......@@ -351,7 +389,24 @@ Polymer({
/** @override */
ready() {
this.showMainMenu();
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();
}
},
/**
......@@ -366,15 +421,12 @@ Polymer({
this.curriculum = Curriculum.EXPERIENCED_USER;
} else if (id === 'developerButton') {
this.curriculum = Curriculum.DEVELOPER;
} else if (id === 'oobeButton') {
this.curriculum = Curriculum.OOBE;
} else {
throw new Error('Invalid target for chooseCurriculum: ' + evt.target.id);
}
this.showLessonMenu();
},
/** @private */
showNextLesson() {
this.showLesson(this.activeLessonIndex + 1);
},
......@@ -384,6 +436,11 @@ Polymer({
this.showLesson(this.activeLessonIndex - 1);
},
/** @private */
showFirstLesson() {
this.showLesson(0);
},
/**
* @param {number} index
* @private
......@@ -403,12 +460,18 @@ Polymer({
const lesson = this.includedLessons[this.activeLessonIndex];
if (lesson.autoInteractive) {
this.startInteractiveMode(lesson.actions);
} else {
this.stopInteractiveMode();
}
},
// Methods for hiding and showing screens.
/** @private */
hideAllScreens() {
this.activeScreen = Screen.NONE;
},
/** @private */
showMainMenu() {
this.activeScreen = Screen.MAIN_MENU;
......@@ -476,12 +539,8 @@ Polymer({
* @private
*/
shouldHideNextLessonButton(activeLessonIndex, activeScreen) {
if (activeLessonIndex === this.numLessons - 1 ||
activeScreen !== Screen.LESSON) {
return true;
}
return false;
return activeLessonIndex === this.numLessons - 1 ||
activeScreen !== Screen.LESSON;
},
/**
......@@ -491,11 +550,17 @@ Polymer({
* @private
*/
shouldHidePreviousLessonButton(activeLessonIndex, activeScreen) {
if (activeLessonIndex === 0 || activeScreen !== Screen.LESSON) {
return true;
}
return activeLessonIndex === 0 || activeScreen !== Screen.LESSON;
},
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({
* @return {boolean}
* @private
*/
shouldHideMainMenu(activeScreen) {
if (activeScreen === Screen.MAIN_MENU) {
return false;
}
shouldHideMainMenuButton(activeScreen) {
return this.curriculum === Curriculum.OOBE ||
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({
* @return {boolean}
* @private
*/
shouldHideLessonContainer(activeScreen) {
if (activeScreen === Screen.LESSON) {
return false;
}
shouldHideMainMenu(activeScreen) {
return activeScreen !== Screen.MAIN_MENU;
},
return true;
/**
* @param {Screen} activeScreen
* @return {boolean}
* @private
*/
shouldHideLessonContainer(activeScreen) {
return activeScreen !== Screen.LESSON;
},
/**
......@@ -530,11 +609,7 @@ Polymer({
* @private
*/
shouldHideLessonMenu(activeScreen) {
if (activeScreen === Screen.LESSON_MENU) {
return false;
}
return true;
return activeScreen !== Screen.LESSON_MENU;
},
/**
......@@ -554,8 +629,35 @@ Polymer({
// Interactive mode.
/**
* @param {!Array<{
* type: string,
* value: (string|Object),
* beforeActionMsg: (string|undefined),
* afterActionMsg: (string|undefined)}>} actions
* @private
*/
startInteractiveMode(actions) {
this.dispatchEvent(new CustomEvent(
'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({
'A lesson must have an element which specifies tabindex.');
}
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 */
......
......@@ -189,7 +189,6 @@ Panel = class {
/** @private {boolean} */
Panel.iTutorialEnabled_ = false;
chrome.commandLinePrivate.hasSwitch(
'enable-experimental-accessibility-chromevox-tutorial', (enabled) => {
Panel.iTutorialEnabled_ = enabled;
......@@ -1136,64 +1135,23 @@ Panel = class {
* @param {string=} opt_page Show a specific 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 ($('i-tutorial') === null) {
// Load resources if this is the first time opening the tutorial.
const tutorialScript = document.createElement('script');
tutorialScript.src = '../i_tutorial/i_tutorial.js';
tutorialScript.setAttribute('type', 'module');
const lessonScript = document.createElement('script');
lessonScript.src = '../i_tutorial/tutorial_lesson.js';
lessonScript.setAttribute('type', 'module');
document.body.appendChild(tutorialScript);
document.body.appendChild(lessonScript);
// Create tutorial container and element.
const tutorialContainer = document.createElement('div');
tutorialContainer.setAttribute('id', 'i-tutorial-container');
tutorialContainer.hidden = true;
const tutorialElement = document.createElement('i-tutorial');
tutorialElement.setAttribute('id', 'i-tutorial');
tutorialContainer.appendChild(tutorialElement);
document.body.appendChild(tutorialContainer);
Panel.iTutorial = tutorialElement;
// Add listeners. These are custom events fired from custom components.
$('i-tutorial').addEventListener('closetutorial', (evt) => {
// Ensure UserActionMonitor is destroyed before closing tutorial.
const background =
chrome.extension
.getBackgroundPage()['ChromeVoxState']['instance'];
background.destroyUserActionMonitor();
Panel.onCloseTutorial();
});
$('i-tutorial').addEventListener('requestspeech', (evt) => {
const text = evt.detail.text;
const background = chrome.extension.getBackgroundPage();
const cvox = background['ChromeVox'];
cvox.tts.speak(
text, background.QueueMode.FLUSH, {'doNotInterrupt': true});
});
$('i-tutorial').addEventListener('startinteractivemode', (evt) => {
const actions = evt.detail.actions;
const background =
chrome.extension
.getBackgroundPage()['ChromeVoxState']['instance'];
background.createUserActionMonitor(actions, () => {
background.destroyUserActionMonitor();
Panel.iTutorial.showNextLesson();
});
});
if (!$('i-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();
......@@ -1203,6 +1161,64 @@ Panel = class {
}
}
/**
* Creates an <i-tutorial> element and adds it to the dom.
* @param {(string|null)} curriculum
*/
static createITutorial(curriculum) {
const tutorialScript = document.createElement('script');
tutorialScript.src = '../i_tutorial/i_tutorial.js';
tutorialScript.setAttribute('type', 'module');
const lessonScript = document.createElement('script');
lessonScript.src = '../i_tutorial/tutorial_lesson.js';
lessonScript.setAttribute('type', 'module');
document.body.appendChild(tutorialScript);
document.body.appendChild(lessonScript);
// Create tutorial container and element.
const tutorialContainer = document.createElement('div');
tutorialContainer.setAttribute('id', 'i-tutorial-container');
tutorialContainer.hidden = true;
const tutorialElement = document.createElement('i-tutorial');
tutorialElement.setAttribute('id', 'i-tutorial');
if (curriculum) {
tutorialElement.curriculum = curriculum;
}
tutorialContainer.appendChild(tutorialElement);
document.body.appendChild(tutorialContainer);
Panel.iTutorial = tutorialElement;
// Add listeners. These are custom events fired from custom components.
$('i-tutorial').addEventListener('closetutorial', (evt) => {
// Ensure UserActionMonitor is destroyed before closing tutorial.
const background =
chrome.extension.getBackgroundPage()['ChromeVoxState']['instance'];
background.destroyUserActionMonitor();
Panel.onCloseTutorial();
});
$('i-tutorial').addEventListener('requestspeech', (evt) => {
const text = evt.detail.text;
const background = chrome.extension.getBackgroundPage();
const cvox = background['ChromeVox'];
cvox.tts.speak(
text, background.QueueMode.INTERJECT, {'doNotInterrupt': true});
});
$('i-tutorial').addEventListener('startinteractivemode', (evt) => {
const actions = evt.detail.actions;
const background =
chrome.extension.getBackgroundPage()['ChromeVoxState']['instance'];
background.createUserActionMonitor(actions, () => {
background.destroyUserActionMonitor();
Panel.iTutorial.showNextLesson();
});
});
$('i-tutorial').addEventListener('stopinteractivemode', (evt) => {
const background =
chrome.extension.getBackgroundPage()['ChromeVoxState']['instance'];
background.destroyUserActionMonitor();
});
}
/**
* Move to the next page in the tutorial.
*/
......
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