Skip to content

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
dtq-dev-9-basefrom
ufal/port-x02a-unique-ids-9-base
Open

CLARIN-DSpace v9/Restore per-instance DOM ids for ds-select and pool-task actions (X1 sweep, X-02a part A)#1515
milanmajchrak wants to merge 1 commit into
dtq-dev-9-basefrom
ufal/port-x02a-unique-ids-9-base

Conversation

@milanmajchrak

Copy link
Copy Markdown
Collaborator

What

Restores the surviving intent of dataquest-dev/dspace-angular #1221 (6628aaf493, "UFAL/Resolve
duplicate 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.3
on the current base:

file 9-base blob dspace-9.3 blob
ds-select.component.ts 95e076ac7d 95e076ac7d
ds-select.component.html c5fa369ceb c5fa369ceb
pool-search-result-list-element.component.html 14c39dfa6c 14c39dfa6c

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 duplicated them, and each instance's
aria-describedby / aria-labelledby then resolved to the first instance's elements, so 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. ds-pool-task-actions gets
id="actions-{{ dso?.id }}" for the same reason.

Changes

File Change
shared/ds-select/ds-select.component.ts module-level nextDsSelectId counter + uniqueId field
shared/ds-select/ds-select.component.html three ids interpolated with uniqueId; aria-describedby and aria-labelledby bound
shared/object-list/…/pool-search-result-list-element.component.html id="actions"id="actions-{{ dso?.id }}"
shared/ds-select/ds-select.component.spec.ts 3 new cases (the source commit ships none)
shared/object-list/…/pool-search-result-list-element.component.spec.ts 1 new case, and the mock pool task gains an id so the per-row id can be asserted

Adaptations from the 7.x hunk

  1. aria-describedby is bound, not hardcoded. The label <span> lives inside @if (label)
    (*ngIf on 7.x), so a constant aria-describedby="dsSelectMenuLabel-…" would point at an element
    that 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 source
    commit does too; restated here because the surrounding control flow differs.
  2. [dsBtnDisabled], @if and the v9 indentation are untouched — only the id/aria lines move.
  3. No aria attribute is added that the fork did not have. The two that are touched sit on a
    <button> and on the div[ngbDropdownMenu] (role menu); both roles permit them.

Tests added beyond the source commit

6628aaf493 ships no spec, and ds-select.component.spec.ts on this branch had a single
it('should create') — a gate that cannot see duplicate ids. Added:

test asserts
should not emit duplicate DOM ids a host rendering two <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 card
should resolve every aria reference inside its own instance for each instance, aria-describedby resolves to a label inside that instance, no other element in the document answers to that id, aria-labelledby equals that instance's own button id, and the two button ids differ
should not reference a label element when no label is set aria-describedby is null, and the menu still names its own button
should give the pool task actions a row-specific id the rendered ds-pool-task-actions id is not the constant actions and contains the row's dso.id

Negative controls — each new case proven load-bearing

Applied on top of the commit, so git checkout HEAD -- <path> restores the ported file. Every
revert and restore was proved with git diff HEAD --statnot the bare git diff --stat, which
is empty by construction because git checkout <ref> -- <path> stages what it writes:

--- revert proof (git diff HEAD --stat) ---
 src/app/shared/ds-select/ds-select.component.html | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)
--- bare 'git diff --stat' for contrast ---
(empty)
  • NC-1 — revert ds-select.component.html to the base3 FAILED, 6 SUCCESS

    should not emit duplicate DOM ids FAILED
    	Error: Expected 6 to equal 3.
    should resolve every aria reference inside its own instance FAILED
    	Error: Expected 2 to equal 1.
    	Error: Expected 'dsSelectMenuButton' not to equal 'dsSelectMenuButton'.
    should not reference a label element when no label is set FAILED
    	Error: Expected 'dsSelectMenuLabel' to be null.
    

    Six ids of which three are distinct; two elements answering to one id; and the dangling
    aria-describedby the base emits when @if (label) rendered nothing.

  • NC-2 — keep the template, break only the counter (${nextDsSelectId++}${nextDsSelectId})
    2 FAILED, 7 SUCCESS

    	Error: Expected 6 to equal 3.
    	Error: Expected 'dsSelectMenuButton-ds-select-0' not to equal 'dsSelectMenuButton-ds-select-0'.
    

    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 have
    passed NC-2.

  • NC-3 — revert the pool-task row id1 FAILED, 8 SUCCESS

    should give the pool task actions a row-specific id FAILED
    	Error: Expected 'actions' not to equal 'actions'.
    	Error: Expected 'actions' to contain 'pool-task-1'.
    

Testing

$ npm run test:headless -- --include='src/app/shared/ds-select/ds-select.component.spec.ts'     --include='src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.spec.ts'     --code-coverage=false
    ✔ should create
    ✔ should not reference a label element when no label is set
      ✔ should not emit duplicate DOM ids
      ✔ should resolve every aria reference inside its own instance
    ✔ should give the pool task actions a row-specific id
    ✔ should have correct badge context
    ✔ should init workflowitem properly
    ✔ should add an offset to the actions element
    ✔ should forward pool-task-actions processCompleted event to the reloadedObject event emitter
TOTAL: 9 SUCCESS

$ npm run lint:nobuild -- --quiet
Linting "dspace-angular"...
All files pass linting.

npm run build:prod is the CI Run build step (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], written
inline trips the project's own dspace-angular-ts/sort-standalone-imports rule — it has to be one
entry per line. Also note npm run lint:nobuild … | tail reports [exited with code 0] even when it
prints Lint errors found in the listed files.; the pipe masks the exit status, so the gate must be
read 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

… 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant