Skip to content

Commit

Permalink
fix(module:date-picker): open on enter and focus on inner input (#3804)
Browse files Browse the repository at this point in the history
Extracted the good parts suitable for a11y from #3146. See discussion there.
  • Loading branch information
jimmytheneutrino authored and wenqi73 committed Aug 6, 2019
1 parent 31dbe1a commit 3f03ee1
Show file tree
Hide file tree
Showing 9 changed files with 145 additions and 15 deletions.
Expand Up @@ -5,6 +5,7 @@
placeholder="{{ placeholder || locale.dateSelect }}" placeholder="{{ placeholder || locale.dateSelect }}"
value="{{ toReadableInput(value) }}" value="{{ toReadableInput(value) }}"
(keyup)="onInputKeyup($event)" (keyup)="onInputKeyup($event)"
#inputElement
/> />
</div> </div>
<a class="{{ prefixCls }}-clear-btn" role="button" title="{{ locale.clear }}"> <a class="{{ prefixCls }}-clear-btn" role="button" title="{{ locale.clear }}">
Expand Down
15 changes: 11 additions & 4 deletions components/date-picker/lib/calendar/calendar-input.component.ts
Expand Up @@ -9,10 +9,12 @@
import { import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
Component, Component,
ElementRef,
EventEmitter, EventEmitter,
Input, Input,
OnInit, OnInit,
Output, Output,
ViewChild,
ViewEncapsulation ViewEncapsulation
} from '@angular/core'; } from '@angular/core';


Expand All @@ -34,24 +36,29 @@ export class CalendarInputComponent implements OnInit {
@Input() disabledDate: (d: Date) => boolean; @Input() disabledDate: (d: Date) => boolean;


@Input() value: CandyDate; @Input() value: CandyDate;
@Input() autoFocus: boolean;
@Output() readonly valueChange = new EventEmitter<CandyDate>(); @Output() readonly valueChange = new EventEmitter<CandyDate>();
@ViewChild('inputElement', { static: true }) inputRef: ElementRef;


prefixCls: string = 'ant-calendar'; prefixCls: string = 'ant-calendar';
invalidInputClass: string = ''; invalidInputClass: string = '';


constructor(private dateHelper: DateHelperService) {} constructor(private dateHelper: DateHelperService) {}


ngOnInit(): void {} ngOnInit(): void {
if (this.autoFocus) {
this.inputRef.nativeElement.focus();
}
}


onInputKeyup(event: Event): void { onInputKeyup(event: KeyboardEvent): void {
const date = this.checkValidInputDate(event); const date = this.checkValidInputDate(event);


if (!date || (this.disabledDate && this.disabledDate(date.nativeDate))) { if (!date || (this.disabledDate && this.disabledDate(date.nativeDate))) {
return; return;
} }


if (!date.isSame(this.value, 'second')) { if (event.key === 'Enter') {
// Not same with original value
this.value = date; this.value = date;
this.valueChange.emit(this.value); this.valueChange.emit(this.value);
} }
Expand Down
Expand Up @@ -33,6 +33,7 @@
[locale]="locale" [locale]="locale"
[disabledDate]="disabledDate" [disabledDate]="disabledDate"
[format]="format" [format]="format"
[autoFocus]="partType !== 'right'"
[placeholder]="getPlaceholder(partType)" [placeholder]="getPlaceholder(partType)"
></calendar-input> ></calendar-input>
</ng-template> </ng-template>
Expand Down
73 changes: 72 additions & 1 deletion components/date-picker/nz-date-picker.component.spec.ts
Expand Up @@ -71,6 +71,62 @@ describe('NzDatePickerComponent', () => {
expect(getPickerContainer()).toBeNull(); expect(getPickerContainer()).toBeNull();
})); }));


it('should focus on the trigger after a click outside', fakeAsync(() => {
fixture.detectChanges();
openPickerByClickTrigger();

dispatchMouseEvent(queryFromOverlay('.cdk-overlay-backdrop'), 'click');
fixture.detectChanges();
tick(500);
fixture.detectChanges();
expect(document.activeElement).toEqual(getPickerTrigger());
}));

it('should open on enter', fakeAsync(() => {
fixture.detectChanges();
getPickerTriggerWrapper().dispatchEvent(new KeyboardEvent('keyup', { key: 'enter' }));
fixture.detectChanges();
tick(500);
fixture.detectChanges();
expect(getPickerContainer()).not.toBeNull();
}));

it('should open by click and focus on inner calendar input', fakeAsync(() => {
fixture.detectChanges();
openPickerByClickTrigger();
expect(document.activeElement).toEqual(queryFromOverlay('input.ant-calendar-input'));
}));

