Merge branch 'dropdown-input-fix' into 'master'
Do not allow text input in dropdown while loading ## What does this MR do? It moves the focus event of the text input of a filterable dropdown to after loading of options is complete. After the fix, the user cannot edit the while it's greyed out and loading. ## Are there points in the code the reviewer needs to double check? ## Why was this MR needed? It fixes the bug in https://gitlab.com/gitlab-org/gitlab-ce/issues/23496. ## Screenshots (if relevant) ![https://gitlab.com/gitlab-org/gitlab-ce/uploads/a7bc2f0228fcde5a85cccb333b52f0e3/2016-10-18_12.00.54.gif](https://gitlab.com/gitlab-org/gitlab-ce/uploads/a7bc2f0228fcde5a85cccb333b52f0e3/2016-10-18_12.00.54.gif) ## What are the relevant issue numbers? Fixes #23496 See merge request !7054
This commit is contained in:
commit
4d3a080364
3 changed files with 73 additions and 17 deletions
|
@ -36,6 +36,7 @@ Please view this file on the master branch, on stable branches it's out of date.
|
|||
- Fix documents and comments on Build API `scope`
|
||||
- Refactor email, use setter method instead AR callbacks for email attribute (Semyon Pupkov)
|
||||
- Shortened merge request modal to let clipboard button not overlap
|
||||
- In all filterable drop downs, put input field in focus only after load is complete (Ido @leibo)
|
||||
|
||||
## 8.13.2
|
||||
- Fix builds dropdown overlapping bug !7124
|
||||
|
|
|
@ -239,6 +239,7 @@
|
|||
this.fullData = this.options.data;
|
||||
currentIndex = -1;
|
||||
this.parseData(this.options.data);
|
||||
this.focusTextInput();
|
||||
} else {
|
||||
this.remote = new GitLabDropdownRemote(this.options.data, {
|
||||
dataType: this.options.dataType,
|
||||
|
@ -247,6 +248,7 @@
|
|||
return function(data) {
|
||||
_this.fullData = data;
|
||||
_this.parseData(_this.fullData);
|
||||
_this.focusTextInput();
|
||||
if (_this.options.filterable && _this.filter && _this.filter.input) {
|
||||
return _this.filter.input.trigger('input');
|
||||
}
|
||||
|
@ -452,9 +454,8 @@
|
|||
contentHtml = $('.dropdown-content', this.dropdown).html();
|
||||
if (this.remote && contentHtml === "") {
|
||||
this.remote.execute();
|
||||
}
|
||||
if (this.options.filterable) {
|
||||
this.filterInput.focus();
|
||||
} else {
|
||||
this.focusTextInput();
|
||||
}
|
||||
|
||||
if (this.options.showMenuAbove) {
|
||||
|
@ -691,6 +692,10 @@
|
|||
return selectedObject;
|
||||
};
|
||||
|
||||
GitLabDropdown.prototype.focusTextInput = function() {
|
||||
if (this.options.filterable) { this.filterInput.focus() }
|
||||
}
|
||||
|
||||
GitLabDropdown.prototype.addInput = function(fieldName, value, selectedObject) {
|
||||
var $input;
|
||||
// Create hidden input for form
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
(() => {
|
||||
const NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-link';
|
||||
const SEARCH_INPUT_SELECTOR = '.dropdown-input-field';
|
||||
const ITEM_SELECTOR = `.dropdown-content li:not(${NON_SELECTABLE_CLASSES})`;
|
||||
const FOCUSED_ITEM_SELECTOR = `${ITEM_SELECTOR} a.is-focused`;
|
||||
|
||||
|
@ -17,6 +18,8 @@
|
|||
ESC: 27
|
||||
};
|
||||
|
||||
let remoteCallback;
|
||||
|
||||
let navigateWithKeys = function navigateWithKeys(direction, steps, cb, i) {
|
||||
i = i || 0;
|
||||
if (!i) direction = direction.toUpperCase();
|
||||
|
@ -33,18 +36,19 @@
|
|||
}
|
||||
};
|
||||
|
||||
let remoteMock = function remoteMock(data, term, callback) {
|
||||
remoteCallback = callback.bind({}, data);
|
||||
}
|
||||
|
||||
describe('Dropdown', function describeDropdown() {
|
||||
fixture.preload('gl_dropdown.html');
|
||||
fixture.preload('projects.json');
|
||||
|
||||
beforeEach(() => {
|
||||
fixture.load('gl_dropdown.html');
|
||||
this.dropdownContainerElement = $('.dropdown.inline');
|
||||
this.dropdownMenuElement = $('.dropdown-menu', this.dropdownContainerElement);
|
||||
this.projectsData = fixture.load('projects.json')[0];
|
||||
function initDropDown(hasRemote, isFilterable) {
|
||||
this.dropdownButtonElement = $('#js-project-dropdown', this.dropdownContainerElement).glDropdown({
|
||||
selectable: true,
|
||||
data: this.projectsData,
|
||||
filterable: isFilterable,
|
||||
data: hasRemote ? remoteMock.bind({}, this.projectsData) : this.projectsData,
|
||||
text: (project) => {
|
||||
(project.name_with_namespace || project.name);
|
||||
},
|
||||
|
@ -52,6 +56,13 @@
|
|||
project.id;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
fixture.load('gl_dropdown.html');
|
||||
this.dropdownContainerElement = $('.dropdown.inline');
|
||||
this.$dropdownMenuElement = $('.dropdown-menu', this.dropdownContainerElement);
|
||||
this.projectsData = fixture.load('projects.json')[0];
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -60,6 +71,7 @@
|
|||
});
|
||||
|
||||
it('should open on click', () => {
|
||||
initDropDown.call(this, false);
|
||||
expect(this.dropdownContainerElement).not.toHaveClass('open');
|
||||
this.dropdownButtonElement.click();
|
||||
expect(this.dropdownContainerElement).toHaveClass('open');
|
||||
|
@ -67,26 +79,27 @@
|
|||
|
||||
describe('that is open', () => {
|
||||
beforeEach(() => {
|
||||
initDropDown.call(this, false, false);
|
||||
this.dropdownButtonElement.click();
|
||||
});
|
||||
|
||||
it('should select a following item on DOWN keypress', () => {
|
||||
expect($(FOCUSED_ITEM_SELECTOR, this.dropdownMenuElement).length).toBe(0);
|
||||
expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(0);
|
||||
let randomIndex = (Math.floor(Math.random() * (this.projectsData.length - 1)) + 0);
|
||||
navigateWithKeys('down', randomIndex, () => {
|
||||
expect($(FOCUSED_ITEM_SELECTOR, this.dropdownMenuElement).length).toBe(1);
|
||||
expect($(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.dropdownMenuElement)).toHaveClass('is-focused');
|
||||
expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(1);
|
||||
expect($(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.$dropdownMenuElement)).toHaveClass('is-focused');
|
||||
});
|
||||
});
|
||||
|
||||
it('should select a previous item on UP keypress', () => {
|
||||
expect($(FOCUSED_ITEM_SELECTOR, this.dropdownMenuElement).length).toBe(0);
|
||||
expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(0);
|
||||
navigateWithKeys('down', (this.projectsData.length - 1), () => {
|
||||
expect($(FOCUSED_ITEM_SELECTOR, this.dropdownMenuElement).length).toBe(1);
|
||||
expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(1);
|
||||
let randomIndex = (Math.floor(Math.random() * (this.projectsData.length - 2)) + 0);
|
||||
navigateWithKeys('up', randomIndex, () => {
|
||||
expect($(FOCUSED_ITEM_SELECTOR, this.dropdownMenuElement).length).toBe(1);
|
||||
expect($(`${ITEM_SELECTOR}:eq(${((this.projectsData.length - 2) - randomIndex)}) a`, this.dropdownMenuElement)).toHaveClass('is-focused');
|
||||
expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(1);
|
||||
expect($(`${ITEM_SELECTOR}:eq(${((this.projectsData.length - 2) - randomIndex)}) a`, this.$dropdownMenuElement)).toHaveClass('is-focused');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -98,7 +111,7 @@
|
|||
spyOn(Turbolinks, 'visit').and.stub();
|
||||
navigateWithKeys('enter', null, () => {
|
||||
expect(this.dropdownContainerElement).not.toHaveClass('open');
|
||||
let link = $(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.dropdownMenuElement);
|
||||
let link = $(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.$dropdownMenuElement);
|
||||
expect(link).toHaveClass('is-active');
|
||||
let linkedLocation = link.attr('href');
|
||||
if (linkedLocation && linkedLocation !== '#') expect(Turbolinks.visit).toHaveBeenCalledWith(linkedLocation);
|
||||
|
@ -116,5 +129,42 @@
|
|||
expect(this.dropdownContainerElement).not.toHaveClass('open');
|
||||
});
|
||||
});
|
||||
|
||||
describe('opened and waiting for a remote callback', () => {
|
||||
beforeEach(() => {
|
||||
initDropDown.call(this, true, true);
|
||||
this.dropdownButtonElement.click();
|
||||
});
|
||||
|
||||
it('should not focus search input while remote task is not complete', ()=> {
|
||||
expect($(document.activeElement)).not.toEqual($(SEARCH_INPUT_SELECTOR));
|
||||
remoteCallback();
|
||||
expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
|
||||
});
|
||||
|
||||
it('should focus search input after remote task is complete', ()=> {
|
||||
remoteCallback();
|
||||
expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
|
||||
});
|
||||
|
||||
it('should focus on input when opening for the second time', ()=> {
|
||||
remoteCallback();
|
||||
this.dropdownContainerElement.trigger({
|
||||
type: 'keyup',
|
||||
which: ARROW_KEYS.ESC,
|
||||
keyCode: ARROW_KEYS.ESC
|
||||
});
|
||||
this.dropdownButtonElement.click();
|
||||
expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
|
||||
});
|
||||
});
|
||||
|
||||
describe('input focus with array data', () => {
|
||||
it('should focus input when passing array data to drop down', ()=> {
|
||||
initDropDown.call(this, false, true);
|
||||
this.dropdownButtonElement.click();
|
||||
expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
|
||||
});
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
|
Loading…
Reference in a new issue