diff --git a/src/app/shared/dso-selector/dso-selector/dso-selector.component.html b/src/app/shared/dso-selector/dso-selector/dso-selector.component.html
index cfe8da849ff..d89f489ec8c 100644
--- a/src/app/shared/dso-selector/dso-selector/dso-selector.component.html
+++ b/src/app/shared/dso-selector/dso-selector/dso-selector.component.html
@@ -22,15 +22,17 @@
{{'dso-selector.no-results' | translate: { type: typesString } }}
}
- @for (listEntry of (listEntries$ | async); track listEntry) {
+ @for (listEntry of (listEntries$ | async); track listEntry; let i = $index) {
}
}
diff --git a/src/app/shared/dso-selector/dso-selector/dso-selector.component.spec.ts b/src/app/shared/dso-selector/dso-selector/dso-selector.component.spec.ts
index a2125859f07..88765d0da8a 100644
--- a/src/app/shared/dso-selector/dso-selector/dso-selector.component.spec.ts
+++ b/src/app/shared/dso-selector/dso-selector/dso-selector.component.spec.ts
@@ -1,6 +1,8 @@
import {
DebugElement,
+ ElementRef,
NO_ERRORS_SCHEMA,
+ QueryList,
} from '@angular/core';
import {
ComponentFixture,
@@ -110,6 +112,66 @@ describe('DSOSelectorComponent', () => {
expect(component).toBeTruthy();
});
+ describe('getDsoId', () => {
+ it('should return the id of the indexable object', () => {
+ expect(component.getDsoId(searchResult)).toEqual(searchResult.indexableObject.id);
+ });
+ });
+
+ describe('isCurrentDso', () => {
+ it('should return true when the list entry represents the current DSO', () => {
+ component.currentDSOId = searchResult.indexableObject.id;
+
+ expect(component.isCurrentDso(searchResult)).toBeTrue();
+ });
+
+ it('should return false when the list entry does not represent the current DSO', () => {
+ component.currentDSOId = 'another-id';
+
+ expect(component.isCurrentDso(searchResult)).toBeFalse();
+ });
+ });
+
+ describe('keyboard navigation', () => {
+ let firstElement: HTMLButtonElement;
+ let secondElement: HTMLButtonElement;
+
+ beforeEach(() => {
+ firstElement = document.createElement('button');
+ secondElement = document.createElement('button');
+
+ const listElements = new QueryList();
+ listElements.reset([
+ new ElementRef(firstElement),
+ new ElementRef(secondElement),
+ ]);
+
+ component.listElements = listElements;
+ });
+
+ it('should move focus to the next list entry when pressing ArrowDown', () => {
+ const event = new KeyboardEvent('keydown', { key: 'ArrowDown' });
+ spyOn(event, 'preventDefault');
+ spyOn(secondElement, 'focus');
+
+ component.onListEntryKeydown(event, 0);
+
+ expect(event.preventDefault).toHaveBeenCalled();
+ expect(secondElement.focus).toHaveBeenCalled();
+ });
+
+ it('should move focus to the previous list entry when pressing ArrowUp', () => {
+ const event = new KeyboardEvent('keydown', { key: 'ArrowUp' });
+ spyOn(event, 'preventDefault');
+ spyOn(firstElement, 'focus');
+
+ component.onListEntryKeydown(event, 1);
+
+ expect(event.preventDefault).toHaveBeenCalled();
+ expect(firstElement.focus).toHaveBeenCalled();
+ });
+ });
+
describe('populating listEntries', () => {
it('should not be empty', (done) => {
component.listEntries$.subscribe((listEntries) => {
diff --git a/src/app/shared/dso-selector/dso-selector/dso-selector.component.ts b/src/app/shared/dso-selector/dso-selector/dso-selector.component.ts
index a3388f499b8..e7436ba4313 100644
--- a/src/app/shared/dso-selector/dso-selector/dso-selector.component.ts
+++ b/src/app/shared/dso-selector/dso-selector/dso-selector.component.ts
@@ -1,6 +1,6 @@
import {
AsyncPipe,
- NgClass,
+ NgClass
} from '@angular/common';
import {
Component,
@@ -335,6 +335,56 @@ export class DSOSelectorComponent implements OnInit, OnDestroy {
}
}
+ /**
+ * Get the DSO id from the given listable object
+ * @param listableObject The {@link ListableObject} to evaluate
+ */
+ getDsoId(listableObject: ListableObject): string {
+ const searchResult = listableObject as SearchResult;
+ return hasValue(searchResult.indexableObject) ? searchResult.indexableObject.id : null;
+ }
+
+ /**
+ * Check if the given list entry represents the currently selected DSO
+ * @param listableObject The {@link ListableObject} to evaluate
+ */
+ isCurrentDso(listableObject: ListableObject): boolean {
+ return this.getDsoId(listableObject) === this.currentDSOId;
+ }
+
+ /**
+ * Handles keyboard navigation between list entries
+ * @param event The keyboard event
+ * @param index The index of the current list entry
+ */
+ onListEntryKeydown(event: KeyboardEvent, index: number): void {
+ switch (event.key) {
+ case 'ArrowDown':
+ event.preventDefault();
+ this.focusListEntry(index + 1);
+ break;
+ case 'ArrowUp':
+ event.preventDefault();
+ this.focusListEntry(index - 1);
+ break;
+ }
+ }
+
+ /**
+ * Set focus on the list entry at the given index
+ * @param index The index of the list entry to focus
+ */
+ private focusListEntry(index: number): void {
+ if (this.listElements.length > 0) {
+ const boundedIndex = Math.max(0, Math.min(index, this.listElements.length - 1));
+ const element = this.listElements.get(boundedIndex);
+
+ if (hasValue(element)) {
+ element.nativeElement.focus();
+ }
+ }
+ }
+
/**
* Unsubscribe from all subscriptions
*/