mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-11-03 03:16:10 -06:00 
			
		
		
		
	Better keyboard nav for filter/edit dropdowns
This commit is contained in:
		@@ -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}}" />
 | 
			
		||||
@@ -31,9 +31,11 @@
 | 
			
		||||
          <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">
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
      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
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user