Skip to content

Commit 48acc62

Browse files
authored
fix!: prevent focusout when closing combo-box on outside click (#7846)
1 parent 2a08414 commit 48acc62

File tree

9 files changed

+109
-59
lines changed

9 files changed

+109
-59
lines changed

packages/combo-box/src/vaadin-combo-box-light.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,6 @@ class ComboBoxLight extends ComboBoxLightMixin(ThemableMixin(PolymerElement)) {
8484
theme$="[[_theme]]"
8585
position-target="[[inputElement]]"
8686
no-vertical-overlap
87-
restore-focus-node="[[inputElement]]"
8887
></vaadin-combo-box-overlay>
8988
`;
9089
}

packages/combo-box/src/vaadin-combo-box-mixin.js

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -619,8 +619,6 @@ export const ComboBoxMixin = (subclass) =>
619619
this.inputElement.focus();
620620
}
621621
}
622-
623-
this._overlayElement.restoreFocusOnClose = true;
624622
} else {
625623
this._onClosed();
626624
}
@@ -719,9 +717,7 @@ export const ComboBoxMixin = (subclass) =>
719717
_onKeyDown(e) {
720718
super._onKeyDown(e);
721719

722-
if (e.key === 'Tab') {
723-
this._overlayElement.restoreFocusOnClose = false;
724-
} else if (e.key === 'ArrowDown') {
720+
if (e.key === 'ArrowDown') {
725721
this._onArrowDown();
726722

727723
// Prevent caret from moving

packages/combo-box/src/vaadin-combo-box-overlay-mixin.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* Copyright (c) 2015 - 2024 Vaadin Ltd.
44
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
55
*/
6+
import { isElementFocusable } from '@vaadin/a11y-base/src/focus-utils.js';
67
import { PositionMixin } from '@vaadin/overlay/src/vaadin-overlay-position-mixin.js';
78

89
/**
@@ -46,6 +47,20 @@ export const ComboBoxOverlayMixin = (superClass) =>
4647
return !eventPath.includes(this.positionTarget) && !eventPath.includes(this);
4748
}
4849

50+
/**
51+
* @protected
52+
* @override
53+
*/
54+
_mouseDownListener(event) {
55+
super._mouseDownListener(event);
56+
57+
// Prevent global mousedown event to avoid losing focus on outside click,
58+
// unless the clicked element is also focusable (e.g. in date-time-picker).
59+
if (this._shouldCloseOnOutsideClick(event) && !isElementFocusable(event.composedPath()[0])) {
60+
event.preventDefault();
61+
}
62+
}
63+
4964
/** @protected */
5065
_updateOverlayWidth() {
5166
const propPrefix = this.localName;

packages/combo-box/src/vaadin-combo-box.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,6 @@ class ComboBox extends ComboBoxDataProviderMixin(
206206
theme$="[[_theme]]"
207207
position-target="[[_positionTarget]]"
208208
no-vertical-overlap
209-
restore-focus-node="[[inputElement]]"
210209
></vaadin-combo-box-overlay>
211210
212211
<slot name="tooltip"></slot>

packages/combo-box/src/vaadin-lit-combo-box-light.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ class ComboBoxLight extends ComboBoxLightMixin(ThemableMixin(PolylitMixin(LitEle
4646
?loading="${this.loading}"
4747
theme="${ifDefined(this._theme)}"
4848
.positionTarget="${this.inputElement}"
49-
.restoreFocusNode="${this.inputElement}"
5049
no-vertical-overlap
5150
></vaadin-combo-box-overlay>
5251
`;

packages/combo-box/src/vaadin-lit-combo-box.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,6 @@ class ComboBox extends ComboBoxDataProviderMixin(
106106
?loading="${this.loading}"
107107
theme="${ifDefined(this._theme)}"
108108
.positionTarget="${this._positionTarget}"
109-
.restoreFocusNode="${this.inputElement}"
110109
no-vertical-overlap
111110
></vaadin-combo-box-overlay>
112111

packages/combo-box/test/interactions.common.js

Lines changed: 29 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
tap,
1414
touchstart,
1515
} from '@vaadin/testing-helpers';
16-
import { sendKeys } from '@web/test-runner-commands';
16+
import { resetMouse, sendKeys, sendMouse } from '@web/test-runner-commands';
1717
import sinon from 'sinon';
1818
import { isTouch } from '@vaadin/component-base/src/browser-utils.js';
1919
import { getFirstItem, setInputValue } from './helpers.js';
@@ -161,30 +161,6 @@ describe('interactions', () => {
161161
expect(event.defaultPrevented).to.be.true;
162162
});
163163

164-
it('should restore focus to the input on outside click', async () => {
165-
comboBox.focus();
166-
comboBox.open();
167-
outsideClick();
168-
await aTimeout(0);
169-
expect(document.activeElement).to.equal(input);
170-
});
171-
172-
it('should restore focus to the input on toggle button click', async () => {
173-
comboBox.focus();
174-
comboBox.open();
175-
comboBox._toggleElement.click();
176-
await aTimeout(0);
177-
expect(document.activeElement).to.equal(input);
178-
});
179-
180-
it('should focus the input on outside click if not focused before opening', async () => {
181-
expect(document.activeElement).to.equal(document.body);
182-
comboBox.open();
183-
outsideClick();
184-
await aTimeout(0);
185-
expect(document.activeElement).to.equal(input);
186-
});
187-
188164
// NOTE: WebKit incorrectly detects touch environment
189165
// See https://github.com/vaadin/web-components/issues/257
190166
(isTouch ? it.skip : it)('should focus the input on opening if not focused', () => {
@@ -225,14 +201,33 @@ describe('interactions', () => {
225201
expect(comboBox.hasAttribute('focus-ring')).to.be.true;
226202
});
227203

228-
it('should not keep focus-ring attribute after closing with outside click', () => {
229-
comboBox.focus();
230-
comboBox.setAttribute('focus-ring', '');
231-
comboBox.open();
232-
outsideClick();
233-
expect(comboBox.hasAttribute('focus-ring')).to.be.false;
234-
// FIXME: see https://github.com/vaadin/web-components/issues/4148
235-
// expect(comboBox.hasAttribute('focus-ring')).to.be.true;
204+
describe('close on click', () => {
205+
afterEach(async () => {
206+
await resetMouse();
207+
});
208+
209+
it('should restore focus to the input on outside click', async () => {
210+
comboBox.focus();
211+
comboBox.open();
212+
await sendMouse({ type: 'click', position: [200, 200] });
213+
expect(document.activeElement).to.equal(input);
214+
});
215+
216+
it('should keep focus in the input on toggle button click', async () => {
217+
comboBox.focus();
218+
comboBox.open();
219+
const rect = comboBox._toggleElement.getBoundingClientRect();
220+
await sendMouse({ type: 'click', position: [rect.x, rect.y] });
221+
expect(document.activeElement).to.equal(input);
222+
});
223+
224+
it('should keep focus-ring attribute after closing with outside click', async () => {
225+
comboBox.focus();
226+
comboBox.setAttribute('focus-ring', '');
227+
comboBox.open();
228+
await sendMouse({ type: 'click', position: [200, 200] });
229+
expect(comboBox.hasAttribute('focus-ring')).to.be.true;
230+
});
236231
});
237232
});
238233

@@ -251,6 +246,7 @@ describe('interactions', () => {
251246
});
252247

253248
it('should re-enable virtual keyboard on blur', async () => {
249+
comboBox.focus();
254250
comboBox.open();
255251
comboBox.close();
256252
await aTimeout(0);

packages/date-time-picker/test/basic.test.js

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { expect } from '@vaadin/chai-plugins';
22
import { aTimeout, fixtureSync, focusin, focusout, nextFrame, nextRender, outsideClick } from '@vaadin/testing-helpers';
3-
import { sendKeys } from '@web/test-runner-commands';
3+
import { resetMouse, sendKeys, sendMouse } from '@web/test-runner-commands';
44
import sinon from 'sinon';
55
import '../src/vaadin-date-time-picker.js';
66
import { changeInputValue } from './helpers.js';
@@ -41,12 +41,23 @@ describe('Basic features', () => {
4141
let datePicker;
4242
let timePicker;
4343

44+
async function click(element) {
45+
const rect = element.inputElement.getBoundingClientRect();
46+
const x = Math.floor(rect.x + rect.width / 2);
47+
const y = Math.floor(rect.y + rect.height / 2);
48+
await sendMouse({ type: 'click', position: [x, y] });
49+
}
50+
4451
beforeEach(() => {
4552
dateTimePicker = fixtureSync('<vaadin-date-time-picker></vaadin-date-time-picker>');
4653
datePicker = getDatePicker(dateTimePicker);
4754
timePicker = getTimePicker(dateTimePicker);
4855
});
4956

57+
afterEach(async () => {
58+
await resetMouse();
59+
});
60+
5061
it('should have default value', () => {
5162
expect(dateTimePicker.value).to.equal('');
5263
});
@@ -189,13 +200,11 @@ describe('Basic features', () => {
189200

190201
describe('date-picker focused', () => {
191202
it('should remove focused attribute on time-picker click', async () => {
192-
datePicker.focus();
193-
datePicker.click();
203+
await click(datePicker);
194204
await nextRender();
195205
expect(datePicker.hasAttribute('focused')).to.be.true;
196206

197-
timePicker.focus();
198-
timePicker.click();
207+
await click(timePicker);
199208
expect(datePicker.hasAttribute('focused')).to.be.false;
200209
});
201210

@@ -207,37 +216,39 @@ describe('Basic features', () => {
207216
await nextRender();
208217
expect(datePicker.hasAttribute('focus-ring')).to.be.true;
209218

210-
timePicker.focus();
211-
timePicker.click();
219+
await click(timePicker);
212220
expect(datePicker.hasAttribute('focus-ring')).to.be.false;
213221
});
214222
});
215223

216224
describe('time-picker focused', () => {
225+
beforeEach(() => {
226+
// Disable auto-open to make tests more reliable by only moving
227+
// focus on mousedown (and not the date-picker overlay opening).
228+
dateTimePicker.autoOpenDisabled = true;
229+
});
230+
217231
it('should remove focused attribute on date-picker click', async () => {
218-
timePicker.focus();
219-
timePicker.click();
232+
await click(timePicker);
233+
// Open the overlay with the keyboard
234+
await sendKeys({ press: 'ArrowDown' });
220235
await nextRender();
221236
expect(timePicker.hasAttribute('focused')).to.be.true;
222237

223-
datePicker.focus();
224-
datePicker.click();
225-
await nextRender();
238+
await click(datePicker);
226239
expect(timePicker.hasAttribute('focused')).to.be.false;
227240
});
228241

229242
it('should remove focus-ring attribute on date-picker click', async () => {
230243
// Focus the time-picker with the keyboard
231-
await sendKeys({ press: 'Tab' });
244+
datePicker.focus();
232245
await sendKeys({ press: 'Tab' });
233246
// Open the overlay with the keyboard
234247
await sendKeys({ press: 'ArrowDown' });
235248
await nextRender();
236249
expect(timePicker.hasAttribute('focus-ring')).to.be.true;
237250

238-
datePicker.focus();
239-
datePicker.click();
240-
await nextRender();
251+
await click(datePicker);
241252
expect(timePicker.hasAttribute('focus-ring')).to.be.false;
242253
});
243254
});

test/integration/grid-pro-custom-editor.test.js

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { expect } from '@vaadin/chai-plugins';
22
import { fixtureSync, nextRender } from '@vaadin/testing-helpers';
3-
import { sendKeys } from '@web/test-runner-commands';
3+
import { resetMouse, sendKeys, sendMouse } from '@web/test-runner-commands';
44
import '@vaadin/combo-box';
55
import '@vaadin/date-picker';
66
import '@vaadin/grid-pro';
@@ -57,6 +57,10 @@ describe('grid-pro custom editor', () => {
5757
await nextRender();
5858
});
5959

60+
afterEach(async () => {
61+
await resetMouse();
62+
});
63+
6064
describe('date-picker', () => {
6165
it('should apply the updated date to the cell when exiting on Tab', async () => {
6266
const cell = getContainerCell(grid.$.items, 0, 2);
@@ -107,6 +111,22 @@ describe('grid-pro custom editor', () => {
107111

108112
expect(cell._content.textContent).to.equal('active');
109113
});
114+
115+
it('should not stop editing and update value when closing on outside click', async () => {
116+
const cell = getContainerCell(grid.$.items, 0, 3);
117+
cell.focus();
118+
119+
await sendKeys({ press: 'Enter' });
120+
121+
await sendKeys({ press: 'ArrowDown' });
122+
await sendKeys({ type: 'active' });
123+
124+
await sendMouse({ type: 'click', position: [10, 10] });
125+
126+
const editor = cell._content.querySelector('vaadin-combo-box');
127+
expect(editor).to.be.ok;
128+
expect(editor.value).to.equal('active');
129+
});
110130
});
111131

112132
describe('time-picker', () => {
@@ -133,5 +153,21 @@ describe('grid-pro custom editor', () => {
133153

134154
expect(cell._content.textContent).to.equal('10:00');
135155
});
156+
157+
it('should not stop editing and update value when closing on outside click', async () => {
158+
const cell = getContainerCell(grid.$.items, 0, 4);
159+
cell.focus();
160+
161+
await sendKeys({ press: 'Enter' });
162+
163+
await sendKeys({ press: 'ArrowDown' });
164+
await sendKeys({ type: '10:00' });
165+
166+
await sendMouse({ type: 'click', position: [10, 10] });
167+
168+
const editor = cell._content.querySelector('vaadin-time-picker');
169+
expect(editor).to.be.ok;
170+
expect(editor.value).to.equal('10:00');
171+
});
136172
});
137173
});

0 commit comments

Comments
 (0)