Skip to content

CLARIN-DSpace v9/Restore the dropped accessibility labels and per-component ids for the search boxes (X1 sweep, X-02a part B) - #1516

Open
milanmajchrak wants to merge 1 commit into
dtq-dev-9-basefrom
ufal/port-x02a-search-labels-9-base
Open

CLARIN-DSpace v9/Restore the dropped accessibility labels and per-component ids for the search boxes (X1 sweep, X-02a part B)#1516
milanmajchrak wants to merge 1 commit into
dtq-dev-9-basefrom
ufal/port-x02a-search-labels-9-base

Conversation

@milanmajchrak

Copy link
Copy Markdown
Collaborator

What

Re-applies the search-box half of dataquest-dev/dspace-angular #1084 (5a92fdfa89, "Lindat to
dtq sync251021") on dtq-dev-9-base. Sync card X-02a, part B of three.

Seven templates that the v9 upgrade took wholesale from vanilla 9.3, so the fork's accessibility hunks
were never applied. Every one of these controls is either unnamed for a screen reader — a
placeholder is not a label — or named in hardcoded English regardless of the UI language:

$ git grep -n 'aria-label="Search input"' origin/dtq-dev-9-base -- src/app/shared/eperson-group-list src/app/access-control/group-registry
…/eperson-search-box.component.html:13
…/group-search-box.component.html:7
…/members-list.component.html:88
…/subgroups-list.component.html:73

Changes

File Change
shared/eperson-group-list/eperson-search-box/…html 2 labels (scope select + query input), both aria-labels bound to labelPrefix + 'search.scope' / 'search.input', ids → eperson-search-scope / eperson-search-query
shared/eperson-group-list/group-search-box/…html 1 label, aria-label bound, id → group-search-query
access-control/…/members-list/…html 1 label, aria-label bound to messagePrefix + '.search.input', id → members-list-query
access-control/…/subgroup-list/…html as above, id → subgroups-list-query
access-control/group-registry/groups-registry.component.html 1 label, id → groups-registry-query
search-navbar/search-navbar.component.html 1 label, static id="search-navbar-query"
shared/starts-with/date/starts-with-date.component.html 3 labels (year, month, free-text date), static id="startsWith-input"
3 spec files 4 new cases (the source commit ships none)

formControlName and name stay query / scope everywhere — only the DOM id changes, so no form
wiring moves. No i18n key is added, removed or renamed: every key used here already exists in
en.json5 and cs.json5 with zero references, which is exactly the symptom of the dropped hunks.

$ for k in admin.access-control.epeople.search.input admin.access-control.epeople.search.scope \
           admin.access-control.groups.search.input \
           admin.access-control.groups.form.members-list.search.input \
           admin.access-control.groups.form.subgroups-list.search.input; do
    printf '%-62s en=%s cs=%s refs=%s\n' "$k" "$(grep -cF "\"$k\"" src/assets/i18n/en.json5)" \
      "$(grep -cF "\"$k\"" src/assets/i18n/cs.json5)" "$(git grep -oF "$k" origin/dtq-dev-9-base -- src/app | wc -l)"; done
admin.access-control.epeople.search.input                        en=1 cs=2 refs=0
admin.access-control.epeople.search.scope                        en=1 cs=2 refs=0
admin.access-control.groups.search.input                         en=1 cs=2 refs=0
admin.access-control.groups.form.members-list.search.input       en=1 cs=2 refs=0
admin.access-control.groups.form.subgroups-list.search.input     en=1 cs=2 refs=0

(cs=2 is the project's convention of keeping the English original as a comment line above the
translation, not a duplicate key.)

