Skip to content

Commit bf6773d

Browse files
milanmajchrakclaude
andcommitted
Port #1084 (part) to dtq-dev-9-base: accessible labels for the suggestion inputs, reject reason and banner
Card X-02a, part C of three. Seven templates the v9 upgrade took wholesale from vanilla 9.3, so the remaining accessibility hunks of 5a92fdf were never applied. The five autocomplete inputs (input-suggestions and its dso, validation, person and org-unit variants) carried a placeholder and nothing else: a placeholder is not a label, so a screen reader announced them as an unnamed edit field. Each input now gets [id]="name + '-input'" and a visually-hidden <label> bound to the same expression. The workflow reject-reason textarea had the same problem and now has id="reason" and a label carrying the reason placeholder key. The dspace theme's home banner image gets the translated [alt] the fork added. Fork mistakes ported fixed rather than literally: 1. class="sr-only" does not exist in Bootstrap 5 and 9-base defines it nowhere (git grep -nE '^[[:space:]]*\.sr-only' -- src => 0 hits, "bootstrap": "^5.3"); a literal port would render a visible label above every autocomplete. visually-hidden is used, defined at src/styles/_global-styles.scss:280. 2. <label for="name + '-input'"> is a static attribute, not a binding - it emits those 15 characters literally and pairs with nothing. It is ported as [attr.for]="name + '-input'", matching the input's [id] expression, so the association is real. 3. validation-suggestions also carried a static id="name" on the same input as the fork's new [id] binding. Leaving both would keep a second element answering to id="name" in the submission form, so the static one is removed and only the per-instance binding remains. The five suggestion templates keep their existing <button class="sr-only" type="submit">: that is pre-existing vanilla markup on 9-base (29 files use the class), out of scope here, and touching it would mix an unrelated Bootstrap-5 migration into an accessibility port. Tests: 5a92fdf ships none. input-suggestions.component.spec.ts gains cases asserting the label resolves to the input it names (label.htmlFor === input.id, id non-empty and derived from the name input) rather than asserting that a string is present in the template. Proven load-bearing with negative controls - see the PR description. Source: 5a92fdf (dtq-dev PR #1084), partial - the search boxes from the same commit are in the sibling PR for card X-02a part B. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 8ac588e commit bf6773d

8 files changed

Lines changed: 47 additions & 2 deletions

File tree

src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-suggestions/org-unit-input-suggestions.component.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
(keydown.arrowdown)="shiftFocusDown($event)"
44
(keydown.arrowup)="shiftFocusUp($event)" (keydown.esc)="close()"
55
(dsClickOutside)="close();">
6+
<label [attr.for]="name + '-input'" class="visually-hidden">{{placeholder}}</label>
67
<input #inputField type="text" [(ngModel)]="value" [name]="name"
8+
[id]="name + '-input'"
79
class="form-control suggestion_input"
810
(focus)="open()"
911
(click)="open()"

src/app/entity-groups/research-entities/submission/item-list-elements/person/person-suggestions/person-input-suggestions.component.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
(keydown.arrowdown)="shiftFocusDown($event)"
44
(keydown.arrowup)="shiftFocusUp($event)" (keydown.esc)="close()"
55
(dsClickOutside)="close();">
6+
<label [attr.for]="name + '-input'" class="visually-hidden">{{placeholder}}</label>
67
<input #inputField type="text" [ngModel]="value" [name]="name"
8+
[id]="name + '-input'"
79
class="form-control suggestion_input"
810
(focus)="open()"
911
(blur)="onSubmit(inputField.value)"

src/app/shared/input-suggestions/dso-input-suggestions/dso-input-suggestions.component.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
(keydown.arrowdown)="shiftFocusDown($event)"
44
(keydown.arrowup)="shiftFocusUp($event)" (keydown.esc)="close()"
55
(dsClickOutside)="close();">
6+
<label [attr.for]="name + '-input'" class="visually-hidden">{{placeholder}}</label>
67
<input #inputField type="text" [(ngModel)]="value" [name]="name"
8+
[id]="name + '-input'"
79
class="form-control suggestion_input mb-2"
810
[ngClass]="{'is-invalid': !valid}"
911
[dsDebounce]="debounceTime" (onDebounce)="find($event)"

src/app/shared/input-suggestions/input-suggestions.component.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
(keydown.arrowdown)="shiftFocusDown($event)"
44
(keydown.arrowup)="shiftFocusUp($event)" (keydown.esc)="close()"
55
(dsClickOutside)="close();">
6+
<label [attr.for]="name + '-input'" class="visually-hidden">{{placeholder}}</label>
67
<input #inputField type="text" [(ngModel)]="value" [name]="name"
8+
[id]="name + '-input'"
79
class="form-control suggestion_input"
810
[ngClass]="{'is-invalid': !valid}"
911
[dsDebounce]="debounceTime" (onDebounce)="find($event)"

