Add button to delete filters from filtered search bar
This commit is contained in:
parent
e2be17b78e
commit
09f09139e4
8 changed files with 145 additions and 21 deletions
|
@ -77,13 +77,14 @@ class FilteredSearchManager {
|
|||
this.checkForEnterWrapper = this.checkForEnter.bind(this);
|
||||
this.onClearSearchWrapper = this.onClearSearch.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.editTokenWrapper = this.editToken.bind(this);
|
||||
this.tokenChange = this.tokenChange.bind(this);
|
||||
this.addInputContainerFocusWrapper = this.addInputContainerFocus.bind(this);
|
||||
this.removeInputContainerFocusWrapper = this.removeInputContainerFocus.bind(this);
|
||||
this.onrecentSearchesItemSelectedWrapper = this.onrecentSearchesItemSelected.bind(this);
|
||||
this.removeTokenWrapper = this.removeToken.bind(this);
|
||||
|
||||
this.filteredSearchInputForm.addEventListener('submit', this.handleFormSubmit);
|
||||
this.filteredSearchInput.addEventListener('input', this.setDropdownWrapper);
|
||||
|
@ -96,12 +97,13 @@ class FilteredSearchManager {
|
|||
this.filteredSearchInput.addEventListener('keyup', this.tokenChange);
|
||||
this.filteredSearchInput.addEventListener('focus', this.addInputContainerFocusWrapper);
|
||||
this.tokensContainer.addEventListener('click', FilteredSearchManager.selectToken);
|
||||
this.tokensContainer.addEventListener('click', this.removeTokenWrapper);
|
||||
this.tokensContainer.addEventListener('dblclick', this.editTokenWrapper);
|
||||
this.clearSearchButton.addEventListener('click', this.onClearSearchWrapper);
|
||||
document.addEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens);
|
||||
document.addEventListener('click', this.unselectEditTokensWrapper);
|
||||
document.addEventListener('click', this.removeInputContainerFocusWrapper);
|
||||
document.addEventListener('keydown', this.removeSelectedTokenWrapper);
|
||||
document.addEventListener('keydown', this.removeSelectedTokenKeydownWrapper);
|
||||
eventHub.$on('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper);
|
||||
}
|
||||
|
||||
|
@ -117,12 +119,13 @@ class FilteredSearchManager {
|
|||
this.filteredSearchInput.removeEventListener('keyup', this.tokenChange);
|
||||
this.filteredSearchInput.removeEventListener('focus', this.addInputContainerFocusWrapper);
|
||||
this.tokensContainer.removeEventListener('click', FilteredSearchManager.selectToken);
|
||||
this.tokensContainer.removeEventListener('click', this.removeTokenWrapper);
|
||||
this.tokensContainer.removeEventListener('dblclick', this.editTokenWrapper);
|
||||
this.clearSearchButton.removeEventListener('click', this.onClearSearchWrapper);
|
||||
document.removeEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens);
|
||||
document.removeEventListener('click', this.unselectEditTokensWrapper);
|
||||
document.removeEventListener('click', this.removeInputContainerFocusWrapper);
|
||||
document.removeEventListener('keydown', this.removeSelectedTokenWrapper);
|
||||
document.removeEventListener('keydown', this.removeSelectedTokenKeydownWrapper);
|
||||
eventHub.$off('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper);
|
||||
}
|
||||
|
||||
|
@ -195,14 +198,28 @@ class FilteredSearchManager {
|
|||
|
||||
static selectToken(e) {
|
||||
const button = e.target.closest('.selectable');
|
||||
const removeButtonSelected = e.target.closest('.remove-token');
|
||||
|
||||
if (button) {
|
||||
if (!removeButtonSelected && button) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
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) {
|
||||
const inputContainer = this.container.querySelector('.filtered-search-box');
|
||||
const isElementInFilteredSearch = inputContainer && inputContainer.contains(e.target);
|
||||
|
@ -248,16 +265,21 @@ class FilteredSearchManager {
|
|||
}
|
||||
}
|
||||
|
||||
removeSelectedToken(e) {
|
||||
removeSelectedTokenKeydown(e) {
|
||||
// 8 = Backspace Key
|
||||
// 46 = Delete Key
|
||||
if (e.keyCode === 8 || e.keyCode === 46) {
|
||||
gl.FilteredSearchVisualTokens.removeSelectedToken();
|
||||
this.handleInputPlaceholder();
|
||||
this.toggleClearSearchButton();
|
||||
this.removeSelectedToken();
|
||||
}
|
||||
}
|
||||
|
||||
removeSelectedToken() {
|
||||
gl.FilteredSearchVisualTokens.removeSelectedToken();
|
||||
this.handleInputPlaceholder();
|
||||
this.toggleClearSearchButton();
|
||||
this.dropdownManager.updateCurrentDropdownOffset();
|
||||
}
|
||||
|
||||
onClearSearch(e) {
|
||||
e.preventDefault();
|
||||
this.clearSearch();
|
||||
|
|
|
@ -16,11 +16,11 @@ class FilteredSearchVisualTokens {
|
|||
[].forEach.call(otherTokens, t => t.classList.remove('selected'));
|
||||
}
|
||||
|
||||
static selectToken(tokenButton) {
|
||||
static selectToken(tokenButton, forceSelection = false) {
|
||||
const selected = tokenButton.classList.contains('selected');
|
||||
FilteredSearchVisualTokens.unselectTokens();
|
||||
|
||||
if (!selected) {
|
||||
if (!selected || forceSelection) {
|
||||
tokenButton.classList.add('selected');
|
||||
}
|
||||
}
|
||||
|
@ -38,7 +38,12 @@ class FilteredSearchVisualTokens {
|
|||
return `
|
||||
<div class="selectable" role="button">
|
||||
<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>
|
||||
`;
|
||||
}
|
||||
|
@ -122,7 +127,8 @@ class FilteredSearchVisualTokens {
|
|||
|
||||
if (value) {
|
||||
const button = lastVisualToken.querySelector('.selectable');
|
||||
button.removeChild(value);
|
||||
const valueContainer = lastVisualToken.querySelector('.value-container');
|
||||
button.removeChild(valueContainer);
|
||||
lastVisualToken.innerHTML = button.innerHTML;
|
||||
} else {
|
||||
lastVisualToken.closest('.tokens-container').removeChild(lastVisualToken);
|
||||
|
|
|
@ -104,6 +104,24 @@
|
|||
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 {
|
||||
background-color: $filter-name-resting-color;
|
||||
color: $filter-name-text-color;
|
||||
|
@ -112,7 +130,7 @@
|
|||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.value {
|
||||
.value-container {
|
||||
background-color: $white-normal;
|
||||
color: $filter-value-text-color;
|
||||
border-radius: 0 2px 2px 0;
|
||||
|
@ -124,7 +142,7 @@
|
|||
background-color: $filter-name-selected-color;
|
||||
}
|
||||
|
||||
.value {
|
||||
.value-container {
|
||||
background-color: $filter-value-selected-color;
|
||||
}
|
||||
}
|
||||
|
|
4
changelogs/unreleased/30466-click-x-to-remove-filter.yml
Normal file
4
changelogs/unreleased/30466-click-x-to-remove-filter.yml
Normal file
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Add button to delete filters from filtered search bar
|
||||
merge_request:
|
||||
author:
|
|
@ -12,7 +12,7 @@ describe 'Filter issues', js: true, feature: true do
|
|||
let!(:wontfix) { create(:label, project: project, title: "Won't fix") }
|
||||
|
||||
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!(:multiple_words_label) { create(:label, project: project, title: "Two words") }
|
||||
|
||||
|
|
|
@ -26,6 +26,10 @@ describe('Filtered Search Manager', () => {
|
|||
element.dispatchEvent(event);
|
||||
}
|
||||
|
||||
function getVisualTokens() {
|
||||
return tokensContainer.querySelectorAll('.js-visual-token');
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
setFixtures(`
|
||||
<div class="filtered-search-box">
|
||||
|
@ -170,11 +174,37 @@ describe('Filtered Search Manager', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('removeSelectedToken', () => {
|
||||
function getVisualTokens() {
|
||||
return tokensContainer.querySelectorAll('.js-visual-token');
|
||||
}
|
||||
describe('removeToken', () => {
|
||||
it('removes token even when it is already selected', () => {
|
||||
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(() => {
|
||||
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
|
||||
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', () => {
|
||||
beforeEach(() => {
|
||||
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
|
||||
|
|
|
@ -214,8 +214,12 @@ describe('Filtered Search Visual Tokens', () => {
|
|||
expect(tokenElement.querySelector('.name')).toEqual(jasmine.anything());
|
||||
});
|
||||
|
||||
it('contains value container div', () => {
|
||||
expect(tokenElement.querySelector('.value-container')).toEqual(jasmine.anything());
|
||||
});
|
||||
|
||||
it('contains value div', () => {
|
||||
expect(tokenElement.querySelector('.value')).toEqual(jasmine.anything());
|
||||
expect(tokenElement.querySelector('.value-container .value')).toEqual(jasmine.anything());
|
||||
});
|
||||
|
||||
it('contains selectable class', () => {
|
||||
|
@ -225,6 +229,16 @@ describe('Filtered Search Visual Tokens', () => {
|
|||
it('contains button role', () => {
|
||||
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', () => {
|
||||
|
|
|
@ -10,7 +10,12 @@ class FilteredSearchSpecHelper {
|
|||
li.innerHTML = `
|
||||
<div class="selectable ${isSelected ? 'selected' : ''}" role="button">
|
||||
<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>
|
||||
`;
|
||||
|
||||
|
|
Loading…
Reference in a new issue