Skip to content

Commit e903526

Browse files
authored
Merge pull request #5393 from 4Science/task/main/CST-22298_squashed
Add audio transcript and video description features for a11y
2 parents 518eddf + dbf0971 commit e903526

10 files changed

Lines changed: 485 additions & 15 deletions

src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.spec.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,21 @@ describe('EditBitstreamPageComponent', () => {
195195
value: 'Bitstream title',
196196
},
197197
],
198+
'dc.type': [
199+
{
200+
value: 'audio',
201+
},
202+
],
203+
'dspace.bitstream.transcript': [
204+
{
205+
value: 'Audio transcript content',
206+
},
207+
],
208+
'dspace.bitstream.textalternative': [
209+
{
210+
value: 'Text alternative content',
211+
},
212+
],
198213
},
199214
format: createSuccessfulRemoteDataObject$(selectedFormat),
200215
_links: {
@@ -266,6 +281,18 @@ describe('EditBitstreamPageComponent', () => {
266281
expect(rawForm.descriptionContainer.description).toEqual(bitstream.firstMetadataValue('dc.description'));
267282
});
268283

284+
it('should fill in the media type', () => {
285+
expect(rawForm.mediaInfoContainer.mediaType).toEqual('audio');
286+
});
287+
288+
it('should fill in the audio transcript', () => {
289+
expect(rawForm.mediaInfoContainer.audioTranscript).toEqual('Audio transcript content');
290+
});
291+
292+
it('should fill in the text alternative', () => {
293+
expect(rawForm.mediaInfoContainer.videoDescription).toEqual('Text alternative content');
294+
});
295+
269296
it('should select the correct format', () => {
270297
expect(rawForm.formatContainer.selectedFormat).toEqual(selectedFormat.shortDescription);
271298
});

src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts

Lines changed: 139 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ import {
4646
DynamicFormLayout,
4747
DynamicFormService,
4848
DynamicInputModel,
49+
DynamicSelectModel,
50+
MATCH_VISIBLE,
51+
OR_OPERATOR,
4952
} from '@ng-dynamic-forms/core';
5053
import {
5154
TranslateModule,
@@ -338,11 +341,92 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
338341
},
339342
});
340343

344+
345+
/**
346+
* The Dynamic Select Model for the media type
347+
*/
348+
mediaTypeModel = new DynamicSelectModel({
349+
id: 'mediaType',
350+
name: 'mediaType',
351+
options: [
352+
{
353+
label: this.translate.instant('bitstream.edit.form.mediaType.option.neither'),
354+
value: 'neither',
355+
},
356+
{
357+
label: this.translate.instant('bitstream.edit.form.mediaType.option.audio'),
358+
value: 'audio',
359+
},
360+
{
361+
label: this.translate.instant('bitstream.edit.form.mediaType.option.video'),
362+
value: 'video',
363+
},
364+
{
365+
label: this.translate.instant('bitstream.edit.form.mediaType.option.audio-video'),
366+
value: 'audio+video',
367+
},
368+
],
369+
value: 'neither',
370+
});
371+
372+
/**
373+
* The Dynamic TextArea Model for the audio transcript
374+
*/
375+
audioTranscriptModel = new DsDynamicTextAreaModel({
376+
hasSelectableMetadata: false, metadataFields: [], repeatable: false, submissionId: '',
377+
id: 'audioTranscript',
378+
name: 'audioTranscript',
379+
rows: 10,
380+
relations: [
381+
{
382+
match: MATCH_VISIBLE,
383+
operator: OR_OPERATOR,
384+
when: [
385+
{
386+
id: 'mediaType',
387+
value: 'audio',
388+
},
389+
{
390+
id: 'mediaType',
391+
value: 'audio+video',
392+
},
393+
],
394+
},
395+
],
396+
});
397+
398+
/**
399+
* The Dynamic TextArea Model for the video description
400+
*/
401+
videoDescriptionModel = new DsDynamicTextAreaModel({
402+
hasSelectableMetadata: false, metadataFields: [], repeatable: false, submissionId: '',
403+
id: 'videoDescription',
404+
name: 'videoDescription',
405+
rows: 10,
406+
relations: [
407+
{
408+
match: MATCH_VISIBLE,
409+
operator: OR_OPERATOR,
410+
when: [
411+
{
412+
id: 'mediaType',
413+
value: 'video',
414+
},
415+
{
416+
id: 'mediaType',
417+
value: 'audio+video',
418+
},
419+
],
420+
},
421+
],
422+
});
423+
424+
341425
/**
342426
* All input models in a simple array for easier iterations
343427
*/
344-
inputModels = [this.primaryBitstreamModel, this.fileNameModel, this.descriptionModel, this.selectedFormatModel,
345-
this.newFormatModel];
428+
inputModels = [this.primaryBitstreamModel, this.fileNameModel, this.descriptionModel, this.mediaTypeModel,
429+
this.audioTranscriptModel, this.videoDescriptionModel, this.selectedFormatModel, this.newFormatModel];
346430

