mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-30 03:56:23 -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:
		| @@ -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) | ||||
|       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 | ||||
|   } | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 shamoon
					shamoon