Skip to content

Commit 543b4ad

Browse files
Merged in DSC-106 (pull request DSpace#643)
[DSC-106] Date input usable via keyboard using tab Approved-by: Vincenzo Mecca
1 parent e1e2941 commit 543b4ad

2 files changed

Lines changed: 178 additions & 5 deletions

File tree

src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.spec.ts

Lines changed: 106 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Load the implementations that should be tested
2-
import { ChangeDetectorRef, Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
3-
import { ComponentFixture, inject, TestBed, waitForAsync, } from '@angular/core/testing';
42
import { UntypedFormControl, UntypedFormGroup } from '@angular/forms';
3+
import { ChangeDetectorRef, Component, CUSTOM_ELEMENTS_SCHEMA, Renderer2 } from '@angular/core';
4+
import { ComponentFixture, fakeAsync, inject, TestBed, tick, waitForAsync, } from '@angular/core/testing';
55

66
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
77
import { DynamicFormLayoutService, DynamicFormValidationService } from '@ng-dynamic-forms/core';
@@ -13,6 +13,7 @@ import {
1313
mockDynamicFormLayoutService,
1414
mockDynamicFormValidationService
1515
} from '../../../../../testing/dynamic-form-mock-services';
16+
import { By } from '@angular/platform-browser';
1617

1718

1819
export const DATE_TEST_GROUP = new UntypedFormGroup({
@@ -39,6 +40,11 @@ describe('DsDatePickerComponent test suite', () => {
3940
let dateFixture: ComponentFixture<DsDatePickerComponent>;
4041
let html;
4142

43+
const renderer2: Renderer2 = {
44+
selectRootElement: jasmine.createSpy('selectRootElement'),
45+
querySelector: jasmine.createSpy('querySelector'),
46+
} as unknown as Renderer2;
47+
4248
// waitForAsync beforeEach
4349
beforeEach(waitForAsync(() => {
4450

@@ -54,7 +60,8 @@ describe('DsDatePickerComponent test suite', () => {
5460
ChangeDetectorRef,
5561
DsDatePickerComponent,
5662
{ provide: DynamicFormLayoutService, useValue: mockDynamicFormLayoutService },
57-
{ provide: DynamicFormValidationService, useValue: mockDynamicFormValidationService }
63+
{ provide: DynamicFormValidationService, useValue: mockDynamicFormValidationService },
64+
{ provide: Renderer2, useValue: renderer2 },
5865
],
5966
schemas: [CUSTOM_ELEMENTS_SCHEMA]
6067
});
@@ -233,6 +240,102 @@ describe('DsDatePickerComponent test suite', () => {
233240
expect(dateComp.disabledMonth).toBeFalsy();
234241
expect(dateComp.disabledDay).toBeFalsy();
235242
});
243+
244+
it('should move focus on month field when on year field and tab pressed', fakeAsync(() => {
245+
const event = {
246+
field: 'day',
247+
value: null
248+
};
249+
const event1 = {
250+
field: 'month',
251+
value: null
252+
};
253+
dateComp.onChange(event);
254+
dateComp.onChange(event1);
255+
256+
const yearElement = dateFixture.debugElement.query(By.css(`#${dateComp.model.id}_year`));
257+
const monthElement = dateFixture.debugElement.query(By.css(`#${dateComp.model.id}_month`));
258+
259+
yearElement.nativeElement.focus();
260+
dateFixture.detectChanges();
261+
262+
expect(document.activeElement).toBe(yearElement.nativeElement);
263+
264+
dateFixture.nativeElement.dispatchEvent(new KeyboardEvent('keydown', { key: 'tab' }));
265+
dateFixture.detectChanges();
266+
267+
tick(200);
268+
dateFixture.detectChanges();
269+
270+
expect(document.activeElement).toBe(monthElement.nativeElement);
271+
}));
272+
273+
it('should move focus on day field when on month field and tab pressed', fakeAsync(() => {
274+
const event = {
275+
field: 'day',
276+
value: null
277+
};
278+
dateComp.onChange(event);
279+
280+
const monthElement = dateFixture.debugElement.query(By.css(`#${dateComp.model.id}_month`));
281+
const dayElement = dateFixture.debugElement.query(By.css(`#${dateComp.model.id}_day`));
282+
283+
monthElement.nativeElement.focus();
284+
dateFixture.detectChanges();
285+
286+
expect(document.activeElement).toBe(monthElement.nativeElement);
287+
288+
dateFixture.nativeElement.dispatchEvent(new KeyboardEvent('keydown', { key: 'tab' }));
289+
dateFixture.detectChanges();
290+
291+
tick(200);
292+
dateFixture.detectChanges();
293+
294+
expect(document.activeElement).toBe(dayElement.nativeElement);
295+
}));
296+
297+
it('should move focus on month field when on day field and shift tab pressed', fakeAsync(() => {
298+
const event = {
299+
field: 'day',
300+
value: null
301+
};
302+
dateComp.onChange(event);
303+
304+
const monthElement = dateFixture.debugElement.query(By.css(`#${dateComp.model.id}_month`));
305+
const dayElement = dateFixture.debugElement.query(By.css(`#${dateComp.model.id}_day`));
306+
307+
dayElement.nativeElement.focus();
308+
dateFixture.detectChanges();
309+
310+
expect(document.activeElement).toBe(dayElement.nativeElement);
311+
312+
dateFixture.nativeElement.dispatchEvent(new KeyboardEvent('keydown', { key: 'shift.tab' }));
313+
dateFixture.detectChanges();
314+
315+
tick(200);
316+
dateFixture.detectChanges();
317+
318+
expect(document.activeElement).toBe(monthElement.nativeElement);
319+
}));
320+
321+
it('should move focus on year field when on month field and shift tab pressed', fakeAsync(() => {
322+
const yearElement = dateFixture.debugElement.query(By.css(`#${dateComp.model.id}_year`));
323+
const monthElement = dateFixture.debugElement.query(By.css(`#${dateComp.model.id}_month`));
324+
325+
monthElement.nativeElement.focus();
326+
dateFixture.detectChanges();
327+
328+
expect(document.activeElement).toBe(monthElement.nativeElement);
329+
330+
dateFixture.nativeElement.dispatchEvent(new KeyboardEvent('keydown', { key: 'shift.tab' }));
331+
dateFixture.detectChanges();
332+
333+
tick(200);
334+
dateFixture.detectChanges();
335+
336+
expect(document.activeElement).toBe(yearElement.nativeElement);
337+
}));
338+
236339
});
237340
});
238341

src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.ts

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
1-
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
21
import { UntypedFormGroup } from '@angular/forms';
2+
import { Component, EventEmitter, HostListener, Inject, Input, OnInit, Output, Renderer2 } from '@angular/core';
33
import { DynamicDsDatePickerModel } from './date-picker.model';
44
import { hasValue } from '../../../../../empty.util';
55
import {
66
DynamicFormControlComponent,
77
DynamicFormLayoutService,
88
DynamicFormValidationService
99
} from '@ng-dynamic-forms/core';
10+
import { DOCUMENT } from '@angular/common';
11+
import isEqual from 'lodash/isEqual';
12+
13+
14+
export type DatePickerFieldType = '_year' | '_month' | '_day';
1015

1116
export const DS_DATE_PICKER_SEPARATOR = '-';
1217

@@ -50,8 +55,12 @@ export class DsDatePickerComponent extends DynamicFormControlComponent implement
5055
disabledMonth = true;
5156
disabledDay = true;
5257

58+
private readonly fields: DatePickerFieldType[] = ['_year', '_month', '_day'];
59+
5360
constructor(protected layoutService: DynamicFormLayoutService,
54-
protected validationService: DynamicFormValidationService
61+
protected validationService: DynamicFormValidationService,
62+
private renderer: Renderer2,
63+
@Inject(DOCUMENT) private _document: Document
5564
) {
5665
super(layoutService, validationService);
5766
}
@@ -166,6 +175,67 @@ export class DsDatePickerComponent extends DynamicFormControlComponent implement
166175
this.change.emit(value);
167176
}
168177

178+
/**
179+
* Listen to keydown Tab event.
180+
* Get the active element and blur it, in order to focus the next input field.
181+
*/
182+
@HostListener('keydown.tab', ['$event'])
183+
onTabKeydown(event: KeyboardEvent) {
184+
event.preventDefault();
185+
const activeElement: Element = this._document.activeElement;
186+
(activeElement as any).blur();
187+
const index = this.selectedFieldIndex(activeElement);
188+
if (index < 0) {
189+
return;
190+
}
191+
let fieldToFocusOn = index + 1;
192+
if (fieldToFocusOn < this.fields.length) {
193+
this.focusInput(this.fields[fieldToFocusOn]);
194+
}
195+
}
196+
197+
@HostListener('keydown.shift.tab', ['$event'])
198+
onShiftTabKeyDown(event: KeyboardEvent) {
199+
event.preventDefault();
200+
const activeElement: Element = this._document.activeElement;
201+
(activeElement as any).blur();
202+
const index = this.selectedFieldIndex(activeElement);
203+
let fieldToFocusOn = index - 1;
204+
if (fieldToFocusOn >= 0) {
205+
this.focusInput(this.fields[fieldToFocusOn]);
206+
}
207+
}
208+
209+
private selectedFieldIndex(activeElement: Element): number {
210+
return this.fields.findIndex(field => isEqual(activeElement.id, this.model.id.concat(field)));
211+
}
212+
213+
/**
214+
* Focus the input field for the given type
215+
* based on the model id.
216+
* Used to focus the next input field
217+
* in case of a disabled field.
218+
* @param type DatePickerFieldType
219+
*/
220+
focusInput(type: DatePickerFieldType) {
221+
const field = this._document.getElementById(this.model.id.concat(type));
222+
if (field) {
223+
224+
if (hasValue(this.year) && isEqual(type, '_year')) {
225+
this.disabledMonth = true;
226+
this.disabledDay = true;
227+
}
228+
if (hasValue(this.year) && isEqual(type, '_month')) {
229+
this.disabledMonth = false;
230+
} else if (hasValue(this.month) && isEqual(type, '_day')) {
231+
this.disabledDay = false;
232+
}
233+
setTimeout(() => {
234+
this.renderer.selectRootElement(field).focus();
235+
}, 100);
236+
}
237+
}
238+
169239
onFocus(event) {
170240
this.focus.emit(event);
171241
}

0 commit comments

Comments
 (0)