Add button to delete filters from filtered search bar

This commit is contained in:
Clement Ho 2017-04-26 20:12:33 +00:00 committed by Jacob Schatz
parent e2be17b78e
commit 09f09139e4
8 changed files with 145 additions and 21 deletions

View file

@ -77,13 +77,14 @@ class FilteredSearchManager {
this.checkForEnterWrapper = this.checkForEnter.bind(this); this.checkForEnterWrapper = this.checkForEnter.bind(this);
this.onClearSearchWrapper = this.onClearSearch.bind(this); this.onClearSearchWrapper = this.onClearSearch.bind(this);
this.checkForBackspaceWrapper = this.checkForBackspace.bind(this); this.checkForBackspaceWrapper = this.checkForBackspace.bind(this);
this.removeSelectedTokenWrapper = this.removeSelectedToken.bind(this); this.removeSelectedTokenKeydownWrapper = this.removeSelectedTokenKeydown.bind(this);
this.unselectEditTokensWrapper = this.unselectEditTokens.bind(this); this.unselectEditTokensWrapper = this.unselectEditTokens.bind(this);
this.editTokenWrapper = this.editToken.bind(this); this.editTokenWrapper = this.editToken.bind(this);
this.tokenChange = this.tokenChange.bind(this); this.tokenChange = this.tokenChange.bind(this);
this.addInputContainerFocusWrapper = this.addInputContainerFocus.bind(this); this.addInputContainerFocusWrapper = this.addInputContainerFocus.bind(this);
this.removeInputContainerFocusWrapper = this.removeInputContainerFocus.bind(this); this.removeInputContainerFocusWrapper = this.removeInputContainerFocus.bind(this);
this.onrecentSearchesItemSelectedWrapper = this.onrecentSearchesItemSelected.bind(this); this.onrecentSearchesItemSelectedWrapper = this.onrecentSearchesItemSelected.bind(this);
this.removeTokenWrapper = this.removeToken.bind(this);
this.filteredSearchInputForm.addEventListener('submit', this.handleFormSubmit); this.filteredSearchInputForm.addEventListener('submit', this.handleFormSubmit);
this.filteredSearchInput.addEventListener('input', this.setDropdownWrapper); this.filteredSearchInput.addEventListener('input', this.setDropdownWrapper);
@ -96,12 +97,13 @@ class FilteredSearchManager {
this.filteredSearchInput.addEventListener('keyup', this.tokenChange); this.filteredSearchInput.addEventListener('keyup', this.tokenChange);
this.filteredSearchInput.addEventListener('focus', this.addInputContainerFocusWrapper); this.filteredSearchInput.addEventListener('focus', this.addInputContainerFocusWrapper);
this.tokensContainer.addEventListener('click', FilteredSearchManager.selectToken); this.tokensContainer.addEventListener('click', FilteredSearchManager.selectToken);
this.tokensContainer.addEventListener('click', this.removeTokenWrapper);
this.tokensContainer.addEventListener('dblclick', this.editTokenWrapper); this.tokensContainer.addEventListener('dblclick', this.editTokenWrapper);
this.clearSearchButton.addEventListener('click', this.onClearSearchWrapper); this.clearSearchButton.addEventListener('click', this.onClearSearchWrapper);
document.addEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens); document.addEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens);
document.addEventListener('click', this.unselectEditTokensWrapper); document.addEventListener('click', this.unselectEditTokensWrapper);
document.addEventListener('click', this.removeInputContainerFocusWrapper); document.addEventListener('click', this.removeInputContainerFocusWrapper);
document.addEventListener('keydown', this.removeSelectedTokenWrapper); document.addEventListener('keydown', this.removeSelectedTokenKeydownWrapper);
eventHub.$on('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper); eventHub.$on('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper);
} }
@ -117,12 +119,13 @@ class FilteredSearchManager {
this.filteredSearchInput.removeEventListener('keyup', this.tokenChange); this.filteredSearchInput.removeEventListener('keyup', this.tokenChange);
this.filteredSearchInput.removeEventListener('focus', this.addInputContainerFocusWrapper); this.filteredSearchInput.removeEventListener('focus', this.addInputContainerFocusWrapper);
this.tokensContainer.removeEventListener('click', FilteredSearchManager.selectToken); this.tokensContainer.removeEventListener('click', FilteredSearchManager.selectToken);
this.tokensContainer.removeEventListener('click', this.removeTokenWrapper);
this.tokensContainer.removeEventListener('dblclick', this.editTokenWrapper); this.tokensContainer.removeEventListener('dblclick', this.editTokenWrapper);
this.clearSearchButton.removeEventListener('click', this.onClearSearchWrapper); this.clearSearchButton.removeEventListener('click', this.onClearSearchWrapper);
document.removeEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens); document.removeEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens);
document.removeEventListener('click', this.unselectEditTokensWrapper); document.removeEventListener('click', this.unselectEditTokensWrapper);
document.removeEventListener('click', this.removeInputContainerFocusWrapper); document.removeEventListener('click', this.removeInputContainerFocusWrapper);
document.removeEventListener('keydown', this.removeSelectedTokenWrapper); document.removeEventListener('keydown', this.removeSelectedTokenKeydownWrapper);
eventHub.$off('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper); eventHub.$off('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper);
} }
@ -195,14 +198,28 @@ class FilteredSearchManager {
static selectToken(e) { static selectToken(e) {
const button = e.target.closest('.selectable'); const button = e.target.closest('.selectable');
const removeButtonSelected = e.target.closest('.remove-token');
if (button) { if (!removeButtonSelected && button) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
gl.FilteredSearchVisualTokens.selectToken(button); gl.FilteredSearchVisualTokens.selectToken(button);
} }
} }
removeToken(e) {
const removeButtonSelected = e.target.closest('.remove-token');
if (removeButtonSelected) {
e.preventDefault();
e.stopPropagation();
const button = e.target.closest('.selectable');
gl.FilteredSearchVisualTokens.selectToken(button, true);
this.removeSelectedToken();
}
}
unselectEditTokens(e) { unselectEditTokens(e) {
const inputContainer = this.container.querySelector('.filtered-search-box'); const inputContainer = this.container.querySelector('.filtered-search-box');
const isElementInFilteredSearch = inputContainer && inputContainer.contains(e.target); const isElementInFilteredSearch = inputContainer && inputContainer.contains(e.target);
@ -248,16 +265,21 @@ class FilteredSearchManager {
} }
} }
removeSelectedToken(e) { removeSelectedTokenKeydown(e) {
// 8 = Backspace Key // 8 = Backspace Key
// 46 = Delete Key // 46 = Delete Key
if (e.keyCode === 8 || e.keyCode === 46) { if (e.keyCode === 8 || e.keyCode === 46) {
gl.FilteredSearchVisualTokens.removeSelectedToken(); this.removeSelectedToken();
this.handleInputPlaceholder();
this.toggleClearSearchButton();
} }
} }
removeSelectedToken() {
gl.FilteredSearchVisualTokens.removeSelectedToken();
this.handleInputPlaceholder();
this.toggleClearSearchButton();
this.dropdownManager.updateCurrentDropdownOffset();
}
onClearSearch(e) { onClearSearch(e) {
e.preventDefault(); e.preventDefault();
this.clearSearch(); this.clearSearch();

View file

@ -16,11 +16,11 @@ class FilteredSearchVisualTokens {
[].forEach.call(otherTokens, t => t.classList.remove('selected')); [].forEach.call(otherTokens, t => t.classList.remove('selected'));
} }
static selectToken(tokenButton) { static selectToken(tokenButton, forceSelection = false) {
const selected = tokenButton.classList.contains('selected'); const selected = tokenButton.classList.contains('selected');
FilteredSearchVisualTokens.unselectTokens(); FilteredSearchVisualTokens.unselectTokens();
if (!selected) { if (!selected || forceSelection) {
tokenButton.classList.add('selected'); tokenButton.classList.add('selected');
} }
} }
@ -38,7 +38,12 @@ class FilteredSearchVisualTokens {
return ` return `
<div class="selectable" role="button"> <div class="selectable" role="button">
<div class="name"></div> <div class="name"></div>
<div class="value"></div> <div class="value-container">
<div class="value"></div>
<div class="remove-token" role="button">
<i class="fa fa-close"></i>
</div>
</div>
</div> </div>
`; `;
} }
@ -122,7 +127,8 @@ class FilteredSearchVisualTokens {
if (value) { if (value) {
const button = lastVisualToken.querySelector('.selectable'); const button = lastVisualToken.querySelector('.selectable');
button.removeChild(value); const valueContainer = lastVisualToken.querySelector('.value-container');
button.removeChild(valueContainer);
lastVisualToken.innerHTML = button.innerHTML; lastVisualToken.innerHTML = button.innerHTML;
} else { } else {
lastVisualToken.closest('.tokens-container').removeChild(lastVisualToken); lastVisualToken.closest('.tokens-container').removeChild(lastVisualToken);

View file

@ -104,6 +104,24 @@
padding: 2px 7px; padding: 2px 7px;
} }
.value {
padding-right: 0;
}
.remove-token {
display: inline-block;
padding-left: 4px;
padding-right: 8px;
.fa-close {
color: $gl-text-color-disabled;
}
&:hover .fa-close {
color: $gl-text-color-secondary;
}
}
.name { .name {
background-color: $filter-name-resting-color; background-color: $filter-name-resting-color;
color: $filter-name-text-color; color: $filter-name-text-color;
@ -112,7 +130,7 @@
text-transform: capitalize; text-transform: capitalize;
} }
.value { .value-container {
background-color: $white-normal; background-color: $white-normal;
color: $filter-value-text-color; color: $filter-value-text-color;
border-radius: 0 2px 2px 0; border-radius: 0 2px 2px 0;
@ -124,7 +142,7 @@
background-color: $filter-name-selected-color; background-color: $filter-name-selected-color;
} }
.value { .value-container {
background-color: $filter-value-selected-color; background-color: $filter-value-selected-color;
} }
} }

