Commit 71a0306e authored by Regan Hsu's avatar Regan Hsu Committed by Commit Bot

[OsSettingsSearch] Add no results UI to search box.

* No results UI shown when there are no results and the input text
  is nonempty.
* Dropdown is closed if input text is empty.
* Adds a separator that covers top box-shadow of dropdown.

Mock (See Zero State):
https://docs.google.com/presentation/d/1-DftlpMfwFvvCi5RDpT4chSRpNk13EUTyX3AzT80I1c

Screenshot:
* No results UI and no shadow on top of dropdown section:
  https://screenshot.googleplex.com/iv7QUkg28wn

Bug: 1056909
Change-Id: I1a26e1c9b933f540bf294db6165ef0c11e187c53
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2138165
Commit-Queue: Kyle Horimoto <khorimoto@chromium.org>
Reviewed-by: default avatarKyle Horimoto <khorimoto@chromium.org>
Cr-Commit-Position: refs/heads/master@{#758362}
parent 868fd8fc
...@@ -19,6 +19,7 @@ ...@@ -19,6 +19,7 @@
--cr-toolbar-field-width: 480px; --cr-toolbar-field-width: 480px;
--cr-toolbar-icon-container-size: 32px; --cr-toolbar-icon-container-size: 32px;
--cr-toolbar-icon-margin: 6px 12px; --cr-toolbar-icon-margin: 6px 12px;
--separator-height: 8px;
display: flex; display: flex;
flex-basis: var(--cr-toolbar-field-width); flex-basis: var(--cr-toolbar-field-width);
flex-direction: row; flex-direction: row;
...@@ -39,7 +40,6 @@ ...@@ -39,7 +40,6 @@
--cr-toolbar-search-field-border-radius: 20px; --cr-toolbar-search-field-border-radius: 20px;
--cr-toolbar-search-field-paper-spinner-margin: 0 12px; --cr-toolbar-search-field-paper-spinner-margin: 0 12px;
--cr-toolbar-search-field-prompt-margin-inline-start: 140px; --cr-toolbar-search-field-prompt-margin-inline-start: 140px;
transition: height 150ms;
} }
:host(:focus-within) cr-toolbar-search-field { :host(:focus-within) cr-toolbar-search-field {
...@@ -48,8 +48,8 @@ ...@@ -48,8 +48,8 @@
:host(:focus-within[should-show-dropdown_]) cr-toolbar-search-field { :host(:focus-within[should-show-dropdown_]) cr-toolbar-search-field {
--cr-toolbar-search-field-border-radius: 20px 20px 0 0; --cr-toolbar-search-field-border-radius: 20px 20px 0 0;
border-bottom: var(--cr-separator-line); height: 56px;
height: 48px; margin-top: var(--separator-height);
} }
:host(:focus-within:not([narrow])) cr-toolbar-search-field { :host(:focus-within:not([narrow])) cr-toolbar-search-field {
...@@ -66,7 +66,7 @@ ...@@ -66,7 +66,7 @@
border-radius: 0 0 20px 20px; border-radius: 0 0 20px 20px;
box-shadow: var(--cr-menu-shadow); box-shadow: var(--cr-menu-shadow);
display: table; display: table;
padding: 8px 0 12px 0; padding-bottom: 16px;
} }
:host([narrow]) iron-dropdown [slot='dropdown-content'] { :host([narrow]) iron-dropdown [slot='dropdown-content'] {
...@@ -74,12 +74,39 @@ ...@@ -74,12 +74,39 @@
} }
:host(:not([narrow])) iron-dropdown [slot='dropdown-content'] { :host(:not([narrow])) iron-dropdown [slot='dropdown-content'] {
width: var(--cr-toolbar-field-width, 680px); width: var(--cr-toolbar-field-width);
} }
:host([narrow]) os-search-result-row { :host([narrow]) os-search-result-row {
--cr-toolbar-icon-margin: 6px 12px 6px 18px; --cr-toolbar-icon-margin: 6px 12px 6px 18px;
} }
#noSearchResultsContainer {
height: 24px;
margin-top: var(--separator-height);
}
:host(:not([narrow])) #noSearchResultsContainer {
margin-inline-start: 56px;
}
:host([narrow]) #noSearchResultsContainer {
margin-inline-start: 62px;
}
/* The separator covers the top box shadow of the dropdown so that
* var(--cr-menu-shadow) can be used instead of custom values. */
.separator {
background-color: var(--cr-menu-background-color);
border-bottom: none;
border-inline-end: none;
border-inline-start: none;
border-top: 2px solid var(--cr-separator-color);
height: var(--separator-height);
margin-inline-end: 0;
margin-inline-start: 0;
margin-top: calc(-1*var(--separator-height));
}
</style> </style>
<cr-toolbar-search-field id="search" narrow="[[narrow]]" <cr-toolbar-search-field id="search" narrow="[[narrow]]"
label="$i18n{searchPrompt}" clear-label="$i18n{clearSearch}" label="$i18n{searchPrompt}" clear-label="$i18n{clearSearch}"
...@@ -91,6 +118,7 @@ ...@@ -91,6 +118,7 @@
hidden until iron-dropdown's opened attribute is set true, or when hidden until iron-dropdown's opened attribute is set true, or when
iron-dropdown's open() is called on the JS side. --> iron-dropdown's open() is called on the JS side. -->
<div slot="dropdown-content"> <div slot="dropdown-content">
<div class="separator"></div>
<iron-list id="searchResultList" selection-enabled <iron-list id="searchResultList" selection-enabled
items="[[searchResults_]]" selected-item="{{selectedItem_}}" items="[[searchResults_]]" selected-item="{{selectedItem_}}"
on-selected-item-changed="onSelectedItemChanged_"> on-selected-item-changed="onSelectedItemChanged_">
...@@ -109,6 +137,9 @@ ...@@ -109,6 +137,9 @@
</os-search-result-row> </os-search-result-row>
</template> </template>
</iron-list> </iron-list>
<div id="noSearchResultsContainer" hidden="[[searchResultsExist_]]">
$i18n{searchNoResults}
</div>
</div> </div>
</iron-dropdown> </iron-dropdown>
</template> </template>
......
...@@ -117,17 +117,23 @@ Polymer({ ...@@ -117,17 +117,23 @@ Polymer({
*/ */
searchResults_: { searchResults_: {
type: Array, type: Array,
observer: 'selectFirstRow_', observer: 'onSearchResultsChanged_',
}, },
/** @private */ /** @private */
shouldShowDropdown_: { shouldShowDropdown_: {
type: Boolean, type: Boolean,
value: false, value: false,
computed: 'computeShouldShowDropdown_(searchResults_)',
reflectToAttribute: true, reflectToAttribute: true,
}, },
/** @private */
searchResultsExist_: {
type: Boolean,
value: false,
computed: 'computeSearchResultsExist_(searchResults_)',
},
/** /**
* Used by FocusRowBehavior to track the last focused element inside a * Used by FocusRowBehavior to track the last focused element inside a
* <os-search-result-row> with the attribute 'focus-row-control'. * <os-search-result-row> with the attribute 'focus-row-control'.
...@@ -176,17 +182,25 @@ Polymer({ ...@@ -176,17 +182,25 @@ Polymer({
'No OsSearchResultRow is selected.'); 'No OsSearchResultRow is selected.');
}, },
/**
* @return {string} The current input string.
* @private
*/
getCurrentQuery_() {
return this.$.search.getSearchInput().value;
},
/** /**
* @return {boolean} * @return {boolean}
* @private * @private
*/ */
computeShouldShowDropdown_() { computeSearchResultsExist_() {
return this.searchResults_.length !== 0; return this.searchResults_.length !== 0;
}, },
/** @private */ /** @private */
fetchSearchResults_() { fetchSearchResults_() {
const query = this.$.search.getSearchInput().value; const query = this.getCurrentQuery_();
if (query === '') { if (query === '') {
this.searchResults_ = []; this.searchResults_ = [];
return; return;
...@@ -218,7 +232,7 @@ Polymer({ ...@@ -218,7 +232,7 @@ Polymer({
* @private * @private
*/ */
onSearchResultsReceived_(query, results) { onSearchResultsReceived_(query, results) {
if (query !== this.$.search.getSearchInput().value) { if (query !== this.getCurrentQuery_()) {
// Received search results are invalid as the query has since changed. // Received search results are invalid as the query has since changed.
return; return;
} }
...@@ -232,7 +246,7 @@ Polymer({ ...@@ -232,7 +246,7 @@ Polymer({
/** @private */ /** @private */
onNavigatedtoResultRowRoute_() { onNavigatedtoResultRowRoute_() {
// Settings has navigated to another page; close search results dropdown. // Settings has navigated to another page; close search results dropdown.
this.$.searchResults.close(); this.shouldShowDropdown_ = false;
// Blur search input to prevent blinking caret. // Blur search input to prevent blinking caret.
this.$.search.blur(); this.$.search.blur();
...@@ -245,18 +259,17 @@ Polymer({ ...@@ -245,18 +259,17 @@ Polymer({
onBlur_(e) { onBlur_(e) {
e.stopPropagation(); e.stopPropagation();
// The user has clicked a region outside the search box or the input has // Close the dropdown because a region outside the search box was clicked.
// been blurred; close the dropdown regardless if there are searchResults_. this.shouldShowDropdown_ = false;
this.$.searchResults.close();
}, },
/** @private */ /** @private */
onSearchInputFocused_() { onSearchInputFocused_() {
this.lastFocused_ = null; this.lastFocused_ = null;
if (this.shouldShowDropdown_) { if (this.searchResultsExist_) {
// Restore previous results instead of re-fetching. // Restore previous results instead of re-fetching.
this.$.searchResults.open(); this.shouldShowDropdown_ = true;
return; return;
} }
...@@ -287,11 +300,16 @@ Polymer({ ...@@ -287,11 +300,16 @@ Polymer({
}, },
/** @private */ /** @private */
selectFirstRow_() { onSearchResultsChanged_() {
if (!this.shouldShowDropdown_) { // Only show dropdown if focus is on search field with a non empty query.
this.shouldShowDropdown_ =
this.$.search.isSearchFocused() && !!this.getCurrentQuery_();
if (!this.searchResultsExist_) {
return; return;
} }
// Select the first search result.
this.selectedItem_ = this.searchResults_[0]; this.selectedItem_ = this.searchResults_[0];
}, },
...@@ -348,7 +366,7 @@ Polymer({ ...@@ -348,7 +366,7 @@ Polymer({
* @private * @private
*/ */
onKeyDown_(e) { onKeyDown_(e) {
if (!this.shouldShowDropdown_) { if (!this.searchResultsExist_) {
// No action should be taken if there are no search results. // No action should be taken if there are no search results.
return; return;
} }
......
...@@ -26,6 +26,9 @@ suite('OSSettingsSearchBox', () => { ...@@ -26,6 +26,9 @@ suite('OSSettingsSearchBox', () => {
/** @type {?chromeos.settings.mojom.UserActionRecorderInterface} */ /** @type {?chromeos.settings.mojom.UserActionRecorderInterface} */
let userActionRecorder; let userActionRecorder;
/** @type {?HTMLElement} */
let noResultsSection;
/** @param {string} term */ /** @param {string} term */
async function simulateSearch(term) { async function simulateSearch(term) {
field.$.searchInput.value = term; field.$.searchInput.value = term;
...@@ -68,6 +71,8 @@ suite('OSSettingsSearchBox', () => { ...@@ -68,6 +71,8 @@ suite('OSSettingsSearchBox', () => {
assertTrue(!!dropDown); assertTrue(!!dropDown);
resultList = searchBox.$$('iron-list'); resultList = searchBox.$$('iron-list');
assertTrue(!!resultList); assertTrue(!!resultList);
noResultsSection = searchBox.$$('#noSearchResultsContainer');
assertTrue(!!noResultsSection);
settingsSearchHandler = new settings.FakeSettingsSearchHandler(); settingsSearchHandler = new settings.FakeSettingsSearchHandler();
settings.setSearchHandlerForTesting(settingsSearchHandler); settings.setSearchHandlerForTesting(settingsSearchHandler);
...@@ -93,17 +98,21 @@ suite('OSSettingsSearchBox', () => { ...@@ -93,17 +98,21 @@ suite('OSSettingsSearchBox', () => {
}); });
test('Dropdown opens correctly when results are fetched', async () => { test('Dropdown opens correctly when results are fetched', async () => {
// Closed dropdown if no results are returned. // Show no results in dropdown if no results are returned.
settingsSearchHandler.setFakeResults([]); settingsSearchHandler.setFakeResults([]);
assertFalse(dropDown.opened); assertFalse(dropDown.opened);
await simulateSearch('query 1'); await simulateSearch('query 1');
assertFalse(dropDown.opened); assertTrue(dropDown.opened);
assertEquals(searchBox.searchResults_.length, 0);
assertFalse(noResultsSection.hidden);
assertEquals(userActionRecorder.searchCount, 1); assertEquals(userActionRecorder.searchCount, 1);
// Open dropdown if results are returned. // Show result list if results are returned, and hide no results div.
settingsSearchHandler.setFakeResults([fakeResult('result')]); settingsSearchHandler.setFakeResults([fakeResult('result')]);
await simulateSearch('query 2'); await simulateSearch('query 2');
assertTrue(dropDown.opened); assertNotEquals(searchBox.searchResults_.length, 0);
assertTrue(noResultsSection.hidden);
}); });
test('Restore previous existing search results', async () => { test('Restore previous existing search results', async () => {
......
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