Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Port dspace-7_x] Use keyboard to select values #2764

Merged
merged 1 commit into from
Jan 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
(keydown)="selectOnKeyDown($event, sdRef)">
</div>

<div ngbDropdownMenu
<div #dropdownMenu ngbDropdownMenu
class="dropdown-menu scrollable-dropdown-menu w-100"
[attr.aria-label]="model.placeholder">
<div class="scrollable-menu"
Expand All @@ -41,7 +41,8 @@
[scrollWindow]="false">

<button class="dropdown-item disabled" *ngIf="optionsList && optionsList.length == 0">{{'form.no-results' | translate}}</button>
<button class="dropdown-item collection-item text-truncate" *ngFor="let listEntry of optionsList"
<button class="dropdown-item collection-item text-truncate" *ngFor="let listEntry of optionsList; let i = index"
[class.active]="i === selectedIndex"
(keydown.enter)="onSelect(listEntry); sdRef.close()" (mousedown)="onSelect(listEntry); sdRef.close()"
title="{{ listEntry.display }}" role="option"
[attr.id]="listEntry.display == (currentValue|async) ? ('combobox_' + id + '_selected') : null">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import {
ChangeDetectorRef,
Component,
EventEmitter,
Input,
OnInit,
Output,
ViewChild,
ElementRef
} from '@angular/core';
import { UntypedFormGroup } from '@angular/forms';

import { Observable, of as observableOf } from 'rxjs';
import { catchError, distinctUntilChanged, map, tap } from 'rxjs/operators';
import { catchError, map, tap } from 'rxjs/operators';
import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap';
import { DynamicFormLayoutService, DynamicFormValidationService } from '@ng-dynamic-forms/core';