View file

@ -0,0 +1,4 @@
---
title: Add button to delete filters from filtered search bar
merge_request:
author:

View file

@ -12,7 +12,7 @@ describe 'Filter issues', js: true, feature: true do
let!(:wontfix) { create(:label, project: project, title: "Won't fix") } let!(:wontfix) { create(:label, project: project, title: "Won't fix") }
let!(:bug_label) { create(:label, project: project, title: 'bug') } let!(:bug_label) { create(:label, project: project, title: 'bug') }
let!(:caps_sensitive_label) { create(:label, project: project, title: 'CAPS_sensitive') } let!(:caps_sensitive_label) { create(:label, project: project, title: 'CaPs') }
let!(:milestone) { create(:milestone, title: "8", project: project, start_date: 2.days.ago) } let!(:milestone) { create(:milestone, title: "8", project: project, start_date: 2.days.ago) }
let!(:multiple_words_label) { create(:label, project: project, title: "Two words") } let!(:multiple_words_label) { create(:label, project: project, title: "Two words") }

View file

@ -26,6 +26,10 @@ describe('Filtered Search Manager', () => {
element.dispatchEvent(event); element.dispatchEvent(event);
} }
function getVisualTokens() {
return tokensContainer.querySelectorAll('.js-visual-token');
}
beforeEach(() => { beforeEach(() => {
setFixtures(` setFixtures(`
<div class="filtered-search-box"> <div class="filtered-search-box">
@ -170,11 +174,37 @@ describe('Filtered Search Manager', () => {
}); });
}); });
describe('removeSelectedToken', () => { describe('removeToken', () => {
function getVisualTokens() { it('removes token even when it is already selected', () => {
return tokensContainer.querySelectorAll('.js-visual-token'); tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
} FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'none', true),
);
tokensContainer.querySelector('.js-visual-token .remove-token').click();
expect(tokensContainer.querySelector('.js-visual-token')).toEqual(null);
});
describe('unselected token', () => {
beforeEach(() => {
spyOn(gl.FilteredSearchManager.prototype, 'removeSelectedToken').and.callThrough();
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'none'),
);
tokensContainer.querySelector('.js-visual-token .remove-token').click();
});
it('removes token when remove button is selected', () => {
expect(tokensContainer.querySelector('.js-visual-token')).toEqual(null);
});
it('calls removeSelectedToken', () => {
expect(manager.removeSelectedToken).toHaveBeenCalled();
});
});
});
describe('removeSelectedTokenKeydown', () => {
beforeEach(() => { beforeEach(() => {
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML( tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'none', true), FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'none', true),
@ -224,6 +254,31 @@ describe('Filtered Search Manager', () => {
}); });
}); });
describe('removeSelectedToken', () => {
beforeEach(() => {
spyOn(gl.FilteredSearchVisualTokens, 'removeSelectedToken').and.callThrough();
spyOn(gl.FilteredSearchManager.prototype, 'handleInputPlaceholder').and.callThrough();
spyOn(gl.FilteredSearchManager.prototype, 'toggleClearSearchButton').and.callThrough();
manager.removeSelectedToken();
});
it('calls FilteredSearchVisualTokens.removeSelectedToken', () => {
expect(gl.FilteredSearchVisualTokens.removeSelectedToken).toHaveBeenCalled();
});
it('calls handleInputPlaceholder', () => {
expect(manager.handleInputPlaceholder).toHaveBeenCalled();
});
it('calls toggleClearSearchButton', () => {
expect(manager.toggleClearSearchButton).toHaveBeenCalled();
});
it('calls update dropdown offset', () => {
expect(manager.dropdownManager.updateDropdownOffset).toHaveBeenCalled();
});
});
describe('unselects token', () => { describe('unselects token', () => {
beforeEach(() => { beforeEach(() => {
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(` tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`

View file

@ -214,8 +214,12 @@ describe('Filtered Search Visual Tokens', () => {
expect(tokenElement.querySelector('.name')).toEqual(jasmine.anything()); expect(tokenElement.querySelector('.name')).toEqual(jasmine.anything());
}); });
it('contains value container div', () => {
expect(tokenElement.querySelector('.value-container')).toEqual(jasmine.anything());
});
it('contains value div', () => { it('contains value div', () => {
expect(tokenElement.querySelector('.value')).toEqual(jasmine.anything()); expect(tokenElement.querySelector('.value-container .value')).toEqual(jasmine.anything());
}); });
it('contains selectable class', () => { it('contains selectable class', () => {
@ -225,6 +229,16 @@ describe('Filtered Search Visual Tokens', () => {
it('contains button role', () => { it('contains button role', () => {
expect(tokenElement.getAttribute('role')).toEqual('button'); expect(tokenElement.getAttribute('role')).toEqual('button');
}); });
describe('remove token', () => {
it('contains remove-token button', () => {
expect(tokenElement.querySelector('.value-container .remove-token')).toEqual(jasmine.anything());
});
it('contains fa-close icon', () => {
expect(tokenElement.querySelector('.remove-token .fa-close')).toEqual(jasmine.anything());
});
});
}); });
describe('addVisualTokenElement', () => { describe('addVisualTokenElement', () => {

View file

@ -10,7 +10,12 @@ class FilteredSearchSpecHelper {
li.innerHTML = ` li.innerHTML = `
<div class="selectable ${isSelected ? 'selected' : ''}" role="button"> <div class="selectable ${isSelected ? 'selected' : ''}" role="button">
<div class="name">${name}</div> <div class="name">${name}</div>
<div class="value">${value}</div> <div class="value-container">
<div class="value">${value}</div>
<div class="remove-token" role="button">
<i class="fa fa-close"></i>
</div>
</div>
</div> </div>
`; `;