diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.spec.ts index ffc36008c69..8ee90fb230b 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.spec.ts @@ -1,7 +1,7 @@ // Load the implementations that should be tested -import { ChangeDetectorRef, Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; -import { ComponentFixture, inject, TestBed, waitForAsync, } from '@angular/core/testing'; import { UntypedFormControl, UntypedFormGroup } from '@angular/forms'; +import { ChangeDetectorRef, Component, CUSTOM_ELEMENTS_SCHEMA, Renderer2 } from '@angular/core'; +import { ComponentFixture, fakeAsync, inject, TestBed, tick, waitForAsync, } from '@angular/core/testing'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { DynamicFormLayoutService, DynamicFormValidationService } from '@ng-dynamic-forms/core'; @@ -13,6 +13,7 @@ import { mockDynamicFormLayoutService, mockDynamicFormValidationService } from '../../../../../testing/dynamic-form-mock-services'; +import { By } from '@angular/platform-browser'; export const DATE_TEST_GROUP = new UntypedFormGroup({ @@ -39,6 +40,11 @@ describe('DsDatePickerComponent test suite', () => { let dateFixture: ComponentFixture; let html; + const renderer2: Renderer2 = { + selectRootElement: jasmine.createSpy('selectRootElement'), + querySelector: jasmine.createSpy('querySelector'), + } as unknown as Renderer2; + // waitForAsync beforeEach beforeEach(waitForAsync(() => { @@ -54,7 +60,8 @@ describe('DsDatePickerComponent test suite', () => { ChangeDetectorRef, DsDatePickerComponent, { provide: DynamicFormLayoutService, useValue: mockDynamicFormLayoutService }, - { provide: DynamicFormValidationService, useValue: mockDynamicFormValidationService } + { provide: DynamicFormValidationService, useValue: mockDynamicFormValidationService }, + { provide: Renderer2, useValue: renderer2 }, ], schemas: [CUSTOM_ELEMENTS_SCHEMA] }); @@ -233,6 +240,102 @@ describe('DsDatePickerComponent test suite', () => { expect(dateComp.disabledMonth).toBeFalsy(); expect(dateComp.disabledDay).toBeFalsy(); }); + + it('should move focus on month field when on year field and tab pressed', fakeAsync(() => { + const event = { + field: 'day', + value: null + }; + const event1 = { + field: 'month', + value: null + }; + dateComp.onChange(event); + dateComp.onChange(event1); + + const yearElement = dateFixture.debugElement.query(By.css(`#${dateComp.model.id}_year`)); + const monthElement = dateFixture.debugElement.query(By.css(`#${dateComp.model.id}_month`)); + + yearElement.nativeElement.focus(); + dateFixture.detectChanges(); + + expect(document.activeElement).toBe(yearElement.nativeElement); + + dateFixture.nativeElement.dispatchEvent(new KeyboardEvent('keydown', { key: 'tab' })); + dateFixture.detectChanges(); + + tick(200); + dateFixture.detectChanges(); + + expect(document.activeElement).toBe(monthElement.nativeElement); + })); + + it('should move focus on day field when on month field and tab pressed', fakeAsync(() => { + const event = { + field: 'day', + value: null + }; + dateComp.onChange(event); + + const monthElement = dateFixture.debugElement.query(By.css(`#${dateComp.model.id}_month`)); + const dayElement = dateFixture.debugElement.query(By.css(`#${dateComp.model.id}_day`)); + + monthElement.nativeElement.focus(); + dateFixture.detectChanges(); + + expect(document.activeElement).toBe(monthElement.nativeElement); + + dateFixture.nativeElement.dispatchEvent(new KeyboardEvent('keydown', { key: 'tab' })); + dateFixture.detectChanges(); + + tick(200); + dateFixture.detectChanges(); + + expect(document.activeElement).toBe(dayElement.nativeElement); + })); + + it('should move focus on month field when on day field and shift tab pressed', fakeAsync(() => { + const event = { + field: 'day', + value: null + }; + dateComp.onChange(event); + + const monthElement = dateFixture.debugElement.query(By.css(`#${dateComp.model.id}_month`)); + const dayElement = dateFixture.debugElement.query(By.css(`#${dateComp.model.id}_day`)); + + dayElement.nativeElement.focus(); + dateFixture.detectChanges(); + + expect(document.activeElement).toBe(dayElement.nativeElement); + + dateFixture.nativeElement.dispatchEvent(new KeyboardEvent('keydown', { key: 'shift.tab' })); + dateFixture.detectChanges(); + + tick(200); + dateFixture.detectChanges(); + + expect(document.activeElement).toBe(monthElement.nativeElement); + })); + + it('should move focus on year field when on month field and shift tab pressed', fakeAsync(() => { + const yearElement = dateFixture.debugElement.query(By.css(`#${dateComp.model.id}_year`)); + const monthElement = dateFixture.debugElement.query(By.css(`#${dateComp.model.id}_month`)); + + monthElement.nativeElement.focus(); + dateFixture.detectChanges(); + + expect(document.activeElement).toBe(monthElement.nativeElement); + + dateFixture.nativeElement.dispatchEvent(new KeyboardEvent('keydown', { key: 'shift.tab' })); + dateFixture.detectChanges(); + + tick(200); + dateFixture.detectChanges(); + + expect(document.activeElement).toBe(yearElement.nativeElement); + })); + }); }); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.ts index 8d5ce5b48ee..404e8514933 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.ts @@ -1,5 +1,5 @@ -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { UntypedFormGroup } from '@angular/forms'; +import { Component, EventEmitter, HostListener, Inject, Input, OnInit, Output, Renderer2 } from '@angular/core'; import { DynamicDsDatePickerModel } from './date-picker.model'; import { hasValue } from '../../../../../empty.util'; import { @@ -7,6 +7,11 @@ import { DynamicFormLayoutService, DynamicFormValidationService } from '@ng-dynamic-forms/core'; +import { DOCUMENT } from '@angular/common'; +import isEqual from 'lodash/isEqual'; + + +export type DatePickerFieldType = '_year' | '_month' | '_day'; export const DS_DATE_PICKER_SEPARATOR = '-'; @@ -50,8 +55,12 @@ export class DsDatePickerComponent extends DynamicFormControlComponent implement disabledMonth = true; disabledDay = true; + private readonly fields: DatePickerFieldType[] = ['_year', '_month', '_day']; + constructor(protected layoutService: DynamicFormLayoutService, - protected validationService: DynamicFormValidationService + protected validationService: DynamicFormValidationService, + private renderer: Renderer2, + @Inject(DOCUMENT) private _document: Document ) { super(layoutService, validationService); } @@ -80,9 +89,8 @@ export class DsDatePickerComponent extends DynamicFormControlComponent implement } } - this.maxYear = this.initialYear + 100; - - } + this.maxYear = now.getUTCFullYear() + 100; + } onBlur(event) { this.blur.emit(); @@ -166,6 +174,67 @@ export class DsDatePickerComponent extends DynamicFormControlComponent implement this.change.emit(value); } + /** + * Listen to keydown Tab event. + * Get the active element and blur it, in order to focus the next input field. + */ + @HostListener('keydown.tab', ['$event']) + onTabKeydown(event: KeyboardEvent) { + event.preventDefault(); + const activeElement: Element = this._document.activeElement; + (activeElement as any).blur(); + const index = this.selectedFieldIndex(activeElement); + if (index < 0) { + return; + } + let fieldToFocusOn = index + 1; + if (fieldToFocusOn < this.fields.length) { + this.focusInput(this.fields[fieldToFocusOn]); + } + } + + @HostListener('keydown.shift.tab', ['$event']) + onShiftTabKeyDown(event: KeyboardEvent) { + event.preventDefault(); + const activeElement: Element = this._document.activeElement; + (activeElement as any).blur(); + const index = this.selectedFieldIndex(activeElement); + let fieldToFocusOn = index - 1; + if (fieldToFocusOn >= 0) { + this.focusInput(this.fields[fieldToFocusOn]); + } + } + + private selectedFieldIndex(activeElement: Element): number { + return this.fields.findIndex(field => isEqual(activeElement.id, this.model.id.concat(field))); + } + + /** + * Focus the input field for the given type + * based on the model id. + * Used to focus the next input field + * in case of a disabled field. + * @param type DatePickerFieldType + */ + focusInput(type: DatePickerFieldType) { + const field = this._document.getElementById(this.model.id.concat(type)); + if (field) { + + if (hasValue(this.year) && isEqual(type, '_year')) { + this.disabledMonth = true; + this.disabledDay = true; + } + if (hasValue(this.year) && isEqual(type, '_month')) { + this.disabledMonth = false; + } else if (hasValue(this.month) && isEqual(type, '_day')) { + this.disabledDay = false; + } + setTimeout(() => { + this.renderer.selectRootElement(field).focus(); + }, 100); + } + } + onFocus(event) { this.focus.emit(event); } diff --git a/src/app/shared/form/number-picker/number-picker.component.ts b/src/app/shared/form/number-picker/number-picker.component.ts index 82240c41d11..40562dd61c0 100644 --- a/src/app/shared/form/number-picker/number-picker.component.ts +++ b/src/app/shared/form/number-picker/number-picker.component.ts @@ -103,13 +103,12 @@ export class NumberPickerComponent implements OnInit, ControlValueAccessor { if (i >= this.min && i <= this.max) { this.value = i; - this.emitChange(); } else if (event.target.value === null || event.target.value === '') { this.value = null; - this.emitChange(); } else { this.value = undefined; } + this.emitChange(); } catch (e) { this.value = undefined; }