Expand All @@ -28,6 +37,8 @@
templateUrl: './dynamic-scrollable-dropdown.component.html'
})
export class DsDynamicScrollableDropdownComponent extends DsDynamicVocabularyComponent implements OnInit {
@ViewChild('dropdownMenu', { read: ElementRef }) dropdownMenu: ElementRef;

@Input() bindId = true;
@Input() group: UntypedFormGroup;
@Input() model: DynamicScrollableDropdownModel;
Expand All @@ -40,6 +51,9 @@
public loading = false;
public pageInfo: PageInfo;
public optionsList: any;
public inputText: string = null;
public selectedIndex = 0;
public acceptableKeys = ['Space', 'NumpadMultiply', 'NumpadAdd', 'NumpadSubtract', 'NumpadDecimal', 'Semicolon', 'Equal', 'Comma', 'Minus', 'Period', 'Quote', 'Backquote'];

constructor(protected vocabularyService: VocabularyService,
protected cdr: ChangeDetectorRef,
Expand All @@ -54,32 +68,26 @@
*/
ngOnInit() {
this.updatePageInfo(this.model.maxOptions, 1);
this.vocabularyService.getVocabularyEntries(this.model.vocabularyOptions, this.pageInfo).pipe(
this.loadOptions();
}

loadOptions() {
this.loading = true;
this.vocabularyService.getVocabularyEntriesByValue(this.inputText, false, this.model.vocabularyOptions, this.pageInfo).pipe(
getFirstSucceededRemoteDataPayload(),
catchError(() => observableOf(buildPaginatedList(
new PageInfo(),
[]
))
))
.subscribe((list: PaginatedList<VocabularyEntry>) => {
this.optionsList = list.page;
if (this.model.value) {
this.setCurrentValue(this.model.value, true);
}

this.updatePageInfo(
list.pageInfo.elementsPerPage,
list.pageInfo.currentPage,
list.pageInfo.totalElements,
list.pageInfo.totalPages
);
this.cdr.detectChanges();
});

this.group.get(this.model.id).valueChanges.pipe(distinctUntilChanged())
.subscribe((value) => {
this.setCurrentValue(value);
});
catchError(() => observableOf(buildPaginatedList(new PageInfo(), []))),

Check warning on line 78 in src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts

View check run for this annotation

Codecov / codecov/patch

src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts#L78

Added line #L78 was not covered by tests
tap(() => this.loading = false)
).subscribe((list: PaginatedList<VocabularyEntry>) => {
this.optionsList = list.page;
this.updatePageInfo(
list.pageInfo.elementsPerPage,
list.pageInfo.currentPage,
list.pageInfo.totalElements,
list.pageInfo.totalPages
);
this.selectedIndex = 0;
this.cdr.detectChanges();
});
}

/**
Expand All @@ -94,10 +102,30 @@
openDropdown(sdRef: NgbDropdown) {
if (!this.model.readOnly) {
this.group.markAsUntouched();
this.inputText = null;
this.updatePageInfo(this.model.maxOptions, 1);
this.loadOptions();
sdRef.open();
}
}

navigateDropdown(event: KeyboardEvent) {
if (event.key === 'ArrowDown') {
this.selectedIndex = Math.min(this.selectedIndex + 1, this.optionsList.length - 1);

Check warning on line 114 in src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts

View check run for this annotation

Codecov / codecov/patch

src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts#L114

Added line #L114 was not covered by tests
} else if (event.key === 'ArrowUp') {
this.selectedIndex = Math.max(this.selectedIndex - 1, 0);

Check warning on line 116 in src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts

View check run for this annotation

Codecov / codecov/patch

src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts#L116

Added line #L116 was not covered by tests
}
this.scrollToSelected();

Check warning on line 118 in src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts

View check run for this annotation

Codecov / codecov/patch

src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts#L118

Added line #L118 was not covered by tests
}

scrollToSelected() {
const dropdownItems = this.dropdownMenu.nativeElement.querySelectorAll('.dropdown-item');
const selectedItem = dropdownItems[this.selectedIndex];

Check warning on line 123 in src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts

View check run for this annotation

Codecov / codecov/patch

src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts#L122-L123

Added lines #L122 - L123 were not covered by tests
if (selectedItem) {
selectedItem.scrollIntoView({ block: 'nearest' });

Check warning on line 125 in src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts

View check run for this annotation

Codecov / codecov/patch

src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts#L125

Added line #L125 was not covered by tests
}
}

/**
* KeyDown handler to allow toggling the dropdown via keyboard
* @param event KeyboardEvent
Expand All @@ -106,13 +134,54 @@
selectOnKeyDown(event: KeyboardEvent, sdRef: NgbDropdown) {
const keyName = event.key;

if (keyName === ' ' || keyName === 'Enter') {
if (keyName === 'Enter') {
event.preventDefault();
event.stopPropagation();
sdRef.toggle();
if (sdRef.isOpen()) {
this.onSelect(this.optionsList[this.selectedIndex]);
sdRef.close();

Check warning on line 142 in src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts

View check run for this annotation

Codecov / codecov/patch

src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts#L141-L142

Added lines #L141 - L142 were not covered by tests
} else {
sdRef.open();

Check warning on line 144 in src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts

View check run for this annotation

Codecov / codecov/patch

src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts#L144

Added line #L144 was not covered by tests
}
} else if (keyName === 'ArrowDown' || keyName === 'ArrowUp') {
this.openDropdown(sdRef);
event.preventDefault();
event.stopPropagation();
this.navigateDropdown(event);

Check warning on line 149 in src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts

View check run for this annotation

Codecov / codecov/patch

src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts#L147-L149

Added lines #L147 - L149 were not covered by tests
} else if (keyName === 'Backspace') {
this.removeKeyFromInput();

Check warning on line 151 in src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts

View check run for this annotation

Codecov / codecov/patch

src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts#L151

Added line #L151 was not covered by tests
} else if (this.isAcceptableKey(keyName)) {
this.addKeyToInput(keyName);

Check warning on line 153 in src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts

View check run for this annotation

Codecov / codecov/patch

src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts#L153

Added line #L153 was not covered by tests
}
}

addKeyToInput(keyName: string) {
if (this.inputText === null) {
this.inputText = '';

Check warning on line 159 in src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts

View check run for this annotation

Codecov / codecov/patch

src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts#L159

Added line #L159 was not covered by tests
}
this.inputText += keyName;

Check warning on line 161 in src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts

View check run for this annotation

Codecov / codecov/patch

src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts#L161

Added line #L161 was not covered by tests
// When a new key is added, we need to reset the page info
this.updatePageInfo(this.model.maxOptions, 1);
this.loadOptions();

Check warning on line 164 in src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts

View check run for this annotation

Codecov / codecov/patch

src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts#L163-L164

Added lines #L163 - L164 were not covered by tests
}

removeKeyFromInput() {
if (this.inputText !== null) {
this.inputText = this.inputText.slice(0, -1);

Check warning on line 169 in src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts

View check run for this annotation

Codecov / codecov/patch

src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts#L169

Added line #L169 was not covered by tests
if (this.inputText === '') {
this.inputText = null;

Check warning on line 171 in src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts

View check run for this annotation

Codecov / codecov/patch

src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts#L171

Added line #L171 was not covered by tests
}
this.loadOptions();

Check warning on line 173 in src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts

View check run for this annotation

Codecov / codecov/patch

src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts#L173

Added line #L173 was not covered by tests
}
}


isAcceptableKey(keyPress: string): boolean {
// allow all letters and numbers
if (keyPress.length === 1 && keyPress.match(/^[a-zA-Z0-9]*$/)) {
return true;

Check warning on line 181 in src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts

View check run for this annotation

Codecov / codecov/patch

src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts#L181

Added line #L181 was not covered by tests
}
// Some other characters like space, dash, etc should be allowed as well
return this.acceptableKeys.includes(keyPress);

Check warning on line 184 in src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts

View check run for this annotation

Codecov / codecov/patch

src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts#L184

Added line #L184 was not covered by tests
}

/**
Expand All @@ -127,7 +196,7 @@
this.pageInfo.totalElements,
this.pageInfo.totalPages
);
this.vocabularyService.getVocabularyEntries(this.model.vocabularyOptions, this.pageInfo).pipe(
this.vocabularyService.getVocabularyEntriesByValue(this.inputText, false, this.model.vocabularyOptions, this.pageInfo).pipe(
getFirstSucceededRemoteDataPayload(),
catchError(() => observableOf(buildPaginatedList(
new PageInfo(),
Expand Down
Loading