Bug fixes Magic Search

This patch fixes two bugs and minor UX enhancements:
- User can now delete last character in searchInput without causing the
  facetSelected to disappear
- User can now use arrow keys normally, preventing the table to be reloaded
- Menu follows search input position as the user adds more facets

Closes-bug: 1618235
Closes-bug: 1635505
Change-Id: I56b980f7c6739fc4fba06c0028764b2e4e908de6
This commit is contained in:
Eddie Ramirez 2016-10-19 21:01:41 +00:00 committed by Cindy Lu
parent bbd1637bbb
commit 91ab13aa66
5 changed files with 166 additions and 81 deletions

View File

@ -109,6 +109,8 @@
var key = service.getEventCode($event); var key = service.getEventCode($event);
if (key === 9) { // prevent default when we can. if (key === 9) { // prevent default when we can.
$event.preventDefault(); $event.preventDefault();
} else if (key === 8) {
backspaceKeyDown();
} }
} }
@ -118,7 +120,6 @@
return; return;
} }
ctrl.facetClicked(0, '', ctrl.filteredObj[0].name); ctrl.facetClicked(0, '', ctrl.filteredObj[0].name);
setSearchInput('');
} else { } else {
if (angular.isUndefined(ctrl.filteredOptions) || if (angular.isUndefined(ctrl.filteredOptions) ||
ctrl.filteredOptions.length !== 1) { ctrl.filteredOptions.length !== 1) {
@ -130,7 +131,11 @@
} }
function escapeKeyUp() { function escapeKeyUp() {
setMenuOpen(false); if (angular.isDefined(ctrl.facetSelected)) {
setMenuOpen(true);
} else {
setMenuOpen(false);
}
resetState(); resetState();
var textFilter = ctrl.textSearch; var textFilter = ctrl.textSearch;
if (angular.isUndefined(textFilter)) { if (angular.isUndefined(textFilter)) {
@ -142,43 +147,71 @@
function enterKeyUp() { function enterKeyUp() {
var searchVal = searchInput.val(); var searchVal = searchInput.val();
// if tag search, treat as regular facet // if tag search, treat as regular facet
if (ctrl.facetSelected && angular.isUndefined(ctrl.facetSelected.options)) { if (searchVal !== '') {
var curr = ctrl.facetSelected; if (ctrl.facetSelected) {
curr.name = curr.name.split('=')[0] + '=' + searchVal; var curr = ctrl.facetSelected;
curr.label[1] = searchVal; curr.name = curr.name.split('=')[0] + '=' + searchVal;
ctrl.currentSearch.push(curr); curr.label[1] = searchVal;
resetState(); ctrl.currentSearch.push(curr);
emitQuery(); resetState();
emitQuery();
setMenuOpen(true);
} else {
// if text search treat as search
ctrl.currentSearch = ctrl.currentSearch.filter(notTextSearch);
ctrl.currentSearch.push(service.getTextFacet(searchVal, $scope.strings.text));
$scope.$apply();
setMenuOpen(true);
setSearchInput('');
emitTextSearch(searchVal);
ctrl.textSearch = searchVal;
}
} else if (ctrl.isMenuOpen) {
setMenuOpen(false); setMenuOpen(false);
} else { } else {
// if text search treat as search setMenuOpen(true);
ctrl.currentSearch = ctrl.currentSearch.filter(notTextSearch);
ctrl.currentSearch.push(service.getTextFacet(searchVal, $scope.strings.text));
$scope.$apply();
setMenuOpen(false);
setSearchInput('');
emitTextSearch(searchVal);
ctrl.textSearch = searchVal;
} }
ctrl.filteredObj = ctrl.unusedFacetChoices; ctrl.filteredObj = ctrl.unusedFacetChoices;
} }
function backspaceKeyDown() {
var searchVal = searchInput.val();
if (searchVal === '') {
if (ctrl.currentSearch.length > 0 && angular.isUndefined(ctrl.facetSelected)) {
ctrl.removeFacet(ctrl.currentSearch.length - 1);
setMenuOpen(true);
} else {
escapeKeyUp();
}
}
}
function backspaceKeyUp() {
var searchVal = searchInput.val();
// if there's no current search and facet selected, then clear all search
if (searchVal === '' && angular.isUndefined(ctrl.facetSelected)) {
if (ctrl.currentSearch.length === 0) {
ctrl.clearSearch();
} else {
resetState();
emitTextSearch(ctrl.textSearch || '');
}
} else {
filterFacets(searchVal);
}
}
function deleteKeyUp() {
return backspaceKeyUp();
}
function notTextSearch(item) { function notTextSearch(item) {
return item.name.indexOf('text') !== 0; return item.name.indexOf('text') !== 0;
} }
function defaultKeyUp() { function defaultKeyUp() {
var searchVal = searchInput.val(); var searchVal = searchInput.val();
if (searchVal === '') { filterFacets(searchVal);
ctrl.filteredObj = ctrl.unusedFacetChoices;
$scope.$apply();
emitTextSearch('');
if (ctrl.facetSelected && angular.isUndefined(ctrl.facetSelected.options)) {
resetState();
}
} else {
filterFacets(searchVal);
}
} }
function keyUpHandler($event) { // handle ctrl-char input function keyUpHandler($event) { // handle ctrl-char input
@ -186,7 +219,13 @@
return; return;
} }
var key = service.getEventCode($event); var key = service.getEventCode($event);
var handlers = { 9: tabKeyUp, 27: escapeKeyUp, 13: enterKeyUp }; var handlers = {
8: backspaceKeyUp,
9: tabKeyUp,
27: escapeKeyUp,
13: enterKeyUp,
46: deleteKeyUp
};
if (handlers[key]) { if (handlers[key]) {
handlers[key](); handlers[key]();
} else { } else {
@ -208,16 +247,10 @@
return; return;
} }
if (searchVal === '') { if (searchVal === '') {
ctrl.filteredObj = ctrl.unusedFacetChoices;
$scope.$apply();
emitTextSearch('');
if (ctrl.facetSelected && angular.isUndefined(ctrl.facetSelected.options)) {
resetState();
}
return; return;
} }
// Backspace, Delete // Backspace, Delete and arrow keys
if (key !== 8 && key !== 46) { if (key !== 8 && key !== 46 && !(key >= 37 && key <= 40)) {
filterFacets(searchVal); filterFacets(searchVal);
} }
} }
@ -257,7 +290,6 @@
} }
function facetClickHandler($index) { function facetClickHandler($index) {
setMenuOpen(false);
var facet = ctrl.filteredObj[$index]; var facet = ctrl.filteredObj[$index];
var label = facet.label; var label = facet.label;
if (angular.isArray(label)) { if (angular.isArray(label)) {
@ -268,12 +300,8 @@
if (angular.isDefined(facet.options)) { if (angular.isDefined(facet.options)) {
ctrl.filteredOptions = ctrl.facetOptions = facet.options; ctrl.filteredOptions = ctrl.facetOptions = facet.options;
setMenuOpen(true); setMenuOpen(true);
} } else {
var searchVal = searchInput.val(); setMenuOpen(false);
if (searchVal) {
ctrl.currentSearch = ctrl.currentSearch.filter(notTextSearch);
ctrl.currentSearch.push(service.getTextFacet(searchVal, $scope.strings.text));
ctrl.textSearch = searchVal;
} }
setSearchInput(''); setSearchInput('');
setPrompt(''); setPrompt('');

View File

@ -148,7 +148,7 @@
expect(keyDownHandler).toBeDefined(); expect(keyDownHandler).toBeDefined();
}); });
it("does nothing with keys other than 9", function() { it("does nothing with keys other than 9 and 8", function() {
spyOn(evt, 'preventDefault'); spyOn(evt, 'preventDefault');
keyDownHandler(evt); keyDownHandler(evt);
expect(evt.preventDefault).not.toHaveBeenCalled(); expect(evt.preventDefault).not.toHaveBeenCalled();
@ -160,6 +160,30 @@
keyDownHandler(evt); keyDownHandler(evt);
expect(evt.preventDefault).toHaveBeenCalled(); expect(evt.preventDefault).toHaveBeenCalled();
}); });
describe("'Backspace' key", function() {
beforeEach(function() {
evt.keyCode = 8;
});
it("removes last facet if length larger than 1 and searchVal empty", function() {
spyOn(searchInput, 'val').and.returnValue('');
spyOn(ctrl, 'removeFacet');
delete ctrl.facetSelected;
ctrl.currentSearch = [{name: 'name=foo'}, {name: 'flavor=m1'}, {name: 'key=value'}];
keyDownHandler(evt);
$timeout.flush();
expect(ctrl.removeFacet).toHaveBeenCalledWith(2);
});
it("removes selectedFacet if searchVal is empty", function() {
spyOn(searchInput, 'val').and.returnValue('');
ctrl.facetSelected = {name: 'waldo=undefined', label: ['a']};
keyDownHandler(evt);
$timeout.flush();
expect(ctrl.facetSelect).toBeUndefined();
});
});
}); });
describe("keyup handler", function() { describe("keyup handler", function() {
@ -181,6 +205,34 @@
expect(scope.$emit).not.toHaveBeenCalled(); expect(scope.$emit).not.toHaveBeenCalled();
}); });
describe("'Backspace' key", function() {
beforeEach(function() {
evt.keyCode = 8;
});
it("calls clearSearch if facetSelected undefined and currentSearch empty", function() {
spyOn(searchInput, 'val').and.returnValue('');
spyOn(ctrl, 'clearSearch');
delete ctrl.facetSelected;
ctrl.currentSearch = [];
keyUpHandler(evt);
expect(ctrl.clearSearch).toHaveBeenCalled();
});
it("emits textSearch if facetSeleted undefined and currentSearch not empty", function() {
spyOn(searchInput, 'val').and.returnValue('');
spyOn(scope, '$emit');
delete ctrl.facetSelected;
ctrl.currentSearch = [{name: 'textstuff'}, {name: 'texting'}];
scope.filter_keys = [1,2,3];
keyUpHandler(evt);
expectResetState();
expect(scope.$emit).toHaveBeenCalledWith(magicSearchEvents.TEXT_SEARCH, '', [1, 2, 3]);
});
});
describe("'Escape' key", function() { describe("'Escape' key", function() {
beforeEach(function() { beforeEach(function() {
evt.keyCode = 27; evt.keyCode = 27;
@ -298,6 +350,26 @@
expect(ctrl.currentSearch).toEqual([{name: 'nontext'}, {name: 'nottext'}, expect(ctrl.currentSearch).toEqual([{name: 'nontext'}, {name: 'nottext'},
{name: 'text=searchval', label: ['stringtext', 'searchval']}]); {name: 'text=searchval', label: ['stringtext', 'searchval']}]);
}); });
it("opens menu when searchVal is an empty string", function() {
ctrl.isMenuOpen = false;
spyOn(searchInput, 'val').and.returnValue('');
spyOn(scope, '$emit');
scope.filter_keys = [1,2,3];
keyUpHandler(evt);
$timeout.flush();
expect(ctrl.isMenuOpen).toBe(true);
});
it("emits a Query when not empty string and a facet is selected", function() {
spyOn(searchInput, 'val').and.returnValue('foo');
ctrl.currentSearch = [];
keyUpHandler(evt);
$timeout.flush();
expect(ctrl.currentSearch).toEqual([{name: 'waldo=foo', label: ['a', 'foo']}]);
expectResetState();
expect(ctrl.isMenuOpen).toBe(true);
});
}); });
describe("Any other key", function() { describe("Any other key", function() {
@ -314,14 +386,6 @@
magicSearchEvents.TEXT_SEARCH, '', ['a', 'b', 'c']); magicSearchEvents.TEXT_SEARCH, '', ['a', 'b', 'c']);
}); });
it("resets state if facetSelected and no options", function() {
spyOn(searchInput, 'val').and.returnValue('');
scope.filter_keys = ['a', 'b', 'c'];
ctrl.facetSelected = {};
keyUpHandler(evt);
expectResetState();
});
it("filters if there is a search term", function() { it("filters if there is a search term", function() {
spyOn(searchInput, 'val').and.returnValue('searchterm'); spyOn(searchInput, 'val').and.returnValue('searchterm');
spyOn(scope, '$emit'); spyOn(scope, '$emit');
@ -357,36 +421,9 @@
it("opens menu when searchVal is a space", function() { it("opens menu when searchVal is a space", function() {
evt.which = 32; evt.which = 32;
spyOn(searchInput, 'val').and.returnValue(' ');
spyOn(scope, '$emit');
scope.filter_keys = [1,2,3];
keyPressHandler(evt); keyPressHandler(evt);
expect(scope.$emit).toHaveBeenCalledWith( $timeout.flush();
magicSearchEvents.TEXT_SEARCH, ' ', [1,2,3]); expect(ctrl.isMenuOpen).toBe(true);
});
it("opens menu when searchVal is an empty string", function() {
spyOn(searchInput, 'val').and.returnValue('');
spyOn(scope, '$emit');
evt.which = 13; // not alter search
scope.filter_keys = [1,2,3];
keyPressHandler(evt);
expect(scope.$emit).toHaveBeenCalledWith(
magicSearchEvents.TEXT_SEARCH, '', [1,2,3]);
});
it("resets state when ctrl.facetSelected exists but has no options", function() {
spyOn(searchInput, 'val').and.returnValue('');
spyOn(scope, '$emit');
evt.which = 13; // not alter search
scope.filter_keys = [1,2,3];
ctrl.facetSelected = {};
ctrl.facetOptions = {};
ctrl.filteredOptions = {};
keyPressHandler(evt);
expect(scope.$emit).toHaveBeenCalledWith(
magicSearchEvents.TEXT_SEARCH, '', [1,2,3]);
expectResetState();
}); });
it("filters when searchval has content and key is not delete/backspace", function() { it("filters when searchval has content and key is not delete/backspace", function() {
@ -406,6 +443,7 @@
keyPressHandler(evt); keyPressHandler(evt);
expect(scope.$emit).not.toHaveBeenCalled(); expect(scope.$emit).not.toHaveBeenCalled();
}); });
}); });
describe("optionClicked", function() { describe("optionClicked", function() {

View File

@ -89,6 +89,10 @@
.search-entry { .search-entry {
flex: 1 0 auto; flex: 1 0 auto;
} }
.dropdown-menu {
left: initial;
}
} }
.fi-filter { .fi-filter {

View File

@ -0,0 +1,8 @@
---
fixes:
- |
[`bug 1618235 <https://bugs.launchpad.net/horizon/+bug/1618235>`__]
User can now delete all characters typed in input search without causing
the selected facet to disappear when the last character is deleted.
other:
- Menu follows the search input position as the user adds more facets

View File

@ -0,0 +1,7 @@
---
fixes:
- |
[`bug 1635505 <https://bugs.launchpad.net/horizon/+bug/1635505>`__]
Horizon now properly allows to use arrow keys inside of the input search,
without triggering a new text search that refreshes the content of the
table below.