Three fork mistakes ported fixed, not literally

  1. class="sr-only" does not exist in Bootstrap 5, and this branch defines it nowhere:
    $ grep '"bootstrap"' package.json                                   ->  "bootstrap": "^5.3",
    $ git grep -nE '^[[:space:]]*\.sr-only' origin/dtq-dev-9-base -- src | wc -l   ->  0
    $ git grep -nE '^[[:space:]]*\.visually-hidden' origin/dtq-dev-9-base -- src   ->  src/styles/_global-styles.scss:280
    
    A literal port would render a visible label in the middle of every search form. visually-hidden
    is used instead; it is position: absolute !important, so it adds nothing to the .input-group
    flex layout.
  2. <label for="startsWith + 'input'"> and <label for="'query' + '-input'"> are static
    attributes, not bindings
    — Angular emits those characters literally and the label pairs with
    nothing. The ids here are static, so plain for="…" values matching the real id are used.
  3. The group edit page renders ds-members-list and ds-subgroups-list side by side
    (group-form.component.html:60 and :65) and both hardcoded id="query". Adding
    <label for="query"> before disambiguating would have bound both labels to the first input.
    Hence the per-component ids.

One more, which the card calls out and the fork got wrong: the fork bound the date input as
[id]="startsWith + 'input'". startsWith is reassigned as the user types
(starts-with-date.component.ts:123), so the id — and therefore the label association — would change
on every keystroke. A static id="startsWith-input" is used.

No aria attribute is added that the fork did not have, and nothing gains an aria attribute its role
forbids: the changes are <label> elements plus aria-label on <input type="text"> and <select>,
both of which permit it.

Tests added beyond the source commit

spec test asserts
eperson-search-box should label the scope select and the query input, each resolving to its own control exactly 2 labels; each label.htmlFor is non-empty and resolves to an element whose id equals it; each label carries visually-hidden
eperson-search-box should name both controls from the translated keys rather than hardcoded English the rendered aria-labels are the translation keys (an empty catalogue echoes the key), and the ids are no longer the generic query / scope
members-list should label its search input with an id of its own, not the generic id="query" the input's id is non-empty and not query, and a visually-hidden label resolves to it
subgroups-list same same — these are the two components that render together and collided

They assert the association (label.htmlFor === input.id) rather than the presence of a string,
so they survive a rename of the id and fail on a mis-pointed label.

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 is proved with git diff HEAD --stat, not the bare git diff --stat: git checkout <ref> -- <path> stages what it writes, so the bare form is empty even when the revert landed.

  • NC-1 — revert eperson-search-box.component.html to the base2 FAILED, 49 SUCCESS

    should name both controls from the translated keys rather than hardcoded English FAILED
    	Error: Expected 'Search input' to equal 'admin.access-control.epeople.search.input'.
    	Error: Expected 'Search scope' to equal 'admin.access-control.epeople.search.scope'.
    	Error: Expected 'query' not to equal 'query'.
    	Error: Expected 'scope' not to equal 'scope'.
    should label the scope select and the query input, each resolving to its own control FAILED
    	Error: Expected 0 to equal 2.
    

    The failure text is the defect report: the accessible name is the literal English Search input,
    the ids are the generic query/scope, and there are zero labels.

  • NC-2 — keep the labels, point them at controls that do not exist
    (for="eperson-search-scope"for="not-the-scope-control", same for the query label)
    1 FAILED, 50 SUCCESS

    should label the scope select and the query input, each resolving to its own control FAILED
    	Error: Expected null to be truthy.
    	TypeError: Cannot read properties of null (reading 'id')
    

    Only the pairing test goes red — the translated-name test correctly stays green, since ids and
    aria-labels are untouched. This proves the test asserts the association
    (label.htmlFor === input.id) rather than the presence of a <label>, which is exactly the fork
    mistake Test first touch #2 shape (for="name + '-input'" written as a literal).

  • NC-3 — revert members-list + subgroups-list (the shared id="query" returns)
    2 FAILED, 49 SUCCESS

    SubgroupsListComponent should label its search input with an id of its own, not the generic id="query" FAILED
    	Error: Expected 'query' not to equal 'query'.
    	Error: Expected null to be truthy.
    MembersListComponent should label its search input with an id of its own, not the generic id="query" FAILED
    	Error: Expected 'query' not to equal 'query'.
    	Error: Expected null to be truthy.
    

    Both of the components that render side by side on the group edit page go red on the exact property
    that produced the duplicate id.

Testing

All seven subject specs are run, not only the three that were extended — the specs on this branch
still assert vanilla behaviour of these templates, so they are the real regression gate:

