CLARIN-DSpace v9/Restore per-instance DOM ids for ds-select and pool-task actions (X1 sweep, X-02a part A) - #1515
Open
milanmajchrak wants to merge 1 commit into
Open
Conversation
… across pages Card X-02a, part A of three. The v9 upgrade took these three templates wholesale from vanilla 9.3, so the fork's hunks from 6628aaf were never applied and the pages emit duplicate DOM ids again. ds-select renders one dropdown per sort option on a browse toolbar and one per pool task on /mydspace, but hardcoded its three ids (dsSelectMenuLabel, dsSelectMenuButton, dsSelectDropdownMenu). Every instance after the first therefore duplicated them, and each instance's aria-describedby / aria-labelledby resolved to the *first* instance's elements - a screen reader announced the wrong label for every dropdown but one. A module-level counter now gives each instance a uniqueId and the three ids are suffixed with it. Two adaptations beyond the source hunk, both because 9-base is not 7.x here: * aria-describedby is bound as [attr.aria-describedby]="label ? 'dsSelectMenuLabel-' + uniqueId : null" rather than left hardcoded. The label span is inside @if (label), so without the null branch the button points at an element that does not exist whenever no label is set - a dangling aria reference. The source commit made the same choice; it is restated here because 9-base uses @if where 7.x used *ngIf. * the pool-task row id is id="actions-{{ dso?.id }}" as in the source. Tests added (the source commit ships none): a host rendering two ds-select instances asserts that the document has no duplicate ids, that each button's aria-describedby resolves to a label inside its own instance and to nothing else, and that the two button ids differ; a third case asserts aria-describedby is null when no label is set; and the pool-task spec asserts the actions id is row-specific rather than the constant "actions". Each was proven load-bearing with a negative control - see the PR description. Source: 6628aaf (dtq-dev PR #1221) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Sep 10, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Restores the surviving intent of
dataquest-dev/dspace-angular#1221 (6628aaf493, "UFAL/Resolveduplicate HTML element IDs across pages") on
dtq-dev-9-base. Sync card X-02a, part A of three.The v9 upgrade took all three of these templates wholesale from vanilla 9.3, so the fork's hunks were
never applied and the pages emit duplicate DOM ids again. All three are byte-identical to
dspace-9.3on the current base:
dspace-9.3blobds-select.component.ts95e076ac7d95e076ac7dds-select.component.htmlc5fa369cebc5fa369cebpool-search-result-list-element.component.html14c39dfa6c14c39dfa6cds-selectrenders one dropdown per sort option on a browse toolbar and one per pool task on/mydspace, but hardcoded its three ids —dsSelectMenuLabel,dsSelectMenuButton,dsSelectDropdownMenu. Every instance after the first duplicated them, and each instance'saria-describedby/aria-labelledbythen resolved to the first instance's elements, so a screenreader announced the wrong label for every dropdown but one. A module-level counter now gives each
instance a
uniqueIdand the three ids are suffixed with it.ds-pool-task-actionsgetsid="actions-{{ dso?.id }}"for the same reason.Changes
shared/ds-select/ds-select.component.tsnextDsSelectIdcounter +uniqueIdfieldshared/ds-select/ds-select.component.htmluniqueId;aria-describedbyandaria-labelledbyboundshared/object-list/…/pool-search-result-list-element.component.htmlid="actions"→id="actions-{{ dso?.id }}"shared/ds-select/ds-select.component.spec.tsshared/object-list/…/pool-search-result-list-element.component.spec.tsidso the per-row id can be assertedAdaptations from the 7.x hunk
aria-describedbyis bound, not hardcoded. The label<span>lives inside@if (label)(
*ngIfon 7.x), so a constantaria-describedby="dsSelectMenuLabel-…"would point at an elementthat was never rendered whenever no label is set — a dangling aria reference. It is bound as
[attr.aria-describedby]="label ? 'dsSelectMenuLabel-' + uniqueId : null", which is what the sourcecommit does too; restated here because the surrounding control flow differs.
[dsBtnDisabled],@ifand the v9 indentation are untouched — only the id/aria lines move.<button>and on thediv[ngbDropdownMenu](rolemenu); both roles permit them.Tests added beyond the source commit
6628aaf493ships no spec, andds-select.component.spec.tson this branch had a singleit('should create')— a gate that cannot see duplicate ids. Added:should not emit duplicate DOM ids<ds-select>s produces an id list with no repeats (ids.length === new Set(ids).size, and the list is non-empty) — the unit-level form of the manual console check in the cardshould resolve every aria reference inside its own instancearia-describedbyresolves to a label inside that instance, no other element in the document answers to that id,aria-labelledbyequals that instance's own button id, and the two button ids differshould not reference a label element when no label is setaria-describedbyisnull, and the menu still names its own buttonshould give the pool task actions a row-specific idds-pool-task-actionsid is not the constantactionsand contains the row'sdso.idNegative controls — each new case proven load-bearing
Applied on top of the commit, so
git checkout HEAD -- <path>restores the ported file. Everyrevert and restore was proved with
git diff HEAD --stat— not the baregit diff --stat, whichis empty by construction because
git checkout <ref> -- <path>stages what it writes:NC-1 — revert
ds-select.component.htmlto the base → 3 FAILED, 6 SUCCESSSix ids of which three are distinct; two elements answering to one id; and the dangling
aria-describedbythe base emits when@if (label)rendered nothing.NC-2 — keep the template, break only the counter (
${nextDsSelectId++}→${nextDsSelectId})→ 2 FAILED, 7 SUCCESS
The "no label" case correctly stays green — it is template-only. NC-1 and NC-2 together separate
the two halves of the fix; a test that merely checked "the id contains
uniqueId" would havepassed NC-2.
NC-3 — revert the pool-task row id → 1 FAILED, 8 SUCCESS
Testing
npm run build:prodis the CIRun buildstep (build.yml:110-111) and runs on this PR.One lint fix worth mentioning for anyone writing a test host in this repo:
imports: [Foo],writteninline trips the project's own
dspace-angular-ts/sort-standalone-importsrule — it has to be oneentry per line. Also note
npm run lint:nobuild … | tailreports[exited with code 0]even when itprints
Lint errors found in the listed files.; the pipe masks the exit status, so the gate must beread from the text.
Card X-02a is split into three PRs by theme with disjoint file sets; this is part A. Parts B and C
carry the
5a92fdfa89(#1084) accessibility labels.Source:
6628aaf493(dtq-dev PR #1221). Sync card: X-02a.🤖 Generated with Claude Code