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
parent 2394397bfd
commit 6f8504d9b8
5 changed files with 166 additions and 81 deletions

View File

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

View File

@ -148,7 +148,7 @@
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');
keyDownHandler(evt);
expect(evt.preventDefault).not.toHaveBeenCalled();
@ -160,6 +160,30 @@
keyDownHandler(evt);
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() {
@ -181,6 +205,34 @@
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() {
beforeEach(function() {
evt.keyCode = 27;
@ -298,6 +350,26 @@
expect(ctrl.currentSearch).toEqual([{name: 'nontext'}, {name: 'nottext'},
{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() {
@ -314,14 +386,6 @@
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() {
spyOn(searchInput, 'val').and.returnValue('searchterm');
spyOn(scope, '$emit');
@ -357,36 +421,9 @@
it("opens menu when searchVal is a space", function() {
evt.which = 32;
spyOn(searchInput, 'val').and.returnValue(' ');
spyOn(scope, '$emit');
scope.filter_keys = [1,2,3];
keyPressHandler(evt);
expect(scope.$emit).toHaveBeenCalledWith(
magicSearchEvents.TEXT_SEARCH, ' ', [1,2,3]);
});
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();
$timeout.flush();
expect(ctrl.isMenuOpen).toBe(true);
});
it("filters when searchval has content and key is not delete/backspace", function() {
@ -406,6 +443,7 @@
keyPressHandler(evt);
expect(scope.$emit).not.toHaveBeenCalled();
});
});
describe("optionClicked", function() {

View File

@ -89,6 +89,10 @@
.search-entry {
flex: 1 0 auto;
}
.dropdown-menu {
left: initial;
}
}
.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.