mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-11-03 03:16:10 -06:00 
			
		
		
		
	Enhancement: improve layout for custom fields dropdown (#6362)
This commit is contained in:
		
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -1,31 +1,27 @@
 | 
			
		||||
<div ngbDropdown #fieldDropdown="ngbDropdown" (openChange)="onOpenClose()">
 | 
			
		||||
<div ngbDropdown #fieldDropdown="ngbDropdown" (openChange)="onOpenClose($event)">
 | 
			
		||||
    <button class="btn btn-sm btn-outline-primary" id="customFieldsDropdown" [disabled]="disabled" ngbDropdownToggle>
 | 
			
		||||
      <i-bs name="ui-radios"></i-bs>
 | 
			
		||||
      <div class="d-none d-sm-inline"> <ng-container i18n>Custom Fields</ng-container></div>
 | 
			
		||||
    </button>
 | 
			
		||||
    <div ngbDropdownMenu aria-labelledby="customFieldsDropdown" class="shadow custom-fields-dropdown">
 | 
			
		||||
        <ul class="list-group list-group-flush">
 | 
			
		||||
            <li class="list-group-item">
 | 
			
		||||
                <pngx-input-select
 | 
			
		||||
                    [items]="unusedFields"
 | 
			
		||||
                    bindLabel="name"
 | 
			
		||||
                    [(ngModel)]="field"
 | 
			
		||||
                    [placeholder]="placeholderText"
 | 
			
		||||
                    [notFoundText]="notFoundText"
 | 
			
		||||
                    [disableCreateNew]="!canCreateFields"
 | 
			
		||||
                    (createNew)="createField($event)"
 | 
			
		||||
                    [hideAddButton]="true"
 | 
			
		||||
                    bindValue="id">
 | 
			
		||||
                </pngx-input-select>
 | 
			
		||||
                <div class="btn-toolbar" role="toolbar">
 | 
			
		||||
                    <button class="btn btn-sm btn-outline-secondary me-auto" type="button" (click)="createField()" [disabled]="!canCreateFields">
 | 
			
		||||
                        <i-bs width="1em" height="1em" name="asterisk"></i-bs> <ng-container i18n>Create New Field</ng-container>
 | 
			
		||||
        <div class="list-group list-group-flush" (keydown)="listKeyDown($event)">
 | 
			
		||||
            <div class="list-group-item">
 | 
			
		||||
                <div class="input-group input-group-sm">
 | 
			
		||||
                  <input class="form-control" type="text" [(ngModel)]="filterText" placeholder="Search fields" i18n-placeholder (keyup.enter)="listFilterEnter()" #listFilterTextInput>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            @for (field of filteredFields; track field.id) {
 | 
			
		||||
                <button class="list-group-item list-group-item-action bg-light" (click)="addField(field)" #button>
 | 
			
		||||
                    <small class="d-flex">{{field.name}} <small class="ms-auto text-muted">{{getDataTypeLabel(field.data_type)}}</small></small>
 | 
			
		||||
                </button>
 | 
			
		||||
                    <button class="btn btn-sm btn-outline-primary" type="button" (click)="addField(); fieldDropdown.close()" [disabled]="field === undefined">
 | 
			
		||||
                        <i-bs width="1.2em" height="1.2em" name="plus-circle"></i-bs> <ng-container i18n>Add to document</ng-container>
 | 
			
		||||
            }
 | 
			
		||||
            @if (!filterText?.length || filteredFields.length === 0) {
 | 
			
		||||
                <button class="list-group-item list-group-item-action bg-light" (click)="createField(filterText)" [disabled]="!canCreateFields" #button>
 | 
			
		||||
                    <small>
 | 
			
		||||
                        <i-bs width=".9em" height=".9em" name="asterisk"></i-bs> <ng-container i18n>Create new field</ng-container>
 | 
			
		||||
                    </small>
 | 
			
		||||
                </button>
 | 
			
		||||
                </div>
 | 
			
		||||
            </li>
 | 
			
		||||
        </ul>
 | 
			
		||||
            }
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
.custom-fields-dropdown {
 | 
			
		||||
    min-width: 380px;
 | 
			
		||||
    min-width: 300px;
 | 
			
		||||
 | 
			
		||||
    // correct position on mobile
 | 
			
		||||
    @media (max-width: 575.98px) {
 | 
			
		||||
@@ -8,13 +8,3 @@
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
::ng-deep .custom-fields-dropdown .ng-select .ng-select-container .ng-value-container .ng-placeholder,
 | 
			
		||||
::ng-deep .custom-fields-dropdown .ng-select .ng-option,
 | 
			
		||||
::ng-deep .custom-fields-dropdown .ng-select .ng-select-container .ng-value-container .ng-value {
 | 
			
		||||
    font-size: 0.875rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
::ng-deep .custom-fields-dropdown .paperless-input-select .ng-select .ng-select-container .ng-value-container .ng-input {
 | 
			
		||||
    top: 4px;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,9 @@
 | 
			
		||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  ComponentFixture,
 | 
			
		||||
  TestBed,
 | 
			
		||||
  fakeAsync,
 | 
			
		||||
  tick,
 | 
			
		||||
} from '@angular/core/testing'
 | 
			
		||||
import { CustomFieldsDropdownComponent } from './custom-fields-dropdown.component'
 | 
			
		||||
import { HttpClientTestingModule } from '@angular/common/http/testing'
 | 
			
		||||
import { ToastService } from 'src/app/services/toast.service'
 | 
			
		||||
@@ -71,28 +75,33 @@ describe('CustomFieldsDropdownComponent', () => {
 | 
			
		||||
    let addedField
 | 
			
		||||
    component.added.subscribe((f) => (addedField = f))
 | 
			
		||||
    component.documentId = 11
 | 
			
		||||
    component.field = fields[0].id
 | 
			
		||||
    component.addField()
 | 
			
		||||
    component.addField({ field: fields[0].id } as any)
 | 
			
		||||
    expect(addedField).not.toBeUndefined()
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should clear field on open / close, updated unused fields', () => {
 | 
			
		||||
    component.field = fields[1].id
 | 
			
		||||
    component.onOpenClose()
 | 
			
		||||
    expect(component.field).toBeUndefined()
 | 
			
		||||
 | 
			
		||||
    expect(component.unusedFields).toEqual(fields)
 | 
			
		||||
    const updateSpy = jest.spyOn(
 | 
			
		||||
      CustomFieldsDropdownComponent.prototype as any,
 | 
			
		||||
      'updateUnusedFields'
 | 
			
		||||
    )
 | 
			
		||||
    component.existingFields = [{ field: fields[1].id } as any]
 | 
			
		||||
    component.onOpenClose()
 | 
			
		||||
    expect(updateSpy).toHaveBeenCalled()
 | 
			
		||||
    expect(component.unusedFields).toEqual([fields[0]])
 | 
			
		||||
  it('should support filtering fields', () => {
 | 
			
		||||
    const input = fixture.debugElement.query(By.css('input'))
 | 
			
		||||
    input.nativeElement.value = 'Field 1'
 | 
			
		||||
    input.triggerEventHandler('input', { target: input.nativeElement })
 | 
			
		||||
    fixture.detectChanges()
 | 
			
		||||
    expect(component.filteredFields.length).toEqual(1)
 | 
			
		||||
    expect(component.filteredFields[0].name).toEqual('Field 1')
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should support creating field, show error if necessary', () => {
 | 
			
		||||
  it('should support update unused fields', () => {
 | 
			
		||||
    component.existingFields = [{ field: fields[0].id } as any]
 | 
			
		||||
    component['updateUnusedFields']()
 | 
			
		||||
    expect(component['unusedFields'].length).toEqual(1)
 | 
			
		||||
    expect(component['unusedFields'][0].name).toEqual('Field 2')
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should support getting data type label', () => {
 | 
			
		||||
    expect(component.getDataTypeLabel(CustomFieldDataType.Integer)).toEqual(
 | 
			
		||||
      'Integer'
 | 
			
		||||
    )
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should support creating field, show error if necessary, then add', fakeAsync(() => {
 | 
			
		||||
    let modal: NgbModalRef
 | 
			
		||||
    modalService.activeInstances.subscribe((m) => (modal = m[m.length - 1]))
 | 
			
		||||
    const toastErrorSpy = jest.spyOn(toastService, 'showError')
 | 
			
		||||
@@ -101,8 +110,9 @@ describe('CustomFieldsDropdownComponent', () => {
 | 
			
		||||
      CustomFieldsDropdownComponent.prototype as any,
 | 
			
		||||
      'getFields'
 | 
			
		||||
    )
 | 
			
		||||
    const addFieldSpy = jest.spyOn(component, 'addField')
 | 
			
		||||
 | 
			
		||||
    const createButton = fixture.debugElement.queryAll(By.css('button'))[1]
 | 
			
		||||
    const createButton = fixture.debugElement.queryAll(By.css('button'))[3]
 | 
			
		||||
    createButton.triggerEventHandler('click')
 | 
			
		||||
 | 
			
		||||
    expect(modal).not.toBeUndefined()
 | 
			
		||||
@@ -115,9 +125,11 @@ describe('CustomFieldsDropdownComponent', () => {
 | 
			
		||||
 | 
			
		||||
    // succeed
 | 
			
		||||
    editDialog.succeeded.emit(fields[0])
 | 
			
		||||
    tick(100)
 | 
			
		||||
    expect(toastInfoSpy).toHaveBeenCalled()
 | 
			
		||||
    expect(getFieldsSpy).toHaveBeenCalled()
 | 
			
		||||
  })
 | 
			
		||||
    expect(addFieldSpy).toHaveBeenCalled()
 | 
			
		||||
  }))
 | 
			
		||||
 | 
			
		||||
  it('should support creating field with name', () => {
 | 
			
		||||
    let modal: NgbModalRef
 | 
			
		||||
@@ -128,4 +140,106 @@ describe('CustomFieldsDropdownComponent', () => {
 | 
			
		||||
    const editDialog = modal.componentInstance as CustomFieldEditDialogComponent
 | 
			
		||||
    expect(editDialog.object.name).toEqual('Foo bar')
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should support arrow keyboard navigation', fakeAsync(() => {
 | 
			
		||||
    fixture.nativeElement
 | 
			
		||||
      .querySelector('button')
 | 
			
		||||
      .dispatchEvent(new MouseEvent('click')) // open
 | 
			
		||||
    fixture.detectChanges()
 | 
			
		||||
    tick(100)
 | 
			
		||||
    const filterInputEl: HTMLInputElement =
 | 
			
		||||
      component.listFilterTextInput.nativeElement
 | 
			
		||||
    expect(document.activeElement).toEqual(filterInputEl)
 | 
			
		||||
    const itemButtons = Array.from(
 | 
			
		||||
      (fixture.nativeElement as HTMLDivElement).querySelectorAll(
 | 
			
		||||
        '.custom-fields-dropdown button'
 | 
			
		||||
      )
 | 
			
		||||
    ).filter((b) => b.textContent.includes('Field'))
 | 
			
		||||
    filterInputEl.dispatchEvent(
 | 
			
		||||
      new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })
 | 
			
		||||
    )
 | 
			
		||||
    expect(document.activeElement).toEqual(itemButtons[0])
 | 
			
		||||
    itemButtons[0].dispatchEvent(
 | 
			
		||||
      new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })
 | 
			
		||||
    )
 | 
			
		||||
    expect(document.activeElement).toEqual(itemButtons[1])
 | 
			
		||||
    itemButtons[1].dispatchEvent(
 | 
			
		||||
      new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true })
 | 
			
		||||
    )
 | 
			
		||||
    expect(document.activeElement).toEqual(itemButtons[0])
 | 
			
		||||
    itemButtons[0].dispatchEvent(
 | 
			
		||||
      new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true })
 | 
			
		||||
    )
 | 
			
		||||
    expect(document.activeElement).toEqual(filterInputEl)
 | 
			
		||||
    filterInputEl.value = 'foo'
 | 
			
		||||
    component.filterText = 'foo'
 | 
			
		||||
 | 
			
		||||
    // dont move focus if we're traversing the field
 | 
			
		||||
    filterInputEl.selectionStart = 1
 | 
			
		||||
    expect(document.activeElement).toEqual(filterInputEl)
 | 
			
		||||
 | 
			
		||||
    // now we're at end, so move focus
 | 
			
		||||
    filterInputEl.selectionStart = 3
 | 
			
		||||
    filterInputEl.dispatchEvent(
 | 
			
		||||
      new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })
 | 
			
		||||
    )
 | 
			
		||||
    expect(document.activeElement).toEqual(itemButtons[0])
 | 
			
		||||
  }))
 | 
			
		||||
 | 
			
		||||
  it('should support arrow keyboard navigation after tab keyboard navigation', fakeAsync(() => {
 | 
			
		||||
    fixture.nativeElement
 | 
			
		||||
      .querySelector('button')
 | 
			
		||||
      .dispatchEvent(new MouseEvent('click')) // open
 | 
			
		||||
    fixture.detectChanges()
 | 
			
		||||
    tick(100)
 | 
			
		||||
    const filterInputEl: HTMLInputElement =
 | 
			
		||||
      component.listFilterTextInput.nativeElement
 | 
			
		||||
    expect(document.activeElement).toEqual(filterInputEl)
 | 
			
		||||
    const itemButtons = Array.from(
 | 
			
		||||
      (fixture.nativeElement as HTMLDivElement).querySelectorAll(
 | 
			
		||||
        '.custom-fields-dropdown button'
 | 
			
		||||
      )
 | 
			
		||||
    ).filter((b) => b.textContent.includes('Field'))
 | 
			
		||||
    filterInputEl.dispatchEvent(
 | 
			
		||||
      new KeyboardEvent('keydown', { key: 'Tab', bubbles: true })
 | 
			
		||||
    )
 | 
			
		||||
    itemButtons[0]['focus']() // normally handled by browser
 | 
			
		||||
    itemButtons[0].dispatchEvent(
 | 
			
		||||
      new KeyboardEvent('keydown', { key: 'Tab', bubbles: true })
 | 
			
		||||
    )
 | 
			
		||||
    itemButtons[1]['focus']() // normally handled by browser
 | 
			
		||||
    itemButtons[1].dispatchEvent(
 | 
			
		||||
      new KeyboardEvent('keydown', {
 | 
			
		||||
        key: 'Tab',
 | 
			
		||||
        shiftKey: true,
 | 
			
		||||
        bubbles: true,
 | 
			
		||||
      })
 | 
			
		||||
    )
 | 
			
		||||
    itemButtons[0]['focus']() // normally handled by browser
 | 
			
		||||
    itemButtons[0].dispatchEvent(
 | 
			
		||||
      new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })
 | 
			
		||||
    )
 | 
			
		||||
    expect(document.activeElement).toEqual(itemButtons[1])
 | 
			
		||||
  }))
 | 
			
		||||
 | 
			
		||||
  it('should support enter keyboard navigation', fakeAsync(() => {
 | 
			
		||||
    jest.spyOn(component, 'canCreateFields', 'get').mockReturnValue(true)
 | 
			
		||||
    const addFieldSpy = jest.spyOn(component, 'addField')
 | 
			
		||||
    const createFieldSpy = jest.spyOn(component, 'createField')
 | 
			
		||||
    component.filterText = 'Field 1'
 | 
			
		||||
    component.listFilterEnter()
 | 
			
		||||
    expect(addFieldSpy).toHaveBeenCalled()
 | 
			
		||||
 | 
			
		||||
    component.filterText = 'Field 3'
 | 
			
		||||
    component.listFilterEnter()
 | 
			
		||||
    expect(createFieldSpy).toHaveBeenCalledWith('Field 3')
 | 
			
		||||
 | 
			
		||||
    addFieldSpy.mockClear()
 | 
			
		||||
    createFieldSpy.mockClear()
 | 
			
		||||
 | 
			
		||||
    component.filterText = undefined
 | 
			
		||||
    component.listFilterEnter()
 | 
			
		||||
    expect(createFieldSpy).not.toHaveBeenCalled()
 | 
			
		||||
    expect(addFieldSpy).not.toHaveBeenCalled()
 | 
			
		||||
  }))
 | 
			
		||||
})
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +1,17 @@
 | 
			
		||||
