Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
<a [routerLink]="(bitstreamPath$| async)?.routerLink" class="dont-break-out" [queryParams]="(bitstreamPath$| async)?.queryParams" [target]="isBlank ? '_blank': '_self'" [ngClass]="cssClasses">
<span *ngIf="!(canDownload$ |async)" class="pr-1"><i class="fas fa-lock"></i></span>
<a [routerLink]="(bitstreamPath$| async)?.routerLink" class="dont-break-out"
[queryParams]="(bitstreamPath$| async)?.queryParams"
[target]="isBlank ? '_blank': '_self'"
[ngClass]="cssClasses"
[attr.aria-label]="getDownloadLinkAriaLabel((canDownload$ | async), ('file-download-link.download' | translate), ('file-download-link.restricted' | translate))"
role="link"
tabindex="0">
<span *ngIf="!(canDownload$ | async)" class="pr-1"><i class="fas fa-lock" aria-hidden="true"></i></span>
<ng-container *ngTemplateOutlet="content"></ng-container>
</a>
<ds-themed-access-status-badge [object]="bitstream"></ds-themed-access-status-badge>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,18 @@ 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;
let fixture: ComponentFixture<FileDownloadLinkComponent>;

let scheduler;
let authorizationService: AuthorizationDataService;
let dsoNameService: DSONameService;

const bitstreamName = 'Test bitstream name';

let bitstream: Bitstream;
let item: Item;
Expand All @@ -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: {
Expand All @@ -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],
})
Expand Down Expand Up @@ -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(() => {
Expand Down Expand Up @@ -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');
});
});
});
});
Expand Down
14 changes: 14 additions & 0 deletions src/app/shared/file-download-link/file-download-link.component.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -48,6 +49,7 @@ export class FileDownloadLinkComponent implements OnInit {

constructor(
private authorizationService: AuthorizationDataService,
public dsoNameService: DSONameService,
) {
}

Expand Down Expand Up @@ -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}`;
}
}
5 changes: 3 additions & 2 deletions src/assets/i18n/cs.json5
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}",
Expand Down
4 changes: 4 additions & 0 deletions src/assets/i18n/en.json5
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}",
Expand Down
Loading