From f0c64f5b192f161ead132a9e23d873ba82203940 Mon Sep 17 00:00:00 2001 From: Mel Dafert Date: Thu, 1 Dec 2022 11:56:56 +0100 Subject: [PATCH] fix(a11y): blur when tabbing out of input Fixes #1926. To be accessible, it should be possible to easily navigate elements using the keyboard. Previously, when using TAB to navigate through form elements, the input would enter its focused state when focused with TAB (and the dropdown would open when using `openOnFocus`). However, when TAB is used to jump to the next focusable element, it would not lose its focused state, and the dropdown would stay open. This commit restores the behavior before #1813 and ensures that the input leaves its focused state and closes the dropdown when blurred using TAB. This commit also fixes some tests to ensure they are self-contained. --- src/selectize.js | 23 ++++++---- test/api.js | 5 +- test/interaction.js | 109 +++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 123 insertions(+), 14 deletions(-) diff --git a/src/selectize.js b/src/selectize.js index a6a24dd74..c055abedf 100644 --- a/src/selectize.js +++ b/src/selectize.js @@ -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); }, + blur : function() { return self.onBlur.apply(self, arguments); }, + focus : function() { return self.onFocus.apply(self, arguments); }, paste : function() { return self.onPaste.apply(self, arguments); } }); @@ -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); } } @@ -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(); diff --git a/test/api.js b/test/api.js index 6c3665c39..b4d5847c6 100644 --- a/test/api.js +++ b/test/api.js @@ -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() { @@ -642,11 +642,12 @@ '', { 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); }); }); diff --git a/test/interaction.js b/test/interaction.js index 81b6d0c50..516e3bcf4 100644 --- a/test/interaction.js +++ b/test/interaction.js @@ -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 @@ -41,7 +49,7 @@ }); }); }); - + it('should reopen dropdown if clicked after being closed by closeAfterSelect: true', function(done) { var test = setup_test('' + + '' + + '' + + '' + + '', {}); + input1 = $(''); + input2 = $(''); + 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'); + }); + }); + + describe('openOnFocus is false', function() { + var test, input1, input2; + + before(function(done) { + test = setup_test('', { openOnFocus: false }); + input1 = $(''); + input2 = $(''); + 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); + }); + }); + }); + }); })();