Skip to content

Commit

Permalink
Merged in DSC-106 (pull request DSpace#643)
Browse files Browse the repository at this point in the history
[DSC-106] Date input usable via keyboard using tab

Approved-by: Vincenzo Mecca
  • Loading branch information
alisaismailati authored and vins01-4science committed Oct 12, 2023
2 parents fcbc5e8 + c5b67d7 commit b9d9fed
Show file tree
Hide file tree
Showing 2 changed files with 178 additions and 5 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// 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 { ChangeDetectorRef, Component, CUSTOM_ELEMENTS_SCHEMA, Renderer2 } from '@angular/core';
import { ComponentFixture, fakeAsync, inject, TestBed, tick, waitForAsync, } from '@angular/core/testing';
import { FormControl, FormGroup } from '@angular/forms';

import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
Expand All @@ -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 FormGroup({
Expand All @@ -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(() => {

Expand All @@ -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]
});
Expand Down Expand Up @@ -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);
}));

});
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { Component, EventEmitter, HostListener, Inject, Input, OnInit, Output, Renderer2 } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { DynamicDsDatePickerModel } from './date-picker.model';
import { hasValue } from '../../../../../empty.util';
Expand All @@ -7,8 +7,12 @@ 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 = '-';

@Component({
Expand Down Expand Up @@ -50,8 +54,13 @@ 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);
}
Expand Down Expand Up @@ -164,6 +173,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);
}
Expand Down

0 comments on commit b9d9fed

Please sign in to comment.