347431
/**
348432
* The dynamic form fields used for editing the information of a bitstream
@@ -366,6 +450,18 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
366450
this.descriptionModel,
367451
],
368452
}),
453+
new DynamicFormGroupModel({
454+
id: 'mediaInfoContainer',
455+
group: [
456+
this.mediaTypeModel,
457+
this.audioTranscriptModel,
458+
this.videoDescriptionModel,
459+
],
460+
}, {
461+
grid: {
462+
host: 'row',
463+
},
464+
}),
369465
new DynamicFormGroupModel({
370466
id: 'formatContainer',
371467
group: [
@@ -417,6 +513,21 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
417513
host: this.newFormatBaseLayout + ' invisible',
418514
},
419515
},
516+
mediaType: {
517+
grid: {
518+
host: 'col-12 d-inline-block',
519+
},
520+
},
521+
audioTranscript: {
522+
grid: {
523+
host: 'col-12 d-inline-block',
524+
},
525+
},
526+
videoDescription: {
527+
grid: {
528+
host: 'col-12 d-inline-block',
529+
},
530+
},
420531
fileNamePrimaryContainer: {
421532
grid: {
422533
host: 'row position-relative',
@@ -427,6 +538,11 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
427538
host: 'row',
428539
},
429540
},
541+
mediaInfoContainer: {
542+
grid: {
543+
host: 'row',
544+
},
545+
},
430546
formatContainer: {
431547
grid: {
432548
host: 'row',
@@ -622,6 +738,11 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
622738
descriptionContainer: {
623739
description: bitstream.firstMetadataValue('dc.description'),
624740
},
741+
mediaInfoContainer: {
742+
mediaType: bitstream.firstMetadataValue('dc.type') ?? 'neither',
743+
audioTranscript: bitstream.firstMetadataValue('dspace.bitstream.transcript'),
744+
videoDescription: bitstream.firstMetadataValue('dspace.bitstream.textalternative'),
745+
},
625746
formatContainer: {
626747
selectedFormat: this.selectedFormat.shortDescription,
627748
newFormat: hasValue(bitstream.firstMetadata('dc.format')) ? bitstream.firstMetadata('dc.format').value : undefined,
@@ -800,6 +921,22 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
800921
} else {
801922
Metadata.setFirstValue(newMetadata, 'dc.description', rawForm.descriptionContainer.description);
802923
}
924+
const mediaType = rawForm.mediaInfoContainer?.mediaType;
925+
if (isEmpty(mediaType) || mediaType === 'neither') {
926+
delete newMetadata['dc.type'];
927+
} else {
928+
Metadata.setFirstValue(newMetadata, 'dc.type', mediaType);
929+
}
930+
if (isEmpty(rawForm.mediaInfoContainer?.audioTranscript)) {
931+
delete newMetadata['dspace.bitstream.transcript'];
932+
} else {
933+
Metadata.setFirstValue(newMetadata, 'dspace.bitstream.transcript', rawForm.mediaInfoContainer.audioTranscript);
934+
}
935+
if (isEmpty(rawForm.mediaInfoContainer?.videoDescription)) {
936+
delete newMetadata['dspace.bitstream.textalternative'];
937+
} else {
938+
Metadata.setFirstValue(newMetadata, 'dspace.bitstream.textalternative', rawForm.mediaInfoContainer.videoDescription);
939+
}
803940
if (this.isIIIF) {
804941
// It's helpful to remove these metadata elements entirely when the form value is empty.
805942
// This avoids potential issues on the REST side and makes it possible to do things like

src/app/shared/file-download-link/file-download-link.component.html

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,26 +7,26 @@
77

88
<a [routerLink]="bitstreamPath?.routerLink" class="d-block dont-break-out mb-1"
99
[queryParams]="bitstreamPath?.queryParams"
10-
[target]="isBlank ? '_blank': '_self'"
11-
[ngClass]="cssClasses"
10+
[target]="isBlank ? '_blank': '_self'"
11+
[ngClass]="cssClasses"
1212
[attr.aria-label]="getDownloadLinkTitle(canDownload, canDownloadWithToken, dsoNameService.getName(bitstream))"
1313
[title]="getDownloadLinkTitle(canDownload, canDownloadWithToken, dsoNameService.getName(bitstream))"
14-
role="link"
15-
tabindex="0">
14+
role="link"
15+
tabindex="0">
1616
@if ((canDownload) === false && (canDownloadWithToken) === false) {
1717
<!-- If the user cannot download the file by auth or token, show a lock icon -->
1818
<span role="img"
19-
[attr.aria-label]="'file-download-link.restricted' | translate"
20-
[title]="'file-download-link.restricted' | translate"
21-
class="pe-1">
19+
[attr.aria-label]="'file-download-link.restricted' | translate"
20+
[title]="'file-download-link.restricted' | translate"
21+
class="pe-1">
2222
<i class="fas fa-lock"></i>
2323
</span>
2424
} @else if (canDownloadWithToken && (canDownload === false)) {
2525
<!-- If the user can download the file by token, and NOT normally show a lock open icon -->
2626
<span role="img"
27-
[attr.aria-label]="'file-download-link.secure-access' | translate"
28-
[title]="'file-download-link.secure-access' | translate"
29-
class="pe-1 request-a-copy-access-icon">
27+
[attr.aria-label]="'file-download-link.secure-access' | translate"
28+
[title]="'file-download-link.secure-access' | translate"
29+
class="pe-1 request-a-copy-access-icon">
3030
<i class="fa-solid fa-lock-open"></i>
3131
</span>
3232
} @else if (showIcon) {
@@ -35,6 +35,41 @@
3535
<ng-container *ngTemplateOutlet="content"></ng-container>
3636
</a>
3737

38+
@let showAudioTranscript = audioTranscript && isAudioMediaType;
39+
@let showVideoDescription = videoDescription && isVideoMediaType;
40+
@if (audioTranscript || videoDescription) {
41+
<span class="file-download-link">
42+
@if (showAudioTranscript) {
43+
<button
44+
class="btn btn-link p-0 file-download-link-button"
45+
type="button"
46+
(click)="openTextModal(textModal, 'file-download-link.audio-transcript.title', audioTranscript)"
47+
[attr.aria-label]="'file-download-link.audio-transcript.aria' | translate">
48+
{{ 'file-download-link.audio-transcript.label' | translate }}
49+
</button>
50+
}
51+
@if (showVideoDescription) {
52+
<button
53+
class="btn btn-link p-0 file-download-link-button"
54+
type="button"
55+
(click)="openTextModal(textModal, 'file-download-link.video-description.title', videoDescription)"
56+
[attr.aria-label]="'file-download-link.video-description.aria' | translate">
57+
{{ 'file-download-link.video-description.label' | translate }}
58+
</button>
59+
}
60+
</span>
61+
}
62+
3863
<ng-template #content>
3964
<ng-content></ng-content>
4065
</ng-template>
66+
67+
<ng-template #textModal let-modal>
68+
<div class="modal-header">
69+
<h2 class="modal-title" id="file-download-link-text-modal-title">{{ modalTitle }}</h2>
70+
<button type="button" class="btn-close" aria-label="Close" (click)="modal.dismiss()"></button>
71+
</div>
72+
<div class="modal-body">
73+
<div>{{ modalContent }}</div>
74+
</div>
75+
</ng-template>

src/app/shared/file-download-link/file-download-link.component.scss

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,12 @@
55
.btn-download{
66
width: fit-content;
77
}
8+
9+
.file-download-link {
10+
display: inline-flex;
11+
gap: 0.5rem;
12+
}
13+
14+
.file-download-link-button:hover {
15+
text-decoration: underline;
16+
}

0 commit comments

Comments
 (0)