it('should open by click, focus on inner calendar input, and submit on enter', fakeAsync(() => {
fixtureInstance.nzValue = new Date();
fixture.detectChanges();
// Do it 2 times to normalize the value of the element.
const action = () => {
openPickerByClickTrigger();
expect(document.activeElement).toEqual(queryFromOverlay('input.ant-calendar-input'));
queryFromOverlay('input.ant-calendar-input').dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter' }));
fixture.detectChanges();
tick(500);
fixture.detectChanges();
expect(getPickerContainer()).toBeNull();
};
action();
action();
}));

it('should not submit with invalid input', fakeAsync(() => {
fixture.detectChanges();
openPickerByClickTrigger();
const input = queryFromOverlay('input.ant-calendar-input') as HTMLInputElement;
input.value = 'invalid input';
fixture.detectChanges();
input.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter' }));
fixture.detectChanges();
tick(500);
fixture.detectChanges();
expect(getPickerContainer()).not.toBeNull();
}));

it('should support changing language at runtime', fakeAsync(() => { it('should support changing language at runtime', fakeAsync(() => {
fixture.detectChanges(); fixture.detectChanges();
expect(getPickerTrigger().placeholder).toBe('请选择日期'); expect(getPickerTrigger().placeholder).toBe('请选择日期');
Expand Down Expand Up @@ -197,6 +253,21 @@ describe('NzDatePickerComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
const disabledCell = queryFromOverlay('tbody.ant-calendar-tbody td.ant-calendar-disabled-cell'); const disabledCell = queryFromOverlay('tbody.ant-calendar-tbody td.ant-calendar-disabled-cell');
expect(disabledCell.textContent!.trim()).toBe('15'); expect(disabledCell.textContent!.trim()).toBe('15');
const input = queryFromOverlay('input.ant-calendar-input') as HTMLInputElement;
const submit = (date: string) => {
input.value = date;
fixture.detectChanges();
input.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter' }));
fixture.detectChanges();
tick(500);
fixture.detectChanges();
};
// Should fail to submit a disabled date
submit('2018-11-15');
expect(getPickerContainer()).not.toBeNull();
// But it should be fine to submit an enabled date
submit('2018-11-11');
expect(getPickerContainer()).toBeNull();
})); }));


it('should support nzLocale', () => { it('should support nzLocale', () => {
Expand Down Expand Up @@ -719,7 +790,7 @@ describe('NzDatePickerComponent', () => {


// Correct inputing // Correct inputing
input.value = '2018-11-22'; input.value = '2018-11-22';
input.dispatchEvent(new KeyboardEvent('keyup')); input.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter' }));
// dispatchKeyboardEvent(input, 'keyup', ENTER); // Not working? // dispatchKeyboardEvent(input, 'keyup', ENTER); // Not working?
fixture.detectChanges(); fixture.detectChanges();
flush(); flush();
Expand Down
9 changes: 9 additions & 0 deletions components/date-picker/nz-month-picker.component.spec.ts
Expand Up @@ -63,6 +63,15 @@ describe('NzMonthPickerComponent', () => {
expect(getPickerContainer()).toBeNull(); expect(getPickerContainer()).toBeNull();
})); }));


it('should open on enter', fakeAsync(() => {
fixture.detectChanges();
getPickerTriggerWrapper().dispatchEvent(new KeyboardEvent('keyup', { key: 'enter' }));
fixture.detectChanges();
tick(500);
fixture.detectChanges();
expect(getPickerContainer()).not.toBeNull();
}));

it('should support nzAllowClear and work properly', fakeAsync(() => { it('should support nzAllowClear and work properly', fakeAsync(() => {
const clearBtnSelector = By.css('nz-picker i.ant-calendar-picker-clear'); const clearBtnSelector = By.css('nz-picker i.ant-calendar-picker-clear');
const initial = (fixtureInstance.nzValue = new Date()); const initial = (fixtureInstance.nzValue = new Date());
Expand Down
30 changes: 28 additions & 2 deletions components/date-picker/nz-range-picker.component.spec.ts
Expand Up @@ -66,6 +66,32 @@ describe('NzRangePickerComponent', () => {
expect(getPickerContainer()).toBeNull(); expect(getPickerContainer()).toBeNull();
})); }));


it('should focus on the trigger after a click outside', fakeAsync(() => {
fixture.detectChanges();
openPickerByClickTrigger();

dispatchMouseEvent(queryFromOverlay('.cdk-overlay-backdrop'), 'click');
fixture.detectChanges();
tick(500);
fixture.detectChanges();
expect(getPickerTrigger().matches(':focus-within')).toBeTruthy();
}));

it('should open on enter', fakeAsync(() => {
fixture.detectChanges();
getPickerTriggerWrapper().dispatchEvent(new KeyboardEvent('keyup', { key: 'enter' }));
fixture.detectChanges();
tick(500);
fixture.detectChanges();
expect(getPickerContainer()).not.toBeNull();
}));

it('should open by click and focus on left inner calendar input', fakeAsync(() => {
fixture.detectChanges();
openPickerByClickTrigger();
expect(document.activeElement).toEqual(queryFromOverlay('.ant-calendar-range-left input.ant-calendar-input'));
}));

it('should support nzAllowClear and work properly', fakeAsync(() => { it('should support nzAllowClear and work properly', fakeAsync(() => {
const clearBtnSelector = By.css('nz-picker i.ant-calendar-picker-clear'); const clearBtnSelector = By.css('nz-picker i.ant-calendar-picker-clear');
const initial = (fixtureInstance.modelValue = [new Date(), new Date()]); const initial = (fixtureInstance.modelValue = [new Date(), new Date()]);
Expand Down Expand Up @@ -643,10 +669,10 @@ describe('NzRangePickerComponent', () => {
const rightInput = queryFromOverlay('.ant-calendar-range-right input.ant-calendar-input') as HTMLInputElement; const rightInput = queryFromOverlay('.ant-calendar-range-right input.ant-calendar-input') as HTMLInputElement;


leftInput.value = '2018-11-11'; leftInput.value = '2018-11-11';
leftInput.dispatchEvent(new KeyboardEvent('keyup')); leftInput.dispatchEvent(new KeyboardEvent('keyup', {key: 'Enter'}));
fixture.detectChanges(); fixture.detectChanges();
rightInput.value = '2018-12-12'; rightInput.value = '2018-12-12';
rightInput.dispatchEvent(new KeyboardEvent('keyup')); rightInput.dispatchEvent(new KeyboardEvent('keyup', {key: 'Enter'}));
fixture.detectChanges(); fixture.detectChanges();
tick(500); tick(500);
expect(nzOnChange).toHaveBeenCalled(); expect(nzOnChange).toHaveBeenCalled();
Expand Down
9 changes: 9 additions & 0 deletions components/date-picker/nz-year-picker.component.spec.ts
Expand Up @@ -58,6 +58,15 @@ describe('NzYearPickerComponent', () => {
expect(getPickerContainer()).toBeNull(); expect(getPickerContainer()).toBeNull();
})); }));


it('should open on enter', fakeAsync(() => {
fixture.detectChanges();
getPickerTriggerWrapper().dispatchEvent(new KeyboardEvent('keyup', { key: 'enter' }));
fixture.detectChanges();
tick(500);
fixture.detectChanges();
expect(getPickerContainer()).not.toBeNull();
}));

it('should support nzAllowClear and work properly', fakeAsync(() => { it('should support nzAllowClear and work properly', fakeAsync(() => {
const clearBtnSelector = By.css('nz-picker i.ant-calendar-picker-clear'); const clearBtnSelector = By.css('nz-picker i.ant-calendar-picker-clear');
const initial = (fixtureInstance.nzValue = new Date()); const initial = (fixtureInstance.nzValue = new Date());
Expand Down
1 change: 1 addition & 0 deletions components/date-picker/picker.component.html
Expand Up @@ -5,6 +5,7 @@
[ngStyle]="style" [ngStyle]="style"
tabindex="0" tabindex="0"
(click)="onClickInputBox()" (click)="onClickInputBox()"
(keyup.enter)="onClickInputBox()"
> >
<!-- Content of single picker --> <!-- Content of single picker -->
<ng-container *ngIf="!isRange"> <ng-container *ngIf="!isRange">
Expand Down
21 changes: 13 additions & 8 deletions components/date-picker/picker.component.ts
Expand Up @@ -105,14 +105,18 @@ export class NzPickerComponent implements OnInit, AfterViewInit {


ngAfterViewInit(): void { ngAfterViewInit(): void {
if (this.autoFocus) { if (this.autoFocus) {
if (this.isRange) { this.focus();
const firstInput = (this.pickerInput.nativeElement as HTMLElement).querySelector( }
'input:first-child' }
) as HTMLInputElement;
firstInput.focus(); // Focus on the first input focus(): void {
} else { if (this.isRange) {
this.pickerInput.nativeElement.focus(); const firstInput = (this.pickerInput.nativeElement as HTMLElement).querySelector(
} 'input:first-child'
) as HTMLInputElement;
firstInput.focus(); // Focus on the first input
} else {
this.pickerInput.nativeElement.focus();
} }
} }


Expand All @@ -133,6 +137,7 @@ export class NzPickerComponent implements OnInit, AfterViewInit {
if (this.realOpenState) { if (this.realOpenState) {
this.overlayOpen = false; this.overlayOpen = false;
this.openChange.emit(this.overlayOpen); this.openChange.emit(this.overlayOpen);
this.focus();
} }
} }


Expand Down

0 comments on commit 3f03ee1

Please sign in to comment.