Skip to content

fix(a11y): blur when tabbing out of input #1927

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Dec 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 13 additions & 10 deletions src/selectize.js
Original file line number Diff line number Diff line change
Expand Up @@ -218,8 +218,8 @@ $.extend(Selectize.prototype, {
keypress : function() { return self.onKeyPress.apply(self, arguments); },
input : function() { return self.onInput.apply(self, arguments); },
resize : function() { self.positionDropdown.apply(self, []); },
// blur : function() { return self.onBlur.apply(self, arguments); },
focus : function() { self.ignoreBlur = false; return self.onFocus.apply(self, arguments); },
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ignoreBlur was not read anywhere anymore. I removed the previous references and re-used it for detecting when an item in the dropdown was clicked, to avoid closing the dropdown in that case.

blur : function() { return self.onBlur.apply(self, arguments); },
focus : function() { return self.onFocus.apply(self, arguments); },
paste : function() { return self.onPaste.apply(self, arguments); }
});

Expand All @@ -243,7 +243,12 @@ $.extend(Selectize.prototype, {
}
// blur on click outside
// do not blur if the dropdown is clicked
if (!self.$dropdown.has(e.target).length && e.target !== self.$control[0]) {
if (self.$dropdown.has(e.target).length) {
self.ignoreBlur = true;
window.setTimeout(function() {
self.ignoreBlur = false;
}, 0);
} else if (e.target !== self.$control[0]) {
self.blur(e.target);
}
}
Expand Down Expand Up @@ -685,19 +690,17 @@ $.extend(Selectize.prototype, {
*/
onBlur: function(e, dest) {
var self = this;

if (self.ignoreBlur) {
return;
}

if (!self.isFocused) return;
self.isFocused = false;

if (self.ignoreFocus) {
return;
}
// Bug fix do not blur dropdown here
// else if (!self.ignoreBlur && document.activeElement === self.$dropdown_content[0]) {
// // necessary to prevent IE closing the dropdown when the scrollbar is clicked
// self.ignoreBlur = true;
// self.onFocus(e);
// return;
// }

var deactivate = function() {
self.close();
Expand Down
5 changes: 3 additions & 2 deletions test/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -633,7 +633,7 @@
test.selectize.search('hello');
}).to.not.throw(Error);
});
it('should normalize a string', function () {
expect('should normalize a string', function () {
var test;

beforeEach(function() {
Comment on lines -636 to 639
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This broke my new tests. Calling beforeEach() inside of it() (instead of inside expect) apparently applies it to all subsequent tests.

Expand All @@ -642,11 +642,12 @@
'</select>', { normalize: true });
});

it('should return query satinized', function() {
it('should return query satinized', function(done) {
var query = test.selectize.search('héllo').query;

window.setTimeout(function () {
expect(query).to.be.equal('hello');
done();
}, 0);
Comment on lines -645 to 651
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Drive-by fix to ensure the test terminates properly.

});
});
Expand Down
109 changes: 107 additions & 2 deletions test/interaction.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
(function() {

var click = function(el, cb) {
syn.click(el).delay(350, cb);
syn.click(el).delay(1, cb);
};

var tabTo = function(elem) {
// emulating keyboard tabbing using focus
// TODO: it would be better to use something like puppeteer instead, then we could simulate real keyboard interactions
// syn.key() is not reliable enough for tabbing
elem.focus();
return new Promise((resolve) => window.setTimeout(resolve));
};

// These tests are functional simulations of
Expand Down Expand Up @@ -41,7 +49,7 @@
});
});
});

it('should reopen dropdown if clicked after being closed by closeAfterSelect: true', function(done) {
var test = setup_test('<select multiple>' +
'<option value="a">A</option>' +
Expand Down Expand Up @@ -413,6 +421,103 @@
});
});

describe('simulate tabbing using native focus()', function() {

describe('defaults', function() {
var test, input1, input2;

before(function(done) {
test = setup_test('<select>' +
'<option value="">No selection</option>' +
'<option value="a">A</option>' +
'<option value="b">B</option>' +
'</select>', {});
input1 = $('<input type="text" class="first">');
input2 = $('<input type="text" class="last">');
test.$select.parent().prepend(input1);
test.$select.parent().append(input2);
done();
});

after(function() {
input1.remove();
input2.remove();
});

it('should give the control focus', async function() {
await tabTo(input1[0]);
expect(test.selectize.isFocused).to.be.equal(false);
await tabTo(test.selectize.$control_input[0]);
expect(test.selectize.isFocused).to.be.equal(true);
});

it('should remove the control focus', async function() {
await tabTo(test.selectize.$control_input[0]);
expect(test.selectize.isFocused).to.be.equal(true);
await tabTo(input2[0]);
expect(test.selectize.isFocused).to.be.equal(false);
});

it('should open the control', async function() {
await tabTo(input1[0]);
expect(test.selectize.isOpen).to.be.equal(false);
await tabTo(test.selectize.$control_input[0]);
expect(test.selectize.isOpen).to.be.equal(true);
});

it('should close the control', async function() {
await tabTo(test.selectize.$control_input[0]);
expect(test.selectize.isOpen).to.be.equal(true);
await tabTo(input2[0]);
expect(test.selectize.isOpen).to.be.equal(false);
});

// TODO: this would work if tabTo was using actual keyboard interactions,
// and not just focus()
xit('should select the first value on blur', async function() {
await tabTo(test.selectize.$control_input[0]);
await tabTo(input2[0]);
expect(test.selectize.getValue()).to.be.equal('a');
});
Comment on lines +477 to +481
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the test that would require a better emulation/control over the headless browser (eg. puppeteer).

});

describe('openOnFocus is false', function() {
var test, input1, input2;

before(function(done) {
test = setup_test('<select>' +
'<option value="">No selection</option>' +
'<option value="a">A</option>' +
'<option value="b">B</option>' +
'</select>', { openOnFocus: false });
input1 = $('<input type="text" class="first">');
input2 = $('<input type="text" class="last">');
test.$select.parent().prepend(input1);
test.$select.parent().append(input2);
done();
});

after(function() {
input1.remove();
input2.remove();
});

it('should give the control focus', async function() {
await tabTo(input1[0]);
expect(test.selectize.isFocused).to.be.equal(false);
await tabTo(test.selectize.$control_input[0]);
expect(test.selectize.isFocused).to.be.equal(true);
});

it('should not open the control', async function() {
await tabTo(input1[0]);
expect(test.selectize.isOpen).to.be.equal(false);
await tabTo(test.selectize.$control_input[0]);
expect(test.selectize.isOpen).to.be.equal(false);
});
});
});

});

})();