Skip to content

Multiple instructions show as one comma-joined paragraph ("questions., Conditional") #1812

Open

Description

@gdevenyi

Problem

The instructions field is a string[] of "Brief sequential instructions":

/** Brief sequential instructions for how the subject should complete the instrument. */
instructions?: InstrumentUIOption<TLanguage, string[]>;

The instrument guidelines show each entry as one full sentence:

instructions: {
en: ['Please answer all required questions.', 'Conditional questions will appear based on your answers.'],

en: ['Please answer all required questions.', 'Conditional questions will appear based on your answers.'],

But both renderers join the entries with ', ' into one paragraph:

Steps to reproduce

  1. Make an instrument with clientDetails.instructions set to the guidelines example above.
  2. Open the instrument overview, or open the instructions dialog in the form.

Expected

Each instruction shows as its own line or list item:

Please answer all required questions.
Conditional questions will appear based on your answers.

Actual

Please answer all required questions., Conditional questions will appear based on your answers.

Note

The instruments in instrument-library use only one entry, so they do not show the problem. Instruments that follow the documented example do show it.

Suggested fix

Render each entry as its own element in both components. For example, use an <ol>/<ul> (the entries are "sequential"), or one <p> for each entry. Do not use join(', ').

cc @thomasbeaudry (last change to these files)

Activity

  1. joshunrau commented on Oct 26, 2026

    @joshunrau
    Collaborator

    Still reproduces on main (d86740e). No commit has touched either component since this was filed, and no open PR touches them. Both are internal parts of ScalarInstrumentRenderer and SeriesInstrumentRenderer, so one fix in @opendatacapture/react-core covers web, gateway, playground and serve-instrument.

    Gotchas

    • The unit tests assert the bug: InstrumentOverview.test.tsx:39 expects 'Step one, Step two' and FormContent.test.tsx:72 expects 'Answer honestly, Be brief'.
    • Use one <p> per entry, not a list. Tailwind's preflight sets list-style: none on ol and ul (tailwindcss 4.3.3, preflight.css:202-206), so a list shows no numbers without list-decimal, and with it every built-in instrument shows 1. before its one sentence. VoiceOver also stops announcing an unstyled list as a list (Tailwind docs), and the usual fix, role="list", fails jsx-a11y/no-redundant-roles, which the shared eslint config sets to error.
    • The series creator stores the whole textarea as one entry, newlines included (apps/web/src/routes/_app/group/manage.tsx:291), and the series overview renders it through the same InstrumentOverview. Put whitespace-pre-line on each <p>. That also fixes series already stored this way, which splitting the text in manage.tsx would not, so apps/web needs no change.
    • Entries can repeat (z.array(z.string().min(1)), packages/schemas/src/instrument/instrument.base.ts:90), so key each <p> by index, not by text.
    • The info button (FormContent.tsx:27-29) has no accessible name, and is already listed in Icon-only buttons on the instrument toolbar, results summary and navigation have no accessible name #1701. The e2e test needs a name to find it, so add an aria-label here and take it off Icon-only buttons on the instrument toolbar, results summary and navigation have no accessible name #1701's list. The dialog title already uses t({ en: 'Instructions', es: 'Instrucciones', fr: 'Instructions' }); put that in one constant for both.

    Plan

    1. Add an internal packages/react-core/src/components/InstrumentInstructions/ (InstrumentInstructions.tsx and index.ts). It takes instructions: string[] and renders <div className="text-muted-foreground space-y-1 text-sm"> with one <p className="whitespace-pre-line"> per entry. Do not export it from src/index.ts.
    2. Use it in place of the <p> at InstrumentOverview.tsx:51 and FormContent.tsx:42, and add the aria-label. In the overview, change Boolean(instructions?.length) && to instructions && instructions.length > 0 &&, which narrows, so the ! goes.

    Tests

    • Unit: add InstrumentInstructions/__tests__/InstrumentInstructions.test.tsx. Rewrite the two joined-text assertions as one getByText per entry. Make getInfoButton (FormContent.test.tsx:35) use getByRole('button', { name: 'Instructions' }).

    • e2e: testing/src/specs/playground.spec.ts already renders share links through generatePlaygroundURL. Add a form with two entries, 'Read each question.' and 'Answer' + newline + 'honestly.'. That spec builds the instrument source in a template literal, so write the newline as \\n; a bare \n puts a raw line break inside a string literal and the instrument does not compile. Assert:

      • The overview shows each entry with getByText(..., { exact: true }). Without exact, Playwright matches substrings, so the joined paragraph would pass too.
      • The second entry's innerText() is 'Answer\nhonestly.'. A browser keeps that line break only under pre-line.
      • After Begin, the dialog opened with getByRole('button', { name: 'Instructions' }) shows the same two entries.

      fix: resolve 26 low-difficulty issues, one commit each #1825 and build: upgrade dependencies held back by breaking changes #1660 also add tests to that spec, so expect a small rebase.

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

  2. added
    BugType: existing behavior is wrong
    Priority: LowNice to have, cosmetic or speculative
    Difficulty: LowIsolated change, about 2 hours or less
    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 26, 2026
  3. added a commit that references this issue on Nov 2, 2026
    3141353
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 areaBugType: 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: LowNice to have, cosmetic or speculative

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions