]> git.ipfire.org Git - thirdparty/paperless-ngx.git/commitdiff
Better keyboard nav for filter/edit dropdowns 3227/head
authorshamoon <4887959+shamoon@users.noreply.github.com>
Fri, 28 Apr 2023 06:51:09 +0000 (23:51 -0700)
committershamoon <4887959+shamoon@users.noreply.github.com>
Fri, 28 Apr 2023 06:54:43 +0000 (23:54 -0700)
src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.html
src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts

index 5bf75d62d4887d90b3c4eb28a62d861b6ed312d7..57197d1eadcf366cb99297bf6fc88d826f2d1b0b 100644 (file)
@@ -1,4 +1,4 @@
-<div class="btn-group w-100" ngbDropdown role="group" (openChange)="dropdownOpenChange($event)" #dropdown="ngbDropdown">
+<div class="btn-group w-100" ngbDropdown role="group" (openChange)="dropdownOpenChange($event)" #dropdown="ngbDropdown" (keydown)="listKeyDown($event)">
   <button class="btn btn-sm" id="dropdown_{{name}}" ngbDropdownToggle [ngClass]="!editing && selectionModel.selectionSize() > 0 ? 'btn-primary' : 'btn-outline-primary'" [disabled]="disabled">
     <svg class="toolbaricon" fill="currentColor">
       <use attr.xlink:href="assets/bootstrap-icons.svg#{{icon}}" />
           <input class="form-control" type="text" [(ngModel)]="filterText" [placeholder]="filterPlaceholder" (keyup.enter)="listFilterEnter()" #listFilterTextInput>
         </div>
       </div>
-      <div *ngIf="selectionModel.items" class="items">
-        <ng-container *ngFor="let item of selectionModel.itemsSorted | filter: filterText">
-          <app-toggleable-dropdown-button *ngIf="allowSelectNone || item.id" [item]="item" [state]="selectionModel.get(item.id)" [count]="getUpdatedDocumentCount(item.id)" (toggle)="selectionModel.toggle(item.id)" (exclude)="excludeClicked(item.id)" [disabled]="disabled"></app-toggleable-dropdown-button>
+      <div *ngIf="selectionModel.items" class="items" #buttonItems>
+        <ng-container *ngFor="let item of selectionModel.itemsSorted | filter: filterText; let i = index">
+          <app-toggleable-dropdown-button
+            *ngIf="allowSelectNone || item.id" [item]="item" [state]="selectionModel.get(item.id)" [count]="getUpdatedDocumentCount(item.id)" (toggle)="selectionModel.toggle(item.id)" (exclude)="excludeClicked(item.id)" (click)="setButtonItemIndex(i)" [disabled]="disabled">
+          </app-toggleable-dropdown-button>
         </ng-container>
       </div>
       <button *ngIf="editing" class="list-group-item list-group-item-action bg-light" (click)="applyClicked()" [disabled]="!modelIsDirty || disabled">
index 12de82693f9ccecb77fb4ccab22ce976976ddaaf..b324ac6a0add323b140817ecf66369d5c3e731d3 100644 (file)
@@ -324,6 +324,7 @@ export class FilterableDropdownSelectionModel {
 export class FilterableDropdownComponent {
   @ViewChild('listFilterTextInput') listFilterTextInput: ElementRef
   @ViewChild('dropdown') dropdown: NgbDropdown
+  @ViewChild('buttonItems') buttonItems: ElementRef
 
   filterText: string
 
@@ -416,14 +417,10 @@ export class FilterableDropdownComponent {
     return this.title ? this.title.replace(/\s/g, '_').toLowerCase() : null
   }
 
-  getUpdatedDocumentCount(id: number) {
-    if (this.documentCounts) {
-      return this.documentCounts.find((c) => c.id === id)?.document_count
-    }
-  }
-
   modelIsDirty: boolean = false
 
+  private keyboardIndex: number
+
   constructor(private filterPipe: FilterPipe) {
     this.selectionModelChange.subscribe((updatedModel) => {
       this.modelIsDirty = updatedModel.isDirty()
@@ -461,11 +458,13 @@ export class FilterableDropdownComponent {
     let filtered = this.filterPipe.transform(this.items, this.filterText)
     if (filtered.length == 1) {
       this.selectionModel.toggle(filtered[0].id)
-      if (this.editing) {
-        this.applyClicked()
-      } else {
-        this.dropdown.close()
-      }
+      setTimeout(() => {
+        if (this.editing) {
+          this.applyClicked()
+        } else {
+          this.dropdown.close()
+        }
+      }, 200)
     }
   }
 
@@ -481,4 +480,76 @@ export class FilterableDropdownComponent {
     this.selectionModel.reset(true)
     this.selectionModelChange.emit(this.selectionModel)
   }
+
+  getUpdatedDocumentCount(id: number) {
+    if (this.documentCounts) {
+      return this.documentCounts.find((c) => c.id === id)?.document_count
+    }
+  }
+
+  listKeyDown(event: KeyboardEvent) {
+    switch (event.key) {
+      case 'ArrowDown':
+        if (event.target instanceof HTMLInputElement) {
+          if (
+            !this.filterText ||
+            event.target.selectionStart === this.filterText.length
+          ) {
+            this.keyboardIndex = -1
+            this.focusNextButtonItem()
+            event.preventDefault()
+          }
+        } else if (event.target instanceof HTMLButtonElement) {
+          this.focusNextButtonItem()
+          event.preventDefault()
+        }
+        break
+      case 'ArrowUp':
+        if (event.target instanceof HTMLButtonElement) {
+          if (this.keyboardIndex === 0) {
+            this.listFilterTextInput.nativeElement.focus()
+          } else {
+            this.focusPreviousButtonItem()
+          }
+          event.preventDefault()
+        }
+        break
+      case 'Tab':
+        // just track the index in case user uses arrows
+        if (event.target instanceof HTMLInputElement) {
+          this.keyboardIndex = 0
+        } else if (event.target instanceof HTMLButtonElement) {
+          if (event.shiftKey) {
+            if (this.keyboardIndex > 0) {
+              this.focusPreviousButtonItem(false)
+            }
+          } else {
+            this.focusNextButtonItem(false)
+          }
+        }
+      default:
+        break
+    }
+  }
+
+  focusNextButtonItem(setFocus: boolean = true) {
+    this.keyboardIndex = Math.min(this.items.length - 1, this.keyboardIndex + 1)
+    if (setFocus) this.setButtonItemFocus()
+  }
+
+  focusPreviousButtonItem(setFocus: boolean = true) {
+    this.keyboardIndex = Math.max(0, this.keyboardIndex - 1)
+    if (setFocus) this.setButtonItemFocus()
+  }
+
+  setButtonItemFocus() {
+    this.buttonItems.nativeElement.children[
+      this.keyboardIndex
+    ]?.children[0].focus()
+  }
+
+  setButtonItemIndex(index: number) {
+    // just track the index in case user uses arrows
+    this.keyboardIndex = index
+  }
 }