Skip to content

Commit e08a6e5

Browse files
authored
feat: enable 1-click switch between pickers when open (#6785)
1 parent b775965 commit e08a6e5

File tree

3 files changed

+136
-4
lines changed

3 files changed

+136
-4
lines changed

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

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,12 @@ export const DatePickerMixin = (subclass) =>
567567
}
568568
});
569569

570+
content.addEventListener('focusout', (event) => {
571+
if (this._shouldRemoveFocus(event)) {
572+
this._setFocused(false);
573+
}
574+
});
575+
570576
// Two-way data binding for `focusedDate` property
571577
content.addEventListener('focused-date-changed', (e) => {
572578
this._focusedDate = e.detail.value;
@@ -650,12 +656,27 @@ export const DatePickerMixin = (subclass) =>
650656
* - when moving focus to the overlay content,
651657
* - when closing on date click / outside click.
652658
*
653-
* @param {!FocusEvent} _event
659+
* @param {FocusEvent} event
654660
* @return {boolean}
655661
* @protected
656662
* @override
657663
*/
658-
_shouldRemoveFocus(_event) {
664+
_shouldRemoveFocus(event) {
665+
// Remove the focused state when clicking outside on a focusable element that is deliberately
666+
// made targetable with pointer-events: auto, such as the time-picker in the date-time-picker.
667+
// In this scenario, focus will move straight to that element and the closing overlay won't
668+
// attempt to restore focus to the input.
669+
const { relatedTarget } = event;
670+
if (
671+
this.opened &&
672+
relatedTarget !== null &&
673+
relatedTarget !== document.body &&
674+
!this.contains(relatedTarget) &&
675+
!this._overlayContent.contains(relatedTarget)
676+
) {
677+
return true;
678+
}
679+
659680
return !this.opened;
660681
}
661682

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,7 @@ class DateTimePicker extends FieldMixin(DisabledMixin(FocusMixin(ThemableMixin(E
413413

414414
this.__changeEventHandler = this.__changeEventHandler.bind(this);
415415
this.__valueChangedEventHandler = this.__valueChangedEventHandler.bind(this);
416+
this.__openedChangedEventHandler = this.__openedChangedEventHandler.bind(this);
416417
}
417418

418419
/** @private */
@@ -521,16 +522,24 @@ class DateTimePicker extends FieldMixin(DisabledMixin(FocusMixin(ThemableMixin(E
521522
this.__dispatchChangeForValue = undefined;
522523
}
523524

525+
/** @private */
526+
__openedChangedEventHandler() {
527+
const opened = this.__datePicker.opened || this.__timePicker.opened;
528+
this.style.pointerEvents = opened ? 'auto' : '';
529+
}
530+
524531
/** @private */
525532
__addInputListeners(node) {
526533
node.addEventListener('change', this.__changeEventHandler);
527534
node.addEventListener('value-changed', this.__valueChangedEventHandler);
535+
node.addEventListener('opened-changed', this.__openedChangedEventHandler);
528536
}
529537

530538
/** @private */
531539
__removeInputListeners(node) {
532540
node.removeEventListener('change', this.__changeEventHandler);
533541
node.removeEventListener('value-changed', this.__valueChangedEventHandler);
542+
node.removeEventListener('opened-changed', this.__openedChangedEventHandler);
534543
}
535544

536545
/** @private */

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

Lines changed: 104 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { expect } from '@vaadin/chai-plugins';
2-
import { aTimeout, fixtureSync, focusin, focusout, nextFrame, nextRender } from '@vaadin/testing-helpers';
2+
import { aTimeout, fixtureSync, focusin, focusout, nextFrame, nextRender, outsideClick } from '@vaadin/testing-helpers';
3+
import { sendKeys } from '@web/test-runner-commands';
34
import sinon from 'sinon';
4-
import '../vaadin-date-time-picker.js';
5+
import '../src/vaadin-date-time-picker.js';
56
import { changeInputValue } from './helpers.js';
67

78
const fixtures = {
@@ -102,6 +103,52 @@ describe('Basic features', () => {
102103
});
103104
});
104105

106+
describe('pointer-events', () => {
107+
it('should not have by default', () => {
108+
expect(dateTimePicker.style.pointerEvents).to.be.empty;
109+
});
110+
111+
it('should set to `auto` when opening date-picker', async () => {
112+
datePicker.click();
113+
await nextRender();
114+
expect(dateTimePicker.style.pointerEvents).to.equal('auto');
115+
});
116+
117+
it('should remove when closing date-picker', async () => {
118+
datePicker.click();
119+
await nextRender();
120+
outsideClick();
121+
expect(dateTimePicker.style.pointerEvents).to.be.empty;
122+
});
123+
124+
it('should set to `auto` when opening time-picker', async () => {
125+
timePicker.click();
126+
await nextRender();
127+
expect(dateTimePicker.style.pointerEvents).to.equal('auto');
128+
});
129+
130+
it('should remove when closing time-picker', async () => {
131+
timePicker.click();
132+
await nextRender();
133+
outsideClick();
134+
expect(dateTimePicker.style.pointerEvents).to.be.empty;
135+
});
136+
137+
it('should keep `auto` when switching between pickers', async () => {
138+
datePicker.click();
139+
await nextRender();
140+
expect(dateTimePicker.style.pointerEvents).to.equal('auto');
141+
142+
timePicker.click();
143+
await nextRender();
144+
expect(dateTimePicker.style.pointerEvents).to.equal('auto');
145+
146+
datePicker.click();
147+
await nextRender();
148+
expect(dateTimePicker.style.pointerEvents).to.equal('auto');
149+
});
150+
});
151+
105152
describe('focused', () => {
106153
it('should set focused attribute on date-picker focusin', () => {
107154
focusin(datePicker);
@@ -140,6 +187,61 @@ describe('Basic features', () => {
140187
});
141188
});
142189

190+
describe('date-picker focused', () => {
191+
it('should remove focused attribute on time-picker click', async () => {
192+
datePicker.focus();
193+
datePicker.click();
194+
await nextRender();
195+
expect(datePicker.hasAttribute('focused')).to.be.true;
196+
197+
timePicker.focus();
198+
timePicker.click();
199+
expect(datePicker.hasAttribute('focused')).to.be.false;
200+
});
201+
202+
it('should remove focus-ring attribute on time-picker click', async () => {
203+
// Focus the date-picker with the keyboard
204+
await sendKeys({ press: 'Tab' });
205+
// Open the overlay with the keyboard
206+
await sendKeys({ press: 'ArrowDown' });
207+
await nextRender();
208+
expect(datePicker.hasAttribute('focus-ring')).to.be.true;
209+
210+
timePicker.focus();
211+
timePicker.click();
212+
expect(datePicker.hasAttribute('focus-ring')).to.be.false;
213+
});
214+
});
215+
216+
describe('time-picker focused', () => {
217+
it('should remove focused attribute on date-picker click', async () => {
218+
timePicker.focus();
219+
timePicker.click();
220+
await nextRender();
221+
expect(timePicker.hasAttribute('focused')).to.be.true;
222+
223+
datePicker.focus();
224+
datePicker.click();
225+
await nextRender();
226+
expect(timePicker.hasAttribute('focused')).to.be.false;
227+
});
228+
229+
it('should remove focus-ring attribute on date-picker click', async () => {
230+
// Focus the time-picker with the keyboard
231+
await sendKeys({ press: 'Tab' });
232+
await sendKeys({ press: 'Tab' });
233+
// Open the overlay with the keyboard
234+
await sendKeys({ press: 'ArrowDown' });
235+
await nextRender();
236+
expect(timePicker.hasAttribute('focus-ring')).to.be.true;
237+
238+
datePicker.focus();
239+
datePicker.click();
240+
await nextRender();
241+
expect(timePicker.hasAttribute('focus-ring')).to.be.false;
242+
});
243+
});
244+
143245
describe('change event', () => {
144246
let spy;
145247

0 commit comments

Comments
 (0)