diff --git a/e2e/integration/LinkingChildToSchool.cy.ts b/e2e/integration/LinkingChildToSchool.cy.ts
index e34cb8b00f..933341e1eb 100644
--- a/e2e/integration/LinkingChildToSchool.cy.ts
+++ b/e2e/integration/LinkingChildToSchool.cy.ts
@@ -25,7 +25,7 @@ describe("Scenario: Linking a child to a school - E2E test", function () {
// choose the school to add
cy.contains("mat-form-field", "School")
- .find("[matInput]")
+ .find("[matInput]:visible")
.type("E2E School{enter}");
// save school in child profile
diff --git a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.html b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.html
index 95d1138694..248a4fbf65 100644
--- a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.html
+++ b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.html
@@ -1,23 +1,40 @@
+
+
+
+
+
- {{ item.asString }}
+ {{
+ item.asString
+ }}
- Add
- option {{ inputElement.value }}
+ Add option
+ {{ inputElement.value }}
diff --git a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.spec.ts b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.spec.ts
index 33e7ad9c96..ba24cb64b9 100644
--- a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.spec.ts
+++ b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.spec.ts
@@ -15,7 +15,6 @@ import { NoopAnimationsModule } from "@angular/platform-browser/animations";
import { MatDialogModule } from "@angular/material/dialog";
import { TestbedHarnessEnvironment } from "@angular/cdk/testing/testbed";
import { HarnessLoader } from "@angular/cdk/testing";
-import { MatInputHarness } from "@angular/material/input/testing";
import { MatAutocompleteHarness } from "@angular/material/autocomplete/testing";
import {
FormControl,
@@ -24,6 +23,8 @@ import {
NgForm,
Validators,
} from "@angular/forms";
+import { genders } from "../../../child-dev-project/children/model/genders";
+import { ConfirmationDialogService } from "../../confirmation-dialog/confirmation-dialog.service";
describe("BasicAutocompleteComponent", () => {
let component: BasicAutocompleteComponent;
@@ -92,9 +93,7 @@ describe("BasicAutocompleteComponent", () => {
component.ngOnChanges({ value: true, options: true, valueMapper: true });
fixture.detectChanges();
- expect(component.autocompleteForm).toHaveValue("First Child");
- const inputElement = await loader.getHarness(MatInputHarness);
- await expectAsync(inputElement.getValue()).toBeResolvedTo("First Child");
+ expect(component.displayText).toBe("First Child");
});
it("should have the correct entity selected when it's name is entered", () => {
@@ -108,7 +107,7 @@ describe("BasicAutocompleteComponent", () => {
expect(component.value).toBe(child1.getId());
});
- it("should reset if nothing has been selected", fakeAsync(() => {
+ it("should reset if leaving empty autocomplete", fakeAsync(() => {
const first = Child.create("First");
const second = Child.create("Second");
component.options = [first, second];
@@ -117,7 +116,7 @@ describe("BasicAutocompleteComponent", () => {
component.select({ asValue: first.getId() } as any);
expect(component.value).toBe(first.getId());
- component.autocompleteForm.setValue("Non existent");
+ component.autocompleteForm.setValue("");
component.onFocusOut({} as any);
tick(200);
@@ -145,25 +144,30 @@ describe("BasicAutocompleteComponent", () => {
expect(options).toHaveSize(3);
await options[2].click();
+ // When browser is not in foreground, this doesn't happen automatically
+ component.autocomplete.openPanel();
+ fixture.detectChanges();
await options[1].click();
expect(component.value).toEqual([0, 2]);
});
- it("should clear the input when focusing in multi select mode", fakeAsync(() => {
+ it("should switch the input when focusing in multi select mode", fakeAsync(() => {
component.multi = true;
component.options = ["some", "values", "and", "other", "options"];
component.value = ["some", "values"];
component.ngOnChanges({ value: true, options: true });
- expect(component.autocompleteForm).toHaveValue("some, values");
+ expect(component.displayText).toBe("some, values");
- component.onFocusIn();
+ component.showAutocomplete();
expect(component.autocompleteForm).toHaveValue("");
+ expect(component.focused).toBeTrue();
component.onFocusOut({} as any);
tick(200);
- expect(component.autocompleteForm).toHaveValue("some, values");
+ expect(component.displayText).toBe("some, values");
+ expect(component.focused).toBeFalse();
}));
it("should update the error state if the form is invalid", () => {
@@ -178,4 +182,45 @@ describe("BasicAutocompleteComponent", () => {
expect(component.errorState).toBeTrue();
});
+
+ it("should create new option", fakeAsync(() => {
+ const newOption = "new option";
+ const confirmationSpy = spyOn(
+ TestBed.inject(ConfirmationDialogService),
+ "getConfirmation"
+ );
+ component.createOption = (id) => ({ id: id, label: id });
+ const createOptionEventSpy = spyOn(
+ component,
+ "createOption"
+ ).and.callThrough();
+ component.options = genders;
+ const initialValue = genders[0].id;
+ component.value = initialValue;
+ component.valueMapper = (o) => o.id;
+
+ component.ngOnChanges({ value: true, options: true, valueMapper: true });
+
+ component.showAutocomplete();
+ component.autocompleteForm.setValue(newOption);
+
+ // decline confirmation for new option
+ confirmationSpy.and.resolveTo(false);
+ component.select(newOption);
+
+ tick();
+ expect(confirmationSpy).toHaveBeenCalled();
+ expect(createOptionEventSpy).not.toHaveBeenCalled();
+ expect(component.value).toEqual(initialValue);
+
+ // confirm new option
+ confirmationSpy.calls.reset();
+ confirmationSpy.and.resolveTo(true);
+ component.select(newOption);
+
+ tick();
+ expect(confirmationSpy).toHaveBeenCalled();
+ expect(createOptionEventSpy).toHaveBeenCalledWith(newOption);
+ expect(component.value).toEqual(newOption);
+ }));
});
diff --git a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts
index a19a6fc7c2..0a7d7406a4 100644
--- a/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts
+++ b/src/app/core/configurable-enum/basic-autocomplete/basic-autocomplete.component.ts
@@ -23,13 +23,19 @@ import {
MatAutocompleteModule,
MatAutocompleteTrigger,
} from "@angular/material/autocomplete";
-import { concat, of, skip } from "rxjs";
import { MatCheckboxModule } from "@angular/material/checkbox";
-import { distinctUntilChanged, filter, map, startWith } from "rxjs/operators";
+import {
+ distinctUntilChanged,
+ filter,
+ map,
+ skip,
+ startWith,
+} from "rxjs/operators";
import { ConfirmationDialogService } from "../../confirmation-dialog/confirmation-dialog.service";
import { ErrorStateMatcher } from "@angular/material/core";
import { CustomFormControlDirective } from "./custom-form-control.directive";
import { coerceBooleanProperty } from "@angular/cdk/coercion";
+import { concat, of } from "rxjs";
interface SelectableOption {
initial: O;
@@ -82,7 +88,14 @@ export class BasicAutocompleteComponent
startWith([] as SelectableOption[])
);
showAddOption = false;
- private delayedBlur: any;
+
+ get displayText() {
+ const values: V[] = Array.isArray(this.value) ? this.value : [this.value];
+
+ return values
+ .map((v) => this._options.find((o) => o.asValue === v)?.asString)
+ .join(", ");
+ }
get disabled(): boolean {
return this._disabled;
@@ -136,10 +149,25 @@ export class BasicAutocompleteComponent
}
showAutocomplete() {
- this.autocompleteSuggestedOptions = concat(
- of(this._options),
- this.autocompleteSuggestedOptions.pipe(skip(1))
- );
+ if (this.multi) {
+ this.autocompleteForm.setValue("");
+ } else {
+ // cannot setValue to "" here because the current selection would be lost
+ this.autocompleteForm.setValue(this.displayText);
+ this.autocompleteSuggestedOptions = concat(
+ of(this._options),
+ this.autocompleteSuggestedOptions.pipe(skip(1))
+ );
+ }
+ setTimeout(() => {
+ this.inputElement.focus();
+
+ // select all text for easy overwriting when typing to search for options
+ (
+ this.inputElement._elementRef.nativeElement as HTMLInputElement
+ ).select();
+ });
+ this.focus();
}
private updateAutocomplete(inputText: string): SelectableOption[] {
@@ -160,24 +188,9 @@ export class BasicAutocompleteComponent
this._options.forEach(
(o) => (o.selected = (this.value as V[])?.includes(o.asValue))
);
- this.displaySelectedOptions();
- } else {
- const selected = this._options.find(
- ({ asValue }) => asValue === this.value
- );
- this.autocompleteForm.setValue(selected?.asString ?? "");
}
}
- private displaySelectedOptions() {
- this.autocompleteForm.setValue(
- this._options
- .filter((o) => o.selected)
- .map((o) => o.asString)
- .join(", ")
- );
- }
-
select(selected: string | SelectableOption) {
if (typeof selected === "string") {
this.createNewOption(selected);
@@ -202,6 +215,10 @@ export class BasicAutocompleteComponent
const newOption = this.toSelectableOption(this.createOption(option));
this._options.push(newOption);
this.select(newOption);
+ } else {
+ // continue editing
+ this.showAutocomplete();
+ this.autocompleteForm.setValue(option);
}
}
@@ -212,10 +229,8 @@ export class BasicAutocompleteComponent
.filter((o) => o.selected)
.map((o) => o.asValue);
// re-open autocomplete to select next option
- this.autocompleteForm.setValue("");
- setTimeout(() => this.autocomplete.openPanel(), 100);
+ this.showAutocomplete();
} else {
- this.autocompleteForm.setValue(option.asString);
this.value = option.asValue;
}
}
@@ -229,49 +244,23 @@ export class BasicAutocompleteComponent
};
}
- onFocusIn() {
- clearTimeout(this.delayedBlur);
- if (!this.focused) {
- if (this.multi) {
- this.autocompleteForm.setValue("");
- } else {
- this.showAutocomplete();
- }
- this.focus();
- }
- }
-
onFocusOut(event: FocusEvent) {
if (
!this.elementRef.nativeElement.contains(event.relatedTarget as Element)
) {
- // use short timeout in order for creating an option to work
- this.delayedBlur = setTimeout(() => this.notifyFocusOut(), 200);
- }
- }
-
- private notifyFocusOut() {
- if (this.multi) {
- this.displaySelectedOptions();
- } else {
- const inputValue = this.autocompleteForm.value;
- const selectedOption = this._options.find(
- ({ asValue }) => asValue === this._value
- );
- if (selectedOption?.asString !== inputValue) {
- // try to select the option that matches the input string
- const matchingOption = this._options.find(
- ({ asString }) => asString.toLowerCase() === inputValue.toLowerCase()
- );
- this.select(matchingOption);
+ if (!this.multi && this.autocompleteForm.value === "") {
+ this.select(undefined);
}
+ this.blur();
}
- this.blur();
}
onContainerClick(event: MouseEvent) {
- if ((event.target as Element).tagName.toLowerCase() != "input") {
- this.inputElement.focus();
+ if (
+ !this._disabled &&
+ (event.target as Element).tagName.toLowerCase() != "input"
+ ) {
+ this.showAutocomplete();
}
}