src/app/shared/input-suggestions/input-suggestions.component.spec.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,39 @@ describe('InputSuggestionsComponent', () => {
5555
expect(comp).toBeTruthy();
5656
});
5757

58+
describe('the accessible name of the suggestion input', () => {
59+
beforeEach(() => {
60+
comp.name = 'author';
61+
comp.placeholder = 'Search for an author';
62+
fixture.detectChanges();
63+
});
64+
65+
it('should label the input with a label that resolves to it', () => {
66+
const input: HTMLInputElement = el.querySelector('input.suggestion_input');
67+
const label: HTMLLabelElement = el.querySelector('label');
68+
69+
expect(input.id).toBeTruthy();
70+
expect(label).toBeTruthy();
71+
// a placeholder is not a label: the association has to be real
72+
expect(label.htmlFor).toEqual(input.id);
73+
expect(label.textContent.trim()).toEqual('Search for an author');
74+
// and the label must not be rendered visibly (Bootstrap 5 dropped the old screen-reader class)
75+
expect(Array.from(label.classList)).toContain('visually-hidden');
76+
});
77+
78+
it('should derive the id from the name input so two instances do not collide', () => {
79+
const input: HTMLInputElement = el.querySelector('input.suggestion_input');
80+
const firstId: string = input.id;
81+
82+
comp.name = 'subject';
83+
fixture.detectChanges();
84+
const label: HTMLLabelElement = el.querySelector('label');
85+
86+
expect(input.id).not.toEqual(firstId);
87+
expect(label.htmlFor).toEqual(input.id);
88+
});
89+
});
90+
5891
describe('when the input field is in focus', () => {
5992

6093
beforeEach(() => {

src/app/shared/input-suggestions/validation-suggestions/validation-suggestions.component.html

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
(keydown.arrowdown)="shiftFocusDown($event)"
44
(keydown.arrowup)="shiftFocusUp($event)" (keydown.esc)="close()"
55
(dsClickOutside)="checkIfValidInput(form);close();">
6-
<input [readonly]="disable" #inputField type="text" formControlName="metadataNameField" attr.aria-labelledby="fieldName" [(ngModel)]="value" id="name" [name]="name"
6+
<label [attr.for]="name + '-input'" class="visually-hidden">{{placeholder}}</label>
7+
<input [readonly]="disable" #inputField type="text" formControlName="metadataNameField" attr.aria-labelledby="fieldName" [(ngModel)]="value" [name]="name"
8+
[id]="name + '-input'"
79
class="form-control suggestion_input"
810
[ngClass]="{'is-invalid': !valid}"
911
[dsDebounce]="debounceTime" (onDebounce)="find($event)"

src/app/shared/mydspace-actions/claimed-task/reject/claimed-task-actions-reject.component.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ <h4 class="modal-title">{{'submission.workflow.tasks.claimed.reject.reason.title
2525
{{'submission.workflow.tasks.claimed.reject.reason.info' | translate}}
2626
</div>
2727
<form (ngSubmit)="submitTask();" [formGroup]="rejectForm" >
28+
<label for="reason" class="visually-hidden">{{'submission.workflow.tasks.claimed.reject.reason.placeholder' | translate}}</label>
2829
<textarea style="width: 100%"
30+
id="reason"
2931
formControlName="reason"
3032
rows="4"
3133
placeholder="{{'submission.workflow.tasks.claimed.reject.reason.placeholder' | translate}}"></textarea>

src/themes/dspace/app/home-page/home-news/home-news.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ <h1 class="display-2">DSpace 9</h1>
3434
<picture class="background-image">
3535
<source type="image/webp" srcset="assets/dspace/images/banner.webp 2000w, assets/dspace/images/banner-half.webp 1200w, assets/dspace/images/banner-tall.webp 768w">
3636
<source type="image/jpg" srcset="assets/dspace/images/banner.jpg 2000w, assets/dspace/images/banner-half.jpg 1200w, assets/dspace/images/banner-tall.jpg 768w">
37-
<img alt="" [src]="'assets/dspace/images/banner.jpg'"/><!-- without the []="''" Firefox downloads both the fallback and the resolved image -->
37+
<img [alt]="'home.news.image.banner' | translate" [src]="'assets/dspace/images/banner.jpg'"/><!-- without the []="''" Firefox downloads both the fallback and the resolved image -->
3838
</picture>
3939
<small class="credits">Photo by <a href="https://www.pexels.com/@inspiredimages">&#64;inspiredimages</a></small>
4040
</div>

0 commit comments

Comments
 (0)