Better hover & keyboard interaction

This commit is contained in:
shamoon 2024-04-01 00:05:40 -07:00
parent 7a73ce8ad3
commit 7737b24d7d
3 changed files with 50 additions and 10 deletions

View File

@ -9,11 +9,15 @@
</form> </form>
<ng-template #resultItemTemplate let-item="item" let-nameProp="nameProp" let-type="type" let-icon="icon"> <ng-template #resultItemTemplate let-item="item" let-nameProp="nameProp" let-type="type" let-icon="icon">
<div #resultItem ngbDropdownItem class="py-2 d-flex align-items-center focus-ring border-0 cursor-pointer" tabindex="-1" (click)="primaryAction(type, item)"> <div #resultItem ngbDropdownItem class="py-2 d-flex align-items-center focus-ring border-0 cursor-pointer" tabindex="-1"
(click)="primaryAction(type, item)"
(mouseenter)="onItemHover($event)">
<i-bs width="1.2em" height="1.2em" name="{{icon}}" class="me-2 text-muted"></i-bs> <i-bs width="1.2em" height="1.2em" name="{{icon}}" class="me-2 text-muted"></i-bs>
<span class="text-truncate">{{item[nameProp]}}</span> <span class="text-truncate">{{item[nameProp]}}</span>
<div class="btn-group ms-auto"> <div class="btn-group ms-auto">
<button #primaryButton type="button" class="btn btn-sm btn-outline-primary d-flex" (click)="primaryAction(type, item); $event.stopPropagation()"> <button #primaryButton type="button" class="btn btn-sm btn-outline-primary d-flex"
(click)="primaryAction(type, item); $event.stopPropagation()"
(mouseenter)="onButtonHover($event)">
@if (type === 'document' || type === 'workflow' || type === 'customField' || type === 'group' || type === 'user') { @if (type === 'document' || type === 'workflow' || type === 'customField' || type === 'group' || type === 'user') {
<i-bs width="1em" height="1em" name="pencil"></i-bs> <i-bs width="1em" height="1em" name="pencil"></i-bs>
<span class="">&nbsp;<ng-container i18n>Edit</ng-container></span> <span class="">&nbsp;<ng-container i18n>Edit</ng-container></span>
@ -23,7 +27,9 @@
} }
</button> </button>
@if (type !== 'workflow' && type !== 'customField' && type !== 'group' && type !== 'user') { @if (type !== 'workflow' && type !== 'customField' && type !== 'group' && type !== 'user') {
<button #secondaryButton type="button" class="btn btn-sm btn-outline-primary d-flex" (click)="secondaryAction(type, item); $event.stopPropagation()"> <button #secondaryButton type="button" class="btn btn-sm btn-outline-primary d-flex"
(click)="secondaryAction(type, item); $event.stopPropagation()"
(mouseenter)="onButtonHover($event)">
@if (type === 'document') { @if (type === 'document') {
<i-bs width="1em" height="1em" name="download"></i-bs> <i-bs width="1em" height="1em" name="download"></i-bs>
<span class="">&nbsp;<ng-container i18n>Download</ng-container></span> <span class="">&nbsp;<ng-container i18n>Download</ng-container></span>

View File

@ -33,6 +33,7 @@ import { MailAccountEditDialogComponent } from '../../common/edit-dialog/mail-ac
import { GroupEditDialogComponent } from '../../common/edit-dialog/group-edit-dialog/group-edit-dialog.component' import { GroupEditDialogComponent } from '../../common/edit-dialog/group-edit-dialog/group-edit-dialog.component'
import { CustomFieldEditDialogComponent } from '../../common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component' import { CustomFieldEditDialogComponent } from '../../common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
import { WorkflowEditDialogComponent } from '../../common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component' import { WorkflowEditDialogComponent } from '../../common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component'
import { ElementRef } from '@angular/core'
const searchResults = { const searchResults = {
total: 11, total: 11,
@ -144,7 +145,7 @@ describe('GlobalSearchComponent', () => {
fixture.detectChanges() fixture.detectChanges()
}) })
it('should handle keyboard events', () => { it('should handle keyboard nav', () => {
const focusSpy = jest.spyOn(component.searchInput.nativeElement, 'focus') const focusSpy = jest.spyOn(component.searchInput.nativeElement, 'focus')
component.handleKeyboardEvent( component.handleKeyboardEvent(
new KeyboardEvent('keydown', { key: 'k', ctrlKey: true }) new KeyboardEvent('keydown', { key: 'k', ctrlKey: true })
@ -205,7 +206,7 @@ describe('GlobalSearchComponent', () => {
expect(inputFocusSpy).toHaveBeenCalled() expect(inputFocusSpy).toHaveBeenCalled()
}) })
it('should search', fakeAsync(() => { it('should search on query debounce', fakeAsync(() => {
const query = 'test' const query = 'test'
const searchSpy = jest.spyOn(searchService, 'globalSearch') const searchSpy = jest.spyOn(searchService, 'globalSearch')
searchSpy.mockReturnValue(of({} as any)) searchSpy.mockReturnValue(of({} as any))
@ -216,7 +217,7 @@ describe('GlobalSearchComponent', () => {
expect(dropdownOpenSpy).toHaveBeenCalled() expect(dropdownOpenSpy).toHaveBeenCalled()
})) }))
it('should perform primary action', () => { it('should support primary action', () => {
const object = { id: 1 } const object = { id: 1 }
const routerSpy = jest.spyOn(router, 'navigate') const routerSpy = jest.spyOn(router, 'navigate')
const qfSpy = jest.spyOn(documentListViewService, 'quickFilter') const qfSpy = jest.spyOn(documentListViewService, 'quickFilter')
@ -276,7 +277,7 @@ describe('GlobalSearchComponent', () => {
}) })
}) })
it('should perform secondary action', () => { it('should support secondary action', () => {
const doc = searchResults.documents[0] const doc = searchResults.documents[0]
const openSpy = jest.spyOn(window, 'open') const openSpy = jest.spyOn(window, 'open')
component.secondaryAction('document', doc) component.secondaryAction('document', doc)
@ -305,7 +306,7 @@ describe('GlobalSearchComponent', () => {
}) })
}) })
it('should reset', () => { it('should support reset', () => {
const debounce = jest.spyOn(component.queryDebounce, 'next') const debounce = jest.spyOn(component.queryDebounce, 'next')
const closeSpy = jest.spyOn(component.resultsDropdown, 'close') const closeSpy = jest.spyOn(component.resultsDropdown, 'close')
component['reset'](true) component['reset'](true)
@ -315,7 +316,7 @@ describe('GlobalSearchComponent', () => {
expect(closeSpy).toHaveBeenCalled() expect(closeSpy).toHaveBeenCalled()
}) })
it('should set current item', () => { it('should support focus current item', () => {
component.searchResults = searchResults as any component.searchResults = searchResults as any
fixture.detectChanges() fixture.detectChanges()
const focusSpy = jest.spyOn( const focusSpy = jest.spyOn(
@ -327,7 +328,7 @@ describe('GlobalSearchComponent', () => {
expect(focusSpy).toHaveBeenCalled() expect(focusSpy).toHaveBeenCalled()
}) })
it('should handle search input keydown', () => { it('should handle search input keyboard nav', () => {
component.searchResults = searchResults as any component.searchResults = searchResults as any
component.resultsDropdown.open() component.resultsDropdown.open()
fixture.detectChanges() fixture.detectChanges()
@ -353,4 +354,24 @@ describe('GlobalSearchComponent', () => {
component.onDropdownOpenChange(false) component.onDropdownOpenChange(false)
expect(resetSpy).toHaveBeenCalled() expect(resetSpy).toHaveBeenCalled()
}) })
it('should focus button on dropdown item hover', () => {
component.searchResults = searchResults as any
fixture.detectChanges()
const item: ElementRef = component.resultItems.first
const focusSpy = jest.spyOn(
component.primaryButtons.first.nativeElement,
'focus'
)
component.onItemHover({ currentTarget: item.nativeElement } as any)
expect(component['currentItemIndex']).toBe(0)
expect(focusSpy).toHaveBeenCalled()
})
it('should focus on button hover', () => {
const event = { currentTarget: { focus: jest.fn() } }
const focusSpy = jest.spyOn(event.currentTarget, 'focus')
component.onButtonHover(event as any)
expect(focusSpy).toHaveBeenCalled()
})
}) })

View File

@ -47,6 +47,7 @@ export class GlobalSearchComponent {
@ViewChild('searchInput') searchInput: ElementRef @ViewChild('searchInput') searchInput: ElementRef
@ViewChild('resultsDropdown') resultsDropdown: NgbDropdown @ViewChild('resultsDropdown') resultsDropdown: NgbDropdown
@ViewChildren('resultItem') resultItems: QueryList<ElementRef>
@ViewChildren('primaryButton') primaryButtons: QueryList<ElementRef> @ViewChildren('primaryButton') primaryButtons: QueryList<ElementRef>
@ViewChildren('secondaryButton') secondaryButtons: QueryList<ElementRef> @ViewChildren('secondaryButton') secondaryButtons: QueryList<ElementRef>
@ -214,6 +215,18 @@ export class GlobalSearchComponent {
item.nativeElement.focus() item.nativeElement.focus()
} }
onItemHover(event: MouseEvent) {
const item: ElementRef = this.resultItems
.toArray()
.find((item) => item.nativeElement === event.currentTarget)
this.currentItemIndex = this.resultItems.toArray().indexOf(item)
this.setCurrentItem()
}
onButtonHover(event: MouseEvent) {
;(event.currentTarget as HTMLElement).focus()
}
public searchInputKeyDown(event: KeyboardEvent) { public searchInputKeyDown(event: KeyboardEvent) {
if ( if (
event.key === 'ArrowDown' && event.key === 'ArrowDown' &&