Skip to content

Icon-only buttons on the instrument toolbar, results summary and navigation have no accessible name #1701

Open

Description

@joshunrau

Several buttons in packages/react-core and apps/web contain only an icon and have no aria-label or visually hidden text, so they have no accessible name. A screen reader announces each of them as just "button", and voice-control users have no name to say. The worst cases are on the interactive-instrument toolbar, which participants use in the gateway as well as clinicians in the web app, and the copy, download and print buttons on the results summary. Newer code in the same workspaces already does this correctly (the email-template row actions, the permission editor, TimepointTable), so these are the outliers.

Where

packages/react-core:

  • src/components/CopyButton/CopyButton.tsx:17-47: the shared copy button, used for the assignment link in datahub/subjects/$subjectId/assignments.tsx:51 and session/remote-assignment.tsx:61, and on the results summary. It also changes only its icon on success, so nothing announces that the copy worked.
  • src/components/InstrumentSummary/InstrumentSummary.tsx:116-121: download and print.
  • src/components/InteractiveContent/InteractiveContent.tsx:129-153: zoom out, zoom in, full screen and the language menu trigger.
  • src/components/FormContent/FormContent.tsx:27-29: the instructions (info) button next to a form's title.
// InteractiveContent.tsx:141-147
<Button size="icon" type="button" variant="outline" onClick={() => setScale(scale + 25)}>
  <ZoomInIcon />
</Button>
<Separator className="mx-1 h-6" orientation="vertical" />
<Button size="icon" type="button" variant="outline" onClick={() => void handleToggleFullScreen()}>
  <FullscreenIcon />
</Button>

apps/web:

  • src/components/Navbar/Navbar.tsx:47-54: the mobile menu (hamburger) trigger.
  • src/providers/WalkthroughProvider.tsx:473-475: the tutorial's close (X) button.
  • src/components/AssignmentEmailForm/AssignmentEmailForm.tsx:152-154: the "email language" help tooltip trigger.
  • src/components/InstrumentCard/InstrumentCard.tsx:86-92: the open-source licence badge tooltip trigger.
  • src/components/DemoBanner/DemoBanner.tsx:93-95: the "use these credentials" button in each demo-user row.

Reproduce

  1. Turn on VoiceOver (macOS) or NVDA and start any interactive instrument, e.g. from Administer Instrument in a session.
  2. Tab through the toolbar above the instrument.

Actual: four consecutive controls are announced as "button" with no name. The same happens for the copy, download and print buttons on the summary at /datahub/subjects/<id>/table/<recordId>, the hamburger menu on a phone-width window, and the tutorial's close button.
Expected: each control has a name in the interface language, e.g. "Zoom in", "Full screen", "Copy to clipboard", "Download results", "Print", "Open menu", "Close tutorial".

Tests

  • packages/react-core/src/components/InteractiveContent/__tests__/InteractiveContent.test.tsx: it('should give every toolbar button an accessible name, so screen readers can tell zoom, full screen and language apart'), asserting with screen.getByRole('button', { name: ... }).
  • packages/react-core/src/components/CopyButton/__tests__/CopyButton.test.tsx: it('should expose an accessible name, so the copy action is announced').
  • apps/web/src/components/Navbar/__tests__/Navbar.test.tsx: it('should name the menu trigger, so it is announced as more than "button"').
  • e2e: testing/src/specs/instrument-completion.spec.ts can locate the summary's download and print buttons with getByRole('button', { name }) instead of by position.

Suggested fix

Add a translated aria-label (inline t({ en, es, fr })) to each listed button. For CopyButton, also accept a label prop and swap it for a "Copied" label, or add a role="status" live region, when the copy succeeds.

Activity

  1. added
    BugType: existing behavior is wrong
    Difficulty: LowIsolated change, about 2 hours or less
    Area: Webapps/web (clinician-facing React SPA)
    Area: SharedCross-tier packages (schemas, react-core, etc.) not covered by another area
    Good First IssueWell-scoped, low-risk and self-contained; a good entry point for a new contributor
    on Oct 7, 2026
  2. joshunrau commented on Oct 26, 2026

    @joshunrau
    CollaboratorAuthor

    Still reproduces on main (d86740e). Every listed site is unchanged.

    Sites the list misses

    CopyButton

    react-core's copy is libui's with only the import paths changed, and libui 6.18.0 (the latest release) ships the same file, so there is no upstream fix to wait for. It accepts only size, text and variant and spreads nothing, so a caller cannot pass aria-label.

    • Add label?: LocalizedText (the prop shape packages/react-core/AGENTS.md prescribes for copy the host supplies), defaulting to "Copy to clipboard". Pass "Copy link" from assignments.tsx:51 and remote-assignment.tsx:61.
    • Switch the two libui imports to @opendatacapture/react-core.
    • Announce success in a role="status" sr-only span next to the button. Render it empty from the start and fill it on success. Screen readers do not reliably re-announce a focused button whose name changes, so swapping the label is the weaker option.
    • SUCCESS resets only on onMouseLeave (CopyButton.tsx:29-31), and clicks are ignored in that state, so a keyboard user can copy only once. Also reset on onBlur. That keeps the ignore-while-copied test at CopyButton.test.tsx:57 passing.
    • CopyButton.test.tsx never initialises i18n. Once the button calls t(), add i18n.init({ translations: {} }) in beforeAll, as the AGENTS.md says.

    Other notes

    Tests

    • InstrumentCard.test.tsx:49 finds the badge with getByRole('button', { name: '' }), so naming the badge breaks it.
    • testing/src/pages/_app/session/remote-assignment.page.ts:20-22 finds the copy button with input[readonly] + button because it has no name. Switch it to getByRole('button', { name: 'Copy link' }), and remote-assignment.spec.ts:85-109 then covers the name end to end.
    • No e2e spec touches the summary's copy, download or print buttons. The Download click at instrument-completion.spec.ts:104 is the subject table's export menu. Assert the three names after the summaryHeading check at :42.
    • No web spec runs an interactive instrument. playground.spec.ts:218-227 does, and its preview renders the same InteractiveContent, so assert the toolbar names there.
    • Settings help buttons: neither admin-settings.spec.ts nor settings.test.tsx covers them yet.
    • auth.spec.ts:51: find the demo row's button by name. For the hamburger, add a PHONE_VIEWPORT test like datahub.spec.ts:7-8. No spec opens the mobile nav today.
    • These positional lookups should become name lookups: InteractiveContent.test.tsx:161,227,234, InstrumentSummary.test.tsx:68,103,192, FormContent.test.tsx:35, WalkthroughProvider.test.tsx:98.

    Landing

    #1825 edits Navbar.tsx, Navbar.test.tsx and InstrumentSummary.test.tsx. #1730 replaces a test in Navbar.test.tsx. Branch from main after #1825 merges.

    Triage note written with Claude Code. Code references point at main @ d86740e.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Area: SharedCross-tier packages (schemas, react-core, etc.) not covered by another areaArea: Webapps/web (clinician-facing React SPA)BugType: existing behavior is wrongDifficulty: LowIsolated change, about 2 hours or lessGood First IssueWell-scoped, low-risk and self-contained; a good entry point for a new contributorPriority: MediumReal value; schedule normally

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions