diff --git a/lib/core/i18n/en.json b/lib/core/i18n/en.json index c06f967872..cb8264ad59 100644 --- a/lib/core/i18n/en.json +++ b/lib/core/i18n/en.json @@ -353,7 +353,8 @@ "ROTATE_LEFT": "Rotate left", "ROTATE_RIGHT": "Rotate right", "RESET": "Reset", - "THUMBNAILS": "Document thumbnails" + "THUMBNAILS": "Document thumbnails", + "THUMBNAILS_PANLEL_CLOSE": "Close thumbnails panel" }, "PAGE_LABEL": { "SHOWING": "Showing", diff --git a/lib/core/viewer/components/pdf-viewer-thumb.component.html b/lib/core/viewer/components/pdf-viewer-thumb.component.html index b355eabce3..a61d175504 100644 --- a/lib/core/viewer/components/pdf-viewer-thumb.component.html +++ b/lib/core/viewer/components/pdf-viewer-thumb.component.html @@ -1,4 +1,5 @@ + title="{{ 'ADF_VIEWER.SIDEBAR.THUMBNAILS.PAGE' | translate: { pageNum: page.id } }}" + [attr.aria-label]="'ADF_VIEWER.SIDEBAR.THUMBNAILS.PAGE' | translate: { pageNum: page.id }"> diff --git a/lib/core/viewer/components/pdf-viewer-thumb.component.spec.ts b/lib/core/viewer/components/pdf-viewer-thumb.component.spec.ts index 4845f43089..8fdfc52284 100644 --- a/lib/core/viewer/components/pdf-viewer-thumb.component.spec.ts +++ b/lib/core/viewer/components/pdf-viewer-thumb.component.spec.ts @@ -66,4 +66,12 @@ describe('PdfThumbComponent', () => { done(); }); }); + + it('should focus element', () => { + component.page = page; + fixture.detectChanges(); + component.focus(); + + expect(fixture.debugElement.nativeElement.id).toBe(document.activeElement.id); + }); }); diff --git a/lib/core/viewer/components/pdf-viewer-thumb.component.ts b/lib/core/viewer/components/pdf-viewer-thumb.component.ts index 903cc32717..bbf70f8753 100644 --- a/lib/core/viewer/components/pdf-viewer-thumb.component.ts +++ b/lib/core/viewer/components/pdf-viewer-thumb.component.ts @@ -15,28 +15,34 @@ * limitations under the License. */ -import { Component, Input, OnInit, ViewEncapsulation } from '@angular/core'; +import { Component, ElementRef, Input, OnInit, ViewEncapsulation } from '@angular/core'; import { DomSanitizer } from '@angular/platform-browser'; +import { FocusableOption } from '@angular/cdk/a11y'; @Component({ selector: 'adf-pdf-thumb', templateUrl: './pdf-viewer-thumb.component.html', - encapsulation: ViewEncapsulation.None + encapsulation: ViewEncapsulation.None, + host: { tabindex: '0'} }) -export class PdfThumbComponent implements OnInit { +export class PdfThumbComponent implements OnInit, FocusableOption { @Input() page: any = null; image$: Promise; - constructor(private sanitizer: DomSanitizer) { + constructor(private sanitizer: DomSanitizer, private element: ElementRef) { } ngOnInit() { this.image$ = this.page.getPage().then((page) => this.getThumb(page)); } + focus() { + this.element.nativeElement.focus(); + } + private getThumb(page): Promise { const viewport = page.getViewport({ scale: 1 }); diff --git a/lib/core/viewer/components/pdf-viewer-thumbnails.component.html b/lib/core/viewer/components/pdf-viewer-thumbnails.component.html index c9e1baa0a9..9290dc87bd 100644 --- a/lib/core/viewer/components/pdf-viewer-thumbnails.component.html +++ b/lib/core/viewer/components/pdf-viewer-thumbnails.component.html @@ -4,6 +4,7 @@ [style.transform]="'translate(-50%, ' + translateY + 'px)'"> diff --git a/lib/core/viewer/components/pdf-viewer-thumbnails.component.scss b/lib/core/viewer/components/pdf-viewer-thumbnails.component.scss index f64ec6a39a..b4d4b30c33 100644 --- a/lib/core/viewer/components/pdf-viewer-thumbnails.component.scss +++ b/lib/core/viewer/components/pdf-viewer-thumbnails.component.scss @@ -28,9 +28,5 @@ &__thumb:hover { box-shadow: 0 0 5px 0 $black-87-opacity; } - - &__thumb--selected:not(:hover) { - box-shadow: 0 0 5px 0 $black-87-opacity; - } } } diff --git a/lib/core/viewer/components/pdf-viewer-thumbnails.component.spec.ts b/lib/core/viewer/components/pdf-viewer-thumbnails.component.spec.ts index 71e05c051b..ddcb869fa1 100644 --- a/lib/core/viewer/components/pdf-viewer-thumbnails.component.spec.ts +++ b/lib/core/viewer/components/pdf-viewer-thumbnails.component.spec.ts @@ -21,6 +21,7 @@ import { PdfThumbListComponent } from './pdf-viewer-thumbnails.component'; import { setupTestBed } from '../../testing/setup-test-bed'; import { CoreTestingModule } from '../../testing/core.testing.module'; import { TranslateModule } from '@ngx-translate/core'; +import { DOWN_ARROW, UP_ARROW, ESCAPE } from '@angular/cdk/keycodes'; declare const pdfjsViewer: any; @@ -41,7 +42,7 @@ describe('PdfThumbListComponent', () => { set currentPageNumber(pageNum) { this._currentPageNumber = pageNum; /* cspell:disable-next-line */ - this.eventBus.dispatch('pagechange', { pageNumber: pageNum }); + this.eventBus.dispatch('pagechanging', { pageNumber: pageNum }); }, get currentPageNumber() { return this._currentPageNumber; @@ -125,7 +126,7 @@ describe('PdfThumbListComponent', () => { expect(renderedIds).toContain(12); /* cspell:disable-next-line */ - viewerMock.eventBus.dispatch('pagechange', { pageNumber: 12 }); + viewerMock.eventBus.dispatch('pagechanging', { pageNumber: 12 }); const newRenderedIds = component.renderItems.map((item) => item.id); @@ -139,10 +140,17 @@ describe('PdfThumbListComponent', () => { expect(component.renderItems[component.renderItems.length - 1].id).toBe(6); expect(fixture.debugElement.nativeElement.scrollTop).toBe(0); - viewerMock.currentPageNumber = 6; + component.pdfViewer.eventBus.dispatch('pagechanging', { pageNumber: 6 }); expect(component.scrollInto).not.toHaveBeenCalled(); - expect(fixture.debugElement.nativeElement.scrollTop).toBe(129); + expect(fixture.debugElement.nativeElement.scrollTop).toBe(0); + }); + + it('should set active current page on onPageChange event', () => { + fixture.detectChanges(); + component.pdfViewer.eventBus.dispatch('pagechanging', { pageNumber: 6 }); + + expect(document.activeElement.id).toBe('6'); }); it('should return current viewed page as selected', () => { @@ -161,4 +169,58 @@ describe('PdfThumbListComponent', () => { expect(viewerMock.currentPageNumber).toBe(12); }); + + describe('Keyboard events', () => { + it('should select next page in the list on DOWN_ARROW event', () => { + const event = new KeyboardEvent('keydown', {'keyCode': DOWN_ARROW} as KeyboardEventInit); + fixture.detectChanges(); + component.goTo(1); + expect(document.activeElement.id).toBe('1'); + + fixture.debugElement.nativeElement.dispatchEvent(event); + expect(document.activeElement.id).toBe('2'); + }); + + it('should select previous page in the list on UP_ARROW event', () => { + const event = new KeyboardEvent('keydown', {'keyCode': UP_ARROW} as KeyboardEventInit); + fixture.detectChanges(); + component.goTo(2); + expect(document.activeElement.id).toBe('2'); + + fixture.debugElement.nativeElement.dispatchEvent(event); + expect(document.activeElement.id).toBe('1'); + }); + + it('should not select previous page if it is the first page', () => { + const event = new KeyboardEvent('keydown', {'keyCode': UP_ARROW} as KeyboardEventInit); + fixture.detectChanges(); + component.goTo(1); + expect(document.activeElement.id).toBe('1'); + + fixture.debugElement.nativeElement.dispatchEvent(event); + expect(document.activeElement.id).toBe('1'); + }); + + it('should not select next item if it is the last page', () => { + const event = new KeyboardEvent('keydown', {'keyCode': DOWN_ARROW} as KeyboardEventInit); + fixture.detectChanges(); + component.scrollInto(16); + fixture.detectChanges(); + + component.pdfViewer.eventBus.dispatch('pagechanging', { pageNumber: 16 }); + expect(document.activeElement.id).toBe('16'); + + fixture.debugElement.nativeElement.dispatchEvent(event); + expect(document.activeElement.id).toBe('16'); + }); + + it('should emit on ESCAPE event', () => { + const event = new KeyboardEvent('keydown', {'keyCode': ESCAPE} as KeyboardEventInit); + spyOn(component.close, 'emit'); + fixture.detectChanges(); + + fixture.debugElement.nativeElement.dispatchEvent(event); + expect(component.close.emit).toHaveBeenCalled(); + }); + }); }); diff --git a/lib/core/viewer/components/pdf-viewer-thumbnails.component.ts b/lib/core/viewer/components/pdf-viewer-thumbnails.component.ts index 51f3fa8719..4442d01862 100644 --- a/lib/core/viewer/components/pdf-viewer-thumbnails.component.ts +++ b/lib/core/viewer/components/pdf-viewer-thumbnails.component.ts @@ -17,19 +17,27 @@ import { Component, Input, ContentChild, TemplateRef, HostListener, OnInit, - AfterViewInit, ElementRef, OnDestroy, ViewEncapsulation + AfterViewInit, ElementRef, OnDestroy, ViewEncapsulation, EventEmitter, Output, Inject, ViewChildren, QueryList } from '@angular/core'; +import { ESCAPE, UP_ARROW, DOWN_ARROW } from '@angular/cdk/keycodes'; +import { DOCUMENT } from '@angular/common'; +import { FocusKeyManager } from '@angular/cdk/a11y'; +import { PdfThumbComponent } from './pdf-viewer-thumb.component'; +import { delay } from 'rxjs/operators'; @Component({ selector: 'adf-pdf-thumbnails', templateUrl: './pdf-viewer-thumbnails.component.html', styleUrls: ['./pdf-viewer-thumbnails.component.scss'], - host: { 'class': 'adf-pdf-thumbnails' }, + host: { class: 'adf-pdf-thumbnails' }, encapsulation: ViewEncapsulation.None }) export class PdfThumbListComponent implements OnInit, AfterViewInit, OnDestroy { @Input() pdfViewer: any; + @Output() + close: EventEmitter = new EventEmitter(); + virtualHeight: number = 0; translateY: number = 0; renderItems = []; @@ -39,56 +47,96 @@ export class PdfThumbListComponent implements OnInit, AfterViewInit, OnDestroy { private items = []; private margin: number = 15; private itemHeight: number = 114 + this.margin; + private previouslyFocusedElement: HTMLElement | null = null; + private keyManager: FocusKeyManager; @ContentChild(TemplateRef) template: any; + @ViewChildren(PdfThumbComponent) + thumbsList: QueryList; + + @HostListener('keydown', ['$event']) + onKeydown(event: KeyboardEvent): void { + const keyCode = event.keyCode; + + if (keyCode === UP_ARROW && this.canSelectPreviousItem()) { + this.pdfViewer.currentPageNumber -= 1; + } + + if (keyCode === DOWN_ARROW && this.canSelectNextItem()) { + this.pdfViewer.currentPageNumber += 1; + } + + if (keyCode === ESCAPE) { + this.close.emit(); + } + + this.keyManager.setFocusOrigin('keyboard'); + event.preventDefault(); + } + @HostListener('window:resize') onResize() { this.calculateItems(); } - constructor(private element: ElementRef) { + constructor(private element: ElementRef, @Inject(DOCUMENT) private document: any) { this.calculateItems = this.calculateItems.bind(this); this.onPageChange = this.onPageChange.bind(this); } ngOnInit() { /* cspell:disable-next-line */ - this.pdfViewer.eventBus.on('pagechange', this.onPageChange); + this.pdfViewer.eventBus.on('pagechanging', this.onPageChange); this.element.nativeElement.addEventListener('scroll', this.calculateItems, true); this.setHeight(this.pdfViewer.currentPageNumber); this.items = this.getPages(); this.calculateItems(); + this.previouslyFocusedElement = this.document.activeElement as HTMLElement; } ngAfterViewInit() { - setTimeout(() => this.scrollInto(this.pdfViewer.currentPageNumber), 0); + this.keyManager = new FocusKeyManager(this.thumbsList); + + this.thumbsList.changes + .pipe(delay(0)) + .subscribe(() => this.keyManager.setActiveItem(this.getPageIndex(this.pdfViewer.currentPageNumber))); + + setTimeout(() => { + this.scrollInto(this.pdfViewer.currentPageNumber); + this.keyManager.setActiveItem(this.getPageIndex(this.pdfViewer.currentPageNumber)); + }, 0); } ngOnDestroy() { this.element.nativeElement.removeEventListener('scroll', this.calculateItems, true); /* cspell:disable-next-line */ - this.pdfViewer.eventBus.off('pagechange', this.onPageChange); + this.pdfViewer.eventBus.on('pagechanging', this.onPageChange); + + if (this.previouslyFocusedElement) { + this.previouslyFocusedElement.focus(); + this.previouslyFocusedElement = null; + } } trackByFn(_: number, item: any): number { return item.id; } - isSelected(pageNum: number) { - return this.pdfViewer.currentPageNumber === pageNum; + isSelected(pageNumber: number) { + return this.pdfViewer.currentPageNumber === pageNumber; } - goTo(pageNum: number) { - this.pdfViewer.currentPageNumber = pageNum; + goTo(pageNumber: number) { + this.pdfViewer.currentPageNumber = pageNumber; } - scrollInto(item: any) { + scrollInto(pageNumber: number) { if (this.items.length) { - const index: number = this.items.findIndex((element) => element.id === item); + const index: number = this.items.findIndex((element) => element.id === pageNumber); if (index < 0 || index >= this.items.length) { return; @@ -164,5 +212,20 @@ export class PdfThumbListComponent implements OnInit, AfterViewInit, OnDestroy { if (index >= this.renderItems.length - 1) { this.element.nativeElement.scrollTop += this.itemHeight; } + + this.keyManager.setActiveItem(this.getPageIndex(event.pageNumber)); + } + + private getPageIndex(pageNumber: number): number { + const thumbsListArray = this.thumbsList.toArray(); + return thumbsListArray.findIndex(el => el.page.id === pageNumber); + } + + private canSelectNextItem(): boolean { + return this.pdfViewer.currentPageNumber !== this.pdfViewer.pagesCount; + } + + private canSelectPreviousItem(): boolean { + return this.pdfViewer.currentPageNumber !== 1; } } diff --git a/lib/core/viewer/components/pdf-viewer.component.html b/lib/core/viewer/components/pdf-viewer.component.html index ebadf89fb3..a68ea05312 100644 --- a/lib/core/viewer/components/pdf-viewer.component.html +++ b/lib/core/viewer/components/pdf-viewer.component.html @@ -3,7 +3,12 @@
-
@@ -13,7 +18,7 @@ - +
diff --git a/lib/core/viewer/components/pdf-viewer.component.ts b/lib/core/viewer/components/pdf-viewer.component.ts index c730e60a32..1a4de3ea1d 100644 --- a/lib/core/viewer/components/pdf-viewer.component.ts +++ b/lib/core/viewer/components/pdf-viewer.component.ts @@ -100,6 +100,8 @@ export class PdfViewerComponent implements OnChanges, OnDestroy { return Math.round(this.currentScale * 100) + '%'; } + private eventBus = new pdfjsViewer.EventBus(); + constructor( private dialog: MatDialog, private renderingQueueServices: RenderingQueueServices, @@ -200,15 +202,16 @@ export class PdfViewerComponent implements OnChanges, OnDestroy { this.pdfViewer = new pdfjsViewer.PDFViewer({ container: container, viewer: viewer, - renderingQueue: this.renderingQueueServices + renderingQueue: this.renderingQueueServices, + eventBus: this.eventBus }); // cspell: disable-next - this.pdfViewer.eventBus.on('pagechanging', this.onPageChange); + this.eventBus.on('pagechanging', this.onPageChange); // cspell: disable-next - this.pdfViewer.eventBus.on('pagesloaded', this.onPagesLoaded); + this.eventBus.on('pagesloaded', this.onPagesLoaded); // cspell: disable-next - this.pdfViewer.eventBus.on('textlayerrendered', this.onPageRendered); + this.eventBus.on('textlayerrendered', this.onPageRendered); this.renderingQueueServices.setViewer(this.pdfViewer); this.pdfViewer.setDocument(pdfDocument); @@ -219,11 +222,11 @@ export class PdfViewerComponent implements OnChanges, OnDestroy { ngOnDestroy() { if (this.pdfViewer) { // cspell: disable-next - this.pdfViewer.eventBus.off('pagechanging'); + this.eventBus.off('pagechanging'); // cspell: disable-next - this.pdfViewer.eventBus.off('pagesloaded'); + this.eventBus.off('pagesloaded'); // cspell: disable-next - this.pdfViewer.eventBus.off('textlayerrendered'); + this.eventBus.off('textlayerrendered'); } if (this.loadingTask) {