diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.html index 43fdf7733e7..320819ab56d 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.html @@ -51,7 +51,7 @@ [resultTemplate]="rt" [type]="model.inputType" [(ngModel)]="currentValue" - (blur)="onBlur($event)" + (blur)="onBlur($event, false)" (focus)="onFocus($event)" (change)="onChange($event)" (input)="onInput($event)" @@ -79,6 +79,7 @@ [disabled]="model.readOnly" [type]="model.inputType" [value]="currentValue?.display" + (blur)="onBlur($event, true)" (focus)="onFocus($event)" (change)="onChange($event)" (click)="openTree($event)" diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.spec.ts index bf2723eda54..96133aa3d9b 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.spec.ts @@ -90,7 +90,7 @@ function init() { }; } -describe('DsDynamicOneboxComponent test suite', () => { +describe('DsDynamicOneboxComponent', () => { let scheduler: TestScheduler; let testComp: TestComponent; @@ -262,7 +262,9 @@ describe('DsDynamicOneboxComponent test suite', () => { it('should emit blur Event onBlur when popup is closed', () => { spyOn(oneboxComponent.blur, 'emit'); spyOn(oneboxComponent.instance, 'isPopupOpen').and.returnValue(false); - oneboxComponent.onBlur(new Event('blur')); + + oneboxCompFixture.debugElement.query(By.css('input')).triggerEventHandler('blur', new Event('blur')); + expect(oneboxComponent.blur.emit).toHaveBeenCalled(); }); @@ -281,7 +283,9 @@ describe('DsDynamicOneboxComponent test suite', () => { spyOn(oneboxComponent.blur, 'emit'); spyOn(oneboxComponent.change, 'emit'); spyOn(oneboxComponent.instance, 'isPopupOpen').and.returnValue(false); - oneboxComponent.onBlur(new Event('blur')); + + oneboxCompFixture.debugElement.query(By.css('input')).triggerEventHandler('blur', new Event('blur')); + expect(oneboxComponent.change.emit).toHaveBeenCalled(); expect(oneboxComponent.blur.emit).toHaveBeenCalled(); }); @@ -294,7 +298,9 @@ describe('DsDynamicOneboxComponent test suite', () => { spyOn(oneboxComponent.blur, 'emit'); spyOn(oneboxComponent.change, 'emit'); spyOn(oneboxComponent.instance, 'isPopupOpen').and.returnValue(false); - oneboxComponent.onBlur(new Event('blur')); + + oneboxCompFixture.debugElement.query(By.css('input')).triggerEventHandler('blur', new Event('blur')); + expect(oneboxComponent.change.emit).not.toHaveBeenCalled(); expect(oneboxComponent.blur.emit).toHaveBeenCalled(); }); @@ -307,7 +313,9 @@ describe('DsDynamicOneboxComponent test suite', () => { spyOn(oneboxComponent.blur, 'emit'); spyOn(oneboxComponent.change, 'emit'); spyOn(oneboxComponent.instance, 'isPopupOpen').and.returnValue(false); - oneboxComponent.onBlur(new Event('blur')); + + oneboxCompFixture.debugElement.query(By.css('input')).triggerEventHandler('blur', new Event('blur')); + expect(oneboxComponent.change.emit).not.toHaveBeenCalled(); expect(oneboxComponent.blur.emit).toHaveBeenCalled(); }); @@ -429,6 +437,29 @@ describe('DsDynamicOneboxComponent test suite', () => { expect((oneboxComponent as any).modalService.open).toHaveBeenCalled(); done(); }); + + it('should emit the blur event when the popup is closed', () => { + spyOn(oneboxComponent.blur, 'emit'); + + oneboxComponent.vocabularyTreeOpen = false; + oneboxCompFixture.debugElement.query(By.css('input')).triggerEventHandler('blur', new Event('blur')); + + expect(oneboxComponent.blur.emit).toHaveBeenCalled(); + }); + + it('should not emit the blur event when the popup is open', fakeAsync(() => { + spyOn(oneboxComponent.blur, 'emit'); + spyOn(oneboxComponent, 'onBlur').and.callThrough(); + + scheduler.schedule(() => oneboxComponent.openTree(new Event('click'))); + scheduler.flush(); + expect(oneboxComponent.vocabularyTreeOpen).toBeTrue(); + + oneboxCompFixture.debugElement.query(By.css('input')).triggerEventHandler('blur', new Event('blur')); + + expect(oneboxComponent.onBlur).toHaveBeenCalled(); + expect(oneboxComponent.blur.emit).not.toHaveBeenCalled(); + })); }); describe('when init model value is not empty', () => { @@ -464,6 +495,28 @@ describe('DsDynamicOneboxComponent test suite', () => { expect((oneboxComponent as any).modalService.open).toHaveBeenCalled(); done(); }); + + it('should emit the blur event when the popup is closed', () => { + spyOn(oneboxComponent.blur, 'emit'); + + oneboxCompFixture.debugElement.query(By.css('input')).triggerEventHandler('blur', new Event('blur')); + + expect(oneboxComponent.blur.emit).toHaveBeenCalled(); + }); + + it('should not emit the blur event when the popup is open', fakeAsync(() => { + spyOn(oneboxComponent.blur, 'emit'); + spyOn(oneboxComponent, 'onBlur').and.callThrough(); + + scheduler.schedule(() => oneboxComponent.openTree(new Event('click'))); + scheduler.flush(); + expect(oneboxComponent.vocabularyTreeOpen).toBeTrue(); + + oneboxCompFixture.debugElement.query(By.css('input')).triggerEventHandler('blur', new Event('blur')); + + expect(oneboxComponent.onBlur).toHaveBeenCalled(); + expect(oneboxComponent.blur.emit).not.toHaveBeenCalled(); + })); }); }); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.ts index 5b2cfd01a8b..320f7402c4f 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.ts @@ -112,6 +112,12 @@ export class DsDynamicOneboxComponent extends DsDynamicVocabularyComponent imple inputValue: any; preloadLevel: number; + /** + * Whether the controlled vocabulary tree popup modal is open. We use this to know whether the loss of focus was + * caused by opening the modal or if it was caused by the user. + */ + vocabularyTreeOpen = false; + private vocabulary$: Observable; private isHierarchicalVocabulary$: Observable; private subs: Subscription[] = []; @@ -230,26 +236,33 @@ export class DsDynamicOneboxComponent extends DsDynamicVocabularyComponent imple /** * Emits a blur event containing a given value. * @param event The value to emit. + * @param hierarchical Whether this event was emitted from a hierarchical vocabulary field */ - onBlur(event: Event) { - if (!this.instance.isPopupOpen()) { - if (!this.model.vocabularyOptions.closed && isNotEmpty(this.inputValue)) { - if (isNotNull(this.inputValue) && this.model.value !== this.inputValue) { - this.dispatchUpdate(this.inputValue); - } - this.inputValue = null; + onBlur(event: Event, hierarchical: boolean = false): void { + if (hierarchical) { + if (!this.vocabularyTreeOpen) { + super.onBlur(event); } - this.blur.emit(event); } else { - // prevent on blur propagation if typeahed suggestions are showed - event.preventDefault(); - event.stopImmediatePropagation(); - // update the value with the searched text if the user hasn't selected any suggestion - if (!this.model.vocabularyOptions.closed && isNotEmpty(this.inputValue)) { - if (isNotNull(this.inputValue) && this.model.value !== this.inputValue) { - this.dispatchUpdate(this.inputValue); + if (!this.instance.isPopupOpen()) { + if (!this.model.vocabularyOptions.closed && isNotEmpty(this.inputValue)) { + if (isNotNull(this.inputValue) && this.model.value !== this.inputValue) { + this.dispatchUpdate(this.inputValue); + } + this.inputValue = null; + } + super.onBlur(event); + } else { + // prevent on blur propagation if typeahed suggestions are showed + event.preventDefault(); + event.stopImmediatePropagation(); + // update the value with the searched text if the user hasn't selected any suggestion + if (!this.model.vocabularyOptions.closed && isNotEmpty(this.inputValue)) { + if (isNotNull(this.inputValue) && this.model.value !== this.inputValue) { + this.dispatchUpdate(this.inputValue); + } + this.inputValue = null; } - this.inputValue = null; } } } @@ -290,6 +303,7 @@ export class DsDynamicOneboxComponent extends DsDynamicVocabularyComponent imple take(1), ).subscribe((preloadLevel) => { const modalRef: NgbModalRef = this.modalService.open(VocabularyTreeviewModalComponent, { size: 'lg', windowClass: 'treeview' }); + this.vocabularyTreeOpen = true; modalRef.componentInstance.vocabularyOptions = this.model.vocabularyOptions; modalRef.componentInstance.preloadLevel = preloadLevel; modalRef.componentInstance.selectedItems = this.currentValue ? [this.currentValue] : []; @@ -298,8 +312,9 @@ export class DsDynamicOneboxComponent extends DsDynamicVocabularyComponent imple this.currentValue = result; this.dispatchUpdate(result); } + this.vocabularyTreeOpen = false; }, () => { - return; + this.vocabularyTreeOpen = false; }); })); } diff --git a/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.html b/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.html index 1a35ed525c3..bdddc3d7cf9 100644 --- a/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.html +++ b/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.html @@ -74,7 +74,8 @@

container="body" (click)="onSelect(node.item)" role="button" - tabindex="0"> + tabindex="0" + type="button"> {{node.item.display}} } @@ -118,7 +119,8 @@

container="body" (click)="onSelect(node.item)" role="button" - tabindex="0"> + tabindex="0" + type="button"> {{node.item.display}} } @@ -126,14 +128,16 @@