mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-11-04 03:27:12 -05:00 
			
		
		
		
	Merge pull request #3227 from paperless-ngx/feature/better-keyboard-dropdowns
Enhancement: better keyboard nav for filter/edit dropdowns
This commit is contained in:
		
						commit
						a8e12409b5
					
				@ -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">
 | 
					  <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">
 | 
					    <svg class="toolbaricon" fill="currentColor">
 | 
				
			||||||
      <use attr.xlink:href="assets/bootstrap-icons.svg#{{icon}}" />
 | 
					      <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>
 | 
					          <input class="form-control" type="text" [(ngModel)]="filterText" [placeholder]="filterPlaceholder" (keyup.enter)="listFilterEnter()" #listFilterTextInput>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      <div *ngIf="selectionModel.items" class="items">
 | 
					      <div *ngIf="selectionModel.items" class="items" #buttonItems>
 | 
				
			||||||
        <ng-container *ngFor="let item of selectionModel.itemsSorted | filter: filterText">
 | 
					        <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)" [disabled]="disabled"></app-toggleable-dropdown-button>
 | 
					          <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>
 | 
					        </ng-container>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      <button *ngIf="editing" class="list-group-item list-group-item-action bg-light" (click)="applyClicked()" [disabled]="!modelIsDirty || disabled">
 | 
					      <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 {
 | 
					export class FilterableDropdownComponent {
 | 
				
			||||||
  @ViewChild('listFilterTextInput') listFilterTextInput: ElementRef
 | 
					  @ViewChild('listFilterTextInput') listFilterTextInput: ElementRef
 | 
				
			||||||
  @ViewChild('dropdown') dropdown: NgbDropdown
 | 
					  @ViewChild('dropdown') dropdown: NgbDropdown
 | 
				
			||||||
 | 
					  @ViewChild('buttonItems') buttonItems: ElementRef
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  filterText: string
 | 
					  filterText: string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -416,14 +417,10 @@ export class FilterableDropdownComponent {
 | 
				
			|||||||
    return this.title ? this.title.replace(/\s/g, '_').toLowerCase() : null
 | 
					    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
 | 
					  modelIsDirty: boolean = false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private keyboardIndex: number
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  constructor(private filterPipe: FilterPipe) {
 | 
					  constructor(private filterPipe: FilterPipe) {
 | 
				
			||||||
    this.selectionModelChange.subscribe((updatedModel) => {
 | 
					    this.selectionModelChange.subscribe((updatedModel) => {
 | 
				
			||||||
      this.modelIsDirty = updatedModel.isDirty()
 | 
					      this.modelIsDirty = updatedModel.isDirty()
 | 
				
			||||||
@ -461,11 +458,13 @@ export class FilterableDropdownComponent {
 | 
				
			|||||||
    let filtered = this.filterPipe.transform(this.items, this.filterText)
 | 
					    let filtered = this.filterPipe.transform(this.items, this.filterText)
 | 
				
			||||||
    if (filtered.length == 1) {
 | 
					    if (filtered.length == 1) {
 | 
				
			||||||
      this.selectionModel.toggle(filtered[0].id)
 | 
					      this.selectionModel.toggle(filtered[0].id)
 | 
				
			||||||
 | 
					      setTimeout(() => {
 | 
				
			||||||
        if (this.editing) {
 | 
					        if (this.editing) {
 | 
				
			||||||
          this.applyClicked()
 | 
					          this.applyClicked()
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
          this.dropdown.close()
 | 
					          this.dropdown.close()
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					      }, 200)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -481,4 +480,76 @@ export class FilterableDropdownComponent {
 | 
				
			|||||||
    this.selectionModel.reset(true)
 | 
					    this.selectionModel.reset(true)
 | 
				
			||||||
    this.selectionModelChange.emit(this.selectionModel)
 | 
					    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
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user