import {
 | 
			
		||||
  Component,
 | 
			
		||||
  ElementRef,
 | 
			
		||||
  EventEmitter,
 | 
			
		||||
  Input,
 | 
			
		||||
  OnDestroy,
 | 
			
		||||
  Output,
 | 
			
		||||
  QueryList,
 | 
			
		||||
  ViewChild,
 | 
			
		||||
  ViewChildren,
 | 
			
		||||
} from '@angular/core'
 | 
			
		||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
 | 
			
		||||
import { Subject, first, takeUntil } from 'rxjs'
 | 
			
		||||
import { CustomField } from 'src/app/data/custom-field'
 | 
			
		||||
import { CustomField, DATA_TYPE_LABELS } from 'src/app/data/custom-field'
 | 
			
		||||
import { CustomFieldInstance } from 'src/app/data/custom-field-instance'
 | 
			
		||||
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
 | 
			
		||||
import { ToastService } from 'src/app/services/toast.service'
 | 
			
		||||
@@ -39,23 +43,25 @@ export class CustomFieldsDropdownComponent implements OnDestroy {
 | 
			
		||||
  @Output()
 | 
			
		||||
  created: EventEmitter<CustomField> = new EventEmitter()
 | 
			
		||||
 | 
			
		||||
  @ViewChild('listFilterTextInput') listFilterTextInput: ElementRef
 | 
			
		||||
  @ViewChildren('button') buttons: QueryList<ElementRef>
 | 
			
		||||
 | 
			
		||||
  private customFields: CustomField[] = []
 | 
			
		||||
  public unusedFields: CustomField[]
 | 
			
		||||
  private unusedFields: CustomField[] = []
 | 
			
		||||
  private keyboardIndex: number
 | 
			
		||||
 | 
			
		||||
  public name: string
 | 
			
		||||
  public get filteredFields(): CustomField[] {
 | 
			
		||||
    return this.unusedFields.filter(
 | 
			
		||||
      (f) =>
 | 
			
		||||
        !this.filterText ||
 | 
			
		||||
        f.name.toLowerCase().includes(this.filterText.toLowerCase())
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public field: number
 | 
			
		||||
  public filterText: string
 | 
			
		||||
 | 
			
		||||
  private unsubscribeNotifier: Subject<any> = new Subject()
 | 
			
		||||
 | 
			
		||||
  get placeholderText(): string {
 | 
			
		||||
    return $localize`Choose field`
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get notFoundText(): string {
 | 
			
		||||
    return $localize`No unused fields found`
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get canCreateFields(): boolean {
 | 
			
		||||
    return this.permissionsService.currentUserCan(
 | 
			
		||||
      PermissionAction.Add,
 | 
			
		||||
@@ -87,28 +93,26 @@ export class CustomFieldsDropdownComponent implements OnDestroy {
 | 
			
		||||
      })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public getCustomFieldFromInstance(
 | 
			
		||||
    instance: CustomFieldInstance
 | 
			
		||||
  ): CustomField {
 | 
			
		||||
    return this.customFields.find((f) => f.id === instance.field)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private updateUnusedFields() {
 | 
			
		||||
    this.unusedFields = this.customFields.filter(
 | 
			
		||||
      (f) =>
 | 
			
		||||
        !this.existingFields?.find(
 | 
			
		||||
          (e) => this.getCustomFieldFromInstance(e)?.id === f.id
 | 
			
		||||
        )
 | 
			
		||||
      (f) => !this.existingFields?.find((e) => e.field === f.id)
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onOpenClose() {
 | 
			
		||||
    this.field = undefined
 | 
			
		||||
  onOpenClose(open: boolean) {
 | 
			
		||||
    if (open) {
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        this.listFilterTextInput.nativeElement.focus()
 | 
			
		||||
      }, 100)
 | 
			
		||||
    } else {
 | 
			
		||||
      this.filterText = undefined
 | 
			
		||||
    }
 | 
			
		||||
    this.updateUnusedFields()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  addField() {
 | 
			
		||||
    this.added.emit(this.customFields.find((f) => f.id === this.field))
 | 
			
		||||
  addField(field: CustomField) {
 | 
			
		||||
    this.added.emit(field)
 | 
			
		||||
    this.updateUnusedFields()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  createField(newName: string = null) {
 | 
			
		||||
@@ -121,6 +125,7 @@ export class CustomFieldsDropdownComponent implements OnDestroy {
 | 
			
		||||
        this.customFieldsService.clearCache()
 | 
			
		||||
        this.getFields()
 | 
			
		||||
        this.created.emit(newField)
 | 
			
		||||
        setTimeout(() => this.addField(newField), 100)
 | 
			
		||||
      })
 | 
			
		||||
    modal.componentInstance.failed
 | 
			
		||||
      .pipe(takeUntil(this.unsubscribeNotifier))
 | 
			
		||||
@@ -128,4 +133,82 @@ export class CustomFieldsDropdownComponent implements OnDestroy {
 | 
			
		||||
        this.toastService.showError($localize`Error saving field.`, e)
 | 
			
		||||
      })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getDataTypeLabel(dataType: string) {
 | 
			
		||||
    return DATA_TYPE_LABELS.find((l) => l.id === dataType)?.name
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public listFilterEnter() {
 | 
			
		||||
    if (this.filteredFields.length === 1) {
 | 
			
		||||
      this.addField(this.filteredFields[0])
 | 
			
		||||
    } else if (
 | 
			
		||||
      this.filterText &&
 | 
			
		||||
      this.filteredFields.length === 0 &&
 | 
			
		||||
      this.canCreateFields
 | 
			
		||||
    ) {
 | 
			
		||||
      this.createField(this.filterText)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private focusNextButtonItem(setFocus: boolean = true) {
 | 
			
		||||
    this.keyboardIndex = Math.min(
 | 
			
		||||
      this.buttons.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.buttons.get(this.keyboardIndex)?.nativeElement.focus()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public 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
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -945,9 +945,9 @@ describe('DocumentDetailComponent', () => {
 | 
			
		||||
    fixture.detectChanges()
 | 
			
		||||
    expect(component.document.custom_fields).toHaveLength(initialLength - 1)
 | 
			
		||||
    expect(component.customFieldFormFields).toHaveLength(initialLength - 1)
 | 
			
		||||
    expect(fixture.debugElement.nativeElement.textContent).not.toContain(
 | 
			
		||||
      'Field 1'
 | 
			
		||||
    )
 | 
			
		||||
    expect(
 | 
			
		||||
      fixture.debugElement.query(By.css('form')).nativeElement.textContent
 | 
			
		||||
    ).not.toContain('Field 1')
 | 
			
		||||
    const updateSpy = jest.spyOn(documentService, 'update')
 | 
			
		||||
    component.save(true)
 | 
			
		||||
    expect(updateSpy.mock.lastCall[0].custom_fields).toHaveLength(
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user