Commit 31c8738a authored by Leonard Grey's avatar Leonard Grey Committed by Commit Bot

Commander: support composite commands in UI

Styling still WIP. This change enables the UI to show a prompt chip
when a composite command is selected.
- Selecting a composite command clears the input and shows a chip with
a prompt
- Backspace over the prompt when the input is empty informs the backend
that the prompt was cancelled, restores the previous user input, and
requeries

Bug: 1014639
Change-Id: I3fff2bf6cacab28b473c8ddee3a7a41fbb6df945
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2530796Reviewed-by: default avatarAvi Drissman <avi@chromium.org>
Commit-Queue: Leonard Grey <lgrey@chromium.org>
Cr-Commit-Position: refs/heads/master@{#826338}
parent b0146441
......@@ -25,6 +25,19 @@
width: 100%;
}
.chip {
background-color: var(--google-blue-50);
border-radius: 100px;
box-sizing: border-box;
color: var(--google-blue-700);
display: inline-block;
height: 32px;
margin-inline-start: 12px;
padding: 8px;
text-align: center;
white-space: nowrap;
}
::-webkit-scrollbar {
display: none;
}
......@@ -36,6 +49,9 @@
</style>
<div id="input-row">
<iron-icon icon="cr:search"></iron-icon>
<template is="dom-if" if="[[computeShowChip_(promptText_)]]">
<div class="chip">[[promptText_]]</div>
</template>
<input id="input" type="text" on-input="onInput_"
on-keydown="onKeydown_" autofocus></input>
</div>
......
......@@ -4,6 +4,7 @@
import './option.js';
import 'chrome://resources/cr_elements/icons.m.js';
import 'chrome://resources/polymer/v3_0/iron-icon/iron-icon.js';
import 'chrome://resources/cr_elements/shared_vars_css.m.js';
import {addWebUIListener} from 'chrome://resources/js/cr.m.js';
import {html, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
......@@ -27,6 +28,8 @@ export class CommanderAppElement extends PolymerElement {
options_: Array,
/** @private */
focusedIndex_: Number,
/** @private {?string} */
promptText_: String,
};
}
......@@ -37,6 +40,9 @@ export class CommanderAppElement extends PolymerElement {
/** @type {?number} */
this.resultSetId_ = null;
/** @type {!string} */
this.savedInput_ = '';
}
/** @override */
......@@ -55,6 +61,8 @@ export class CommanderAppElement extends PolymerElement {
this.$.input.value = '';
this.focusedIndex_ = -1;
this.resultSetId_ = null;
this.promptText_ = null;
this.savedInput_ = '';
}
/**
......@@ -78,6 +86,12 @@ export class CommanderAppElement extends PolymerElement {
this.focusedIndex_ < this.options_.length) {
this.notifySelectedAtIndex_(this.focusedIndex_);
}
} else if (
this.promptText_ && e.key === 'Backspace' &&
this.$.input.value === '') {
this.browserProxy_.promptCancelled();
this.promptText_ = null;
this.$.input.value = this.savedInput_;
}
}
......@@ -99,8 +113,13 @@ export class CommanderAppElement extends PolymerElement {
if (this.options_.length > 0) {
this.focusedIndex_ = 0;
}
} else if (viewModel.action === Action.PROMPT) {
this.options_ = [];
this.resultSetId_ = viewModel.resultSetId;
this.promptText_ = viewModel.promptText || null;
this.savedInput_ = this.$.input.value;
this.$.input.value = '';
}
// TODO(lgrey): Handle Action.PROMPT
}
/** @private */
......@@ -136,5 +155,12 @@ export class CommanderAppElement extends PolymerElement {
getOptionClass_(index) {
return index === this.focusedIndex_ ? 'focused' : '';
}
/**
* @return {boolean}
*/
computeShowChip_() {
return this.promptText_ !== null;
}
}
customElements.define(CommanderAppElement.is, CommanderAppElement);
......@@ -31,6 +31,12 @@ export class BrowserProxy {
* Notifies the backend that the user wants to dismiss the UI.
*/
dismiss() {}
/**
* Notifies the backend that the user has cancelled entering a composite
* command.
*/
promptCancelled() {}
}
/** @implements {BrowserProxy} */
......@@ -54,6 +60,11 @@ export class BrowserProxyImpl {
dismiss() {
chrome.send('dismiss');
}
/** @override */
promptCancelled() {
chrome.send('compositeCommandCancelled');
}
}
......
......@@ -45,6 +45,7 @@ export let Option;
* action : Action,
* resultSetId : number,
* options : ?Array<Option>,
* promptText : (string|undefined),
* }}
*/
export let ViewModel;
......@@ -57,8 +57,11 @@ struct CommanderViewModel {
// commander_backend.h for more details.
int result_set_id;
// A pre-ranked list of items to display. Can be empty if there are
// no results, or |action| is not kDisplayResults.
// no results, or `action` is not kDisplayResults.
std::vector<CommandItemViewModel> items;
// Prompt text to be shown when entering a composite command. Should only
// be populated if and only if `action` is kPrompt.
base::string16 prompt_text;
// The action the view should take in response to receiving this view model.
Action action;
};
......
......@@ -27,6 +27,7 @@ constexpr char kEntityKey[] = "entity";
constexpr char kAnnotationKey[] = "annotation";
constexpr char kMatchedRangesKey[] = "matchedRanges";
constexpr char kOptionsKey[] = "options";
constexpr char kPromptTextKey[] = "promptText";
} // namespace
CommanderHandler::CommanderHandler() = default;
......@@ -103,11 +104,11 @@ void CommanderHandler::HandleHeightChanged(const base::ListValue* args) {
void CommanderHandler::ViewModelUpdated(
commander::CommanderViewModel view_model) {
base::DictionaryValue vm;
vm.SetIntKey(kActionKey, view_model.action);
vm.SetIntKey(kResultSetIdKey, view_model.result_set_id);
if (view_model.action ==
commander::CommanderViewModel::Action::kDisplayResults) {
base::DictionaryValue vm;
vm.SetIntKey(kActionKey, view_model.action);
vm.SetIntKey(kResultSetIdKey, view_model.result_set_id);
base::ListValue option_list;
for (commander::CommandItemViewModel& item : view_model.items) {
base::DictionaryValue option;
......@@ -126,12 +127,13 @@ void CommanderHandler::ViewModelUpdated(
option_list.Append(std::move(option));
}
vm.SetKey(kOptionsKey, std::move(option_list));
FireWebUIListener(kViewModelUpdatedEvent, vm);
} else {
// kDismiss is handled higher in the stack.
DCHECK_EQ(view_model.action,
commander::CommanderViewModel::Action::kPrompt);
// TODO(lgrey): Handle kPrompt. kDismiss is handled higher up the stack.
vm.SetStringKey(kPromptTextKey, view_model.prompt_text);
}
FireWebUIListener(kViewModelUpdatedEvent, vm);
}
void CommanderHandler::PrepareToShow(Delegate* delegate) {
......
......@@ -134,7 +134,7 @@ IN_PROC_BROWSER_TEST_F(CommanderUITest, CompositeCommandCancelled) {
EXPECT_EQ(composite_command_cancelled_invocation_count(), 1);
}
TEST(CommanderHandlerTest, ViewModelPassed) {
TEST(CommanderHandlerTest, DisplayResultsViewModelPassed) {
content::TestWebUI test_web_ui;
auto handler = std::make_unique<TestCommanderHandler>(&test_web_ui);
......@@ -171,6 +171,27 @@ TEST(CommanderHandlerTest, ViewModelPassed) {
EXPECT_EQ(42, arg->FindPath("resultSetId")->GetInt());
}
TEST(CommanderHandlerTest, PromptViewModelPassed) {
content::TestWebUI test_web_ui;
auto handler = std::make_unique<TestCommanderHandler>(&test_web_ui);
commander::CommanderViewModel vm;
vm.action = commander::CommanderViewModel::Action::kPrompt;
vm.result_set_id = 42;
vm.prompt_text = base::ASCIIToUTF16("Select fruit");
handler->AllowJavascriptForTesting();
handler->ViewModelUpdated(std::move(vm));
const content::TestWebUI::CallData& call_data =
*test_web_ui.call_data().back();
EXPECT_EQ("cr.webUIListenerCallback", call_data.function_name());
EXPECT_EQ("view-model-updated", call_data.arg1()->GetString());
const base::Value* arg = call_data.arg2();
EXPECT_EQ("Select fruit", arg->FindPath("promptText")->GetString());
EXPECT_EQ(42, arg->FindPath("resultSetId")->GetInt());
}
TEST(CommanderHandlerTest, Initialize) {
content::TestWebUI test_web_ui;
auto handler = std::make_unique<TestCommanderHandler>(&test_web_ui);
......
......@@ -81,7 +81,7 @@ suite('CommanderWebUIBrowserTest', () => {
assertEquals(expectedText, actualText);
});
test('view model change renders options', async () => {
test('display results view model change renders options', async () => {
const titles = ['William of Orange', 'Orangutan', 'Orange Juice'];
webUIListenerCallback(
'view-model-updated', createStubViewModel(42, titles));
......@@ -98,7 +98,7 @@ suite('CommanderWebUIBrowserTest', () => {
assertDeepEquals(titles, actualTitles);
});
test('view model change sends heightChanged', async () => {
test('display results view model change sends heightChanged', async () => {
webUIListenerCallback('view-model-updated', createStubViewModel(42, [
'William of Orange', 'Orangutan', 'Orange Juice'
]));
......@@ -172,4 +172,38 @@ suite('CommanderWebUIBrowserTest', () => {
assertEquals(0, optionIndex);
assertEquals(expectedResultSetId, resultID);
});
test('prompt view model draws chip', async () => {
const expectedPrompt = 'Select fruit';
webUIListenerCallback(
'view-model-updated',
{resultSetId: 42, action: Action.PROMPT, promptText: expectedPrompt});
await flushTasks();
const chips = app.shadowRoot.querySelectorAll('.chip');
assertEquals(1, chips.length);
assertEquals(expectedPrompt, chips[0].innerText);
});
test('backspacing over chip cancels prompt', async () => {
const expectedPrompt = 'Select fruit';
webUIListenerCallback(
'view-model-updated',
{resultSetId: 42, action: Action.PROMPT, promptText: expectedPrompt});
await flushTasks();
const input = app.$.input;
input.value = 'A';
// await flushTasks();
// Backspace over text doesn't delete the chip.
keyDownOn(input, 0, [], 'Backspace');
// await flushTasks();
assertEquals(0, testProxy.getCallCount('promptCancelled'));
input.value = '';
keyDownOn(input, 0, [], 'Backspace');
// await flushTasks();
assertEquals(1, testProxy.getCallCount('promptCancelled'));
});
});
......@@ -13,6 +13,7 @@ export class TestCommanderBrowserProxy extends TestBrowserProxy {
'optionSelected',
'heightChanged',
'dismiss',
'promptCancelled',
]);
}
......@@ -35,4 +36,9 @@ export class TestCommanderBrowserProxy extends TestBrowserProxy {
dismiss() {
this.methodCalled('dismiss');
}
/** @override */
promptCancelled() {
this.methodCalled('promptCancelled');
}
}
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