Skip to content

Commit 5c59e40

Browse files
authored
fix!: prevent focusout when closing date-picker on outside click (#7855)
1 parent 48acc62 commit 5c59e40

File tree

5 files changed

+105
-28
lines changed

5 files changed

+105
-28
lines changed
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/**
2+
* @license
3+
* Copyright (c) 2015 - 2024 Vaadin Ltd.
4+
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5+
*/
6+
import { isElementFocusable } from '@vaadin/a11y-base/src/focus-utils.js';
7+
import { OverlayMixin } from '@vaadin/overlay/src/vaadin-overlay-mixin.js';
8+
import { PositionMixin } from '@vaadin/overlay/src/vaadin-overlay-position-mixin.js';
9+
10+
/**
11+
* @polymerMixin
12+
* @mixes OverlayMixin
13+
* @mixes PositionMixin
14+
*/
15+
export const DatePickerOverlayMixin = (superClass) =>
16+
class DatePickerOverlayMixin extends PositionMixin(OverlayMixin(superClass)) {
17+
/**
18+
* Override method inherited from `OverlayMixin` to not close on input click.
19+
* Needed to ignore date-picker's own input in the mousedown listener below.
20+
*
21+
* @param {Event} event
22+
* @return {boolean}
23+
* @protected
24+
*/
25+
_shouldCloseOnOutsideClick(event) {
26+
const eventPath = event.composedPath();
27+
return !eventPath.includes(this.positionTarget);
28+
}
29+
30+
/**
31+
* @protected
32+
* @override
33+
*/
34+
_mouseDownListener(event) {
35+
super._mouseDownListener(event);
36+
37+
// Prevent global mousedown event to avoid losing focus on outside click,
38+
// unless the clicked element is also focusable (e.g. in date-time-picker).
39+
if (this._shouldCloseOnOutsideClick(event) && !isElementFocusable(event.composedPath()[0])) {
40+
event.preventDefault();
41+
}
42+
}
43+
};

packages/date-picker/src/vaadin-date-picker-overlay.js

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,9 @@
66
import { html, PolymerElement } from '@polymer/polymer/polymer-element.js';
77
import { defineCustomElement } from '@vaadin/component-base/src/define.js';
88
import { DirMixin } from '@vaadin/component-base/src/dir-mixin.js';
9-
import { OverlayMixin } from '@vaadin/overlay/src/vaadin-overlay-mixin.js';
10-
import { PositionMixin } from '@vaadin/overlay/src/vaadin-overlay-position-mixin.js';
119
import { overlayStyles } from '@vaadin/overlay/src/vaadin-overlay-styles.js';
1210
import { registerStyles, ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
11+
import { DatePickerOverlayMixin } from './vaadin-date-picker-overlay-mixin.js';
1312
import { datePickerOverlayStyles } from './vaadin-date-picker-overlay-styles.js';
1413

1514
registerStyles('vaadin-date-picker-overlay', [overlayStyles, datePickerOverlayStyles], {
@@ -21,13 +20,12 @@ registerStyles('vaadin-date-picker-overlay', [overlayStyles, datePickerOverlaySt
2120
*
2221
* @customElement
2322
* @extends HTMLElement
24-
* @mixes PositionMixin
25-
* @mixes OverlayMixin
23+
* @mixes DatePickerOverlayMixin
2624
* @mixes DirMixin
2725
* @mixes ThemableMixin
2826
* @private
2927
*/
30-
class DatePickerOverlay extends PositionMixin(OverlayMixin(DirMixin(ThemableMixin(PolymerElement)))) {
28+
class DatePickerOverlay extends DatePickerOverlayMixin(DirMixin(ThemableMixin(PolymerElement))) {
3129
static get is() {
3230
return 'vaadin-date-picker-overlay';
3331
}

packages/date-picker/src/vaadin-lit-date-picker-overlay.js

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,21 @@ import { html, LitElement } from 'lit';
77
import { defineCustomElement } from '@vaadin/component-base/src/define.js';
88
import { DirMixin } from '@vaadin/component-base/src/dir-mixin.js';
99
import { PolylitMixin } from '@vaadin/component-base/src/polylit-mixin.js';
10-
import { OverlayMixin } from '@vaadin/overlay/src/vaadin-overlay-mixin.js';
11-
import { PositionMixin } from '@vaadin/overlay/src/vaadin-overlay-position-mixin.js';
1210
import { overlayStyles } from '@vaadin/overlay/src/vaadin-overlay-styles.js';
1311
import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
12+
import { DatePickerOverlayMixin } from './vaadin-date-picker-overlay-mixin.js';
1413
import { datePickerOverlayStyles } from './vaadin-date-picker-overlay-styles.js';
1514

1615
/**
1716
* An element used internally by `<vaadin-date-picker>`. Not intended to be used separately.
1817
*
1918
* @extends HTMLElement
20-
* @mixes PositionMixin
21-
* @mixes OverlayMixin
19+
* @mixes DatePickerOverlayMixin
2220
* @mixes DirMixin
2321
* @mixes ThemableMixin
2422
* @private
2523
*/
26-
class DatePickerOverlay extends PositionMixin(OverlayMixin(DirMixin(ThemableMixin(PolylitMixin(LitElement))))) {
24+
class DatePickerOverlay extends DatePickerOverlayMixin(DirMixin(ThemableMixin(PolylitMixin(LitElement)))) {
2725
static get is() {
2826
return 'vaadin-date-picker-overlay';
2927
}

packages/date-picker/test/dropdown.common.js

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,9 @@ import {
77
nextRender,
88
nextUpdate,
99
oneEvent,
10-
outsideClick,
1110
touchstart,
1211
} from '@vaadin/testing-helpers';
13-
import { sendKeys } from '@web/test-runner-commands';
12+
import { resetMouse, sendKeys, sendMouse } from '@web/test-runner-commands';
1413
import sinon from 'sinon';
1514
import { getFocusedCell, monthsEqual, open, waitForOverlayRender } from './helpers.js';
1615

@@ -187,19 +186,22 @@ describe('dropdown', () => {
187186
});
188187

189188
describe('outside click', () => {
190-
it('should restore focus to the input on outside click', async () => {
189+
afterEach(async () => {
190+
await resetMouse();
191+
});
192+
193+
it('should keep focus in the input on outside mousedown', async () => {
191194
input.focus();
192195
await open(datePicker);
193-
outsideClick();
194-
await nextUpdate(datePicker);
195-
await aTimeout(0);
196+
await sendMouse({ type: 'move', position: [200, 10] });
197+
await sendMouse({ type: 'down' });
196198
expect(document.activeElement).to.equal(input);
197199
});
198200

199201
it('should focus the input on outside click', async () => {
200202
expect(document.activeElement).to.equal(document.body);
201203
await open(datePicker);
202-
outsideClick();
204+
await sendMouse({ type: 'click', position: [200, 10] });
203205
await nextUpdate(datePicker);
204206
await aTimeout(0);
205207
expect(document.activeElement).to.equal(input);
@@ -209,7 +211,7 @@ describe('dropdown', () => {
209211
// Focus the input with Tab
210212
await sendKeys({ press: 'Tab' });
211213
await open(datePicker);
212-
outsideClick();
214+
await sendMouse({ type: 'click', position: [200, 10] });
213215
await nextUpdate(datePicker);
214216
await aTimeout(0);
215217
expect(datePicker.hasAttribute('focus-ring')).to.be.true;
@@ -220,14 +222,14 @@ describe('dropdown', () => {
220222
input.focus();
221223
// Move focus to the calendar
222224
await sendKeys({ press: 'Tab' });
223-
outsideClick();
225+
await sendMouse({ type: 'click', position: [200, 10] });
224226
await aTimeout(0);
225227
expect(datePicker.hasAttribute('focus-ring')).to.be.true;
226228
});
227229

228230
it('should not set focus-ring attribute if it was not set before opening', async () => {
229231
await open(datePicker);
230-
outsideClick();
232+
await sendMouse({ type: 'click', position: [200, 10] });
231233
await aTimeout(0);
232234
expect(datePicker.hasAttribute('focus-ring')).to.be.false;
233235
});

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

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { expect } from '@vaadin/chai-plugins';
2-
import { fixtureSync, nextRender } from '@vaadin/testing-helpers';
2+
import { fixtureSync, middleOfNode, nextRender } from '@vaadin/testing-helpers';
33
import { resetMouse, sendKeys, sendMouse } from '@web/test-runner-commands';
44
import '@vaadin/combo-box';
55
import '@vaadin/date-picker';
66
import '@vaadin/grid-pro';
77
import '@vaadin/grid-pro/vaadin-grid-pro-edit-column.js';
88
import '@vaadin/time-picker';
9+
import { waitForOverlayRender } from '@vaadin/date-picker/test/helpers.js';
910
import { flushGrid, getContainerCell } from '@vaadin/grid-pro/test/helpers.js';
1011

1112
describe('grid-pro custom editor', () => {
@@ -62,29 +63,64 @@ describe('grid-pro custom editor', () => {
6263
});
6364

6465
describe('date-picker', () => {
65-
it('should apply the updated date to the cell when exiting on Tab', async () => {
66-
const cell = getContainerCell(grid.$.items, 0, 2);
66+
let cell;
67+
68+
beforeEach(async () => {
69+
cell = getContainerCell(grid.$.items, 0, 2);
6770
cell.focus();
6871

6972
await sendKeys({ press: 'Enter' });
73+
});
7074

75+
it('should apply the updated date to the cell when exiting on Tab', async () => {
7176
await sendKeys({ type: '1/12/1984' });
7277
await sendKeys({ press: 'Tab' });
7378

7479
expect(cell._content.textContent).to.equal('1984-01-12');
7580
});
7681

7782
it('should apply the updated date to the cell when exiting on Enter', async () => {
78-
const cell = getContainerCell(grid.$.items, 0, 2);
79-
cell.focus();
80-
81-
await sendKeys({ press: 'Enter' });
82-
8383
await sendKeys({ type: '1/12/1984' });
8484
await sendKeys({ press: 'Enter' });
8585

8686
expect(cell._content.textContent).to.equal('1984-01-12');
8787
});
88+
89+
it('should not stop editing on input click when focus is in the overlay', async () => {
90+
// Open the overlay
91+
await sendKeys({ press: 'ArrowDown' });
92+
93+
const { x, y } = middleOfNode(cell._content.querySelector('input'));
94+
await sendMouse({ type: 'click', position: [Math.floor(x), Math.floor(y)] });
95+
96+
expect(cell._content.querySelector('vaadin-date-picker')).to.be.ok;
97+
});
98+
99+
it('should stop editing and update value when closing on outside click', async () => {
100+
// Open the overlay
101+
await sendKeys({ press: 'ArrowDown' });
102+
await waitForOverlayRender();
103+
104+
// Move focus back to the input
105+
await sendKeys({ down: 'Shift' });
106+
await sendKeys({ press: 'Tab' });
107+
await sendKeys({ up: 'Shift' });
108+
109+
// Change single digit to avoid calendar scroll
110+
const input = cell._content.querySelector('input');
111+
input.setSelectionRange(3, 4);
112+
113+
await sendKeys({ type: '2' });
114+
115+
await sendMouse({ type: 'click', position: [10, 10] });
116+
await nextRender();
117+
118+
// TODO: closing occurs in `vaadin-overlay-outside-click` listener added on global `focusin`
119+
// in grid-pro. Consider replacing it with `_shouldRemoveFocus()` check on editor `focusout`
120+
// so that we don't stop editing on outside click, to align with the combo-box behavior.
121+
expect(cell._content.querySelector('vaadin-date-picker')).to.be.not.ok;
122+
expect(cell._content.textContent).to.equal('1984-01-12');
123+
});
88124
});
89125

90126
describe('combo-box', () => {

0 commit comments

Comments
 (0)