Skip to content

Commit c30c54f

Browse files
IvayloGLipata
authored andcommitted
fix(date-picker, time-picker): manage input focus on outside click #6088 (#6305)
1 parent 17b6309 commit c30c54f

File tree

5 files changed

+276
-12
lines changed

5 files changed

+276
-12
lines changed

projects/igniteui-angular/src/lib/date-picker/date-picker.component.spec.ts

Lines changed: 162 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Component, ViewChild } from '@angular/core';
1+
import { Component, ViewChild, ElementRef } from '@angular/core';
22
import { async, fakeAsync, TestBed, tick, flush, ComponentFixture } from '@angular/core/testing';
33
import { FormsModule } from '@angular/forms';
44
import { By } from '@angular/platform-browser';
@@ -30,7 +30,8 @@ describe('IgxDatePicker', () => {
3030
IgxDatePickerEditableComponent,
3131
IgxDatePickerCustomizedComponent,
3232
IgxDropDownDatePickerRetemplatedComponent,
33-
IgxDatePickerOpeningComponent
33+
IgxDatePickerOpeningComponent,
34+
IgxDatePickerDropdownButtonsComponent
3435
],
3536
imports: [IgxDatePickerModule, FormsModule, NoopAnimationsModule, IgxInputGroupModule, IgxCalendarModule, IgxButtonModule]
3637
})
@@ -179,7 +180,7 @@ describe('IgxDatePicker', () => {
179180
expect(overlays.length).toEqual(0);
180181
}));
181182

