Merged in DSC-106 (pull request #643)

[DSC-106] Date input usable via keyboard using tab

Approved-by: Vincenzo Mecca
(cherry picked from commit 543b4ad576)
This commit is contained in:
Alisa Ismailati
2023-10-12 16:12:32 +00:00
committed by github-actions[bot]
parent 701c6e36b0
commit e54723aa85
2 changed files with 178 additions and 5 deletions

View File

@@ -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<DsDatePickerComponent>;
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);
}));
});
});

View File

@@ -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);
}
@@ -166,6 +175,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);
}