$ npm run test:headless -- --include='…eperson-search-box.component.spec.ts'     --include='…group-search-box.component.spec.ts' --include='…members-list.component.spec.ts'     --include='…subgroups-list.component.spec.ts' --include='…groups-registry.component.spec.ts'     --include='…search-navbar.component.spec.ts' --include='…starts-with-date.component.spec.ts'     --code-coverage=false
    ✔ should label the scope select and the query input, each resolving to its own control
    ✔ should name both controls from the translated keys rather than hardcoded English
    ✔ should label its search input with an id of its own, not the generic id="query"   (members-list)
    ✔ should label its search input with an id of its own, not the generic id="query"   (subgroups-list)
    … 47 pre-existing cases …
TOTAL: 51 SUCCESS

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

47 pre-existing assertions still pass, so rewriting the ids and aria-labels broke none of them.
npm run build:prod is the CI Run build step (build.yml:110-111) and runs on this PR.

Label count per template, which is what the card's acceptance grep checks
(grep -c '<label[^>]*visually-hidden', expected 3 2 1 1 1 1 1 for these seven):

starts-with-date  eperson-search-box  group-search-box  members-list  subgroups-list  groups-registry  search-navbar
       3                  2                  1               1              1                1              1

Card X-02a is split into three PRs by theme with disjoint file sets; this is part B. Part A
(#1515) carries the 6628aaf493 (#1221) unique DOM ids; part C carries the suggestion inputs.

Source: 5a92fdfa89 (dtq-dev PR #1084), partial. Sync card: X-02a.

🤖 Generated with Claude Code

@milanmajchrak
milanmajchrak force-pushed the ufal/port-x02a-search-labels-9-base branch from aefe116 to cabdb94 Compare September 10, 2026 17:47
…ent ids for the search boxes

Card X-02a, part B of three. Seven templates that the v9 upgrade took wholesale from vanilla 9.3, so
the accessibility hunks of 5a92fdf were never applied. Every one of these inputs is either
unnamed for a screen reader or named in hardcoded English regardless of the UI language.

What changes:

* a visually-hidden <label> is added for each control - two in eperson-search-box (the scope select
  and the query input), three in starts-with-date (year, month, free-text date), one each in
  group-search-box, members-list, subgroups-list, groups-registry and search-navbar;
* aria-label="Search input" and aria-label="Search scope", which were hardcoded English strings even
  in the Czech UI, become bindings on the keys that already exist unreferenced in en.json5:
  labelPrefix + 'search.input', labelPrefix + 'search.scope' and messagePrefix + '.search.input'.
  No i18n key is added, removed or renamed;
* the hardcoded id="query" is replaced by a per-component id in all five components that carry it in
  this set. formControlName and name stay "query"/"scope"; only the DOM id changes.

Three fork mistakes are ported fixed rather than literally, each for a reason that is checkable on
this branch:

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 in the middle of every search form. The class used is visually-hidden,
   which is defined at src/styles/_global-styles.scss:280 and is position:absolute, so it adds
   nothing to the input-group layout.
2. <label for="startsWith + 'input'"> and <label for="'query' + '-input'"> are static attributes, not
   bindings: they emit those characters literally and pair with nothing. The ids here are static, so
   the labels use plain static for= values that match the real id.
3. The group edit page renders ds-members-list and ds-subgroups-list side by side
   (group-form.component.html:60 and :65) and both hardcoded id="query". Adding <label for="query">
   without disambiguating first would have bound both labels to the first input.

The fork also gave starts-with-date [id]="startsWith + 'input'". startsWith is mutated as the user
types (starts-with-date.component.ts:123), so the id would change on every keystroke and the label
association would break. A static id="startsWith-input" is used instead.

Tests: 5a92fdf ships none. eperson-search-box.component.spec.ts gains cases asserting that each
control's label resolves to that control (label.htmlFor === input.id, id non-empty) and that the
accessible name is the translated key rather than the literal "Search input". Proven load-bearing
with negative controls - see the PR description.

Source: 5a92fdf (dtq-dev PR #1084), partial - the suggestion inputs from the same commit are in
the sibling PR for card X-02a part C.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@milanmajchrak
milanmajchrak force-pushed the ufal/port-x02a-search-labels-9-base branch from cabdb94 to 3bf96c3 Compare September 10, 2026 17:49
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