182-
it('When datepicker is closed and the dialog disappear, the focus should remain on the input',
183+
it('When modal datepicker is closed via `Escape` Key and the dialog disappear, the focus should remain on the input',
183184
fakeAsync(() => {
184185
const datePickerDom = fixture.debugElement.query(By.css('igx-date-picker'));
185186
let overlayToggle = document.getElementsByClassName('igx-overlay__wrapper--modal');
@@ -203,6 +204,56 @@ describe('IgxDatePicker', () => {
203204
expect(input).toEqual(document.activeElement);
204205
}));
205206

207+
it('When a modal datepicker is closed via outside click, the focus should remain on the input',
208+
fakeAsync(() => {
209+
const datePickerDom = fixture.debugElement.query(By.css('igx-date-picker'));
210+
let overlayToggle = document.getElementsByClassName('igx-overlay__wrapper--modal');
211+
expect(overlayToggle.length).toEqual(0);
212+
213+
UIInteractions.triggerKeyDownEvtUponElem('space', datePickerDom.nativeElement, false);
214+
flush();
215+
fixture.detectChanges();
216+
217+
overlayToggle = document.getElementsByClassName('igx-overlay__wrapper--modal');
218+
expect(overlayToggle[0]).not.toBeNull();
219+
expect(overlayToggle[0]).not.toBeUndefined();
220+
221+
UIInteractions.clickElement(overlayToggle[0]);
222+
flush();
223+
fixture.detectChanges();
224+
225+
const input = fixture.debugElement.query(By.directive(IgxInputDirective)).nativeElement;
226+
overlayToggle = document.getElementsByClassName('igx-overlay__wrapper--modal');
227+
expect(overlayToggle[0]).toEqual(undefined);
228+
expect(input).toEqual(document.activeElement);
229+
}));
230+
231+
it('When datepicker is closed upon selecting a date, the focus should remain on the input',
232+
fakeAsync(() => {
233+
const datePickerDom = fixture.debugElement.query(By.css('igx-date-picker'));
234+
let overlayToggle = document.getElementsByClassName('igx-overlay__wrapper--modal');
235+
expect(overlayToggle.length).toEqual(0);
236+
237+
UIInteractions.triggerKeyDownEvtUponElem('space', datePickerDom.nativeElement, false);
238+
flush();
239+
fixture.detectChanges();
240+
241+
overlayToggle = document.getElementsByClassName('igx-overlay__wrapper--modal');
242+
expect(overlayToggle[0]).not.toBeNull();
243+
expect(overlayToggle[0]).not.toBeUndefined();
244+
245+
// select a date
246+
const dateElemToSelect = document.getElementsByClassName('igx-calendar__date')[10];
247+
UIInteractions.clickElement(dateElemToSelect);
248+
flush();
249+
fixture.detectChanges();
250+
251+
const input = fixture.debugElement.query(By.directive(IgxInputDirective)).nativeElement;
252+
overlayToggle = document.getElementsByClassName('igx-overlay__wrapper--modal');
253+
expect(overlayToggle[0]).toEqual(undefined);
254+
expect(input).toEqual(document.activeElement);
255+
}));
256+
206257
});
207258

208259
describe('DatePicker with passed date', () => {
@@ -247,6 +298,99 @@ describe('IgxDatePicker', () => {
247298
});
248299
});
249300

301+
it('When datepicker in "dropdown" mode is closed via outside click, the input should not receive focus',
302+
fakeAsync(() => {
303+
const fixture = TestBed.createComponent(IgxDatePickerDropdownButtonsComponent);
304+
fixture.detectChanges();
305+
306+
const datePickerDom = fixture.debugElement.query(By.css('igx-date-picker'));
307+
const input = fixture.debugElement.query(By.directive(IgxInputDirective)).nativeElement;
308+
let overlayToggle = document.getElementsByClassName('igx-overlay__wrapper');
309+
310+
expect(overlayToggle.length).toEqual(0);
311+
312+
UIInteractions.triggerKeyDownEvtUponElem('space', datePickerDom.nativeElement, false);
313+
flush();
314+
fixture.detectChanges();
315+
316+
overlayToggle = document.getElementsByClassName('igx-overlay__wrapper');
317+
expect(overlayToggle[0]).not.toBeNull();
318+
expect(overlayToggle[0]).not.toBeUndefined();
319+
320+
const dummyInput = fixture.componentInstance.dummyInput.nativeElement;
321+
dummyInput.focus();
322+
dummyInput.click();
323+
tick();
324+
fixture.detectChanges();
325+
326+
overlayToggle = document.getElementsByClassName('igx-overlay__wrapper');
327+
expect(overlayToggle[0]).toEqual(undefined);
328+
expect(input).not.toEqual(document.activeElement);
329+
expect(dummyInput).toEqual(document.activeElement);
330+
}));
331+
332+
it('When datepicker in "dropdown" mode, should focus input on user interaction with Today btn, Cancel btn, Enter Key, Escape key',
333+
fakeAsync(() => {
334+
const fixture = TestBed.createComponent(IgxDatePickerDropdownButtonsComponent);
335+
fixture.detectChanges();
336+
const datePickerDom = fixture.debugElement.query(By.css('igx-date-picker'));
337+
const input = fixture.debugElement.query(By.directive(IgxInputDirective)).nativeElement;
338+
let overlayToggle = document.getElementsByClassName('igx-overlay__wrapper');
339+
expect(overlayToggle.length).toEqual(0);
340+
341+
UIInteractions.triggerKeyDownEvtUponElem('space', datePickerDom.nativeElement, false);
342+
flush();
343+
fixture.detectChanges();
344+
const buttons = document.getElementsByClassName('igx-button--flat');
345+
expect(buttons.length).toEqual(2);
346+
347+
// Today btn
348+
const todayBtn = buttons[1] as HTMLElement;
349+
expect(todayBtn.innerText).toBe('Today');
350+
todayBtn.click();
351+
tick();
352+
fixture.detectChanges();
353+
overlayToggle = document.getElementsByClassName('igx-overlay__wrapper');
354+
expect(overlayToggle[0]).toEqual(undefined);
355+
expect(input).toEqual(document.activeElement);
356+
357+
// Cancel btn
358+
UIInteractions.triggerKeyDownEvtUponElem('space', datePickerDom.nativeElement, false);
359+
flush();
360+
fixture.detectChanges();
361+
const cancelBtn = buttons[0] as HTMLElement;
362+
expect(cancelBtn.innerText).toBe('Cancel');
363+
cancelBtn.click();
364+
tick();
365+
fixture.detectChanges();
366+
overlayToggle = document.getElementsByClassName('igx-overlay__wrapper');
367+
expect(overlayToggle[0]).toEqual(undefined);
368+
expect(input).toEqual(document.activeElement);
369+
370+
// Enter key
371+
UIInteractions.triggerKeyDownEvtUponElem('space', datePickerDom.nativeElement, false);
372+
flush();
373+
fixture.detectChanges();
374+
document.activeElement.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
375+
tick();
376+
fixture.detectChanges();
377+
overlayToggle = document.getElementsByClassName('igx-overlay__wrapper');
378+
expect(overlayToggle[0]).toEqual(undefined);
379+
expect(input).toEqual(document.activeElement);
380+
381+
// Esc key
382+
UIInteractions.triggerKeyDownEvtUponElem('space', datePickerDom.nativeElement, false);
383+
flush();
384+
fixture.detectChanges();
385+
document.activeElement.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
386+
tick();
387+
fixture.detectChanges();
388+
389+
overlayToggle = document.getElementsByClassName('igx-overlay__wrapper');
390+
expect(overlayToggle[0]).toEqual(undefined);
391+
expect(input).toEqual(document.activeElement);
392+
}));
393+
250394
it('Datepicker week start day (Monday)', () => {
251395
const fixture = TestBed.createComponent(IgxDatePickerWithWeekStartComponent);
252396
fixture.detectChanges();
@@ -1234,3 +1378,18 @@ export class IgxDatePickerCustomizedComponent {
12341378
export class IgxDatePickerOpeningComponent {
12351379
@ViewChild(IgxDatePickerComponent, { static: true }) public datePicker: IgxDatePickerComponent;
12361380
}
1381+
1382+
@Component({
1383+
template: `
1384+
<input class="dummyInput" #dummyInput/>
1385+
<igx-date-picker id="dropdownButtonsDatePicker" mode="dropdown" cancelButtonLabel="Cancel" todayButtonLabel="Today" >
1386+
</igx-date-picker>
1387+
`
1388+
})
1389+
class IgxDatePickerDropdownButtonsComponent {
1390+
@ViewChild('dropdownButtonsDatePicker', { read: IgxDatePickerComponent, static: true })
1391+
public datePicker: IgxDatePickerComponent;
1392+
1393+
@ViewChild('dummyInput', {static: true }) public dummyInput: ElementRef;
1394+
}
1395+

projects/igniteui-angular/src/lib/date-picker/date-picker.component.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -860,6 +860,15 @@ export class IgxDatePickerComponent implements IDatePicker, ControlValueAccessor
860860
filter(overlay => overlay.id === this._componentID),
861861
takeUntil(this._destroy$)).subscribe((event) => {
862862
this.onClosing.emit(event);
863+
// If canceled in a user onClosing handler
864+
if (event.cancel) {
865+
return;
866+
}
867+
// Do not focus the input if clicking outside in dropdown mode
868+
const input = this.getEditElement();
869+
if (input && !(event.event && this.mode === InteractionMode.DropDown)) {
870+
input.focus();
871+
}
863872
});
864873

865874
if (this.mode === InteractionMode.DropDown) {
@@ -1240,10 +1249,6 @@ export class IgxDatePickerComponent implements IDatePicker, ControlValueAccessor
12401249

12411250
// TODO: remove this line after deprecating 'onClose'
12421251
this.onClose.emit(this);
1243-
1244-
if (this.getEditElement()) {
1245-
this.getEditElement().focus();
1246-
}
12471252
}
12481253

12491254
private _initializeCalendarContainer(componentInstance: IgxCalendarContainerComponent) {

projects/igniteui-angular/src/lib/services/overlay/overlay.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -649,7 +649,7 @@ export class IgxOverlayService implements OnDestroy {
649649
if (info.settings.modal) {
650650
fromEvent(info.elementRef.nativeElement.parentElement.parentElement, 'click')
651651
.pipe(takeUntil(this.destroy$))
652-
.subscribe(() => this.hide(info.id));
652+
.subscribe((e: Event) => this._hide(info.id, e));
653653
} else if (
654654
// if all overlays minus closing overlays equals one add the handler
655655
this._overlayInfos.filter(x => x.settings.closeOnOutsideClick && !x.settings.modal).length -

projects/igniteui-angular/src/lib/time-picker/time-picker.component.spec.ts

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Component, ViewChild } from '@angular/core';
1+
import { Component, ViewChild, ElementRef } from '@angular/core';
22
import { async, TestBed, fakeAsync, tick } from '@angular/core/testing';
33
import { FormsModule } from '@angular/forms';
44
import { By } from '@angular/platform-browser';
@@ -1476,6 +1476,98 @@ describe('IgxTimePicker', () => {
14761476
expect(document.getElementsByClassName('igx-time-picker__buttons').length).toEqual(0);
14771477
}));
14781478

1479+
it('should focus input on user interaction with OK btn, Cancel btn, Enter Key, Escape key', fakeAsync(() => {
1480+
fixture.detectChanges();
1481+
let overlayToggle = document.getElementsByClassName('igx-overlay__wrapper');
1482+
expect(overlayToggle.length).toEqual(0);
1483+
1484+
const iconTime = dom.queryAll(By.css('.igx-icon'))[0];
1485+
UIInteractions.clickElement(iconTime);
1486+
tick();
1487+
fixture.detectChanges();
1488+
1489+
const buttons = document.getElementsByClassName('igx-time-picker__buttons')[0];
1490+
expect(buttons.children.length).toEqual(2);
1491+
1492+
const okBtn = dom.queryAll(By.css('.igx-button--flat'))[1];
1493+
expect(okBtn.nativeElement.innerText).toBe('OK');
1494+
1495+
// OK btn
1496+
okBtn.triggerEventHandler('click', {});
1497+
tick();
1498+
fixture.detectChanges();
1499+
1500+
input = fixture.debugElement.query(By.directive(IgxInputDirective)).nativeElement;
1501+
overlayToggle = document.getElementsByClassName('igx-overlay__wrapper');
1502+
expect(overlayToggle[0]).toEqual(undefined);
1503+
expect(input).toEqual(document.activeElement);
1504+
1505+
// Cancel btn
1506+
UIInteractions.clickElement(iconTime);
1507+
tick();
1508+
fixture.detectChanges();
1509+
const cancelBtn = dom.queryAll(By.css('.igx-button--flat'))[0];
1510+
expect(cancelBtn.nativeElement.innerText).toBe('Cancel');
1511+
cancelBtn.triggerEventHandler('click', {});
1512+
tick();
1513+
fixture.detectChanges();
1514+
overlayToggle = document.getElementsByClassName('igx-overlay__wrapper');
1515+
expect(overlayToggle[0]).toEqual(undefined);
1516+
expect(input).toEqual(document.activeElement);
1517+
1518+
// Enter key
1519+
UIInteractions.clickElement(iconTime);
1520+
tick(100);
1521+
fixture.detectChanges();
1522+
document.activeElement.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
1523+
tick();
1524+
fixture.detectChanges();
1525+
overlayToggle = document.getElementsByClassName('igx-overlay__wrapper');
1526+
expect(overlayToggle[0]).toEqual(undefined);
1527+
expect(input).toEqual(document.activeElement);
1528+
1529+
// Esc key
1530+
UIInteractions.clickElement(iconTime);
1531+
tick(100);
1532+
fixture.detectChanges();
1533+
document.activeElement.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
1534+
tick();
1535+
fixture.detectChanges();
1536+
1537+
overlayToggle = document.getElementsByClassName('igx-overlay__wrapper');
1538+
expect(overlayToggle[0]).toEqual(undefined);
1539+
expect(input).toEqual(document.activeElement);
1540+
}));
1541+
1542+
it('When timepicker is closed via outside click, the focus should NOT remain on the input',
1543+
fakeAsync(() => {
1544+
fixture.detectChanges();
1545+
input = fixture.debugElement.query(By.directive(IgxInputDirective)).nativeElement;
1546+
let overlayToggle = document.getElementsByClassName('igx-overlay__wrapper');
1547+
1548+
expect(overlayToggle.length).toEqual(0);
1549+
1550+
const iconTime = dom.queryAll(By.css('.igx-icon'))[0];
1551+
UIInteractions.clickElement(iconTime);
1552+
tick();
1553+
fixture.detectChanges();
1554+
1555+
overlayToggle = document.getElementsByClassName('igx-overlay__wrapper');
1556+
expect(overlayToggle[0]).not.toBeNull();
1557+
expect(overlayToggle[0]).not.toBeUndefined();
1558+
1559+
const dummyInput = fixture.componentInstance.dummyInput.nativeElement;
1560+
dummyInput.focus();
1561+
dummyInput.click();
1562+
tick();
1563+
fixture.detectChanges();
1564+
1565+
overlayToggle = document.getElementsByClassName('igx-overlay__wrapper');
1566+
input = fixture.debugElement.query(By.directive(IgxInputDirective)).nativeElement;
1567+
expect(overlayToggle[0]).toEqual(undefined);
1568+
expect(input).not.toEqual(document.activeElement);
1569+
expect(dummyInput).toEqual(document.activeElement);
1570+
}));
14791571
});
14801572

14811573
describe('Timepicker with outlet', () => {
@@ -1910,6 +2002,7 @@ export class IgxTimePickerRetemplatedComponent { }
19102002

19112003
@Component({
19122004
template: `
2005+
<input class="dummyInput" #dummyInput/>
19132006
<igx-time-picker mode="dropdown"
19142007
[isSpinLoop]="isSpinLoop"
19152008
[(ngModel)]="date"
@@ -1926,6 +2019,7 @@ export class IgxTimePickerDropDownComponent {
19262019
date = new Date(2018, 10, 27, 17, 45, 0, 0);
19272020

19282021
@ViewChild(IgxTimePickerComponent, { static: true }) public timePicker: IgxTimePickerComponent;
2022+
@ViewChild('dummyInput', { static: true }) public dummyInput: ElementRef;
19292023
}
19302024
@Component({
19312025
template: `

projects/igniteui-angular/src/lib/time-picker/time-picker.component.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -803,9 +803,6 @@ export class IgxTimePickerComponent implements
803803
if (this.toggleRef) {
804804
this.toggleRef.onClosed.pipe(takeUntil(this._destroy$)).subscribe(() => {
805805

806-
if (this._input) {
807-
this._input.nativeElement.focus();
808-
}
809806

810807
if (this.mode === InteractionMode.DropDown) {
811808
this._onDropDownClosed();
@@ -826,6 +823,15 @@ export class IgxTimePickerComponent implements
826823

827824
this.toggleRef.onClosing.pipe(takeUntil(this._destroy$)).subscribe((event) => {
828825
this.onClosing.emit(event);
826+
// If canceled in a user onClosing handler
827+
if (event.cancel) {
828+
return;
829+
}
830+
// Do not focus the input if clicking outside in dropdown mode
831+
const input = this.getEditElement();
832+
if (input && !(event.event && this.mode === InteractionMode.DropDown)) {
833+
input.focus();
834+
}
829835
});
830836
}
831837
}

0 commit comments

Comments
 (0)