diff --git a/src/app/shared/file-download-link/file-download-link.component.html b/src/app/shared/file-download-link/file-download-link.component.html index 51a18d7e6e1..db054c83073 100644 --- a/src/app/shared/file-download-link/file-download-link.component.html +++ b/src/app/shared/file-download-link/file-download-link.component.html @@ -1,5 +1,11 @@ - - + + diff --git a/src/app/shared/file-download-link/file-download-link.component.spec.ts b/src/app/shared/file-download-link/file-download-link.component.spec.ts index b9c2815535e..6336230e661 100644 --- a/src/app/shared/file-download-link/file-download-link.component.spec.ts +++ b/src/app/shared/file-download-link/file-download-link.component.spec.ts @@ -9,6 +9,8 @@ import { AuthorizationDataService } from '../../core/data/feature-authorization/ import { cold, getTestScheduler } from 'jasmine-marbles'; import { Item } from '../../core/shared/item.model'; import { RouterLinkDirectiveStub } from '../testing/router-link-directive.stub'; +import { TranslateModule } from '@ngx-translate/core'; +import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; describe('FileDownloadLinkComponent', () => { let component: FileDownloadLinkComponent; @@ -16,6 +18,9 @@ describe('FileDownloadLinkComponent', () => { let scheduler; let authorizationService: AuthorizationDataService; + let dsoNameService: DSONameService; + + const bitstreamName = 'Test bitstream name'; let bitstream: Bitstream; let item: Item; @@ -24,6 +29,9 @@ describe('FileDownloadLinkComponent', () => { authorizationService = jasmine.createSpyObj('authorizationService', { isAuthorized: cold('-a', {a: true}) }); + dsoNameService = jasmine.createSpyObj('dsoNameService', { + getName: bitstreamName + }); bitstream = Object.assign(new Bitstream(), { uuid: 'bitstreamUuid', _links: { @@ -40,9 +48,12 @@ describe('FileDownloadLinkComponent', () => { function initTestbed() { TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot(), + ], declarations: [FileDownloadLinkComponent, RouterLinkDirectiveStub], providers: [ - {provide: AuthorizationDataService, useValue: authorizationService}, + {provide: AuthorizationDataService, useValue: authorizationService, provide: DSONameService, useValue: dsoNameService}, ], schemas: [NO_ERRORS_SCHEMA], }) @@ -78,6 +89,18 @@ describe('FileDownloadLinkComponent', () => { const lock = fixture.debugElement.query(By.css('.fa-lock')); expect(lock).toBeNull(); }); + it('should set an accessible aria-label on the download link containing the bitstream name', () => { + scheduler.flush(); + fixture.detectChanges(); + const link = fixture.debugElement.query(By.css('a')); + expect(link.nativeElement.getAttribute('aria-label')).toContain(bitstreamName); + }); + it('should not include the restricted-bitstream text in the aria-label', () => { + scheduler.flush(); + fixture.detectChanges(); + const link = fixture.debugElement.query(By.css('a')); + expect(link.nativeElement.getAttribute('aria-label')).not.toContain('file-download-link.restricted'); + }); }); // describe('when the user has no download rights but has the right to request a copy', () => { // beforeEach(waitForAsync(() => { @@ -139,6 +162,17 @@ describe('FileDownloadLinkComponent', () => { const lock = fixture.debugElement.query(By.css('.fa-lock')).nativeElement; expect(lock).toBeTruthy(); }); + it('should mark the lock icon as decorative and expose the restricted state via the link\'s own aria-label', () => { + scheduler.flush(); + fixture.detectChanges(); + const lock = fixture.debugElement.query(By.css('.fa-lock')).nativeElement; + expect(lock.getAttribute('aria-hidden')).toBe('true'); + // The restricted state must be part of the link's own aria-label: an element's + // aria-label overrides its accessible-name computation entirely, so a nested + // sr-only text node would never be announced (this is the bug the fix corrects). + const link = fixture.debugElement.query(By.css('a')); + expect(link.nativeElement.getAttribute('aria-label')).toContain('file-download-link.restricted'); + }); }); }); }); diff --git a/src/app/shared/file-download-link/file-download-link.component.ts b/src/app/shared/file-download-link/file-download-link.component.ts index 51d1c3a67cd..48417c2a641 100644 --- a/src/app/shared/file-download-link/file-download-link.component.ts +++ b/src/app/shared/file-download-link/file-download-link.component.ts @@ -1,5 +1,6 @@ import { Component, Input, OnInit } from '@angular/core'; import { Bitstream } from '../../core/shared/bitstream.model'; +import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { getBitstreamDownloadRoute } from '../../app-routing-paths'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; import { FeatureID } from '../../core/data/feature-authorization/feature-id'; @@ -48,6 +49,7 @@ export class FileDownloadLinkComponent implements OnInit { constructor( private authorizationService: AuthorizationDataService, + public dsoNameService: DSONameService, ) { } @@ -77,4 +79,16 @@ export class FileDownloadLinkComponent implements OnInit { queryParams: {} }; } + + /** + * Builds the download link's accessible name. When the bitstream is restricted, the + * restricted state is folded into this same string (rather than relying on a nested + * sr-only text node) because an element's own aria-label overrides its accessible name + * computation entirely - descendant text, including sr-only spans, is not announced + * alongside it. + */ + getDownloadLinkAriaLabel(canDownload: boolean | null, downloadLabel: string, restrictedLabel: string): string { + const download = `${downloadLabel.trim()} ${this.dsoNameService.getName(this.bitstream)}`.trim(); + return canDownload ? download : `${restrictedLabel.trim()}, ${download}`; + } } diff --git a/src/assets/i18n/cs.json5 b/src/assets/i18n/cs.json5 index 705b4bd7b1b..cbc67091f47 100644 --- a/src/assets/i18n/cs.json5 +++ b/src/assets/i18n/cs.json5 @@ -2134,9 +2134,10 @@ "error.validation.metadata.qualifier.max-length": "Toto pole nesmí obsahovat více než 64 znaků", // "feed.description": "Syndication feed", "feed.description": "Synchronizační kanál", + // "file-download-link.download": "Download", + "file-download-link.download": "Stáhnout", // "file-download-link.restricted": "Restricted bitstream", - // TODO New key - Add a translation - "file-download-link.restricted": "Restricted bitstream", + "file-download-link.restricted": "Omezený soubor", // "file-section.error.header": "Error obtaining files for this item", "file-section.error.header": "Chyba při získávání souborů pro tento záznam", // "footer.copyright": "copyright © 2002-{{ year }}", diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index e19e906d8ff..5f34da08959 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -1701,6 +1701,10 @@ "feed.description": "RSS feed", + "file-download-link.download": "Download", + + "file-download-link.restricted": "Restricted bitstream", + "file-section.error.header": "Error obtaining files for this item", "footer.copyright": "copyright © 2002-{{ year }}",